Skip to content

Commit 6b7a2f3

Browse files
committed
Расширена логика создания новостей: добавлена поддержка публикаций для партнёрских программ. Обновлено представление NewsList и разрешения IsNewsCreatorOrReadOnly для учёта менеджеров программ. Добавлена проверка прав на уровне has_permission и has_object_permission. Также оптимизировано представление PartnerProgramDetail: удалена избыточная обработка DoesNotExist
1 parent e2f34d6 commit 6b7a2f3

7 files changed

Lines changed: 120 additions & 62 deletions

File tree

news/permissions.py

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django.contrib.auth import get_user_model
2+
from django.shortcuts import get_object_or_404
23
from rest_framework import permissions
3-
from rest_framework.exceptions import NotFound
44
from rest_framework.permissions import SAFE_METHODS
55

66
from partner_programs.models import PartnerProgram
@@ -10,47 +10,37 @@
1010

1111

1212
class IsNewsCreatorOrReadOnly(permissions.BasePermission):
13-
def has_object_permission(self, request, view, obj):
14-
"""
15-
read/update/delete permission
16-
currently can only be updated/deleted in admin panel
17-
"""
13+
def has_permission(self, request, view):
1814
if request.method in SAFE_METHODS:
1915
return True
20-
if (
21-
isinstance(obj.content_object, Project)
22-
and obj.content_object.leader == request.user
23-
):
24-
return True
25-
if isinstance(obj.content_object, User) and obj.content_object == request.user:
26-
return True
27-
if isinstance(obj.content_object, PartnerProgram):
28-
# TODO: implement
29-
pass
16+
17+
if view.kwargs.get("project_pk"):
18+
project = get_object_or_404(Project, pk=view.kwargs["project_pk"])
19+
return request.user == project.leader
20+
21+
if view.kwargs.get("user_pk"):
22+
user = get_object_or_404(User, pk=view.kwargs["user_pk"])
23+
return request.user == user
24+
25+
if view.kwargs.get("partnerprogram_pk"):
26+
program = get_object_or_404(
27+
PartnerProgram, pk=view.kwargs["partnerprogram_pk"]
28+
)
29+
return program.is_manager(request.user)
30+
3031
return False
3132

32-
def has_permission(self, request, view):
33-
"""
34-
Creation permission
35-
Currently can only be created via admin panel
36-
"""
33+
def has_object_permission(self, request, view, obj):
3734
if request.method in SAFE_METHODS:
3835
return True
3936

40-
if view.kwargs.get("project_pk"):
41-
try:
42-
project = Project.objects.get(pk=view.kwargs["project_pk"])
43-
if request.method in SAFE_METHODS or (request.user == project.leader):
44-
return True
45-
except Project.DoesNotExist:
46-
raise NotFound
37+
if isinstance(obj.content_object, Project):
38+
return obj.content_object.leader == request.user
4739

48-
if view.kwargs.get("user_pk"):
49-
try:
50-
user = User.objects.get(pk=view.kwargs["user_pk"])
51-
if request.method in SAFE_METHODS or (request.user == user):
52-
return True
53-
except User.DoesNotExist:
54-
raise NotFound
40+
if isinstance(obj.content_object, User):
41+
return obj.content_object == request.user
42+
43+
if isinstance(obj.content_object, PartnerProgram):
44+
return obj.content_object.is_manager(request.user)
5545

5646
return False

news/views.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22
from django.shortcuts import get_object_or_404
33
from rest_framework import generics, status
44
from rest_framework.permissions import IsAuthenticated
5-
from rest_framework.response import Response
65
from rest_framework.request import Request
6+
from rest_framework.response import Response
77

8-
from core.serializers import SetViewedSerializer, SetLikedSerializer
8+
from core.serializers import SetLikedSerializer, SetViewedSerializer
99
from core.services import add_view, set_like
1010
from news.mixins import NewsQuerysetMixin
1111
from news.models import News
1212
from news.pagination import NewsPagination
1313
from news.permissions import IsNewsCreatorOrReadOnly
1414
from news.serializers import (
15-
NewsListSerializer,
1615
NewsDetailSerializer,
1716
NewsListCreateSerializer,
17+
NewsListSerializer,
1818
)
19+
from partner_programs.models import PartnerProgram
1920
from projects.models import Project
2021

2122
User = get_user_model()
@@ -44,7 +45,12 @@ def post(self, request: Request, *args, **kwargs) -> Response:
4445
NewsDetailSerializer(news).data, status=status.HTTP_201_CREATED
4546
)
4647

47-
# creating partner program news, not implemented yet, return 400
48+
if kwargs.get("partnerprogram_pk"):
49+
program = get_object_or_404(PartnerProgram, pk=kwargs["partnerprogram_pk"])
50+
news = News.objects.add_news(program, **data)
51+
return Response(
52+
NewsDetailSerializer(news).data, status=status.HTTP_201_CREATED
53+
)
4854
return Response(status=status.HTTP_400_BAD_REQUEST)
4955

5056
def get(self, request: Request, *args, **kwargs) -> Response:

partner_programs/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class PartnerProgramAdmin(admin.ModelAdmin):
4545
)
4646
list_filter = ("city",)
4747

48-
filter_horizontal = ("users",)
48+
filter_horizontal = ("users", "managers")
4949
date_hierarchy = "datetime_started"
5050

5151
def get_queryset(self, request: HttpRequest) -> QuerySet[PartnerProgram]:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 4.2.11 on 2025-07-22 08:44
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
10+
("partner_programs", "0007_partnerprogrammaterial"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="partnerprogram",
16+
name="managers",
17+
field=models.ManyToManyField(
18+
blank=True,
19+
help_text="Пользователи, имеющие право создавать и редактировать новости",
20+
related_name="managed_partner_programs",
21+
to=settings.AUTH_USER_MODEL,
22+
verbose_name="Менеджеры программы",
23+
),
24+
),
25+
migrations.AlterField(
26+
model_name="partnerprogrammaterial",
27+
name="title",
28+
field=models.CharField(
29+
help_text="Укажите текст для гиперссылки",
30+
max_length=255,
31+
verbose_name="Название материала",
32+
),
33+
),
34+
]

partner_programs/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ class PartnerProgram(models.Model):
8080
verbose_name="Участники программы",
8181
through="PartnerProgramUserProfile",
8282
)
83+
managers = models.ManyToManyField(
84+
User,
85+
related_name="managed_partner_programs",
86+
blank=True,
87+
verbose_name="Менеджеры программы",
88+
help_text="Пользователи, имеющие право создавать и редактировать новости",
89+
)
8390
draft = models.BooleanField(blank=False, default=True)
8491
projects_availability = models.CharField(
8592
choices=PROJECTS_AVAILABILITY_CHOISES,
@@ -103,6 +110,14 @@ class PartnerProgram(models.Model):
103110
verbose_name="Дата изменения", auto_now=True
104111
)
105112

113+
def is_manager(self, user: User) -> bool:
114+
"""
115+
Возвращает True, если пользователь — менеджер этой программы.
116+
"""
117+
if not user or not user.is_authenticated:
118+
return False
119+
return self.managers.filter(pk=user.pk).exists()
120+
106121
class Meta:
107122
verbose_name = "Программа"
108123
verbose_name_plural = "Программы"

partner_programs/serializers.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,35 @@ class Meta:
4949
)
5050

5151

52-
class PartnerProgramWithMaterialsMixin(serializers.ModelSerializer):
52+
class PartnerProgramBaseSerializerMixin(serializers.ModelSerializer):
53+
"""
54+
Базовый миксин для сериализаторов PartnerProgram,
55+
включает общие поля: materials и is_user_manager.
56+
"""
57+
5358
materials = serializers.SerializerMethodField()
59+
is_user_manager = serializers.SerializerMethodField()
5460

55-
def get_materials(self, program):
61+
def get_materials(self, program: PartnerProgram):
5662
materials = program.materials.all()
5763
return PartnerProgramMaterialSerializer(materials, many=True).data
5864

65+
def get_is_user_manager(self, program: PartnerProgram) -> bool:
66+
user = self.context.get("user")
67+
return bool(user and program.is_manager(user))
68+
5969
class Meta:
6070
abstract = True
6171

6272

63-
class PartnerProgramForMemberSerializer(PartnerProgramWithMaterialsMixin):
73+
class PartnerProgramForMemberSerializer(PartnerProgramBaseSerializerMixin):
6474
"""Serializer for PartnerProgram model for member of this program"""
6575

6676
views_count = serializers.SerializerMethodField(method_name="count_views")
6777
links = serializers.SerializerMethodField(method_name="get_links")
78+
is_user_manager = serializers.SerializerMethodField(
79+
method_name="get_is_user_manager"
80+
)
6881

6982
def count_views(self, program):
7083
return get_views_count(program)
@@ -96,10 +109,11 @@ class Meta:
96109
"presentation_address",
97110
"views_count",
98111
"datetime_registration_ends",
112+
"is_user_manager",
99113
)
100114

101115

102-
class PartnerProgramForUnregisteredUserSerializer(PartnerProgramWithMaterialsMixin):
116+
class PartnerProgramForUnregisteredUserSerializer(PartnerProgramBaseSerializerMixin):
103117
"""Serializer for PartnerProgram model for unregistered users in the program"""
104118

105119
class Meta:
@@ -115,6 +129,7 @@ class Meta:
115129
"advertisement_image_address",
116130
"presentation_address",
117131
"datetime_registration_ends",
132+
"is_user_manager",
118133
)
119134

120135

partner_programs/views.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,25 @@ class PartnerProgramList(generics.ListCreateAPIView):
3535

3636

3737
class PartnerProgramDetail(generics.RetrieveAPIView):
38-
queryset = PartnerProgram.objects.all()
38+
queryset = PartnerProgram.objects.prefetch_related("materials", "managers").all()
3939
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
40-
serializer_class = PartnerProgramForUnregisteredUserSerializer
4140

4241
def get(self, request, *args, **kwargs):
43-
try:
44-
program = self.get_object()
45-
is_user_member = program.users.filter(pk=request.user.pk).exists()
46-
serializer_class = (
47-
PartnerProgramForMemberSerializer
48-
if is_user_member
49-
else PartnerProgramForUnregisteredUserSerializer
50-
)
51-
data = serializer_class(program).data
52-
data["is_user_member"] = is_user_member
53-
if request.user.is_authenticated:
54-
add_view(program, request.user)
55-
56-
return Response(data=data, status=status.HTTP_200_OK)
57-
except PartnerProgram.DoesNotExist:
58-
return Response(status=status.HTTP_404_NOT_FOUND)
42+
program = self.get_object()
43+
is_user_member = program.users.filter(pk=request.user.pk).exists()
44+
serializer_class = (
45+
PartnerProgramForMemberSerializer
46+
if is_user_member
47+
else PartnerProgramForUnregisteredUserSerializer
48+
)
49+
serializer = serializer_class(
50+
program, context={"request": request, "user": request.user}
51+
)
52+
data = serializer.data
53+
data["is_user_member"] = is_user_member
54+
if request.user.is_authenticated:
55+
add_view(program, request.user)
56+
return Response(data, status=status.HTTP_200_OK)
5957

6058

6159
class PartnerProgramCreateUserAndRegister(generics.GenericAPIView):

0 commit comments

Comments
 (0)