Skip to content

Commit 2aeebe5

Browse files
committed
feat(partner_programs): добавлен список связанных курсов с id, title и is_available в detail-ответ программы
1 parent 5728f8a commit 2aeebe5

File tree

3 files changed

+179
-3
lines changed

3 files changed

+179
-3
lines changed

partner_programs/serializers/programs.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from rest_framework import serializers
33

44
from core.services import get_likes_count, get_links, get_views_count, is_fan
5-
from .fields import PartnerProgramFieldValueUpdateSerializer
5+
from courses.models import CourseContentStatus
6+
from courses.services.access import resolve_course_availability
67
from partner_programs.models import (
78
PartnerProgram,
89
PartnerProgramField,
@@ -12,6 +13,8 @@
1213
from projects.models import Project
1314
from projects.validators import validate_project
1415

16+
from .fields import PartnerProgramFieldValueUpdateSerializer
17+
1518
User = get_user_model()
1619

1720

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

9194
materials = serializers.SerializerMethodField()
9295
is_user_manager = serializers.SerializerMethodField()
96+
courses = serializers.SerializerMethodField()
9397

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

106+
def get_courses(self, program: PartnerProgram) -> list[dict]:
107+
user = self.context.get("user")
108+
prefetched_courses = (
109+
getattr(program, "_prefetched_objects_cache", {}).get("courses")
110+
if hasattr(program, "_prefetched_objects_cache")
111+
else None
112+
)
113+
if prefetched_courses is None:
114+
related_courses = program.courses.exclude(
115+
status=CourseContentStatus.DRAFT
116+
).order_by("id")
117+
else:
118+
related_courses = sorted(
119+
(
120+
course
121+
for course in prefetched_courses
122+
if course.status != CourseContentStatus.DRAFT
123+
),
124+
key=lambda course: course.id,
125+
)
126+
127+
return [
128+
{
129+
"id": course.id,
130+
"title": course.title,
131+
"is_available": resolve_course_availability(course, user).is_available,
132+
}
133+
for course in related_courses
134+
]
135+
102136
class Meta:
103137
abstract = True
104138

@@ -145,6 +179,7 @@ class Meta:
145179
"datetime_evaluation_ends",
146180
"publish_projects_after_finish",
147181
"is_user_manager",
182+
"courses",
148183
)
149184

150185

@@ -169,6 +204,7 @@ class Meta:
169204
"datetime_evaluation_ends",
170205
"publish_projects_after_finish",
171206
"is_user_manager",
207+
"courses",
172208
)
173209

174210

partner_programs/tests.py

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.utils import timezone
44
from rest_framework.test import APIRequestFactory, force_authenticate
55

6+
from courses.models import Course, CourseAccessType, CourseContentStatus
67
from partner_programs.models import (
78
PartnerProgram,
89
PartnerProgramField,
@@ -11,7 +12,7 @@
1112
)
1213
from partner_programs.serializers import PartnerProgramFieldValueUpdateSerializer
1314
from partner_programs.services import publish_finished_program_projects
14-
from partner_programs.views import PartnerProgramProjectSubmitView
15+
from partner_programs.views import PartnerProgramDetail, PartnerProgramProjectSubmitView
1516
from projects.models import Project
1617

1718

@@ -412,3 +413,138 @@ def test_file_valid_url(self):
412413
data = {"field_id": field.id, "value_text": "https://example.com/file.pdf"}
413414
serializer = PartnerProgramFieldValueUpdateSerializer(data=data)
414415
self.assertTrue(serializer.is_valid())
416+
417+
418+
class PartnerProgramDetailCoursesTests(TestCase):
419+
def setUp(self):
420+
self.factory = APIRequestFactory()
421+
self.view = PartnerProgramDetail.as_view()
422+
self.now = timezone.now()
423+
424+
def create_program(self, **overrides):
425+
defaults = {
426+
"name": "Program with courses",
427+
"tag": "program_with_courses",
428+
"description": "Program description",
429+
"city": "Moscow",
430+
"data_schema": {},
431+
"draft": False,
432+
"projects_availability": "all_users",
433+
"datetime_registration_ends": self.now + timezone.timedelta(days=10),
434+
"datetime_started": self.now - timezone.timedelta(days=1),
435+
"datetime_finished": self.now + timezone.timedelta(days=30),
436+
}
437+
defaults.update(overrides)
438+
return PartnerProgram.objects.create(**defaults)
439+
440+
def create_user(self, email: str):
441+
return get_user_model().objects.create_user(
442+
email=email,
443+
password="pass",
444+
first_name="Test",
445+
last_name="User",
446+
birthday="1990-01-01",
447+
)
448+
449+
def create_course(self, program: PartnerProgram, **overrides):
450+
defaults = {
451+
"title": "Program course",
452+
"partner_program": program,
453+
"access_type": CourseAccessType.ALL_USERS,
454+
"status": CourseContentStatus.PUBLISHED,
455+
}
456+
defaults.update(overrides)
457+
return Course.objects.create(**defaults)
458+
459+
def test_detail_includes_related_courses_with_availability_for_member(self):
460+
program = self.create_program()
461+
member = self.create_user("member-program@example.com")
462+
PartnerProgramUserProfile.objects.create(
463+
user=member,
464+
partner_program=program,
465+
project=None,
466+
partner_program_data={},
467+
)
468+
all_users_course = self.create_course(
469+
program,
470+
title="Open course",
471+
access_type=CourseAccessType.ALL_USERS,
472+
)
473+
member_course = self.create_course(
474+
program,
475+
title="Members course",
476+
access_type=CourseAccessType.PROGRAM_MEMBERS,
477+
)
478+
self.create_course(
479+
program,
480+
title="Draft course",
481+
access_type=CourseAccessType.ALL_USERS,
482+
status=CourseContentStatus.DRAFT,
483+
)
484+
485+
request = self.factory.get(f"/programs/{program.id}/")
486+
force_authenticate(request, user=member)
487+
response = self.view(request, pk=program.id)
488+
489+
self.assertEqual(response.status_code, 200)
490+
self.assertEqual(
491+
response.data["courses"],
492+
[
493+
{
494+
"id": all_users_course.id,
495+
"title": "Open course",
496+
"is_available": True,
497+
},
498+
{
499+
"id": member_course.id,
500+
"title": "Members course",
501+
"is_available": True,
502+
},
503+
],
504+
)
505+
506+
def test_detail_includes_empty_courses_list_when_program_has_no_related_courses(self):
507+
program = self.create_program()
508+
user = self.create_user("plain-user@example.com")
509+
510+
request = self.factory.get(f"/programs/{program.id}/")
511+
force_authenticate(request, user=user)
512+
response = self.view(request, pk=program.id)
513+
514+
self.assertEqual(response.status_code, 200)
515+
self.assertEqual(response.data["courses"], [])
516+
517+
def test_detail_marks_program_only_courses_as_unavailable_for_non_member(self):
518+
program = self.create_program()
519+
outsider = self.create_user("outsider-program@example.com")
520+
open_course = self.create_course(
521+
program,
522+
title="Open course",
523+
access_type=CourseAccessType.ALL_USERS,
524+
)
525+
member_course = self.create_course(
526+
program,
527+
title="Members course",
528+
access_type=CourseAccessType.PROGRAM_MEMBERS,
529+
)
530+
531+
request = self.factory.get(f"/programs/{program.id}/")
532+
force_authenticate(request, user=outsider)
533+
response = self.view(request, pk=program.id)
534+
535+
self.assertEqual(response.status_code, 200)
536+
self.assertEqual(
537+
response.data["courses"],
538+
[
539+
{
540+
"id": open_course.id,
541+
"title": "Open course",
542+
"is_available": True,
543+
},
544+
{
545+
"id": member_course.id,
546+
"title": "Members course",
547+
"is_available": False,
548+
},
549+
],
550+
)

partner_programs/views.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ def get_queryset(self):
9898

9999

100100
class PartnerProgramDetail(generics.RetrieveAPIView):
101-
queryset = PartnerProgram.objects.prefetch_related("materials", "managers").all()
101+
queryset = PartnerProgram.objects.prefetch_related(
102+
"materials",
103+
"managers",
104+
"courses",
105+
).all()
102106
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
103107
serializer_class = PartnerProgramForUnregisteredUserSerializer
104108

0 commit comments

Comments
 (0)