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
20 changes: 8 additions & 12 deletions vacancy/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@


def project_id_filter(queryset, name, value) -> QuerySet:
return queryset.filter(
**{
"project_id": value[0],
}
)
return queryset.filter(project_id=value)


class VacancyFilter(filters.FilterSet):
Expand Down Expand Up @@ -44,7 +40,7 @@ def __init__(self, *args, **kwargs):
"""if is_active filter is not passed, default to True"""
super().__init__(*args, **kwargs)
if self.data.get("is_active") is None:
self.data = dict(self.data)
self.data = self.data.copy()
self.data["is_active"] = True

def filter_by_experience(
Expand All @@ -69,33 +65,33 @@ def filter_by_format(
)

def filter_by_salary_min(
self, queryset: QuerySet[Vacancy], name, value: list[str]
self, queryset: QuerySet[Vacancy], name, value: str
) -> QuerySet[Vacancy]:
try:
min_salary = int(value[0])
min_salary = int(value)
return queryset.filter(Q(salary__gte=min_salary) | Q(salary=None)).order_by(
F("salary").asc(nulls_last=True)
)
except ValueError:
return queryset

def filter_by_salary_max(
self, queryset: QuerySet[Vacancy], name, value: list[str]
self, queryset: QuerySet[Vacancy], name, value: str
) -> QuerySet[Vacancy]:
try:
max_salary = int(value[0])
max_salary = int(value)
return queryset.filter(Q(salary__lte=max_salary) | Q(salary=None)).order_by(
F("salary").asc(nulls_last=True)
)
except ValueError:
return queryset

def filter_by_role(
self, queryset: QuerySet[Vacancy], name, value: list[str]
self, queryset: QuerySet[Vacancy], name, value: str
) -> QuerySet[Vacancy]:
if not value:
return queryset
return queryset.filter(role__icontains=value[0])
return queryset.filter(role__icontains=value)

project_id = filters.Filter(method=project_id_filter)
is_active = filters.BooleanFilter(field_name="is_active")
Expand Down
1 change: 1 addition & 0 deletions vacancy/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def get_vacancy_for_list_view(self):
"project__id",
"is_active",
"datetime_created",
"datetime_updated",
)
)

Expand Down
7 changes: 7 additions & 0 deletions vacancy/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,12 +365,19 @@ class Meta:
"datetime_closed",
"response_count",
"date_create_time",
"datetime_created",
"datetime_updated",
"required_experience",
"work_schedule",
"work_format",
"salary",
"city",
]
read_only_fields = [
"date_create_time",
"datetime_created",
"datetime_updated",
]


class VacancyResponseListSerializer(serializers.ModelSerializer):
Expand Down
126 changes: 122 additions & 4 deletions vacancy/tests/test_vacancies_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.test import TestCase
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from rest_framework import status
from rest_framework.test import APIClient

Expand Down Expand Up @@ -156,18 +157,97 @@ def test_public_list_returns_only_active_vacancies_by_default(self):
[active_vacancy.id],
)

def test_list_can_include_inactive_vacancies_by_filter(self):
create_vacancy(role="Active vacancy", is_active=True)
inactive_vacancy = create_vacancy(role="Inactive vacancy", is_active=False)
def test_project_leader_can_list_inactive_vacancies_by_project(self):
leader = create_user(prefix="inactive-vacancy-leader")
project = create_project(leader=leader, draft=True)
while project.id < 10:
project = create_project(leader=leader, draft=True)
inactive_vacancy = create_vacancy(
project=project,
role="Inactive vacancy",
is_active=False,
)
self.client.force_authenticate(leader)

response = self.client.get("/vacancies/", {"is_active": "false"})
response = self.client.get(
"/vacancies/",
{"project_id": str(project.id), "is_active": "false"},
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["id"] for item in response.data["results"]],
[inactive_vacancy.id],
)

def test_outsider_does_not_see_draft_or_private_project_vacancies(self):
outsider = create_user(prefix="catalog-outsider")
draft_project = create_project(draft=True)
private_project = create_project(is_public=False)
public_vacancy = create_vacancy(role="Public project vacancy")
create_vacancy(
project=draft_project,
role="Draft project vacancy",
is_active=False,
)
create_vacancy(project=private_project, role="Private project vacancy")
self.client.force_authenticate(outsider)

response = self.client.get("/vacancies/")
inactive_response = self.client.get("/vacancies/", {"is_active": "false"})

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["id"] for item in response.data["results"]],
[public_vacancy.id],
)
self.assertEqual(inactive_response.status_code, status.HTTP_200_OK)
self.assertEqual(inactive_response.data["results"], [])

def test_anonymous_does_not_see_closed_project_vacancies(self):
draft_project = create_project(draft=True)
private_project = create_project(is_public=False)
public_vacancy = create_vacancy(role="Public project vacancy")
create_vacancy(project=draft_project, role="Draft project vacancy")
create_vacancy(project=private_project, role="Private project vacancy")
create_vacancy(role="Inactive vacancy", is_active=False)

response = self.client.get("/vacancies/")
inactive_response = self.client.get("/vacancies/", {"is_active": "false"})

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["id"] for item in response.data["results"]],
[public_vacancy.id],
)
self.assertEqual(inactive_response.status_code, status.HTTP_200_OK)
self.assertEqual(inactive_response.data["results"], [])

def test_outsider_cannot_use_project_filter_to_disclose_closed_project(self):
outsider = create_user(prefix="project-filter-outsider")
draft_project = create_project(draft=True)
private_project = create_project(is_public=False)
create_vacancy(
project=draft_project,
role="Draft project vacancy",
is_active=False,
)
create_vacancy(project=private_project, role="Private project vacancy")
self.client.force_authenticate(outsider)

draft_response = self.client.get(
"/vacancies/",
{"project_id": str(draft_project.id), "is_active": "false"},
)
private_response = self.client.get(
"/vacancies/", {"project_id": str(private_project.id)}
)

self.assertEqual(draft_response.status_code, status.HTTP_200_OK)
self.assertEqual(draft_response.data["results"], [])
self.assertEqual(private_response.status_code, status.HTTP_200_OK)
self.assertEqual(private_response.data["results"], [])

def test_list_filters_by_project_role_salary_and_work_conditions(self):
project = create_project(name="Target project")
target = create_vacancy(
Expand Down Expand Up @@ -205,6 +285,36 @@ def test_list_excludes_vacancies_older_than_90_days(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual([item["id"] for item in response.data["results"]], [fresh.id])

def test_list_returns_real_vacancy_timestamps(self):
now = timezone.now()
older = create_vacancy(
role="Older vacancy",
datetime_created=now - timedelta(days=2),
)
newer = create_vacancy(
role="Newer vacancy",
datetime_created=now - timedelta(days=1),
)

response = self.client.get("/vacancies/")

self.assertEqual(response.status_code, status.HTTP_200_OK)
vacancies = {item["id"]: item for item in response.data["results"]}
self.assertEqual(
parse_datetime(vacancies[older.id]["datetime_created"]),
older.datetime_created,
)
self.assertEqual(
parse_datetime(vacancies[newer.id]["datetime_created"]),
newer.datetime_created,
)
self.assertNotEqual(
vacancies[older.id]["datetime_created"],
vacancies[newer.id]["datetime_created"],
)
self.assertIn("datetime_updated", vacancies[older.id])
self.assertIn("date_create_time", vacancies[older.id])

def test_detail_returns_vacancy_with_project_info(self):
vacancy = create_vacancy(role="Detail vacancy", city=None)

Expand All @@ -215,6 +325,14 @@ def test_detail_returns_vacancy_with_project_info(self):
self.assertEqual(response.data["role"], "Detail vacancy")
self.assertEqual(response.data["project"]["id"], vacancy.project.id)
self.assertIsNone(response.data["city"])
self.assertEqual(
parse_datetime(response.data["datetime_created"]),
vacancy.datetime_created,
)
self.assertEqual(
parse_datetime(response.data["datetime_updated"]),
vacancy.datetime_updated,
)

def test_patch_validates_city_against_final_work_format(self):
leader = create_user(prefix="patch-city-leader")
Expand Down
23 changes: 22 additions & 1 deletion vacancy/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.db import transaction
from django.db.models import QuerySet
from django.db.models import Q, QuerySet
from django.http import Http404
from django_filters import rest_framework as filters
from django.shortcuts import get_object_or_404
Expand Down Expand Up @@ -50,6 +50,27 @@ class VacancyList(generics.ListCreateAPIView):
filterset_class = VacancyFilter
pagination_class = VacancyPagination

def get_queryset(self):
"""Закрытые вакансии доступны только менеджеру запрошенного проекта."""

queryset = super().get_queryset()
public_catalog = Q(
is_active=True,
project__draft=False,
project__is_public=True,
)
project_id = self.request.query_params.get("project_id")
user = self.request.user

if project_id and user.is_authenticated:
if user.is_staff or user.is_superuser:
return queryset
return queryset.filter(
public_catalog | Q(project_id=project_id, project__leader_id=user.id)
)

return queryset.filter(public_catalog)


class VacancyDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Vacancy.objects.get_vacancy_for_detail_view()
Expand Down
Loading