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
16 changes: 10 additions & 6 deletions vacancy/selectors.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand Down
18 changes: 18 additions & 0 deletions vacancy/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,29 @@ 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)

@staticmethod
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)
Expand Down Expand Up @@ -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",
]
Expand Down
49 changes: 49 additions & 0 deletions vacancy/tests/test_vacancy_response_contract_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand All @@ -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):
Expand Down
Loading