From 83c30c11c251bd93fa6c1833e15cf67c2c7528d2 Mon Sep 17 00:00:00 2001 From: Toksi Date: Wed, 5 Nov 2025 12:50:03 +0500 Subject: [PATCH 1/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=84=D0=B8=D0=BB=D1=8C=D1=80=D1=82=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8B=D0=B4=D0=B0=D1=87=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BE=D1=81=D1=82=D0=B5=D0=B9,=20=D1=87?= =?UTF-8?q?=D1=82=D0=BE=D0=B1=D1=8B=20=D0=B8=D0=B7=D0=B1=D0=B5=D0=B6=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B1=D0=B8=D1=82=D1=8B=D1=85=20=D1=81=D1=81?= =?UTF-8?q?=D1=8B=D0=BB=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feed/views.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/feed/views.py b/feed/views.py index fab314df..0db7865b 100644 --- a/feed/views.py +++ b/feed/views.py @@ -48,6 +48,17 @@ def get_queryset(self) -> QuerySet[News]: ) .order_by("-datetime_created") ) + + existing_object_filters = { + "project": Project.objects.values_list("id", flat=True), + "vacancy": Vacancy.objects.values_list("id", flat=True), + } + for model_name, ids_queryset in existing_object_filters.items(): + queryset = queryset.exclude( + Q(content_type__model=model_name) + & ~Q(object_id__in=ids_queryset) + ) + return queryset def get(self, *args, **kwargs): From 7257869e717d95753f0404c0e7bf01c6d96148b1 Mon Sep 17 00:00:00 2001 From: Toksi Date: Wed, 5 Nov 2025 15:04:31 +0500 Subject: [PATCH 2/8] =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=BC=D0=B0=D1=85=20=D0=B2=20=D0=BF=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=B0=D1=85,=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B1=D0=B8=D1=82=D1=8B=D1=85=20=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- projects/managers.py | 12 ++++++++++-- projects/serializers.py | 28 ++++++++++++++++++++++------ users/serializers.py | 19 ++++++++++++++++++- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/projects/managers.py b/projects/managers.py index bf701459..a5b9a989 100644 --- a/projects/managers.py +++ b/projects/managers.py @@ -16,10 +16,18 @@ def get_draft_projects_for_user(self): class ProjectManager(Manager): def get_projects_for_list_view(self): - return self.get_queryset().filter(draft=False).prefetch_related("program_links") + return ( + self.get_queryset() + .filter(draft=False) + .prefetch_related("program_links__partner_program") + ) def get_user_projects_for_list_view(self): - return self.get_queryset().distinct() + return ( + self.get_queryset() + .prefetch_related("program_links__partner_program") + .distinct() + ) def get_projects_for_detail_view(self): return ( diff --git a/projects/serializers.py b/projects/serializers.py index 00d10dc9..6b75eed9 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -334,7 +334,7 @@ class Meta: class ProjectListSerializer(serializers.ModelSerializer): views_count = serializers.SerializerMethodField(method_name="count_views") short_description = serializers.SerializerMethodField() - partner_program_id = serializers.SerializerMethodField() + partner_program = serializers.SerializerMethodField() @classmethod def count_views(cls, project): @@ -345,12 +345,23 @@ def get_short_description(cls, project): return project.get_short_description() @staticmethod - def get_partner_program_id(project): + def _get_program_link(project): links_cache = getattr(project, "_prefetched_objects_cache", {}).get( "program_links" ) - link = links_cache[0] if links_cache else project.program_links.first() - return link.partner_program_id if link else None + if links_cache: + return links_cache[0] + return project.program_links.select_related("partner_program").first() + + @classmethod + def get_partner_program(cls, project): + link = cls._get_program_link(project) + if link and link.partner_program: + return { + "id": link.partner_program_id, + "name": link.partner_program.name, + } + return None class Meta: model = Project @@ -363,10 +374,15 @@ class Meta: "industry", "views_count", "is_company", - "partner_program_id", + "partner_program", ] - read_only_fields = ["leader", "views_count", "is_company", "partner_program_id"] + read_only_fields = [ + "leader", + "views_count", + "is_company", + "partner_program", + ] def is_valid(self, *, raise_exception=False): return super().is_valid(raise_exception=raise_exception) diff --git a/users/serializers.py b/users/serializers.py index a210adc9..881356f9 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -940,6 +940,7 @@ class ResendVerifyEmailSerializer(serializers.Serializer): class UserProjectListSerializer(serializers.ModelSerializer[Project]): views_count = serializers.SerializerMethodField(method_name="count_views") short_description = serializers.SerializerMethodField() + partner_program = serializers.SerializerMethodField() @classmethod def count_views(cls, project): @@ -949,6 +950,21 @@ def count_views(cls, project): def get_short_description(cls, project): return project.get_short_description() + @staticmethod + def get_partner_program(project): + links_cache = getattr(project, "_prefetched_objects_cache", {}).get( + "program_links" + ) + link = links_cache[0] if links_cache else project.program_links.select_related( + "partner_program" + ).first() + if link and link.partner_program: + return { + "id": link.partner_program_id, + "name": link.partner_program.name, + } + return None + class Meta: model = Project fields = [ @@ -961,9 +977,10 @@ class Meta: "views_count", "draft", "is_company", + "partner_program", ] - read_only_fields = ["leader", "views_count", "is_company"] + read_only_fields = ["leader", "views_count", "is_company", "partner_program"] def is_valid(self, *, raise_exception=False): return super().is_valid(raise_exception=raise_exception) From 5de22ae3d1f90354519cf5b4f3a083760e6ea378 Mon Sep 17 00:00:00 2001 From: Toksi Date: Thu, 6 Nov 2025 11:44:49 +0500 Subject: [PATCH 3/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC=D0=BC?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- partner_programs/serializers.py | 2 ++ partner_programs/views.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/partner_programs/serializers.py b/partner_programs/serializers.py index 21083e1f..500f7f42 100644 --- a/partner_programs/serializers.py +++ b/partner_programs/serializers.py @@ -29,6 +29,8 @@ def count_views(self, program): return get_views_count(program) def get_short_description(self, program): + if not program.description: + return "" return program.description[:125] def get_is_user_liked(self, obj): diff --git a/partner_programs/views.py b/partner_programs/views.py index 3f99cd77..101d0eb7 100644 --- a/partner_programs/views.py +++ b/partner_programs/views.py @@ -65,6 +65,24 @@ class PartnerProgramList(generics.ListCreateAPIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] pagination_class = PartnerProgramPagination + def get_queryset(self): + base_qs = super().get_queryset() + participating_flag = self.request.query_params.get("participating") + if not participating_flag: + return base_qs + + if not self.request.user.is_authenticated: + return PartnerProgram.objects.none() + + now = timezone.now() + return ( + base_qs.filter( + partner_program_profiles__user=self.request.user, + datetime_finished__gte=now, + ) + .distinct() + ) + class PartnerProgramDetail(generics.RetrieveAPIView): queryset = PartnerProgram.objects.prefetch_related("materials", "managers").all() From f30400ab46530ce1fae58398a2b0de2c9eebbfae Mon Sep 17 00:00:00 2001 From: Toksi Date: Thu, 6 Nov 2025 13:30:25 +0500 Subject: [PATCH 4/8] =?UTF-8?q?=D0=A1=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B0=20=D0=BF=D1=80=D0=B8=D0=B3=D0=BB=D0=B0?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D0=B9=20=D1=81=D0=BE=D0=B3=D0=BB=D0=B0?= =?UTF-8?q?=D1=81=D0=BD=D0=BE=20=D0=B1=D0=B8=D0=B7=D0=BD=D0=B5=D1=81-?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- invites/filters.py | 14 ++++---------- invites/views.py | 25 +++++++++++++++++++++---- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/invites/filters.py b/invites/filters.py index 4ce48ddf..1990480b 100644 --- a/invites/filters.py +++ b/invites/filters.py @@ -19,19 +19,13 @@ class InviteFilter(filters.FilterSet): """ def __init__(self, *args, **kwargs): - """if user filter is not passed, default to request.user""" super().__init__(*args, **kwargs) - if self.data.get("user") is None: - # default filtering by current user - self.data = dict(self.data) - self.data["user"] = kwargs.get("request").user.id - # if user == "any", remove the filter - if self.data.get("user") == "any": - self.data = dict(self.data) - self.data.pop("user") + self.data = dict(self.data) + request = kwargs.get("request") + self.data["user"] = request.user.id if request and request.user.is_authenticated else None project = filters.Filter(method=project_id_filter) class Meta: model = Invite - fields = ("project", "user") + fields = ("project",) diff --git a/invites/views.py b/invites/views.py index 3563c0a6..a365fff3 100644 --- a/invites/views.py +++ b/invites/views.py @@ -9,7 +9,7 @@ class InviteList(generics.ListCreateAPIView): - queryset = Invite.objects.get_invite_for_list_view() + queryset = Invite.objects.get_invite_for_list_view().filter(is_accepted__isnull=True) serializer_class = InviteDetailSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] filter_backends = (filters.DjangoFilterBackend,) @@ -47,13 +47,30 @@ def post(self, request, *args, **kwargs): invite = self.get_object() # type: Invite if invite.user != request.user: return Response(status=status.HTTP_403_FORBIDDEN) + if invite.is_accepted is True: + return Response( + {"detail": "Invite has already been accepted."}, + status=status.HTTP_409_CONFLICT, + ) + if invite.is_accepted is False: + return Response( + {"detail": "Invite has already been declined."}, + status=status.HTTP_409_CONFLICT, + ) # add user to project collaborators - Collaborator.objects.create( + collaborator, created = Collaborator.objects.get_or_create( user=invite.user, project=invite.project, - role=invite.role, - specialization=invite.specialization, + defaults={ + "role": invite.role, + "specialization": invite.specialization, + }, ) + if not created: + return Response( + {"detail": "User is already a collaborator of this project."}, + status=status.HTTP_409_CONFLICT, + ) invite.is_accepted = True invite.save() return Response(status=status.HTTP_200_OK) From 712b42043034d6c16013faf44fa7c9020b31eb9d Mon Sep 17 00:00:00 2001 From: Toksi Date: Thu, 6 Nov 2025 13:43:05 +0500 Subject: [PATCH 5/8] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B2=20?= =?UTF-8?q?=D1=81=D0=B2=D1=8F=D0=B7=D0=B8=20=D1=81=20=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=B8=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- invites/serializers.py | 1 + invites/tests.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invites/serializers.py b/invites/serializers.py index 1152ed73..f9947a96 100644 --- a/invites/serializers.py +++ b/invites/serializers.py @@ -19,6 +19,7 @@ class Meta: "specialization", "is_accepted", ] + read_only_fields = ["is_accepted"] def validate(self, attrs): project = attrs["project"] diff --git a/invites/tests.py b/invites/tests.py index 613fd603..afbef07d 100644 --- a/invites/tests.py +++ b/invites/tests.py @@ -29,7 +29,6 @@ def setUp(self) -> None: "user": None, "motivational_letter": "hello", "role": "Developer", - "is_accepted": False, } self.project_create_data = { @@ -59,7 +58,7 @@ def test_invites_creation(self): ) self.assertEqual(response.data["project"]["id"], create_user["project"]) self.assertEqual(response.data["role"], create_user["role"]) - self.assertEqual(response.data["is_accepted"], create_user["is_accepted"]) + self.assertIsNone(response.data["is_accepted"]) def test_invites_creation_with_empty_text(self): user_main = self._user_create("example@gmail.com") From 67fe217c1851f69bed402991702f39aa83ac5473 Mon Sep 17 00:00:00 2001 From: Toksi Date: Thu, 6 Nov 2025 14:58:36 +0500 Subject: [PATCH 6/8] =?UTF-8?q?=D0=92=D0=B5=D1=80=D0=BD=D1=83=D0=BB=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=8E=D0=B7=D0=B5=D1=80=D1=83,=20=D0=B8=D0=B7-?= =?UTF-8?q?=D0=B7=D0=B0=20=D1=81=D0=BB=D0=BE=D0=BC=D0=B0=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- invites/filters.py | 6 ++++-- users/views.py | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/invites/filters.py b/invites/filters.py index 1990480b..a98f2078 100644 --- a/invites/filters.py +++ b/invites/filters.py @@ -22,10 +22,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.data = dict(self.data) request = kwargs.get("request") - self.data["user"] = request.user.id if request and request.user.is_authenticated else None + if request and request.user.is_authenticated: + self.data["user"] = request.user.id project = filters.Filter(method=project_id_filter) + user = filters.NumberFilter(field_name="user_id") class Meta: model = Invite - fields = ("project",) + fields = ("project", "user") diff --git a/users/views.py b/users/views.py index 0d10eeec..e9223709 100644 --- a/users/views.py +++ b/users/views.py @@ -407,8 +407,13 @@ class UserProjectsList(GenericAPIView): serializer_class = UserProjectListSerializer def get(self, request): - self.queryset = Project.objects.get_user_projects_for_list_view().filter( - Q(leader_id=self.request.user.id) | Q(collaborator__user=self.request.user) + self.queryset = ( + Project.objects.filter( + Q(leader_id=self.request.user.id) + | Q(collaborator__user=self.request.user) + ) + .prefetch_related("program_links__partner_program") + .distinct() ) page = self.paginate_queryset(self.queryset) From 172b6e149ea75beda3425d1c829c658815576ca2 Mon Sep 17 00:00:00 2001 From: Toksi Date: Mon, 10 Nov 2025 12:08:34 +0500 Subject: [PATCH 7/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20ID=20=D1=8D=D0=BA=D1=81=D0=BF=D0=B5=D1=80=D1=82=D0=B0?= =?UTF-8?q?=20=D0=B2=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BE=D0=B1?= =?UTF-8?q?=20=D0=BE=D1=86=D0=B5=D0=BD=D1=91=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project_rates/serializers.py | 2 ++ project_rates/views.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/project_rates/serializers.py b/project_rates/serializers.py index 361a8587..7cc166d0 100644 --- a/project_rates/serializers.py +++ b/project_rates/serializers.py @@ -61,6 +61,7 @@ class ProjectListForRateSerializer(serializers.ModelSerializer): views_count = serializers.SerializerMethodField() criterias = serializers.SerializerMethodField() scored = serializers.SerializerMethodField() + scored_expert_id = serializers.IntegerField(read_only=True, allow_null=True) class Meta: model = Project @@ -75,6 +76,7 @@ class Meta: "region", "views_count", "scored", + "scored_expert_id", "criterias", ] diff --git a/project_rates/views.py b/project_rates/views.py index 1d5574f8..8f719f0a 100644 --- a/project_rates/views.py +++ b/project_rates/views.py @@ -1,5 +1,5 @@ from django.contrib.auth import get_user_model -from django.db.models import QuerySet, Count +from django.db.models import QuerySet, Count, OuterRef, Subquery, IntegerField from rest_framework import generics, status from rest_framework.response import Response @@ -98,6 +98,13 @@ def get_queryset(self) -> QuerySet[Project]: project__isnull=False, partner_program__id=self.kwargs.get("program_id") ).values_list("project__id", flat=True) # `Count` the easiest way to check for rate exist (0 -> does not exist). + scored_expert_subquery = ProjectScore.objects.filter( + project=OuterRef("pk") + ).values("user__expert__id")[:1] + return Project.objects.filter(draft=False, id__in=projects_ids).annotate( - scored=Count("scores") + scored=Count("scores"), + scored_expert_id=Subquery( + scored_expert_subquery, output_field=IntegerField() + ), ) From 8c6a09127e970e7a84e5a173c2c3a76029c7fdf5 Mon Sep 17 00:00:00 2001 From: Toksi Date: Tue, 11 Nov 2025 09:58:44 +0500 Subject: [PATCH 8/8] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0?= =?UTF-8?q?=D1=82=20=D0=BF=D0=BE=D0=BB=D1=8F=20scored=5Fexpert=5Fid,=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=B2=D0=BE=D0=B7=D0=B2?= =?UTF-8?q?=D1=80=D0=B0=D1=89=D0=B0=D0=B5=D1=82=20=D0=BA=D0=BE=D1=80=D1=80?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BD=D1=83=D1=8E=20=D0=B8=D0=BD=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BE=20id=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8F,=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=B2=D1=88=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BE=D1=86=D0=B5=D0=BD=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project_rates/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project_rates/views.py b/project_rates/views.py index 8f719f0a..e77e9103 100644 --- a/project_rates/views.py +++ b/project_rates/views.py @@ -100,7 +100,7 @@ def get_queryset(self) -> QuerySet[Project]: # `Count` the easiest way to check for rate exist (0 -> does not exist). scored_expert_subquery = ProjectScore.objects.filter( project=OuterRef("pk") - ).values("user__expert__id")[:1] + ).values("user_id")[:1] return Project.objects.filter(draft=False, id__in=projects_ids).annotate( scored=Count("scores"),