1+ import datetime
12from functools import partial
23
34from django .contrib .auth .models import AbstractUser
45from django .contrib .contenttypes .fields import GenericRelation
56from django .core .exceptions import ValidationError
6- from django .core .validators import URLValidator
7+ from django .core .validators import MaxValueValidator , MinValueValidator , URLValidator
78from django .db import models
89from django .db .models import QuerySet
910from 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
278334class 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