Skip to content

Commit 0545cbb

Browse files
authored
Merge pull request #556 from PROCOLLAB-github/feture/new_fields_for_user_achievements
Feture/new fields for user achievements
2 parents 48eb1e3 + ded251e commit 0545cbb

11 files changed

Lines changed: 366 additions & 77 deletions

File tree

core/serializers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
from .models import SkillToObject, SkillCategory, Skill
44

55

6+
class EmptySerializer(serializers.Serializer):
7+
pass
8+
9+
610
class SetLikedSerializer(serializers.Serializer):
711
is_liked = serializers.BooleanField()
812

feed/views.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
from django.contrib.contenttypes.models import ContentType
2-
from django.db.models import QuerySet, Q
2+
from django.db.models import Q, QuerySet
33
from rest_framework.generics import CreateAPIView
44
from rest_framework.response import Response
55
from rest_framework.views import APIView
66

7+
from core.serializers import EmptySerializer
78
from feed.pagination import FeedPagination
89
from feed.services import get_liked_news
9-
1010
from news.models import News
11-
from .serializers import NewsFeedListSerializer
12-
from projects.models import Project
1311
from partner_programs.models import PartnerProgramUserProfile
12+
from projects.models import Project
1413
from vacancy.models import Vacancy
1514

15+
from .serializers import NewsFeedListSerializer
16+
1617

1718
class NewSimpleFeed(APIView):
1819
serializator_class = NewsFeedListSerializer
@@ -29,11 +30,9 @@ def _get_filter_data(self) -> list[str]:
2930

3031
def _get_excluded_projects_ids(self) -> list[int]:
3132
"""IDs for exclude projects which in Partner Program."""
32-
excluded_projects = (
33-
PartnerProgramUserProfile.objects
34-
.values_list("project_id", flat=True)
35-
.exclude(project_id__isnull=True)
36-
)
33+
excluded_projects = PartnerProgramUserProfile.objects.values_list(
34+
"project_id", flat=True
35+
).exclude(project_id__isnull=True)
3736
return excluded_projects
3837

3938
def get_queryset(self) -> QuerySet[News]:
@@ -80,6 +79,8 @@ def get(self, *args, **kwargs):
8079

8180

8281
class DevScript(CreateAPIView):
82+
serializer_class = EmptySerializer
83+
8384
def create(self, request):
8485
content_type_project = ContentType.objects.filter(model="project").first()
8586
for project in Project.objects.filter(draft=False):

partner_programs/serializers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ class PartnerProgramForMemberSerializer(PartnerProgramBaseSerializerMixin):
8080

8181
views_count = serializers.SerializerMethodField(method_name="count_views")
8282
links = serializers.SerializerMethodField(method_name="get_links")
83-
is_user_manager = serializers.SerializerMethodField(
84-
method_name="get_is_user_manager"
85-
)
83+
is_user_manager = serializers.SerializerMethodField(method_name="get_is_user_manager")
8684

8785
def count_views(self, program):
8886
return get_views_count(program)

partner_programs/views.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from rest_framework.response import Response
2020
from rest_framework.views import APIView
2121

22-
from core.serializers import SetLikedSerializer, SetViewedSerializer
22+
from core.serializers import EmptySerializer, SetLikedSerializer, SetViewedSerializer
2323
from core.services import add_view, set_like
2424
from partner_programs.helpers import date_to_iso
2525
from partner_programs.models import (
@@ -69,6 +69,7 @@ class PartnerProgramList(generics.ListCreateAPIView):
6969
class PartnerProgramDetail(generics.RetrieveAPIView):
7070
queryset = PartnerProgram.objects.prefetch_related("materials", "managers").all()
7171
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
72+
serializer_class = PartnerProgramForUnregisteredUserSerializer
7273

7374
def get(self, request, *args, **kwargs):
7475
program = self.get_object()
@@ -320,7 +321,7 @@ def put(self, request, project_id, *args, **kwargs):
320321

321322
class PartnerProgramProjectSubmitView(GenericAPIView):
322323
permission_classes = [IsAuthenticated, IsProjectLeader]
323-
serializer_class = None
324+
serializer_class = EmptySerializer
324325
queryset = PartnerProgramProject.objects.all()
325326

326327
@swagger_auto_schema(
@@ -375,6 +376,7 @@ class ProgramProjectFilterAPIView(GenericAPIView):
375376
serializer_class = ProgramProjectFilterRequestSerializer
376377
permission_classes = [permissions.IsAuthenticated]
377378
pagination_class = PartnerProgramPagination
379+
queryset = PartnerProgram.objects.none()
378380

379381
def post(self, request, pk):
380382
serializer = self.get_serializer(data=request.data)
@@ -445,6 +447,9 @@ class PartnerProgramProjectsAPIView(generics.ListAPIView):
445447
pagination_class = PartnerProgramPagination
446448

447449
def get_queryset(self):
450+
if "pk" not in self.kwargs:
451+
return Project.objects.none()
452+
448453
program = get_object_or_404(PartnerProgram, pk=self.kwargs["pk"])
449454
return Project.objects.filter(program_links__partner_program=program).distinct()
450455

procollab/celery.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
import os
22

33
from celery import Celery
4-
import django
54

6-
# from celery.schedules import crontab
75
from celery.schedules import crontab
86

97
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "procollab.settings")
10-
django.setup()
118

12-
app = Celery("procollab")
139

10+
app = Celery("procollab")
1411
app.config_from_object("django.conf:settings", namespace="CELERY")
15-
1612
app.autodiscover_tasks()
1713

1814
app.conf.task_serializer = "json"
19-
2015
app.conf.beat_schedule = {
2116
"outdate_vacancy": {
2217
"task": "vacancy.tasks.email_notificate_vacancy_outdated",

projects/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@
5151
IsProjectLeaderOrReadOnlyForNonDrafts,
5252
TimingAfterEndsProgramPermission,
5353
)
54+
from core.serializers import EmptySerializer
5455
from projects.serializers import (
5556
AchievementDetailSerializer,
5657
AchievementListSerializer,
5758
CompanySerializer,
58-
EmptySerializer,
5959
ProjectCollaboratorSerializer,
6060
ProjectCompanySerializer,
6161
ProjectCompanyUpdateSerializer,

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)