Skip to content

Commit 51c4f8d

Browse files
authored
Merge pull request #594 from PROCOLLAB-github/dev
Dev
2 parents 97836ee + d58c124 commit 51c4f8d

12 files changed

Lines changed: 298 additions & 6 deletions

File tree

invites/managers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
class InviteManager(Manager):
55
def get_invite_for_list_view(self):
6-
return self.get_queryset().select_related("project", "user")
6+
return self.get_queryset().select_related("project", "project__leader", "user")

invites/serializers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,22 @@
44
from invites.models import Invite
55
from projects.models import Collaborator
66
from projects.serializers import ProjectListSerializer
7+
from users.models import CustomUser
78
from users.serializers import UserDetailSerializer
89

910

11+
class InviteSenderSerializer(serializers.ModelSerializer[CustomUser]):
12+
class Meta:
13+
model = CustomUser
14+
fields = [
15+
"id",
16+
"first_name",
17+
"last_name",
18+
"patronymic",
19+
"avatar",
20+
]
21+
22+
1023
class InviteListSerializer(serializers.ModelSerializer[Invite]):
1124
class Meta:
1225
model = Invite
@@ -67,6 +80,7 @@ def validate(self, attrs):
6780
class InviteDetailSerializer(serializers.ModelSerializer[Invite]):
6881
user = UserDetailSerializer(many=False, read_only=True)
6982
project = ProjectListSerializer(many=False, read_only=True)
83+
sender = InviteSenderSerializer(source="project.leader", read_only=True)
7084
specialization = serializers.CharField(
7185
required=False, allow_null=True, allow_blank=True
7286
)
@@ -77,6 +91,7 @@ class Meta:
7791
"id",
7892
"project",
7993
"user",
94+
"sender",
8095
"motivational_letter",
8196
"role",
8297
"specialization",

invites/tests.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
from datetime import timedelta
2+
13
from django.test import TestCase
4+
from django.utils import timezone
25
from rest_framework.test import APIRequestFactory, force_authenticate
36

4-
from projects.models import Project
7+
from partner_programs.models import PartnerProgram, PartnerProgramProject
8+
from projects.models import Collaborator, Project
59
from tests.constants import USER_CREATE_DATA
610

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

113117
self.assertEqual(response.status_code, 400)
114118

119+
def test_invites_creation_for_existing_collaborator(self):
120+
sender = self._user_create("sender@example.com")
121+
recipient = self._user_create("recipient@example.com")
122+
project = self._project_create(sender)
123+
Collaborator.objects.create(user=recipient, project=project, role="Developer")
124+
125+
create_user = self.invite_create_data.copy()
126+
create_user["user"] = recipient.id
127+
create_user["project"] = project.id
128+
request = self.factory.post("invites/", create_user, format="json")
129+
force_authenticate(request, user=sender)
130+
131+
response = self.invite_list_view(request)
132+
133+
self.assertEqual(response.status_code, 400)
134+
self.assertIn("user", response.data)
135+
136+
def test_invites_creation_for_non_program_member(self):
137+
sender = self._user_create("sender@example.com")
138+
recipient = self._user_create("recipient@example.com")
139+
project = self._project_create(sender)
140+
now = timezone.now()
141+
program = PartnerProgram.objects.create(
142+
name="Test program",
143+
tag="test",
144+
city="Moscow",
145+
datetime_registration_ends=now + timedelta(days=1),
146+
datetime_started=now,
147+
datetime_finished=now + timedelta(days=30),
148+
)
149+
PartnerProgramProject.objects.create(
150+
partner_program=program, project=project
151+
)
152+
153+
create_user = self.invite_create_data.copy()
154+
create_user["user"] = recipient.id
155+
create_user["project"] = project.id
156+
request = self.factory.post("invites/", create_user, format="json")
157+
force_authenticate(request, user=sender)
158+
159+
response = self.invite_list_view(request)
160+
161+
self.assertEqual(response.status_code, 400)
162+
self.assertIn("user", response.data)
163+
115164
def test_accept_invite_by_intended_user(self):
116165
sender = self._user_create("sender@example.com")
117166
recipient = self._user_create("recipient@example.com")

partner_programs/services.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,48 @@
33

44
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
55
from django.db.models import Prefetch
6+
from django.utils import timezone
67

78
from partner_programs.models import (
9+
PartnerProgram,
810
PartnerProgramField,
911
PartnerProgramFieldValue,
1012
PartnerProgramProject,
1113
PartnerProgramUserProfile,
1214
)
1315
from project_rates.models import Criteria, ProjectScore
16+
from projects.models import Project
1417

1518
logger = logging.getLogger()
1619

1720

21+
def publish_finished_program_projects(now=None) -> int:
22+
if now is None:
23+
now = timezone.now()
24+
25+
program_ids = PartnerProgram.objects.filter(
26+
publish_projects_after_finish=True,
27+
datetime_finished__lte=now,
28+
).values_list("id", flat=True)
29+
if not program_ids.exists():
30+
return 0
31+
32+
link_project_ids = PartnerProgramProject.objects.filter(
33+
partner_program_id__in=program_ids
34+
).values_list("project_id", flat=True)
35+
profile_project_ids = PartnerProgramUserProfile.objects.filter(
36+
partner_program_id__in=program_ids,
37+
project_id__isnull=False,
38+
).values_list("project_id", flat=True)
39+
project_ids = link_project_ids.union(profile_project_ids)
40+
41+
return Project.objects.filter(
42+
id__in=project_ids,
43+
is_public=False,
44+
draft=False,
45+
).update(is_public=True)
46+
47+
1848
class ProjectScoreDataPreparer:
1949
"""
2050
Data preparer about project_rates by experts.

partner_programs/tasks.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import logging
2+
3+
from procollab.celery import app
4+
from partner_programs.services import publish_finished_program_projects
5+
6+
logger = logging.getLogger(__name__)
7+
8+
9+
@app.task
10+
def publish_finished_program_projects_task() -> int:
11+
updated_count = publish_finished_program_projects()
12+
logger.info("Published %s program projects after finish", updated_count)
13+
return updated_count

partner_programs/tests.py

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
from django.contrib.auth import get_user_model
12
from django.test import TestCase
23
from django.utils import timezone
34

4-
from partner_programs.models import PartnerProgram, PartnerProgramField
5+
from partner_programs.models import (
6+
PartnerProgram,
7+
PartnerProgramField,
8+
PartnerProgramProject,
9+
PartnerProgramUserProfile,
10+
)
511
from partner_programs.serializers import PartnerProgramFieldValueUpdateSerializer
12+
from partner_programs.services import publish_finished_program_projects
13+
from projects.models import Project
614

715

816
class PartnerProgramFieldValueUpdateSerializerInvalidTests(TestCase):
@@ -118,6 +126,117 @@ def test_file_empty_required(self):
118126
self.assertIn("Файл обязателен для этого поля.", str(serializer.errors))
119127

120128

129+
class PublishFinishedProgramProjectsTests(TestCase):
130+
def setUp(self):
131+
self.now = timezone.now()
132+
self.user = get_user_model().objects.create_user(
133+
email="user@example.com",
134+
password="pass",
135+
first_name="User",
136+
last_name="Test",
137+
birthday="1990-01-01",
138+
)
139+
140+
def create_program(self, **overrides):
141+
defaults = {
142+
"name": "Program",
143+
"tag": "program_tag",
144+
"description": "Program description",
145+
"city": "Moscow",
146+
"image_address": "https://example.com/image.png",
147+
"cover_image_address": "https://example.com/cover.png",
148+
"advertisement_image_address": "https://example.com/advertisement.png",
149+
"presentation_address": "https://example.com/presentation.pdf",
150+
"data_schema": {},
151+
"draft": False,
152+
"projects_availability": "all_users",
153+
"datetime_registration_ends": self.now - timezone.timedelta(days=5),
154+
"datetime_started": self.now - timezone.timedelta(days=30),
155+
"datetime_finished": self.now - timezone.timedelta(days=1),
156+
}
157+
defaults.update(overrides)
158+
return PartnerProgram.objects.create(**defaults)
159+
160+
def create_project(self, **overrides):
161+
defaults = {
162+
"leader": self.user,
163+
"draft": False,
164+
"is_public": False,
165+
"name": "Project",
166+
}
167+
defaults.update(overrides)
168+
return Project.objects.create(**defaults)
169+
170+
def test_publish_updates_projects_from_both_sources(self):
171+
program = self.create_program(publish_projects_after_finish=True)
172+
173+
link_project = self.create_project(name="Linked Project")
174+
PartnerProgramProject.objects.create(
175+
partner_program=program,
176+
project=link_project,
177+
)
178+
179+
profile_project = self.create_project(name="Profile Project")
180+
PartnerProgramUserProfile.objects.create(
181+
user=self.user,
182+
partner_program=program,
183+
project=profile_project,
184+
partner_program_data={},
185+
)
186+
187+
publish_finished_program_projects()
188+
189+
link_project.refresh_from_db()
190+
profile_project.refresh_from_db()
191+
self.assertTrue(link_project.is_public)
192+
self.assertTrue(profile_project.is_public)
193+
194+
def test_publish_skips_draft_projects(self):
195+
program = self.create_program(publish_projects_after_finish=True)
196+
draft_project = self.create_project(draft=True, name="Draft Project")
197+
PartnerProgramProject.objects.create(
198+
partner_program=program,
199+
project=draft_project,
200+
)
201+
202+
publish_finished_program_projects()
203+
204+
draft_project.refresh_from_db()
205+
self.assertFalse(draft_project.is_public)
206+
207+
def test_publish_skips_when_flag_false(self):
208+
program = self.create_program(publish_projects_after_finish=False)
209+
project = self.create_project(name="Private Project")
210+
PartnerProgramProject.objects.create(
211+
partner_program=program,
212+
project=project,
213+
)
214+
215+
publish_finished_program_projects()
216+
217+
project.refresh_from_db()
218+
self.assertFalse(project.is_public)
219+
220+
def test_publish_after_flag_enabled_post_finish(self):
221+
program = self.create_program(publish_projects_after_finish=False)
222+
project = self.create_project(name="Delayed Project")
223+
PartnerProgramProject.objects.create(
224+
partner_program=program,
225+
project=project,
226+
)
227+
228+
publish_finished_program_projects()
229+
project.refresh_from_db()
230+
self.assertFalse(project.is_public)
231+
232+
program.publish_projects_after_finish = True
233+
program.save(update_fields=["publish_projects_after_finish"])
234+
235+
publish_finished_program_projects()
236+
project.refresh_from_db()
237+
self.assertTrue(project.is_public)
238+
239+
121240
class PartnerProgramFieldValueUpdateSerializerValidTests(TestCase):
122241
def setUp(self):
123242
now = timezone.now()

procollab/celery.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
"task": "vacancy.tasks.email_notificate_vacancy_outdated",
1818
# "schedule": crontab(minute=0, hour=0),
1919
"schedule": crontab(minute="*"),
20-
}
20+
},
21+
"publish_finished_program_projects": {
22+
"task": "partner_programs.tasks.publish_finished_program_projects_task",
23+
"schedule": crontab(minute=0, hour=6),
24+
},
2125
}
2226

2327
if __name__ == "__main__":

procollab/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,3 +418,4 @@
418418
CELERY_ACCEPT_CONTENT = ["application/json"]
419419
CELERY_RESULT_SERIALIZER = "json"
420420
CELERY_TASK_SERIALIZER = "json"
421+
CELERY_TIMEZONE = "Europe/Moscow"

projects/admin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ class ProjectAdmin(admin.ModelAdmin):
5454
"id",
5555
"name",
5656
"draft",
57+
"is_public",
5758
"is_company",
5859
"trl",
5960
"target_audience",
6061
"implementation_deadline",
6162
)
6263
list_display_links = ("id", "name")
6364
search_fields = ("name",)
64-
list_filter = ("draft", "is_company", "trl")
65+
list_filter = ("draft", "is_public", "is_company", "trl")
6566

6667
fieldsets = (
6768
(
@@ -74,6 +75,7 @@ class ProjectAdmin(admin.ModelAdmin):
7475
"industry",
7576
"region",
7677
"draft",
78+
"is_public",
7779
"is_company",
7880
)
7981
},

0 commit comments

Comments
 (0)