From def036041cb5ad808637da179f37e047d10423d4 Mon Sep 17 00:00:00 2001 From: Toksi Date: Fri, 16 Jan 2026 11:00:35 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=B5=D0=B6=D0=B5=D0=B4=D0=BD=D0=B5?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0=D1=8F=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BF=D1=83?= =?UTF-8?q?=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D0=BF=D0=BE=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- partner_programs/services.py | 30 +++++++++ partner_programs/tasks.py | 13 ++++ partner_programs/tests.py | 121 ++++++++++++++++++++++++++++++++++- procollab/celery.py | 6 +- procollab/settings.py | 1 + 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 partner_programs/tasks.py diff --git a/partner_programs/services.py b/partner_programs/services.py index 9beb5e57..b75126b9 100644 --- a/partner_programs/services.py +++ b/partner_programs/services.py @@ -3,18 +3,48 @@ from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE from django.db.models import Prefetch +from django.utils import timezone from partner_programs.models import ( + PartnerProgram, PartnerProgramField, PartnerProgramFieldValue, PartnerProgramProject, PartnerProgramUserProfile, ) from project_rates.models import Criteria, ProjectScore +from projects.models import Project logger = logging.getLogger() +def publish_finished_program_projects(now=None) -> int: + if now is None: + now = timezone.now() + + program_ids = PartnerProgram.objects.filter( + publish_projects_after_finish=True, + datetime_finished__lte=now, + ).values_list("id", flat=True) + if not program_ids.exists(): + return 0 + + link_project_ids = PartnerProgramProject.objects.filter( + partner_program_id__in=program_ids + ).values_list("project_id", flat=True) + profile_project_ids = PartnerProgramUserProfile.objects.filter( + partner_program_id__in=program_ids, + project_id__isnull=False, + ).values_list("project_id", flat=True) + project_ids = link_project_ids.union(profile_project_ids) + + return Project.objects.filter( + id__in=project_ids, + is_public=False, + draft=False, + ).update(is_public=True) + + class ProjectScoreDataPreparer: """ Data preparer about project_rates by experts. diff --git a/partner_programs/tasks.py b/partner_programs/tasks.py new file mode 100644 index 00000000..39baf11c --- /dev/null +++ b/partner_programs/tasks.py @@ -0,0 +1,13 @@ +import logging + +from procollab.celery import app +from partner_programs.services import publish_finished_program_projects + +logger = logging.getLogger(__name__) + + +@app.task +def publish_finished_program_projects_task() -> int: + updated_count = publish_finished_program_projects() + logger.info("Published %s program projects after finish", updated_count) + return updated_count diff --git a/partner_programs/tests.py b/partner_programs/tests.py index d4804970..0fb33174 100644 --- a/partner_programs/tests.py +++ b/partner_programs/tests.py @@ -1,8 +1,16 @@ +from django.contrib.auth import get_user_model from django.test import TestCase from django.utils import timezone -from partner_programs.models import PartnerProgram, PartnerProgramField +from partner_programs.models import ( + PartnerProgram, + PartnerProgramField, + PartnerProgramProject, + PartnerProgramUserProfile, +) from partner_programs.serializers import PartnerProgramFieldValueUpdateSerializer +from partner_programs.services import publish_finished_program_projects +from projects.models import Project class PartnerProgramFieldValueUpdateSerializerInvalidTests(TestCase): @@ -118,6 +126,117 @@ def test_file_empty_required(self): self.assertIn("Файл обязателен для этого поля.", str(serializer.errors)) +class PublishFinishedProgramProjectsTests(TestCase): + def setUp(self): + self.now = timezone.now() + self.user = get_user_model().objects.create_user( + email="user@example.com", + password="pass", + first_name="User", + last_name="Test", + birthday="1990-01-01", + ) + + def create_program(self, **overrides): + defaults = { + "name": "Program", + "tag": "program_tag", + "description": "Program description", + "city": "Moscow", + "image_address": "https://example.com/image.png", + "cover_image_address": "https://example.com/cover.png", + "advertisement_image_address": "https://example.com/advertisement.png", + "presentation_address": "https://example.com/presentation.pdf", + "data_schema": {}, + "draft": False, + "projects_availability": "all_users", + "datetime_registration_ends": self.now - timezone.timedelta(days=5), + "datetime_started": self.now - timezone.timedelta(days=30), + "datetime_finished": self.now - timezone.timedelta(days=1), + } + defaults.update(overrides) + return PartnerProgram.objects.create(**defaults) + + def create_project(self, **overrides): + defaults = { + "leader": self.user, + "draft": False, + "is_public": False, + "name": "Project", + } + defaults.update(overrides) + return Project.objects.create(**defaults) + + def test_publish_updates_projects_from_both_sources(self): + program = self.create_program(publish_projects_after_finish=True) + + link_project = self.create_project(name="Linked Project") + PartnerProgramProject.objects.create( + partner_program=program, + project=link_project, + ) + + profile_project = self.create_project(name="Profile Project") + PartnerProgramUserProfile.objects.create( + user=self.user, + partner_program=program, + project=profile_project, + partner_program_data={}, + ) + + publish_finished_program_projects() + + link_project.refresh_from_db() + profile_project.refresh_from_db() + self.assertTrue(link_project.is_public) + self.assertTrue(profile_project.is_public) + + def test_publish_skips_draft_projects(self): + program = self.create_program(publish_projects_after_finish=True) + draft_project = self.create_project(draft=True, name="Draft Project") + PartnerProgramProject.objects.create( + partner_program=program, + project=draft_project, + ) + + publish_finished_program_projects() + + draft_project.refresh_from_db() + self.assertFalse(draft_project.is_public) + + def test_publish_skips_when_flag_false(self): + program = self.create_program(publish_projects_after_finish=False) + project = self.create_project(name="Private Project") + PartnerProgramProject.objects.create( + partner_program=program, + project=project, + ) + + publish_finished_program_projects() + + project.refresh_from_db() + self.assertFalse(project.is_public) + + def test_publish_after_flag_enabled_post_finish(self): + program = self.create_program(publish_projects_after_finish=False) + project = self.create_project(name="Delayed Project") + PartnerProgramProject.objects.create( + partner_program=program, + project=project, + ) + + publish_finished_program_projects() + project.refresh_from_db() + self.assertFalse(project.is_public) + + program.publish_projects_after_finish = True + program.save(update_fields=["publish_projects_after_finish"]) + + publish_finished_program_projects() + project.refresh_from_db() + self.assertTrue(project.is_public) + + class PartnerProgramFieldValueUpdateSerializerValidTests(TestCase): def setUp(self): now = timezone.now() diff --git a/procollab/celery.py b/procollab/celery.py index 882377a2..a2b9a531 100644 --- a/procollab/celery.py +++ b/procollab/celery.py @@ -17,7 +17,11 @@ "task": "vacancy.tasks.email_notificate_vacancy_outdated", # "schedule": crontab(minute=0, hour=0), "schedule": crontab(minute="*"), - } + }, + "publish_finished_program_projects": { + "task": "partner_programs.tasks.publish_finished_program_projects_task", + "schedule": crontab(minute=0, hour=6), + }, } if __name__ == "__main__": diff --git a/procollab/settings.py b/procollab/settings.py index a5020e37..5a0316b3 100644 --- a/procollab/settings.py +++ b/procollab/settings.py @@ -418,3 +418,4 @@ CELERY_ACCEPT_CONTENT = ["application/json"] CELERY_RESULT_SERIALIZER = "json" CELERY_TASK_SERIALIZER = "json" +CELERY_TIMEZONE = "Europe/Moscow"