diff --git a/vacancy/selectors.py b/vacancy/selectors.py index b735e62b..899b5c8d 100644 --- a/vacancy/selectors.py +++ b/vacancy/selectors.py @@ -1,4 +1,4 @@ -from django.db.models import Count, Exists, OuterRef, Prefetch, Q, QuerySet +from django.db.models import Count, Exists, OuterRef, Prefetch, Q, QuerySet, Subquery from core.models import SkillToObject from projects.models import Collaborator, Project @@ -68,12 +68,16 @@ def with_applicant_state(queryset: QuerySet[Vacancy], user) -> QuerySet[Vacancy] if not user or not user.is_authenticated: return queryset + + current_user_response = VacancyResponse.objects.filter( + vacancy_id=OuterRef("pk"), + user_id=user.id, + ).order_by("-datetime_created", "-pk") + return queryset.annotate( - current_user_has_responded=Exists( - VacancyResponse.objects.filter( - vacancy_id=OuterRef("pk"), - user_id=user.id, - ) + current_user_has_responded=Exists(current_user_response), + current_user_response_is_approved=Subquery( + current_user_response.values("is_approved")[:1] ), current_user_is_collaborator=Exists( Collaborator.objects.filter( diff --git a/vacancy/serializers.py b/vacancy/serializers.py index 1866cda4..183ee20a 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -180,6 +180,7 @@ class VacancyDetailSerializer( ): project = ProjectForVacancySerializer(many=False, read_only=True) has_responded = serializers.SerializerMethodField(read_only=True) + response_status = serializers.SerializerMethodField(read_only=True) can_respond = serializers.SerializerMethodField(read_only=True) can_manage_responses = serializers.SerializerMethodField(read_only=True) @@ -187,6 +188,21 @@ class VacancyDetailSerializer( def get_has_responded(vacancy: Vacancy) -> bool: return bool(getattr(vacancy, "current_user_has_responded", False)) + def get_response_status(self, vacancy: Vacancy) -> str | None: + if not self.get_has_responded(vacancy): + return None + + is_approved = getattr( + vacancy, + "current_user_response_is_approved", + None, + ) + if is_approved is True: + return "accepted" + if is_approved is False: + return "rejected" + return "pending" + def get_can_respond(self, vacancy: Vacancy) -> bool: request = self.context.get("request") user = getattr(request, "user", None) @@ -236,12 +252,14 @@ class Meta: "salary", "city", "has_responded", + "response_status", "can_respond", "can_manage_responses", ] read_only_fields = [ "project", "has_responded", + "response_status", "can_respond", "can_manage_responses", ] diff --git a/vacancy/tests/test_vacancy_response_contract_api.py b/vacancy/tests/test_vacancy_response_contract_api.py index f30e083c..e471c3b3 100644 --- a/vacancy/tests/test_vacancy_response_contract_api.py +++ b/vacancy/tests/test_vacancy_response_contract_api.py @@ -316,17 +316,40 @@ def test_outsider_state_changes_after_response(self): before = self.get_detail(outsider) self.assertEqual(before.status_code, status.HTTP_200_OK) self.assertFalse(before.data["has_responded"]) + self.assertIsNone(before.data["response_status"]) self.assertTrue(before.data["can_respond"]) self.assertFalse(before.data["can_manage_responses"]) create_vacancy_response(user=outsider, vacancy=self.vacancy) after = self.get_detail(outsider) self.assertTrue(after.data["has_responded"]) + self.assertEqual(after.data["response_status"], "pending") self.assertFalse(after.data["can_respond"]) + def test_response_status_maps_processed_responses(self): + for is_approved, expected_status in ( + (True, "accepted"), + (False, "rejected"), + ): + with self.subTest(is_approved=is_approved): + applicant = create_user(prefix=f"applicant-{expected_status}") + create_vacancy_response( + user=applicant, + vacancy=self.vacancy, + is_approved=is_approved, + ) + + response = self.get_detail(applicant) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.data["has_responded"]) + self.assertEqual(response.data["response_status"], expected_status) + self.assertFalse(response.data["can_respond"]) + def test_leader_and_collaborator_states(self): leader_response = self.get_detail(self.leader) self.assertFalse(leader_response.data["has_responded"]) + self.assertIsNone(leader_response.data["response_status"]) self.assertFalse(leader_response.data["can_respond"]) self.assertTrue(leader_response.data["can_manage_responses"]) @@ -348,9 +371,35 @@ def test_closed_and_anonymous_states(self): anonymous_response = self.get_detail(None) self.assertFalse(anonymous_response.data["has_responded"]) + self.assertIsNone(anonymous_response.data["response_status"]) self.assertFalse(anonymous_response.data["can_respond"]) self.assertFalse(anonymous_response.data["can_manage_responses"]) + def test_response_status_does_not_add_separate_detail_query(self): + applicant = create_user(prefix="query-count-applicant") + create_vacancy_response(user=applicant, vacancy=self.vacancy) + self.client.force_authenticate(applicant) + + with CaptureQueriesContext(connection) as queries: + response = self.client.get(f"/vacancies/{self.vacancy.id}/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + response_table = VacancyResponse._meta.db_table + response_queries = [ + query["sql"] + for query in queries.captured_queries + if response_table in query["sql"] + ] + self.assertEqual(len(response_queries), 2) + self.assertEqual( + sum( + 'AS "current_user_response_is_approved"' in query + for query in response_queries + ), + 1, + ) + self.assertEqual(response.data["response_status"], "pending") + class VacancyResponseDecisionContractTests(TestCase): def setUp(self):