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
38 changes: 37 additions & 1 deletion partner_programs/serializers/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from rest_framework import serializers

from core.services import get_likes_count, get_links, get_views_count, is_fan
from .fields import PartnerProgramFieldValueUpdateSerializer
from courses.models import CourseContentStatus
from courses.services.access import resolve_course_availability
from partner_programs.models import (
PartnerProgram,
PartnerProgramField,
Expand All @@ -12,6 +13,8 @@
from projects.models import Project
from projects.validators import validate_project

from .fields import PartnerProgramFieldValueUpdateSerializer

User = get_user_model()


Expand Down Expand Up @@ -90,6 +93,7 @@ class PartnerProgramBaseSerializerMixin(serializers.ModelSerializer):

materials = serializers.SerializerMethodField()
is_user_manager = serializers.SerializerMethodField()
courses = serializers.SerializerMethodField()

def get_materials(self, program: PartnerProgram):
materials = program.materials.all()
Expand All @@ -99,6 +103,36 @@ def get_is_user_manager(self, program: PartnerProgram) -> bool:
user = self.context.get("user")
return bool(user and program.is_manager(user))

def get_courses(self, program: PartnerProgram) -> list[dict]:
user = self.context.get("user")
prefetched_courses = (
getattr(program, "_prefetched_objects_cache", {}).get("courses")
if hasattr(program, "_prefetched_objects_cache")
else None
)
if prefetched_courses is None:
related_courses = program.courses.exclude(
status=CourseContentStatus.DRAFT
).order_by("id")
else:
related_courses = sorted(
(
course
for course in prefetched_courses
if course.status != CourseContentStatus.DRAFT
),
key=lambda course: course.id,
)

return [
{
"id": course.id,
"title": course.title,
"is_available": resolve_course_availability(course, user).is_available,
}
for course in related_courses
]

class Meta:
abstract = True

Expand Down Expand Up @@ -145,6 +179,7 @@ class Meta:
"datetime_evaluation_ends",
"publish_projects_after_finish",
"is_user_manager",
"courses",
)


Expand All @@ -169,6 +204,7 @@ class Meta:
"datetime_evaluation_ends",
"publish_projects_after_finish",
"is_user_manager",
"courses",
)


Expand Down
138 changes: 137 additions & 1 deletion partner_programs/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.utils import timezone
from rest_framework.test import APIRequestFactory, force_authenticate

from courses.models import Course, CourseAccessType, CourseContentStatus
from partner_programs.models import (
PartnerProgram,
PartnerProgramField,
Expand All @@ -11,7 +12,7 @@
)
from partner_programs.serializers import PartnerProgramFieldValueUpdateSerializer
from partner_programs.services import publish_finished_program_projects
from partner_programs.views import PartnerProgramProjectSubmitView
from partner_programs.views import PartnerProgramDetail, PartnerProgramProjectSubmitView
from projects.models import Project


Expand Down Expand Up @@ -412,3 +413,138 @@ def test_file_valid_url(self):
data = {"field_id": field.id, "value_text": "https://example.com/file.pdf"}
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
self.assertTrue(serializer.is_valid())


class PartnerProgramDetailCoursesTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.view = PartnerProgramDetail.as_view()
self.now = timezone.now()

def create_program(self, **overrides):
defaults = {
"name": "Program with courses",
"tag": "program_with_courses",
"description": "Program description",
"city": "Moscow",
"data_schema": {},
"draft": False,
"projects_availability": "all_users",
"datetime_registration_ends": self.now + timezone.timedelta(days=10),
"datetime_started": self.now - timezone.timedelta(days=1),
"datetime_finished": self.now + timezone.timedelta(days=30),
}
defaults.update(overrides)
return PartnerProgram.objects.create(**defaults)

def create_user(self, email: str):
return get_user_model().objects.create_user(
email=email,
password="pass",
first_name="Test",
last_name="User",
birthday="1990-01-01",
)

def create_course(self, program: PartnerProgram, **overrides):
defaults = {
"title": "Program course",
"partner_program": program,
"access_type": CourseAccessType.ALL_USERS,
"status": CourseContentStatus.PUBLISHED,
}
defaults.update(overrides)
return Course.objects.create(**defaults)

def test_detail_includes_related_courses_with_availability_for_member(self):
program = self.create_program()
member = self.create_user("member-program@example.com")
PartnerProgramUserProfile.objects.create(
user=member,
partner_program=program,
project=None,
partner_program_data={},
)
all_users_course = self.create_course(
program,
title="Open course",
access_type=CourseAccessType.ALL_USERS,
)
member_course = self.create_course(
program,
title="Members course",
access_type=CourseAccessType.PROGRAM_MEMBERS,
)
self.create_course(
program,
title="Draft course",
access_type=CourseAccessType.ALL_USERS,
status=CourseContentStatus.DRAFT,
)

request = self.factory.get(f"/programs/{program.id}/")
force_authenticate(request, user=member)
response = self.view(request, pk=program.id)

self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data["courses"],
[
{
"id": all_users_course.id,
"title": "Open course",
"is_available": True,
},
{
"id": member_course.id,
"title": "Members course",
"is_available": True,
},
],
)

def test_detail_includes_empty_courses_list_when_program_has_no_related_courses(self):
program = self.create_program()
user = self.create_user("plain-user@example.com")

request = self.factory.get(f"/programs/{program.id}/")
force_authenticate(request, user=user)
response = self.view(request, pk=program.id)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["courses"], [])

def test_detail_marks_program_only_courses_as_unavailable_for_non_member(self):
program = self.create_program()
outsider = self.create_user("outsider-program@example.com")
open_course = self.create_course(
program,
title="Open course",
access_type=CourseAccessType.ALL_USERS,
)
member_course = self.create_course(
program,
title="Members course",
access_type=CourseAccessType.PROGRAM_MEMBERS,
)

request = self.factory.get(f"/programs/{program.id}/")
force_authenticate(request, user=outsider)
response = self.view(request, pk=program.id)

self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data["courses"],
[
{
"id": open_course.id,
"title": "Open course",
"is_available": True,
},
{
"id": member_course.id,
"title": "Members course",
"is_available": False,
},
],
)
6 changes: 5 additions & 1 deletion partner_programs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ def get_queryset(self):


class PartnerProgramDetail(generics.RetrieveAPIView):
queryset = PartnerProgram.objects.prefetch_related("materials", "managers").all()
queryset = PartnerProgram.objects.prefetch_related(
"materials",
"managers",
"courses",
).all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
serializer_class = PartnerProgramForUnregisteredUserSerializer

Expand Down
Loading