Skip to content

Commit 04726df

Browse files
authored
Merge pull request #544 from PROCOLLAB-github/feature/addotional_project_fields
Реализованы цели для проекта
2 parents 66be8f6 + 93368bb commit 04726df

7 files changed

Lines changed: 239 additions & 4 deletions

File tree

projects/admin.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@
66
DefaultProjectAvatar,
77
DefaultProjectCover,
88
Project,
9+
ProjectGoal,
910
ProjectLink,
1011
ProjectNews,
1112
)
1213

1314

15+
class ProjectGoalInline(admin.TabularInline):
16+
model = ProjectGoal
17+
extra = 0
18+
fields = ("title", "completion_date", "responsible", "is_done")
19+
show_change_link = True
20+
autocomplete_fields = ("responsible",)
21+
22+
1423
@admin.register(Project)
1524
class ProjectAdmin(admin.ModelAdmin):
1625
list_display = (
@@ -76,6 +85,28 @@ class ProjectAdmin(admin.ModelAdmin):
7685
),
7786
)
7887
readonly_fields = ("datetime_created", "datetime_updated")
88+
inlines = [ProjectGoalInline]
89+
90+
91+
@admin.register(ProjectGoal)
92+
class ProjectGoalAdmin(admin.ModelAdmin):
93+
list_display = (
94+
"id",
95+
"title",
96+
"project",
97+
"completion_date",
98+
"responsible",
99+
"is_done",
100+
)
101+
list_filter = ("is_done", "completion_date", "project")
102+
search_fields = (
103+
"title",
104+
"project__name",
105+
"responsible__username",
106+
"responsible__email",
107+
)
108+
list_select_related = ("project", "responsible")
109+
autocomplete_fields = ("project", "responsible")
79110

80111

81112
@admin.register(ProjectNews)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Generated by Django 4.2.11 on 2025-09-08 06:45
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
("projects", "0028_remove_project_direction_remove_project_goal_and_more"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="ProjectGoal",
18+
fields=[
19+
(
20+
"id",
21+
models.BigAutoField(
22+
auto_created=True,
23+
primary_key=True,
24+
serialize=False,
25+
verbose_name="ID",
26+
),
27+
),
28+
("title", models.CharField(max_length=255, verbose_name="Название цели")),
29+
(
30+
"completion_date",
31+
models.DateField(
32+
blank=True, null=True, verbose_name="Срок реализации цели"
33+
),
34+
),
35+
("is_done", models.BooleanField(default=False, verbose_name="Выполнено")),
36+
(
37+
"project",
38+
models.ForeignKey(
39+
on_delete=django.db.models.deletion.CASCADE,
40+
related_name="goals",
41+
to="projects.project",
42+
verbose_name="Проект",
43+
),
44+
),
45+
(
46+
"responsible",
47+
models.ForeignKey(
48+
on_delete=django.db.models.deletion.CASCADE,
49+
related_name="responsible_goals",
50+
to=settings.AUTH_USER_MODEL,
51+
verbose_name="Ответственный",
52+
),
53+
),
54+
],
55+
options={
56+
"verbose_name": "Цель",
57+
"verbose_name_plural": "Цели",
58+
},
59+
),
60+
]

projects/models.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,3 +383,46 @@ class Meta:
383383
verbose_name = "Новость проекта"
384384
verbose_name_plural = "Новости проекта"
385385
ordering = ["-datetime_created"]
386+
387+
388+
class ProjectGoal(models.Model):
389+
"""
390+
Цель проекта (минимальная версия).
391+
"""
392+
393+
project = models.ForeignKey(
394+
"Project",
395+
on_delete=models.CASCADE,
396+
related_name="goals",
397+
verbose_name="Проект",
398+
)
399+
400+
title = models.CharField(
401+
max_length=255,
402+
verbose_name="Название цели",
403+
)
404+
405+
completion_date = models.DateField(
406+
null=True,
407+
blank=True,
408+
verbose_name="Срок реализации цели",
409+
)
410+
411+
responsible = models.ForeignKey(
412+
User,
413+
on_delete=models.CASCADE,
414+
related_name="responsible_goals",
415+
verbose_name="Ответственный",
416+
)
417+
418+
is_done = models.BooleanField(
419+
default=False,
420+
verbose_name="Выполнено",
421+
)
422+
423+
def __str__(self) -> str:
424+
return f"Проект [{self.project_id}] - {self.title}"
425+
426+
class Meta:
427+
verbose_name = "Цель"
428+
verbose_name_plural = "Цели"

projects/permissions.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,44 @@ def has_object_permission(self, request, view, obj):
153153
return False
154154

155155

156+
class IsProjectLeaderOrReadOnly(BasePermission):
157+
"""
158+
Читать могут все (в т.ч. анонимы).
159+
Создавать/изменять/удалять может только лидер проекта.
160+
"""
161+
162+
message = "Только лидер проекта может создавать, изменять или удалять цели."
163+
164+
def has_permission(self, request, view):
165+
if request.method in SAFE_METHODS:
166+
return True
167+
168+
if not request.user or not request.user.is_authenticated:
169+
return False
170+
171+
project_pk = view.kwargs.get("project_pk")
172+
project_id = project_pk or request.data.get("project")
173+
if not project_id:
174+
return False
175+
176+
try:
177+
project = Project.objects.only("id", "leader_id").get(pk=project_id)
178+
except Project.DoesNotExist:
179+
return False
180+
181+
return project.leader_id == request.user.id
182+
183+
def has_object_permission(self, request, view, obj):
184+
if request.method in SAFE_METHODS:
185+
return True
186+
187+
return (
188+
request.user
189+
and request.user.is_authenticated
190+
and obj.project.leader_id == request.user.id
191+
)
192+
193+
156194
class CanBindProjectToProgram(BasePermission):
157195
message = "Привязать проект к программе может только её участник (или менеджер)."
158196

projects/serializers.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
PartnerProgramFieldSerializer,
2020
PartnerProgramFieldValueSerializer,
2121
)
22-
from projects.models import Achievement, Collaborator, Project, ProjectNews
22+
from projects.models import Achievement, Collaborator, Project, ProjectGoal, ProjectNews
2323
from projects.validators import validate_project
2424
from vacancy.serializers import ProjectVacancyListSerializer
2525

@@ -109,6 +109,31 @@ def get_program_field_values(self, obj):
109109
return PartnerProgramFieldValueSerializer(values_qs, many=True).data
110110

111111

112+
class ResponsibleMiniSerializer(serializers.ModelSerializer):
113+
class Meta:
114+
model = User
115+
fields = ("id", "first_name", "last_name", "avatar")
116+
117+
118+
class ProjectGoalSerializer(serializers.ModelSerializer):
119+
project = serializers.PrimaryKeyRelatedField(read_only=True)
120+
responsible = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())
121+
responsible_info = ResponsibleMiniSerializer(source="responsible", read_only=True)
122+
123+
class Meta:
124+
model = ProjectGoal
125+
fields = [
126+
"id",
127+
"project",
128+
"title",
129+
"completion_date",
130+
"responsible",
131+
"responsible_info",
132+
"is_done",
133+
]
134+
read_only_fields = ["id", "project", "responsible_info"]
135+
136+
112137
class ProjectDetailSerializer(serializers.ModelSerializer):
113138
achievements = AchievementListSerializer(many=True, read_only=True)
114139
cover = UserFileSerializer(required=False)

projects/urls.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
AchievementDetail,
77
AchievementList,
88
DuplicateProjectView,
9+
GoalViewSet,
910
LeaveProject,
1011
ProjectCollaborators,
1112
ProjectCountView,
@@ -21,14 +22,28 @@
2122
)
2223

2324
app_name = "projects"
24-
25+
project_goal_list = GoalViewSet.as_view({"get": "list", "post": "create"})
26+
project_goal_detail = GoalViewSet.as_view(
27+
{
28+
"get": "retrieve",
29+
"put": "update",
30+
"patch": "partial_update",
31+
"delete": "destroy",
32+
}
33+
)
2534
urlpatterns = [
2635
path("", ProjectList.as_view()),
2736
path("<int:pk>/like/", SetLikeOnProject.as_view()),
2837
path("<int:project_pk>/news/", NewsList.as_view()),
2938
path("<int:project_pk>/subscribe/", ProjectSubscribe.as_view()),
3039
path("<int:project_pk>/unsubscribe/", ProjectUnsubscribe.as_view()),
3140
path("<int:project_pk>/subscribers/", ProjectSubscribers.as_view()),
41+
path("<int:project_pk>/goals/", project_goal_list, name="project-goals"),
42+
path(
43+
"<int:project_pk>/goals/<int:pk>/",
44+
project_goal_detail,
45+
name="project-goal-detail",
46+
),
3247
path("<int:project_pk>/news/<int:pk>/", NewsDetail.as_view()),
3348
path("<int:project_pk>/news/<int:pk>/set_viewed/", NewsDetailSetViewed.as_view()),
3449
path("<int:project_pk>/news/<int:pk>/set_liked/", NewsDetailSetLiked.as_view()),

projects/views.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from django_filters import rest_framework as filters
1010
from drf_yasg import openapi
1111
from drf_yasg.utils import swagger_auto_schema
12-
from rest_framework import generics, permissions, status
12+
from rest_framework import generics, permissions, status, viewsets
1313
from rest_framework.exceptions import NotFound
1414
from rest_framework.permissions import IsAuthenticated
1515
from rest_framework.response import Response
@@ -30,13 +30,14 @@
3030
get_recommended_users,
3131
update_partner_program,
3232
)
33-
from projects.models import Achievement, Collaborator, Project, ProjectNews
33+
from projects.models import Achievement, Collaborator, Project, ProjectGoal, ProjectNews
3434
from projects.pagination import ProjectNewsPagination, ProjectsPagination
3535
from projects.permissions import (
3636
CanBindProjectToProgram,
3737
HasInvolvementInProjectOrReadOnly,
3838
IsNewsAuthorIsProjectLeaderOrReadOnly,
3939
IsProjectLeader,
40+
IsProjectLeaderOrReadOnly,
4041
IsProjectLeaderOrReadOnlyForNonDrafts,
4142
TimingAfterEndsProgramPermission,
4243
)
@@ -46,6 +47,7 @@
4647
ProjectCollaboratorSerializer,
4748
ProjectDetailSerializer,
4849
ProjectDuplicateRequestSerializer,
50+
ProjectGoalSerializer,
4951
ProjectListSerializer,
5052
ProjectNewsDetailSerializer,
5153
ProjectNewsListSerializer,
@@ -711,3 +713,24 @@ def post(self, request):
711713
},
712714
status=status.HTTP_201_CREATED,
713715
)
716+
717+
718+
class GoalViewSet(viewsets.ModelViewSet):
719+
queryset = ProjectGoal.objects.select_related("project", "responsible")
720+
serializer_class = ProjectGoalSerializer
721+
permission_classes = [IsProjectLeaderOrReadOnly]
722+
723+
def get_queryset(self):
724+
qs = super().get_queryset()
725+
project_pk = self.kwargs.get("project_pk")
726+
return qs.filter(project_id=project_pk) if project_pk is not None else qs
727+
728+
def perform_create(self, serializer):
729+
project_pk = self.kwargs.get("project_pk")
730+
if project_pk is None:
731+
serializer.save()
732+
else:
733+
serializer.save(project_id=project_pk)
734+
735+
def perform_update(self, serializer):
736+
serializer.save(project=self.get_object().project)

0 commit comments

Comments
 (0)