Skip to content

Commit ded251e

Browse files
committed
Расширена модель "Достижения Пользователя"
1 parent 0d58831 commit ded251e

5 files changed

Lines changed: 342 additions & 56 deletions

File tree

users/admin.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
from django.http import HttpResponse
99
from django.shortcuts import redirect
1010
from django.urls import path
11+
from django.utils.html import format_html
1112
from django.utils.timezone import now
1213

1314
from core.admin import SkillToObjectInline
1415
from core.utils import XlsxFileToExport
1516
from mailing.views import MailingTemplateRender
17+
from users.models import UserAchievementFile
1618
from users.services.users_activity import UserActivityDataPreparer
1719

1820
from .helpers import force_verify_user, send_verification_completed_email
@@ -377,9 +379,30 @@ def get_export_users_emails(self, users):
377379
return response
378380

379381

382+
class UserAchievementFileInline(admin.TabularInline):
383+
model = UserAchievementFile
384+
extra = 0
385+
autocomplete_fields = ("file",)
386+
387+
380388
@admin.register(UserAchievement)
381389
class UserAchievementAdmin(admin.ModelAdmin):
382-
list_display = ("id", "title", "status", "user")
390+
list_display = ("id", "title", "status", "user", "year", "file_link")
391+
list_filter = ("year",)
392+
search_fields = ("title", "status", "user__email", "user__username")
393+
inlines = [UserAchievementFileInline]
394+
395+
def file_link(self, obj):
396+
uf = obj.files.select_related("file").first()
397+
if uf and uf.file and uf.file.link:
398+
return format_html(
399+
"<a href='{}' target='_blank'>открыть</a> ({} файл(ов))",
400+
uf.file.link,
401+
obj.files.count(),
402+
)
403+
return "—"
404+
405+
file_link.short_description = "Файл(ы)"
383406

384407

385408
@admin.register(UserLink)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Generated by Django 4.2.24 on 2025-10-20 08:06
2+
3+
import django.core.validators
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("files", "0007_auto_20230929_1727"),
12+
("users", "0055_alter_customuser_avatar_alter_customuser_email"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="UserAchievementFile",
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+
],
29+
options={
30+
"verbose_name": "Файл достижения",
31+
"verbose_name_plural": "Файлы достижения",
32+
},
33+
),
34+
migrations.AddField(
35+
model_name="userachievement",
36+
name="year",
37+
field=models.PositiveSmallIntegerField(
38+
blank=True,
39+
db_index=True,
40+
null=True,
41+
validators=[
42+
django.core.validators.MinValueValidator(1900),
43+
django.core.validators.MaxValueValidator(2025),
44+
],
45+
verbose_name="Год достижения",
46+
),
47+
),
48+
migrations.AddConstraint(
49+
model_name="userachievement",
50+
constraint=models.UniqueConstraint(
51+
fields=("user", "title", "year"), name="uniq_user_achievement_title_year"
52+
),
53+
),
54+
migrations.AddField(
55+
model_name="userachievementfile",
56+
name="achievement",
57+
field=models.ForeignKey(
58+
on_delete=django.db.models.deletion.CASCADE,
59+
related_name="files",
60+
to="users.userachievement",
61+
verbose_name="Достижение",
62+
),
63+
),
64+
migrations.AddField(
65+
model_name="userachievementfile",
66+
name="file",
67+
field=models.ForeignKey(
68+
on_delete=django.db.models.deletion.CASCADE,
69+
related_name="achievement_links",
70+
to="files.userfile",
71+
verbose_name="Файл",
72+
),
73+
),
74+
migrations.AddConstraint(
75+
model_name="userachievementfile",
76+
constraint=models.UniqueConstraint(
77+
fields=("achievement", "file"), name="uniq_achievement_file_link"
78+
),
79+
),
80+
]

users/models.py

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import datetime
12
from functools import partial
23

34
from django.contrib.auth.models import AbstractUser
45
from django.contrib.contenttypes.fields import GenericRelation
56
from django.core.exceptions import ValidationError
6-
from django.core.validators import URLValidator
7+
from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator
78
from django.db import models
89
from django.db.models import QuerySet
910
from django.utils import timezone
@@ -211,9 +212,7 @@ def calculate_ordering_score(self) -> int:
211212
def get_project_chats(self) -> QuerySet:
212213
from chats.models import ProjectChat
213214

214-
user_project_ids = self.collaborations.all().values_list(
215-
"project_id", flat=True
216-
)
215+
user_project_ids = self.collaborations.all().values_list("project_id", flat=True)
217216
return ProjectChat.objects.filter(project__in=user_project_ids)
218217

219218
def get_full_name(self) -> str:
@@ -258,7 +257,16 @@ class UserAchievement(models.Model):
258257

259258
title = models.CharField(max_length=256)
260259
status = models.CharField(max_length=256)
261-
260+
year = models.PositiveSmallIntegerField(
261+
"Год достижения",
262+
null=True,
263+
blank=True,
264+
db_index=True,
265+
validators=[
266+
MinValueValidator(1900),
267+
MaxValueValidator(datetime.date.today().year),
268+
],
269+
)
262270
user = models.ForeignKey(
263271
CustomUser,
264272
on_delete=models.CASCADE,
@@ -274,6 +282,54 @@ class Meta(TypedModelMeta):
274282
verbose_name = "Достижение"
275283
verbose_name_plural = "Достижения"
276284

285+
constraints = [
286+
models.UniqueConstraint(
287+
fields=["user", "title", "year"],
288+
name="uniq_user_achievement_title_year",
289+
),
290+
]
291+
292+
293+
class UserAchievementFile(models.Model):
294+
ALLOWED_EXTENSIONS = {"pdf", "doc", "docx", "jpg", "jpeg", "png"}
295+
MAX_UPLOAD_SIZE = 50 * 1024 * 1024
296+
achievement = models.ForeignKey(
297+
UserAchievement,
298+
on_delete=models.CASCADE,
299+
related_name="files",
300+
verbose_name="Достижение",
301+
)
302+
file = models.ForeignKey(
303+
"files.UserFile",
304+
on_delete=models.CASCADE,
305+
related_name="achievement_links",
306+
verbose_name="Файл",
307+
)
308+
309+
class Meta:
310+
constraints = [
311+
models.UniqueConstraint(
312+
fields=["achievement", "file"], name="uniq_achievement_file_link"
313+
)
314+
]
315+
verbose_name = "Файл достижения"
316+
verbose_name_plural = "Файлы достижения"
317+
318+
def clean(self):
319+
super().clean()
320+
if not self.file or not self.achievement:
321+
return
322+
323+
if self.file.user_id is None or self.file.user_id != self.achievement.user_id:
324+
raise ValidationError("Файл должен принадлежать тому же пользователю.")
325+
326+
if self.file.size and self.file.size > self.MAX_UPLOAD_SIZE:
327+
raise ValidationError("Размер файла превышает 50 МБ.")
328+
329+
ext = (self.file.extension or "").lower()
330+
if ext and ext not in self.ALLOWED_EXTENSIONS:
331+
raise ValidationError("Недопустимое расширение файла.")
332+
277333

278334
class AbstractUserWithRole(models.Model):
279335
"""
@@ -556,6 +612,12 @@ class UserEducation(AbstractUserExperience):
556612
null=True,
557613
verbose_name="Статус по обучению",
558614
)
615+
description = models.TextField(
616+
max_length=1000,
617+
null=True,
618+
blank=True,
619+
verbose_name="Направление обучения",
620+
)
559621

560622
class Meta:
561623
verbose_name = "Образование пользователя"
@@ -583,6 +645,12 @@ class UserWorkExperience(AbstractUserExperience):
583645
related_name="work_experience",
584646
verbose_name="Пользователь",
585647
)
648+
description = models.TextField(
649+
max_length=1000,
650+
null=True,
651+
blank=True,
652+
verbose_name="Краткое описание деятельности",
653+
)
586654
job_position = models.CharField(
587655
max_length=256,
588656
null=True,

0 commit comments

Comments
 (0)