Skip to content
2 changes: 1 addition & 1 deletion invites/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

class InviteManager(Manager):
def get_invite_for_list_view(self):
return self.get_queryset().select_related("project", "user")
return self.get_queryset().select_related("project", "project__leader", "user")
15 changes: 15 additions & 0 deletions invites/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,22 @@
from invites.models import Invite
from projects.models import Collaborator
from projects.serializers import ProjectListSerializer
from users.models import CustomUser
from users.serializers import UserDetailSerializer


class InviteSenderSerializer(serializers.ModelSerializer[CustomUser]):
class Meta:
model = CustomUser
fields = [
"id",
"first_name",
"last_name",
"patronymic",
"avatar",
]


class InviteListSerializer(serializers.ModelSerializer[Invite]):
class Meta:
model = Invite
Expand Down Expand Up @@ -67,6 +80,7 @@ def validate(self, attrs):
class InviteDetailSerializer(serializers.ModelSerializer[Invite]):
user = UserDetailSerializer(many=False, read_only=True)
project = ProjectListSerializer(many=False, read_only=True)
sender = InviteSenderSerializer(source="project.leader", read_only=True)
specialization = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
Expand All @@ -77,6 +91,7 @@ class Meta:
"id",
"project",
"user",
"sender",
"motivational_letter",
"role",
"specialization",
Expand Down
51 changes: 50 additions & 1 deletion invites/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from datetime import timedelta

from django.test import TestCase
from django.utils import timezone
from rest_framework.test import APIRequestFactory, force_authenticate

from projects.models import Project
from partner_programs.models import PartnerProgram, PartnerProgramProject
from projects.models import Collaborator, Project
from tests.constants import USER_CREATE_DATA

from users.views import UserList
Expand Down Expand Up @@ -112,6 +116,51 @@ def test_invites_creation_with_empty_data(self):

self.assertEqual(response.status_code, 400)

def test_invites_creation_for_existing_collaborator(self):
sender = self._user_create("sender@example.com")
recipient = self._user_create("recipient@example.com")
project = self._project_create(sender)
Collaborator.objects.create(user=recipient, project=project, role="Developer")

create_user = self.invite_create_data.copy()
create_user["user"] = recipient.id
create_user["project"] = project.id
request = self.factory.post("invites/", create_user, format="json")
force_authenticate(request, user=sender)

response = self.invite_list_view(request)

self.assertEqual(response.status_code, 400)
self.assertIn("user", response.data)

def test_invites_creation_for_non_program_member(self):
sender = self._user_create("sender@example.com")
recipient = self._user_create("recipient@example.com")
project = self._project_create(sender)
now = timezone.now()
program = PartnerProgram.objects.create(
name="Test program",
tag="test",
city="Moscow",
datetime_registration_ends=now + timedelta(days=1),
datetime_started=now,
datetime_finished=now + timedelta(days=30),
)
PartnerProgramProject.objects.create(
partner_program=program, project=project
)

create_user = self.invite_create_data.copy()
create_user["user"] = recipient.id
create_user["project"] = project.id
request = self.factory.post("invites/", create_user, format="json")
force_authenticate(request, user=sender)

response = self.invite_list_view(request)

self.assertEqual(response.status_code, 400)
self.assertIn("user", response.data)

def test_accept_invite_by_intended_user(self):
sender = self._user_create("sender@example.com")
recipient = self._user_create("recipient@example.com")
Expand Down
30 changes: 30 additions & 0 deletions partner_programs/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions partner_programs/tasks.py
Original file line number Diff line number Diff line change
@@ -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
121 changes: 120 additions & 1 deletion partner_programs/tests.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 5 additions & 1 deletion procollab/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down
1 change: 1 addition & 0 deletions procollab/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,3 +418,4 @@
CELERY_ACCEPT_CONTENT = ["application/json"]
CELERY_RESULT_SERIALIZER = "json"
CELERY_TASK_SERIALIZER = "json"
CELERY_TIMEZONE = "Europe/Moscow"
4 changes: 3 additions & 1 deletion projects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ class ProjectAdmin(admin.ModelAdmin):
"id",
"name",
"draft",
"is_public",
"is_company",
"trl",
"target_audience",
"implementation_deadline",
)
list_display_links = ("id", "name")
search_fields = ("name",)
list_filter = ("draft", "is_company", "trl")
list_filter = ("draft", "is_public", "is_company", "trl")

fieldsets = (
(
Expand All @@ -74,6 +75,7 @@ class ProjectAdmin(admin.ModelAdmin):
"industry",
"region",
"draft",
"is_public",
"is_company",
)
},
Expand Down
Loading