From 1e505005015e8a0d0952d4a74f633ccb1a115391 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Wed, 2 Jul 2025 14:12:53 +0500 Subject: [PATCH 1/3] =?UTF-8?q?=D0=92=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8F=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D0=BE=D0=BB=D1=8F=20=D0=9C=D0=BE=D1=81?= =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D0=B8=D1=82=D0=B5=D1=85=D0=B0;=20=D0=9D?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D1=8C=20=D0=B0=D0=B4=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B0;?= =?UTF-8?q?=20=D0=9F=D1=80=D0=BE=D0=B8=D0=B7=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/admin.py | 5 +++ ...tomuser_is_mospolytech_student_and_more.py | 33 +++++++++++++++++++ users/models.py | 12 +++++++ 3 files changed, 50 insertions(+) create mode 100644 users/migrations/0053_customuser_is_mospolytech_student_and_more.py diff --git a/users/admin.py b/users/admin.py index 5e5a6ae7..49085e9b 100644 --- a/users/admin.py +++ b/users/admin.py @@ -124,6 +124,11 @@ class CustomUserAdmin(admin.ModelAdmin): "Важные даты", {"fields": ("last_login", "date_joined")}, ), + ( + "Студенты мосполитеха", + {"fields": ("is_mospolytech_student", "study_group")}, + ), + ) list_display = ( diff --git a/users/migrations/0053_customuser_is_mospolytech_student_and_more.py b/users/migrations/0053_customuser_is_mospolytech_student_and_more.py new file mode 100644 index 00000000..e873913a --- /dev/null +++ b/users/migrations/0053_customuser_is_mospolytech_student_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.11 on 2025-07-02 08:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0052_remove_customuser_organization"), + ] + + operations = [ + migrations.AddField( + model_name="customuser", + name="is_mospolytech_student", + field=models.BooleanField( + default=False, + help_text="Флаг, указывающий, является ли пользователь студентом МосПолитеха", + verbose_name="Студент Московского Политеха", + ), + ), + migrations.AddField( + model_name="customuser", + name="study_group", + field=models.CharField( + blank=True, + help_text="Краткое обозначение учебной группы (до 10 символов)", + max_length=10, + null=True, + verbose_name="Учебная группа", + ), + ), + ] diff --git a/users/models.py b/users/models.py index b21af20a..6b3d3276 100644 --- a/users/models.py +++ b/users/models.py @@ -140,6 +140,18 @@ class CustomUser(AbstractUser): verbose_name="Временная мера для переноса навыка", help_text="Yes если оба поля `v2_speciality` и `skills` есть, No если поля не перенеслись" ) + is_mospolytech_student = models.BooleanField( + default=False, + verbose_name="Студент Московского Политеха", + help_text="Флаг, указывающий, является ли пользователь студентом МосПолитеха" + ) + study_group = models.CharField( + max_length=10, + null=True, + blank=True, + verbose_name="Учебная группа", + help_text="Краткое обозначение учебной группы (до 10 символов)" + ) USERNAME_FIELD = "email" REQUIRED_FIELDS = [] From 326980a073c2e7b0a211f0d5bbaefc4737ecec59 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Wed, 2 Jul 2025 14:43:00 +0500 Subject: [PATCH 2/3] =?UTF-8?q?=D0=92=20=D1=80=D1=83=D1=87=D0=BA=D0=B8=20u?= =?UTF-8?q?sers/id=20users/current=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8F=20=D1=81=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=BC=20=D0=9C=D0=BE=D1=81=D0=9F=D0=BE=D0=BB=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D1=85=D0=B0=20=D0=BF=D1=91=D1=82=D0=BC=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=82=D0=BE=D1=80=20UserDetailSe?= =?UTF-8?q?rializer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/users/serializers.py b/users/serializers.py index ed66d290..e3fa9712 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -451,6 +451,8 @@ class Meta: "projects", "programs", "dataset_migration_applied", + "is_mospolytech_student", # новое булево поле + "study_group", ] @transaction.atomic From 614db3d7f7ee3deaa488e30343bf64145525e170 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Wed, 2 Jul 2025 14:52:34 +0500 Subject: [PATCH 3/3] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B2=D1=91=D0=BB=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=20=D0=BA=20=D0=B5=D0=B4=D0=B8=D0=BD?= =?UTF-8?q?=D0=BE=D0=BC=D1=83=20=D1=81=D1=82=D0=B8=D0=BB=D1=8E=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B4=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F:?= =?UTF-8?q?=20=D0=BE=D1=82=D1=81=D0=BE=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B,=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BE=D1=82?= =?UTF-8?q?=D1=81=D1=82=D1=83=D0=BF=D1=8B=20=D0=B8=20=D0=BE=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=87=D0=B8=D0=BB=20=D0=B4=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D1=83=20=D1=81=D1=82=D1=80=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/admin.py | 16 ++++--------- users/models.py | 57 +++++++++++++++++++++++--------------------- users/serializers.py | 19 +++++++-------- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/users/admin.py b/users/admin.py index 49085e9b..c10356bd 100644 --- a/users/admin.py +++ b/users/admin.py @@ -124,11 +124,10 @@ class CustomUserAdmin(admin.ModelAdmin): "Важные даты", {"fields": ("last_login", "date_joined")}, ), - ( + ( "Студенты мосполитеха", {"fields": ("is_mospolytech_student", "study_group")}, ), - ) list_display = ( @@ -301,10 +300,7 @@ def get_export_users_emails(self, users): users = ( CustomUser.objects.all() .select_related("v2_speciality") - .filter( - birthday__lte=date_limit_18, - birthday__gte=date_limit_22 - ) + .filter(birthday__lte=date_limit_18, birthday__gte=date_limit_22) ) # little_mans = users.filter(birthday__lte=date_limit_18) # big_mans = users.exclude(id__in=little_mans.values_list("id", flat=True)) @@ -317,13 +313,9 @@ def get_export_users_emails(self, users): response_data.append( [ user.first_name + " " + user.last_name, - (today.year - user.birthday.year) - if user.birthday.year - else None, + (today.year - user.birthday.year) if user.birthday.year else None, user.city, - user.v2_speciality - if user.v2_speciality - else user.speciality, + user.v2_speciality if user.v2_speciality else user.speciality, user.email, ] ) diff --git a/users/models.py b/users/models.py index 6b3d3276..3f231842 100644 --- a/users/models.py +++ b/users/models.py @@ -1,25 +1,24 @@ +from django.contrib.auth.models import AbstractUser +from django.contrib.contenttypes.fields import GenericRelation +from django.core.exceptions import ValidationError from django.db import models from django.db.models import QuerySet from django.utils import timezone -from django.core.exceptions import ValidationError -from django.contrib.auth.models import AbstractUser -from django.contrib.contenttypes.fields import GenericRelation - from django_stubs_ext.db.models import TypedModelMeta from users import constants from users.managers import ( CustomUserManager, - UserAchievementManager, LikesOnProjectManager, + UserAchievementManager, ) +from users.utils import normalize_user_phone from users.validators import ( user_birthday_validator, - user_name_validator, user_experience_years_range_validator, + user_name_validator, user_phone_number_validation, ) -from users.utils import normalize_user_phone def get_default_user_type(): @@ -102,7 +101,7 @@ class CustomUser(AbstractUser): null=True, blank=True, verbose_name="Номер телефона", - help_text="Пример: +7 XXX XX-XX-XX | +7XXXXXXXXX | +7 (XXX) XX-XX-XX" + help_text="Пример: +7 XXX XX-XX-XX | +7XXXXXXXXX | +7 (XXX) XX-XX-XX", ) v2_speciality = models.ForeignKey( on_delete=models.SET_NULL, @@ -138,19 +137,19 @@ class CustomUser(AbstractUser): blank=True, default=False, verbose_name="Временная мера для переноса навыка", - help_text="Yes если оба поля `v2_speciality` и `skills` есть, No если поля не перенеслись" + help_text="Yes если оба поля `v2_speciality` и `skills` есть, No если поля не перенеслись", ) is_mospolytech_student = models.BooleanField( default=False, verbose_name="Студент Московского Политеха", - help_text="Флаг, указывающий, является ли пользователь студентом МосПолитеха" + help_text="Флаг, указывающий, является ли пользователь студентом МосПолитеха", ) study_group = models.CharField( max_length=10, null=True, blank=True, verbose_name="Учебная группа", - help_text="Краткое обозначение учебной группы (до 10 символов)" + help_text="Краткое обозначение учебной группы (до 10 символов)", ) USERNAME_FIELD = "email" @@ -193,7 +192,9 @@ def calculate_ordering_score(self) -> int: def get_project_chats(self) -> QuerySet: from chats.models import ProjectChat - user_project_ids = self.collaborations.all().values_list("project_id", flat=True) + user_project_ids = self.collaborations.all().values_list( + "project_id", flat=True + ) return ProjectChat.objects.filter(project__in=user_project_ids) def get_full_name(self) -> str: @@ -204,7 +205,11 @@ def get_user_age(self) -> int: return None today = timezone.now() birthday = self.birthday - return today.year - birthday.year - ((today.month, today.day) < (birthday.month, birthday.day)) + return ( + today.year + - birthday.year + - ((today.month, today.day) < (birthday.month, birthday.day)) + ) def __str__(self) -> str: return f"User<{self.id}> - {self.first_name} {self.last_name}" @@ -454,6 +459,7 @@ class Meta(TypedModelMeta): class AbstractUserExperience(models.Model): """Abstact help model for user work|education experience.""" + organization_name = models.CharField( max_length=255, verbose_name="Наименование организации", @@ -481,9 +487,7 @@ class Meta: abstract = True def __str__(self) -> str: - return ( - f"id: {self.id} - ({self.user.first_name} {self.user.last_name} user_id: {self.user.id})" - ) + return f"id: {self.id} - ({self.user.first_name} {self.user.last_name} user_id: {self.user.id})" def clean(self) -> None: """Validate both years `entry` <`completion`""" @@ -553,6 +557,7 @@ class UserWorkExperience(AbstractUserExperience): entry_year: PositiveSmallIntegerField Year of admission. completion_year: PositiveSmallIntegerField Year of dismissal. """ + user = models.ForeignKey( to=CustomUser, on_delete=models.CASCADE, @@ -582,6 +587,7 @@ class UserLanguages(models.Model): language: CharField(choise) languages. language_level: CharField(choise) language level. """ + user = models.ForeignKey( to=CustomUser, on_delete=models.CASCADE, @@ -616,7 +622,9 @@ def clean(self) -> None: """ super().clean() user_languages = self.user.user_languages.values_list("language", flat=True) - if (self.language not in user_languages) and len(user_languages) == constants.USER_MAX_LANGUAGES_COUNT: + if (self.language not in user_languages) and len( + user_languages + ) == constants.USER_MAX_LANGUAGES_COUNT: raise ValidationError(constants.COUNT_LANGUAGES_VALIDATION_MESSAGE) def save(self, *args, **kwargs): @@ -624,9 +632,7 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) def __str__(self) -> str: - return ( - f"id: {self.id} - ({self.user.first_name} {self.user.last_name} user_id: {self.user.id})" - ) + return f"id: {self.id} - ({self.user.first_name} {self.user.last_name} user_id: {self.user.id})" class UserSkillConfirmation(models.Model): @@ -638,15 +644,12 @@ class UserSkillConfirmation(models.Model): confirmed_by: FK CustomUser. confirmed_at: DateTimeField. """ + skill_to_object = models.ForeignKey( - "core.SkillToObject", - on_delete=models.CASCADE, - related_name="confirmations" + "core.SkillToObject", on_delete=models.CASCADE, related_name="confirmations" ) confirmed_by = models.ForeignKey( - CustomUser, - on_delete=models.CASCADE, - related_name="skill_confirmations" + CustomUser, on_delete=models.CASCADE, related_name="skill_confirmations" ) confirmed_at = models.DateTimeField(auto_now_add=True) @@ -654,7 +657,7 @@ class Meta: constraints = [ models.UniqueConstraint( fields=["skill_to_object", "confirmed_by"], - name="unique_skill_confirmed_by" + name="unique_skill_confirmed_by", ) ] verbose_name = "Подтверждение навыка" diff --git a/users/serializers.py b/users/serializers.py index e3fa9712..fe01c8d4 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,23 +1,22 @@ from typing import Any -from django.db import transaction from django.contrib.contenttypes.models import ContentType -from django.forms.models import model_to_dict from django.core.cache import cache from django.core.exceptions import ValidationError as DjangoValidationError +from django.db import transaction +from django.forms.models import model_to_dict from rest_framework import serializers from rest_framework.exceptions import ValidationError +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from core.models import Skill, SkillToObject, Specialization, SpecializationCategory from core.serializers import SkillToObjectSerializer -from core.models import SpecializationCategory, Specialization, Skill, SkillToObject from core.services import get_views_count from core.utils import get_user_online_cache_key from partner_programs.models import PartnerProgram, PartnerProgramUserProfile -from projects.models import Project, Collaborator +from projects.models import Collaborator, Project from projects.validators import validate_project from users import constants -from users.utils import normalize_user_phone -from users.validators import specialization_exists_validator from users.models import ( CustomUser, Expert, @@ -26,12 +25,12 @@ Mentor, UserAchievement, UserEducation, - UserWorkExperience, - UserSkillConfirmation, UserLanguages, + UserSkillConfirmation, + UserWorkExperience, ) - -from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from users.utils import normalize_user_phone +from users.validators import specialization_exists_validator class AchievementListSerializer(serializers.ModelSerializer[UserAchievement]):