Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions partner_programs/published_projects_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Public legacy-project reader; manager lists and exports retain their permissions."""

from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import generics
from rest_framework.permissions import AllowAny

from partner_programs.models import PartnerProgram, PartnerProgramUserProfile
from partner_programs.pagination import PartnerProgramPagination
from projects.models import Project
from projects.serializers import ProjectListSerializer


def published_projects_available(program):
return bool(
not program.draft
and program.solution_storage_mode == "legacy_project"
and program.publish_projects_after_finish
and program.datetime_finished <= timezone.now()
)


class PublishedProjectsPagination(PartnerProgramPagination):
max_limit = 100


class PublishedProgramProjectsView(generics.ListAPIView):
permission_classes = [AllowAny]
serializer_class = ProjectListSerializer
pagination_class = PublishedProjectsPagination

def get_queryset(self):
program = get_object_or_404(
PartnerProgram,
pk=self.kwargs["pk"],
draft=False,
solution_storage_mode="legacy_project",
publish_projects_after_finish=True,
datetime_finished__lte=timezone.now(),
)
# The existing publisher can use either source. Never publish as a GET side effect.
projects = (
Project.objects.filter(
Q(program_links__partner_program=program)
| Q(
id__in=PartnerProgramUserProfile.objects.filter(
partner_program=program
).values("project_id")
),
draft=False,
is_public=True,
)
.distinct()
.order_by("pk")
)
search = self.request.query_params.get("search", "").strip()
return projects.filter(name__icontains=search) if search else projects
8 changes: 8 additions & 0 deletions partner_programs/serializers/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ class PartnerProgramBaseSerializerMixin(serializers.ModelSerializer):
pending_expert_invitation_id = serializers.SerializerMethodField()
courses = serializers.SerializerMethodField()
registration_managed = serializers.SerializerMethodField()
published_projects_available = serializers.SerializerMethodField()

def get_published_projects_available(self, program):
from partner_programs.published_projects_api import published_projects_available

return published_projects_available(program)

def get_registration_managed(self, program):
from partner_programs.services.native_registration import config_for
Expand Down Expand Up @@ -252,6 +258,7 @@ class Meta:
"legacy_team_max_size",
"is_user_manager",
"welcome_acknowledged_at",
"published_projects_available",
"is_user_expert",
"solution_storage_mode",
"pending_expert_invitation_id",
Expand Down Expand Up @@ -282,6 +289,7 @@ class Meta:
"datetime_project_submission_ends",
"datetime_evaluation_ends",
"publish_projects_after_finish",
"published_projects_available",
"legacy_team_min_size",
"legacy_team_max_size",
"is_user_manager",
Expand Down
100 changes: 100 additions & 0 deletions partner_programs/tests/test_published_projects_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from django.test import TestCase
from django.utils import timezone
from rest_framework.test import APIClient

from partner_programs.models import PartnerProgramProject
from partner_programs.tests.helpers import (
create_partner_program,
create_project,
create_user,
create_program_member,
)


class PublishedProjectsTests(TestCase):
def setUp(self):
self.client = APIClient()
self.program = create_partner_program(
publish_projects_after_finish=True,
datetime_finished=timezone.now() - timezone.timedelta(days=1),
)
self.user = create_user()
self.public = create_project(leader=self.user, is_public=True, name="Public work")
self.private = create_project(leader=self.user, is_public=False)
self.draft = create_project(leader=self.user, is_public=True, draft=True)
for project in (self.public, self.private, self.draft):
PartnerProgramProject.objects.create(
partner_program=self.program, project=project
)
self.url = f"/programs/{self.program.pk}/published-projects/"

def read(self, path=None, **kwargs):
return self.client.get(path or self.url, secure=True, **kwargs)

def test_anonymous_reader_only_returns_published_non_draft_projects(self):
response = self.read()
self.assertEqual(response.status_code, 200)
self.assertEqual(
[row["id"] for row in response.data["results"]], [self.public.pk]
)
self.assertNotIn("program_data", response.data["results"][0])
self.assertNotIn("scores", response.data["results"][0])
self.private.refresh_from_db()
self.assertFalse(self.private.is_public) # GET does not publish.

def test_member_and_unregistered_detail_expose_server_capability(self):
for registered in (False, True):
with self.subTest(registered=registered):
if registered:
create_program_member(self.program, user=self.user)
self.client.force_authenticate(self.user)
response = self.read(f"/programs/{self.program.pk}/")
self.assertEqual(response.status_code, 200)
self.assertTrue(response.data["published_projects_available"])

def test_flag_future_draft_and_solution_mode_do_not_open_legacy_reader(self):
cases = [
{"publish_projects_after_finish": False},
{"datetime_finished": timezone.now() + timezone.timedelta(days=1)},
{"draft": True},
{"solution_storage_mode": "solution"},
]
for values in cases:
with self.subTest(values=values):
program = create_partner_program(
publish_projects_after_finish=True,
datetime_finished=timezone.now() - timezone.timedelta(days=1),
)
for key, value in values.items():
setattr(program, key, value)
program.save()
self.assertEqual(
self.read(f"/programs/{program.pk}/published-projects/").status_code,
404,
)

def test_profile_source_search_pagination_and_duplicate_links(self):
create_program_member(self.program, user=self.user, project=self.public)
extra = create_project(is_public=True, name="Public second")
create_program_member(self.program, project=extra)
response = self.read(self.url + "?search=Public&limit=1&offset=1")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], 2)
self.assertEqual([row["id"] for row in response.data["results"]], [extra.pk])

def test_private_manager_list_filters_and_exports_keep_permissions(self):
self.client.force_authenticate(self.user)
for suffix in ("projects/", "export-projects/"):
self.assertEqual(
self.read(f"/programs/{self.program.pk}/{suffix}").status_code, 403
)
self.assertEqual(
self.client.post(
f"/programs/{self.program.pk}/projects/filter/",
{},
format="json",
secure=True,
).status_code,
403,
)
self.assertEqual(self.client.post(self.url, {}, secure=True).status_code, 405)
2 changes: 2 additions & 0 deletions partner_programs/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.urls import path
from partner_programs.published_projects_api import PublishedProgramProjectsView

from partner_programs.solution_action_api import SolutionActionView
from partner_programs.solution_case_action_api import SolutionCaseChangeView
Expand Down Expand Up @@ -147,6 +148,7 @@
app_name = "partner_programs"

urlpatterns = [
path("<int:pk>/published-projects/", PublishedProgramProjectsView.as_view()),
path("<int:pk>/published-solutions/", PublishedSolutionListView.as_view()),
path(
"<int:pk>/published-solutions/<int:solution_id>/",
Expand Down
Loading