Skip to content

Commit 046c5fa

Browse files
committed
Информация о файлах в достижениях пользователя возвращается в подробном виде при GET запросе, но принимается в сокращённом при POST запросе
1 parent 1e9b6a3 commit 046c5fa

6 files changed

Lines changed: 157 additions & 83 deletions

File tree

users/admin.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,14 @@ class UserAchievementAdmin(admin.ModelAdmin):
393393
inlines = [UserAchievementFileInline]
394394

395395
def file_link(self, obj):
396-
uf = obj.files.select_related("file").first()
397-
if uf and uf.file and uf.file.link:
396+
first_file = obj.files.first()
397+
count = obj.files.count()
398+
399+
if first_file and getattr(first_file, "link", None):
398400
return format_html(
399401
"<a href='{}' target='_blank'>открыть</a> ({} файл(ов))",
400-
uf.file.link,
401-
obj.files.count(),
402+
first_file.link,
403+
count,
402404
)
403405
return "—"
404406

users/managers.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from django.apps import apps
12
from django.contrib.auth.hashers import make_password
23
from django.contrib.auth.models import UserManager
3-
from django.db.models import Manager
4+
from django.db import models
5+
from django.db.models import Manager, Prefetch
46

57
from users.constants import MEMBER
68

@@ -53,19 +55,35 @@ def _create_user(self, email, password, **extra_fields):
5355
return user
5456

5557

56-
class UserAchievementManager(Manager):
57-
def get_achievements_for_list_view(self):
58+
FILE_FIELDS = (
59+
"id",
60+
"name",
61+
"extension",
62+
"mime_type",
63+
"size",
64+
"link",
65+
"user_id",
66+
"datetime_uploaded",
67+
)
68+
69+
70+
class UserAchievementManager(models.Manager):
71+
def _with_user_and_files(self):
72+
UserFile = apps.get_model("files", "UserFile")
5873
return (
5974
self.get_queryset()
6075
.select_related("user")
61-
.only("id", "title", "status", "user__id")
76+
.prefetch_related(Prefetch("files", queryset=UserFile.objects.all()))
77+
)
78+
79+
def get_achievements_for_list_view(self):
80+
return self._with_user_and_files().only(
81+
"id", "title", "status", "year", "user_id"
6282
)
6383

6484
def get_achievements_for_detail_view(self):
65-
return (
66-
self.get_queryset()
67-
.select_related("user")
68-
.only("id", "title", "status", "user")
85+
return self._with_user_and_files().only(
86+
"id", "title", "status", "year", "user_id"
6987
)
7088

7189

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Generated by Django 4.2.24 on 2025-10-24 06:41
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("files", "0007_auto_20230929_1727"),
11+
("users", "0057_alter_usereducation_description_and_more"),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name="userachievement",
17+
name="files",
18+
field=models.ManyToManyField(
19+
blank=True,
20+
related_name="achievements",
21+
related_query_name="achievement",
22+
through="users.UserAchievementFile",
23+
to="files.userfile",
24+
),
25+
),
26+
migrations.AlterField(
27+
model_name="userachievementfile",
28+
name="achievement",
29+
field=models.ForeignKey(
30+
on_delete=django.db.models.deletion.CASCADE,
31+
related_name="file_links",
32+
related_query_name="file_link",
33+
to="users.userachievement",
34+
verbose_name="Достижение",
35+
),
36+
),
37+
migrations.AlterField(
38+
model_name="userachievementfile",
39+
name="file",
40+
field=models.ForeignKey(
41+
on_delete=django.db.models.deletion.CASCADE,
42+
related_name="achievement_links",
43+
related_query_name="achievement_link",
44+
to="files.userfile",
45+
verbose_name="Файл",
46+
),
47+
),
48+
]

users/models.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,14 @@ class UserAchievement(models.Model):
272272
on_delete=models.CASCADE,
273273
related_name="achievements",
274274
)
275+
files = models.ManyToManyField(
276+
"files.UserFile",
277+
through="UserAchievementFile",
278+
through_fields=("achievement", "file"),
279+
related_name="achievements",
280+
related_query_name="achievement",
281+
blank=True,
282+
)
275283

276284
objects = UserAchievementManager()
277285

@@ -293,16 +301,19 @@ class Meta(TypedModelMeta):
293301
class UserAchievementFile(models.Model):
294302
ALLOWED_EXTENSIONS = {"pdf", "doc", "docx", "jpg", "jpeg", "png", "webp"}
295303
MAX_UPLOAD_SIZE = 50 * 1024 * 1024
304+
296305
achievement = models.ForeignKey(
297306
UserAchievement,
298307
on_delete=models.CASCADE,
299-
related_name="files",
308+
related_name="file_links",
309+
related_query_name="file_link",
300310
verbose_name="Достижение",
301311
)
302312
file = models.ForeignKey(
303313
"files.UserFile",
304314
on_delete=models.CASCADE,
305315
related_name="achievement_links",
316+
related_query_name="achievement_link",
306317
verbose_name="Файл",
307318
)
308319

users/serializers.py

Lines changed: 55 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from core.services import get_views_count
1616
from core.utils import get_user_online_cache_key
1717
from files.models import UserFile
18+
from files.serializers import UserFileSerializer
1819
from partner_programs.models import PartnerProgram, PartnerProgramUserProfile
1920
from projects.models import Collaborator, Project
2021
from projects.validators import validate_project
@@ -36,23 +37,69 @@
3637
from users.validators import specialization_exists_validator
3738

3839

40+
class UserFileReadSerializer(serializers.ModelSerializer):
41+
class Meta:
42+
model = UserFile
43+
fields = ("link", "name", "extension", "mime_type", "size")
44+
45+
46+
class FileLinkField(serializers.SlugRelatedField):
47+
"""
48+
write-only: принимает link, маппит на UserFile текущего пользователя.
49+
"""
50+
51+
def get_queryset(self):
52+
request = self.context.get("request")
53+
qs = UserFile.objects.all()
54+
if request and request.user.is_authenticated:
55+
return qs.filter(user=request.user)
56+
return qs.none()
57+
58+
3959
class AchievementListSerializer(serializers.ModelSerializer):
4060
year = serializers.IntegerField(required=False, allow_null=True)
41-
file_link = serializers.SerializerMethodField()
61+
files = UserFileSerializer(many=True, read_only=True)
4262

4363
class Meta:
4464
model = UserAchievement
45-
fields = ["id", "title", "status", "year", "file_link"]
65+
fields = ["id", "title", "status", "year", "files"]
4666

47-
def get_file_link(self, obj):
48-
uaf = obj.files.first()
49-
return uaf.file.link if (uaf and uaf.file) else None
5067

68+
class AchievementDetailSerializer(serializers.ModelSerializer):
69+
files = UserFileSerializer(many=True, read_only=True)
70+
file_links = FileLinkField(
71+
slug_field="link",
72+
many=True,
73+
write_only=True,
74+
required=False,
75+
)
5176

52-
class UserFileReadSerializer(serializers.ModelSerializer):
5377
class Meta:
54-
model = UserFile
55-
fields = ("link", "name", "extension", "mime_type", "size")
78+
model = UserAchievement
79+
fields = [
80+
"id",
81+
"title",
82+
"status",
83+
"year",
84+
"files",
85+
"file_links",
86+
]
87+
88+
@transaction.atomic
89+
def create(self, validated_data):
90+
file_objs = validated_data.pop("file_links", [])
91+
achievement = super().create(validated_data)
92+
if file_objs:
93+
achievement.files.set(file_objs)
94+
return achievement
95+
96+
@transaction.atomic
97+
def update(self, instance, validated_data):
98+
file_objs = validated_data.pop("file_links", None)
99+
achievement = super().update(instance, validated_data)
100+
if file_objs is not None:
101+
achievement.files.set(file_objs)
102+
return achievement
56103

57104

58105
class AchievementFileReadSerializer(serializers.ModelSerializer):
@@ -873,66 +920,6 @@ class Meta:
873920
]
874921

875922

876-
class AchievementDetailSerializer(serializers.ModelSerializer):
877-
file_link = serializers.URLField(required=False, allow_null=True)
878-
user = serializers.PrimaryKeyRelatedField(read_only=True)
879-
880-
class Meta:
881-
model = UserAchievement
882-
fields = ["id", "title", "status", "year", "user", "file_link"]
883-
read_only_fields = ["id", "user"]
884-
885-
def to_representation(self, instance):
886-
data = super().to_representation(instance)
887-
rel = instance.files.first() # UserAchievementFile
888-
data["file_link"] = rel.file.link if (rel and rel.file) else None
889-
return data
890-
891-
def validate_year(self, value):
892-
import datetime
893-
894-
if value is None:
895-
return value
896-
cur = datetime.date.today().year
897-
if value < 1900 or value > cur:
898-
raise serializers.ValidationError("Год вне допустимого диапазона.")
899-
return value
900-
901-
@transaction.atomic
902-
def _set_single_file(self, achievement: UserAchievement, link: str | None):
903-
UserAchievementFile.objects.filter(achievement=achievement).delete()
904-
905-
if not link:
906-
return
907-
908-
uf, _ = UserFile.objects.get_or_create(link=link)
909-
910-
rel = UserAchievementFile(achievement=achievement, file=uf)
911-
rel.clean()
912-
rel.save()
913-
914-
@transaction.atomic
915-
def create(self, validated_data):
916-
link = validated_data.pop("file_link", None)
917-
achievement = UserAchievement.objects.create(**validated_data)
918-
self._set_single_file(achievement, link)
919-
return achievement
920-
921-
@transaction.atomic
922-
def update(self, instance, validated_data):
923-
sentinel = object()
924-
link = validated_data.pop("file_link", sentinel)
925-
926-
for attr, val in validated_data.items():
927-
setattr(instance, attr, val)
928-
instance.save()
929-
930-
if link is not sentinel:
931-
self._set_single_file(instance, link)
932-
933-
return instance
934-
935-
936923
class EmailSerializer(serializers.Serializer):
937924
email = serializers.EmailField()
938925

users/views.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,11 @@ class AchievementList(ListCreateAPIView):
343343
POST /api/users/achievements/
344344
"""
345345

346-
queryset = UserAchievement.objects.get_achievements_for_list_view()
346+
queryset = (
347+
UserAchievement.objects.get_achievements_for_list_view()
348+
.select_related("user")
349+
.prefetch_related("files")
350+
)
347351

348352
def get_permissions(self):
349353
if self.request.method == "POST":
@@ -388,7 +392,11 @@ class AchievementDetail(RetrieveUpdateDestroyAPIView):
388392
DELETE /api/users/achievements/{id}/
389393
"""
390394

391-
queryset = UserAchievement.objects.get_achievements_for_detail_view()
395+
queryset = (
396+
UserAchievement.objects.get_achievements_for_detail_view()
397+
.select_related("user")
398+
.prefetch_related("files")
399+
)
392400
serializer_class = AchievementDetailSerializer
393401
permission_classes = [IsAchievementOwnerOrReadOnly]
394402

0 commit comments

Comments
 (0)