Skip to content

Commit 14bdd99

Browse files
committed
Реализован новый способ подачи проектов на программы, добавлена возможность создавать непубличныые проекты
1 parent 16469a1 commit 14bdd99

20 files changed

Lines changed: 595 additions & 187 deletions

feed/views.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from feed.pagination import FeedPagination
99
from feed.services import get_liked_news
1010
from news.models import News
11-
from partner_programs.models import PartnerProgramUserProfile
1211
from projects.models import Project
1312
from vacancy.models import Vacancy
1413

@@ -28,29 +27,20 @@ def _get_filter_data(self) -> list[str]:
2827
news_types.append("customuser")
2928
return news_types
3029

31-
def _get_excluded_projects_ids(self) -> list[int]:
32-
"""IDs for exclude projects which in Partner Program."""
33-
excluded_projects = PartnerProgramUserProfile.objects.values_list(
34-
"project_id", flat=True
35-
).exclude(project_id__isnull=True)
36-
return excluded_projects
37-
3830
def get_queryset(self) -> QuerySet[News]:
3931
filters = self._get_filter_data()
40-
excluded_project_ids: list[int] = self._get_excluded_projects_ids()
4132

4233
queryset = (
4334
News.objects.select_related("content_type")
4435
.prefetch_related("content_object", "files")
4536
.filter(content_type__model__in=filters)
46-
.exclude(
47-
Q(content_type__model="project") & Q(object_id__in=excluded_project_ids)
48-
)
4937
.order_by("-datetime_created")
5038
)
5139

5240
existing_object_filters = {
53-
"project": Project.objects.values_list("id", flat=True),
41+
"project": Project.objects.filter(draft=False, is_public=True).values_list(
42+
"id", flat=True
43+
),
5444
"vacancy": Vacancy.objects.values_list("id", flat=True),
5545
}
5646
for model_name, ids_queryset in existing_object_filters.items():

news/views.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
)
1919
from partner_programs.models import PartnerProgram
2020
from projects.models import Project
21+
from projects.permissions import ProjectVisibilityPermission
2122

2223
User = get_user_model()
2324

2425

2526
class NewsList(NewsQuerysetMixin, generics.ListCreateAPIView):
2627
serializer_class = NewsListSerializer
27-
permission_classes = [IsNewsCreatorOrReadOnly]
28+
permission_classes = [ProjectVisibilityPermission, IsNewsCreatorOrReadOnly]
2829
pagination_class = NewsPagination
2930

3031
def post(self, request: Request, *args, **kwargs) -> Response:
@@ -62,7 +63,7 @@ def get(self, request: Request, *args, **kwargs) -> Response:
6263

6364
class NewsDetail(NewsQuerysetMixin, generics.RetrieveUpdateDestroyAPIView):
6465
serializer_class = NewsDetailSerializer
65-
permission_classes = [IsNewsCreatorOrReadOnly]
66+
permission_classes = [ProjectVisibilityPermission, IsNewsCreatorOrReadOnly]
6667

6768
def get(self, request: Request, *args, **kwargs) -> Response:
6869
try:
@@ -87,7 +88,7 @@ def update(self, request: Request, *args, **kwargs) -> Response:
8788

8889
class NewsDetailSetViewed(NewsQuerysetMixin, generics.CreateAPIView):
8990
serializer_class = SetViewedSerializer
90-
permission_classes = [IsAuthenticated]
91+
permission_classes = [IsAuthenticated, ProjectVisibilityPermission]
9192

9293
def post(self, request: Request, *args, **kwargs) -> Response:
9394
try:
@@ -100,7 +101,7 @@ def post(self, request: Request, *args, **kwargs) -> Response:
100101

101102
class NewsDetailSetLiked(NewsQuerysetMixin, generics.CreateAPIView):
102103
serializer_class = SetLikedSerializer
103-
permission_classes = [IsAuthenticated]
104+
permission_classes = [IsAuthenticated, ProjectVisibilityPermission]
104105

105106
def post(self, request: Request, *args, **kwargs) -> Response:
106107
try:

partner_programs/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class Meta:
7979
"city",
8080
"is_competitive",
8181
"projects_availability",
82+
"publish_projects_after_finish",
8283
"max_project_rates",
8384
"draft",
8485
(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Codex CLI on 2025-12-19
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("partner_programs", "0014_partnerprogram_datetime_evaluation_ends_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="partnerprogram",
15+
name="publish_projects_after_finish",
16+
field=models.BooleanField(
17+
default=False,
18+
help_text="Если включено, проекты участников могут стать публичными после завершения программы",
19+
verbose_name="Публиковать проекты после окончания программы",
20+
),
21+
),
22+
]
23+

partner_programs/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ class PartnerProgram(models.Model):
113113
default="all_users",
114114
verbose_name="Доступность к дочерним проектам",
115115
)
116+
publish_projects_after_finish = models.BooleanField(
117+
default=False,
118+
verbose_name="Публиковать проекты после окончания программы",
119+
help_text="Если включено, проекты участников могут стать публичными после завершения программы",
120+
)
116121
datetime_registration_ends = models.DateTimeField(
117122
verbose_name="Дата окончания регистрации",
118123
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from .fields import PartnerProgramFieldValueUpdateSerializer
2+
from .programs import (
3+
PartnerProgramBaseSerializerMixin,
4+
PartnerProgramDataSchemaSerializer,
5+
PartnerProgramFieldSerializer,
6+
PartnerProgramFieldValueSerializer,
7+
PartnerProgramForMemberSerializer,
8+
PartnerProgramForUnregisteredUserSerializer,
9+
PartnerProgramListSerializer,
10+
PartnerProgramMaterialSerializer,
11+
PartnerProgramNewUserSerializer,
12+
PartnerProgramProjectApplySerializer,
13+
PartnerProgramUserSerializer,
14+
ProgramProjectCreateSerializer,
15+
ProgramProjectFilterRequestSerializer,
16+
UserProgramsSerializer,
17+
)
18+
19+
__all__ = [
20+
"PartnerProgramBaseSerializerMixin",
21+
"PartnerProgramDataSchemaSerializer",
22+
"PartnerProgramFieldSerializer",
23+
"PartnerProgramFieldValueSerializer",
24+
"PartnerProgramFieldValueUpdateSerializer",
25+
"PartnerProgramForMemberSerializer",
26+
"PartnerProgramForUnregisteredUserSerializer",
27+
"PartnerProgramListSerializer",
28+
"PartnerProgramMaterialSerializer",
29+
"PartnerProgramNewUserSerializer",
30+
"PartnerProgramProjectApplySerializer",
31+
"PartnerProgramUserSerializer",
32+
"ProgramProjectCreateSerializer",
33+
"ProgramProjectFilterRequestSerializer",
34+
"UserProgramsSerializer",
35+
]
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from urllib.parse import urlparse
2+
3+
from rest_framework import serializers
4+
5+
from partner_programs.models import PartnerProgramField
6+
7+
8+
class PartnerProgramFieldValueUpdateSerializer(serializers.Serializer):
9+
field_id = serializers.PrimaryKeyRelatedField(
10+
queryset=PartnerProgramField.objects.all(),
11+
source="field",
12+
)
13+
value_text = serializers.CharField(
14+
required=False,
15+
allow_blank=True,
16+
allow_null=True,
17+
help_text="Укажите значение для поля.",
18+
)
19+
20+
def validate(self, attrs):
21+
field = attrs.get("field")
22+
value_text = attrs.get("value_text")
23+
24+
validator = self._get_validator(field)
25+
validator(field, value_text, attrs)
26+
27+
return attrs
28+
29+
def _get_validator(self, field: PartnerProgramField):
30+
validators = {
31+
"text": self._validate_text,
32+
"textarea": self._validate_text,
33+
"checkbox": self._validate_checkbox,
34+
"select": self._validate_select,
35+
"radio": self._validate_radio,
36+
"file": self._validate_file,
37+
}
38+
try:
39+
return validators[field.field_type]
40+
except KeyError:
41+
raise serializers.ValidationError(
42+
f"Тип поля '{field.field_type}' не поддерживается."
43+
)
44+
45+
def _validate_text(self, field: PartnerProgramField, value, attrs):
46+
if field.is_required:
47+
if value is None or str(value).strip() == "":
48+
raise serializers.ValidationError(
49+
"Поле должно содержать текстовое значение."
50+
)
51+
else:
52+
if value is not None and not isinstance(value, str):
53+
raise serializers.ValidationError("Ожидается строка для текстового поля.")
54+
55+
def _validate_checkbox(self, field: PartnerProgramField, value, attrs):
56+
if field.is_required and value in (None, ""):
57+
raise serializers.ValidationError(
58+
"Значение обязательно для поля типа 'checkbox'."
59+
)
60+
61+
if value is not None:
62+
if isinstance(value, bool):
63+
attrs["value_text"] = "true" if value else "false"
64+
elif isinstance(value, str):
65+
normalized = value.strip().lower()
66+
if normalized not in ("true", "false"):
67+
raise serializers.ValidationError(
68+
"Для поля типа 'checkbox' ожидается 'true' или 'false'."
69+
)
70+
attrs["value_text"] = normalized
71+
else:
72+
raise serializers.ValidationError(
73+
"Неверный тип значения для поля 'checkbox'."
74+
)
75+
76+
def _validate_select(self, field: PartnerProgramField, value, attrs):
77+
self._validate_choice_field(field, value, "select")
78+
79+
def _validate_radio(self, field: PartnerProgramField, value, attrs):
80+
self._validate_choice_field(field, value, "radio")
81+
82+
def _validate_choice_field(self, field: PartnerProgramField, value, field_type):
83+
options = field.get_options_list()
84+
85+
if not options:
86+
raise serializers.ValidationError(
87+
f"Для поля типа '{field_type}' не заданы допустимые значения."
88+
)
89+
90+
if field.is_required:
91+
if value is None or value == "":
92+
raise serializers.ValidationError(
93+
f"Значение обязательно для поля типа '{field_type}'."
94+
)
95+
else:
96+
if value is None or value == "":
97+
return
98+
99+
if value is not None:
100+
if not isinstance(value, str):
101+
raise serializers.ValidationError(
102+
f"Ожидается строковое значение для поля типа '{field_type}'."
103+
)
104+
if value not in options:
105+
raise serializers.ValidationError(
106+
f"Недопустимое значение для поля типа '{field_type}'. "
107+
f"Ожидается одно из: {options}."
108+
)
109+
110+
def _validate_file(self, field: PartnerProgramField, value, attrs):
111+
if field.is_required:
112+
if value is None or value == "":
113+
raise serializers.ValidationError("Файл обязателен для этого поля.")
114+
115+
if value is not None:
116+
if not isinstance(value, str):
117+
raise serializers.ValidationError(
118+
"Ожидается строковое значение для поля 'file'."
119+
)
120+
121+
if not self._is_valid_url(value):
122+
raise serializers.ValidationError(
123+
"Ожидается корректная ссылка (URL) на файл."
124+
)
125+
126+
def _is_valid_url(self, url: str) -> bool:
127+
try:
128+
parsed = urlparse(url)
129+
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
130+
except Exception:
131+
return False
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
from rest_framework import serializers
33

44
from core.services import get_likes_count, get_links, get_views_count, is_fan
5+
from .fields import PartnerProgramFieldValueUpdateSerializer
56
from partner_programs.models import (
67
PartnerProgram,
78
PartnerProgramField,
89
PartnerProgramFieldValue,
910
PartnerProgramMaterial,
1011
)
12+
from projects.models import Project
13+
from projects.validators import validate_project
1114

1215
User = get_user_model()
1316

@@ -51,6 +54,7 @@ class Meta:
5154
"datetime_registration_ends",
5255
"datetime_project_submission_ends",
5356
"datetime_evaluation_ends",
57+
"publish_projects_after_finish",
5458
"datetime_started",
5559
"datetime_finished",
5660
"views_count",
@@ -120,6 +124,7 @@ class Meta:
120124
"datetime_registration_ends",
121125
"datetime_project_submission_ends",
122126
"datetime_evaluation_ends",
127+
"publish_projects_after_finish",
123128
"is_user_manager",
124129
)
125130

@@ -143,6 +148,7 @@ class Meta:
143148
"datetime_registration_ends",
144149
"datetime_project_submission_ends",
145150
"datetime_evaluation_ends",
151+
"publish_projects_after_finish",
146152
"is_user_manager",
147153
)
148154

@@ -270,3 +276,34 @@ def validate_filters(self, value):
270276
cleaned[key.strip()] = normalized_values
271277

272278
return cleaned
279+
280+
281+
class ProgramProjectCreateSerializer(serializers.ModelSerializer):
282+
class Meta:
283+
model = Project
284+
fields = [
285+
"name",
286+
"description",
287+
"region",
288+
"industry",
289+
"presentation_address",
290+
"image_address",
291+
"cover_image_address",
292+
"actuality",
293+
"problem",
294+
"target_audience",
295+
"implementation_deadline",
296+
"trl",
297+
"is_company",
298+
]
299+
300+
def validate(self, data):
301+
validate_project({**data, "draft": True})
302+
return data
303+
304+
305+
class PartnerProgramProjectApplySerializer(serializers.Serializer):
306+
project = ProgramProjectCreateSerializer()
307+
program_field_values = PartnerProgramFieldValueUpdateSerializer(
308+
many=True, required=False
309+
)

partner_programs/tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.utils import timezone
33

44
from partner_programs.models import PartnerProgram, PartnerProgramField
5-
from projects.serializers import PartnerProgramFieldValueUpdateSerializer
5+
from partner_programs.serializers import PartnerProgramFieldValueUpdateSerializer
66

77

88
class PartnerProgramFieldValueUpdateSerializerInvalidTests(TestCase):

partner_programs/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
PartnerProgramDetail,
88
PartnerProgramExportProjectsAPIView,
99
PartnerProgramList,
10+
PartnerProgramProjectApplyView,
1011
PartnerProgramProjectsAPIView,
1112
PartnerProgramProjectSubmitView,
1213
PartnerProgramRegister,
@@ -51,6 +52,11 @@
5152
PartnerProgramProjectsAPIView.as_view(),
5253
name="partner-program-projects",
5354
),
55+
path(
56+
"<int:pk>/projects/apply/",
57+
PartnerProgramProjectApplyView.as_view(),
58+
name="partner-program-project-apply",
59+
),
5460
path(
5561
"<int:pk>/export-projects/",
5662
PartnerProgramExportProjectsAPIView.as_view(),

0 commit comments

Comments
 (0)