diff --git a/users/admin.py b/users/admin.py index 5e5a6ae7..c10356bd 100644 --- a/users/admin.py +++ b/users/admin.py @@ -124,6 +124,10 @@ class CustomUserAdmin(admin.ModelAdmin): "Важные даты", {"fields": ("last_login", "date_joined")}, ), + ( + "Студенты мосполитеха", + {"fields": ("is_mospolytech_student", "study_group")}, + ), ) list_display = ( @@ -296,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)) @@ -312,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/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..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,7 +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="Флаг, указывающий, является ли пользователь студентом МосПолитеха", + ) + study_group = models.CharField( + max_length=10, + null=True, + blank=True, + verbose_name="Учебная группа", + help_text="Краткое обозначение учебной группы (до 10 символов)", ) USERNAME_FIELD = "email" @@ -181,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: @@ -192,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}" @@ -442,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="Наименование организации", @@ -469,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`""" @@ -541,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, @@ -570,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, @@ -604,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): @@ -612,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): @@ -626,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) @@ -642,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 ed66d290..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]): @@ -451,6 +450,8 @@ class Meta: "projects", "programs", "dataset_migration_applied", + "is_mospolytech_student", # новое булево поле + "study_group", ] @transaction.atomic