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
65 changes: 54 additions & 11 deletions projects/signals.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
from django.db.models.signals import post_save
from django.db.models import Q
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.utils import timezone

from chats.models import ProjectChat
from feed.services import delete_news_for_model, create_news_for_model
from projects.models import Collaborator, Project
from vacancy.models import Vacancy


@receiver(pre_save, sender=Project)
def remember_project_draft_state(
sender, instance, raw=False, update_fields=None, **kwargs
):
"""Сохраняет предыдущее состояние draft для обработки реального перехода."""

if raw or instance._state.adding:
instance._previous_draft = None
return
if update_fields is not None and "draft" not in update_fields:
instance._previous_draft = instance.draft
return
instance._previous_draft = (
sender.objects.filter(pk=instance.pk).values_list("draft", flat=True).first()
)


@receiver(post_save, sender=Project)
def create_project(sender, instance, created, **kwargs):
"""
Expand All @@ -23,15 +42,39 @@ def create_project(sender, instance, created, **kwargs):


@receiver(post_save, sender=Project)
def update_vacancy(sender, instance, created, **kwargs):
vacancies = Vacancy.objects.filter(project=instance)
old_values_by_id = dict(vacancies.values_list("id", "is_active"))
vacancies.update(is_active=False if instance.draft else True)

for vacancy in vacancies:
old = old_values_by_id[vacancy.id]
new = vacancy.is_active
if old != new and new is False:
def update_vacancy(sender, instance, created, raw=False, **kwargs):
previous_draft = getattr(instance, "_previous_draft", None)
if raw or created or previous_draft is None or previous_draft == instance.draft:
return

vacancies = list(Vacancy.objects.filter(project=instance))
target_is_active = not instance.draft
changed_vacancies = [
vacancy for vacancy in vacancies if vacancy.is_active != target_is_active
]
now = timezone.now()

if target_is_active:
Vacancy.objects.filter(project=instance).filter(
Q(is_active=False) | Q(datetime_closed__isnull=False)
).update(
is_active=True,
datetime_closed=None,
datetime_updated=now,
)
else:
Vacancy.objects.filter(project=instance).filter(
Q(is_active=True) | Q(datetime_closed__isnull=True)
).update(
is_active=False,
datetime_closed=now,
datetime_updated=now,
)

for vacancy in changed_vacancies:
vacancy.is_active = target_is_active
vacancy.datetime_closed = None if target_is_active else now
if target_is_active is False:
delete_news_for_model(vacancy)
elif old != new and new is True:
else:
create_news_for_model(vacancy)
38 changes: 38 additions & 0 deletions projects/tests/test_project_signals.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import patch

from django.test import TestCase

from chats.models import ProjectChat
Expand All @@ -10,12 +12,14 @@ class ProjectDraftSignalRegressionTests(TestCase):
def test_publish_project_activates_vacancies_creates_feed_news_and_chat(self):
project = create_project(draft=True)
vacancy = create_vacancy(project, is_active=False)
self.assertIsNotNone(vacancy.datetime_closed)

project.draft = False
project.save()

vacancy.refresh_from_db()
self.assertTrue(vacancy.is_active)
self.assertIsNone(vacancy.datetime_closed)
self.assertTrue(News.objects.get_news(vacancy).filter(text="").exists())
self.assertTrue(ProjectChat.objects.filter(project=project).exists())

Expand All @@ -30,8 +34,42 @@ def test_return_project_to_draft_deactivates_vacancies_and_removes_feed_news(sel

vacancy.refresh_from_db()
self.assertFalse(vacancy.is_active)
self.assertIsNotNone(vacancy.datetime_closed)
self.assertFalse(News.objects.get_news(vacancy).filter(text="").exists())

def test_save_published_project_does_not_change_active_vacancy(self):
project = create_project(draft=False)
vacancy = create_vacancy(project, is_active=True)

project.description = "Updated description"
with patch("projects.signals.create_news_for_model") as create_news, patch(
"projects.signals.delete_news_for_model"
) as delete_news:
project.save()

vacancy.refresh_from_db()
self.assertTrue(vacancy.is_active)
self.assertIsNone(vacancy.datetime_closed)
create_news.assert_not_called()
delete_news.assert_not_called()

def test_save_published_project_does_not_reopen_closed_vacancy(self):
project = create_project(draft=False)
vacancy = create_vacancy(project, is_active=False)
closed_at = vacancy.datetime_closed

project.description = "Updated description"
with patch("projects.signals.create_news_for_model") as create_news, patch(
"projects.signals.delete_news_for_model"
) as delete_news:
project.save()

vacancy.refresh_from_db()
self.assertFalse(vacancy.is_active)
self.assertEqual(vacancy.datetime_closed, closed_at)
create_news.assert_not_called()
delete_news.assert_not_called()

def test_repeated_publish_does_not_duplicate_feed_news_or_chat(self):
project = create_project(draft=True)
vacancy = create_vacancy(project, is_active=False)
Expand Down
41 changes: 41 additions & 0 deletions vacancy/tests/test_vacancy_responses_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,47 @@ def test_project_leader_can_accept_response(self, send_email_delay):
)
self.assertEqual(send_email_delay.call_args.args[0]["user_id"], applicant.id)

@patch("vacancy.response_services.send_email.delay")
def test_project_save_does_not_reopen_vacancy_after_response_acceptance(
self, send_email_delay
):
leader = create_user(prefix="leader")
accepted_applicant = create_user(prefix="accepted-applicant")
rejected_applicant = create_user(prefix="rejected-applicant")
project = create_project(leader=leader, draft=False)
vacancy = create_vacancy(project=project, is_active=True, role="Designer")
accepted_response = create_vacancy_response(
user=accepted_applicant,
vacancy=vacancy,
)
rejected_response = create_vacancy_response(
user=rejected_applicant,
vacancy=vacancy,
)
self.client.force_authenticate(leader)

with self.captureOnCommitCallbacks(execute=True):
response = self.client.post(
f"/vacancies/responses/{accepted_response.id}/accept/"
)

accepted_response.refresh_from_db()
rejected_response.refresh_from_db()
vacancy.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(accepted_response.is_approved)
self.assertFalse(rejected_response.is_approved)
self.assertFalse(vacancy.is_active)
self.assertIsNotNone(vacancy.datetime_closed)
closed_at = vacancy.datetime_closed

project.description = "Updated after accepting a candidate"
project.save()

vacancy.refresh_from_db()
self.assertFalse(vacancy.is_active)
self.assertEqual(vacancy.datetime_closed, closed_at)

@patch("vacancy.response_services.send_email.delay")
def test_project_leader_can_decline_response(self, send_email_delay):
leader = create_user(prefix="leader")
Expand Down
Loading