From 1e505005015e8a0d0952d4a74f633ccb1a115391 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Wed, 2 Jul 2025 14:12:53 +0500 Subject: [PATCH 01/15] =?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 02/15] =?UTF-8?q?=D0=92=20=D1=80=D1=83=D1=87=D0=BA=D0=B8?= =?UTF-8?q?=20users/id=20users/current=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8F=20=D1=81=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=BC=20=D0=9C=D0=BE=D1=81=D0=9F=D0=BE=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=20=D0=BF=D1=91=D1=82=D0=BC=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=82=D0=BE=D1=80=20UserDe?= =?UTF-8?q?tailSerializer?= 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 03/15] =?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]): From 868defd44da75db5e1453319bcc9773d07d3b5cd Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Fri, 4 Jul 2025 15:48:11 +0500 Subject: [PATCH 04/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=BB=D0=B2?= =?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=D0=B2=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20=D0=9F=D1=80=D0=BE=D0=B5=D0=BA=D1=82=20=D1=81=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=9C=D0=BE=D1=81=D0=9F=D0=BE=D0=BB=D0=B8=D1=82=D0=B5=D1=85?= =?UTF-8?q?=D0=B0;=20=D0=94=D0=BE=D0=B1=D0=B0=D0=BB=D0=B5=D0=B2=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B2=20=D0=9F=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B5=D0=B0=D0=B4=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=B0?= =?UTF-8?q?=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- projects/admin.py | 56 +++++++++++++ ...project_direction_project_goal_and_more.py | 80 +++++++++++++++++++ projects/models.py | 45 +++++++++++ 3 files changed, 181 insertions(+) create mode 100644 projects/migrations/0024_project_actuality_project_direction_project_goal_and_more.py diff --git a/projects/admin.py b/projects/admin.py index 62785951..ffc35da6 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -17,11 +17,67 @@ class ProjectAdmin(admin.ModelAdmin): "name", "draft", "is_company", + "track", + "direction", + "specialty", ) list_display_links = ( "id", "name", ) + search_fields = ( + "name", + "track", + "specialty", + ) + list_filter = ( + "draft", + "is_company", + "track", + "direction", + "specialty", + ) + + fieldsets = ( + ("Основная информация", { + "fields": ( + "name", + "description", + "leader", + "industry", + "region", + "step", + "draft", + "is_company", + ) + }), + ("Для проектов ПД МосПолитеха", { + "fields": ( + "track", + "direction", + "specialty", + "actuality", + "goal", + "problem", + ) + }), + ("Медиа и обложка", { + "fields": ( + "presentation_address", + "image_address", + "cover", + "cover_image_address", + ) + }), + ("Служебные поля", { + "fields": ( + "hidden_score", + "datetime_created", + "datetime_updated", + ) + }), + ) + readonly_fields = ("datetime_created", "datetime_updated") @admin.register(ProjectNews) diff --git a/projects/migrations/0024_project_actuality_project_direction_project_goal_and_more.py b/projects/migrations/0024_project_actuality_project_direction_project_goal_and_more.py new file mode 100644 index 00000000..7c028914 --- /dev/null +++ b/projects/migrations/0024_project_actuality_project_direction_project_goal_and_more.py @@ -0,0 +1,80 @@ +# Generated by Django 4.2.11 on 2025-07-03 08:53 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0023_project_cover_image_address"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="actuality", + field=models.TextField( + blank=True, + help_text="Почему проект важен (до 1000\u202fсимв.)", + null=True, + validators=[django.core.validators.MaxLengthValidator(1000)], + verbose_name="Актуальность", + ), + ), + migrations.AddField( + model_name="project", + name="direction", + field=models.CharField( + blank=True, + help_text="Более общее направление деятельности проекта", + max_length=256, + null=True, + verbose_name="Направление", + ), + ), + migrations.AddField( + model_name="project", + name="goal", + field=models.CharField( + blank=True, + help_text="Главная цель проекта (до 500\u202fсимв.)", + max_length=500, + null=True, + verbose_name="Цель", + ), + ), + migrations.AddField( + model_name="project", + name="problem", + field=models.TextField( + blank=True, + help_text="Какую проблему решает проект (до 1000\u202fсимв.)", + null=True, + validators=[django.core.validators.MaxLengthValidator(1000)], + verbose_name="Проблема", + ), + ), + migrations.AddField( + model_name="project", + name="specialty", + field=models.CharField( + blank=True, + help_text="Специализация проекта", + max_length=256, + null=True, + verbose_name="Специальность", + ), + ), + migrations.AddField( + model_name="project", + name="track", + field=models.CharField( + blank=True, + help_text="Направление/курс, в рамках которого реализуется проект", + max_length=256, + null=True, + verbose_name="Трек", + ), + ), + ] diff --git a/projects/models.py b/projects/models.py index b38a939e..7e1f9973 100644 --- a/projects/models.py +++ b/projects/models.py @@ -2,9 +2,11 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import GenericRelation +from django.core.validators import MaxLengthValidator from django.db import models from django.db.models import UniqueConstraint + from core.models import Like, View from files.models import UserFile from industries.models import Industry @@ -88,6 +90,49 @@ class Project(models.Model): step = models.PositiveSmallIntegerField(choices=VERBOSE_STEPS, null=True, blank=True) hidden_score = models.PositiveSmallIntegerField(default=100) + track = models.CharField( + max_length=256, + blank=True, + null=True, + verbose_name="Трек", + help_text="Направление/курс, в рамках которого реализуется проект", + ) + direction = models.CharField( + max_length=256, + blank=True, + null=True, + verbose_name="Направление", + help_text="Более общее направление деятельности проекта", + ) + specialty = models.CharField( + max_length=256, + blank=True, + null=True, + verbose_name="Специальность", + help_text="Специализация проекта", + ) + actuality = models.TextField( + blank=True, + null=True, + validators=[MaxLengthValidator(1000)], + verbose_name="Актуальность", + help_text="Почему проект важен (до 1000 симв.)", + ) + goal = models.CharField( + max_length=500, + blank=True, + null=True, + verbose_name="Цель", + help_text="Главная цель проекта (до 500 симв.)", + ) + problem = models.TextField( + blank=True, + null=True, + validators=[MaxLengthValidator(1000)], + verbose_name="Проблема", + help_text="Какую проблему решает проект (до 1000 симв.)", + ) + industry = models.ForeignKey( Industry, on_delete=models.SET_NULL, From 8e85f18718350e8f117ea40cf0b1ba6c58c4ac58 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Fri, 4 Jul 2025 15:48:50 +0500 Subject: [PATCH 05/15] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B0=D0=BD=20ProjectDetailSerializer=20?= =?UTF-8?q?=D0=B2=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B8=D0=B8=20=D1=81=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=8F=D0=BC=D0=B8=20=D0=B2=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20Project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- projects/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/projects/serializers.py b/projects/serializers.py index a68cadb4..098dcad8 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -130,6 +130,12 @@ class Meta: "cover", "cover_image_address", "partner_programs_tags", + "track", + "direction", + "specialty", + "actuality", + "goal", + "problem", ] read_only_fields = [ "leader", From 99c392e8906d5ac247a5ba9ff26e81b24ba0edde Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Fri, 4 Jul 2025 15:52:32 +0500 Subject: [PATCH 06/15] =?UTF-8?q?=D0=9A=D0=BE=D0=B4=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B2=D0=B4=D1=91=D0=BD=20=D0=B2=20=D1=81=D0=BE=D0=BE=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D0=B8=D0=B5=20=D1=81=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=D0=BC=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F;=20?= =?UTF-8?q?=D0=9E=D1=82=D1=81=D0=BE=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- projects/admin.py | 90 +++++++++++++++++++++++------------------ projects/models.py | 12 ++++-- projects/serializers.py | 8 ++-- 3 files changed, 63 insertions(+), 47 deletions(-) diff --git a/projects/admin.py b/projects/admin.py index ffc35da6..70f42356 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -1,10 +1,10 @@ from django.contrib import admin from projects.models import ( - DefaultProjectCover, - Project, Achievement, Collaborator, + DefaultProjectCover, + Project, ProjectLink, ProjectNews, ) @@ -39,43 +39,55 @@ class ProjectAdmin(admin.ModelAdmin): ) fieldsets = ( - ("Основная информация", { - "fields": ( - "name", - "description", - "leader", - "industry", - "region", - "step", - "draft", - "is_company", - ) - }), - ("Для проектов ПД МосПолитеха", { - "fields": ( - "track", - "direction", - "specialty", - "actuality", - "goal", - "problem", - ) - }), - ("Медиа и обложка", { - "fields": ( - "presentation_address", - "image_address", - "cover", - "cover_image_address", - ) - }), - ("Служебные поля", { - "fields": ( - "hidden_score", - "datetime_created", - "datetime_updated", - ) - }), + ( + "Основная информация", + { + "fields": ( + "name", + "description", + "leader", + "industry", + "region", + "step", + "draft", + "is_company", + ) + }, + ), + ( + "Для проектов ПД МосПолитеха", + { + "fields": ( + "track", + "direction", + "specialty", + "actuality", + "goal", + "problem", + ) + }, + ), + ( + "Медиа и обложка", + { + "fields": ( + "presentation_address", + "image_address", + "cover", + "cover_image_address", + ) + }, + ), + ( + "Служебные поля", + { + "fields": ( + "hidden_score", + "datetime_created", + "datetime_updated", + ) + }, + ), ) readonly_fields = ("datetime_created", "datetime_updated") diff --git a/projects/models.py b/projects/models.py index 7e1f9973..d66cefda 100644 --- a/projects/models.py +++ b/projects/models.py @@ -6,11 +6,9 @@ from django.db import models from django.db.models import UniqueConstraint - from core.models import Like, View from files.models import UserFile from industries.models import Industry - from projects.constants import VERBOSE_STEPS from projects.managers import AchievementManager, CollaboratorManager, ProjectManager from users.models import CustomUser @@ -55,7 +53,11 @@ def get_random_file(cls): @classmethod def get_random_file_link(cls): # FIXME: this is not efficient, but for ~10 default covers it should be ok - return cls.objects.order_by("?").first().image.link if cls.objects.order_by("?").first().image else None + return ( + cls.objects.order_by("?").first().image.link + if cls.objects.order_by("?").first().image + else None + ) class Meta: verbose_name = "Обложка проекта" @@ -87,7 +89,9 @@ class Project(models.Model): name = models.CharField(max_length=256, null=True, blank=True) description = models.TextField(null=True, blank=True) region = models.CharField(max_length=256, null=True, blank=True) - step = models.PositiveSmallIntegerField(choices=VERBOSE_STEPS, null=True, blank=True) + step = models.PositiveSmallIntegerField( + choices=VERBOSE_STEPS, null=True, blank=True + ) hidden_score = models.PositiveSmallIntegerField(default=100) track = models.CharField( diff --git a/projects/serializers.py b/projects/serializers.py index 098dcad8..95757559 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -1,12 +1,13 @@ from django.contrib.auth import get_user_model -from rest_framework import serializers from django.core.cache import cache +from rest_framework import serializers + from core.serializers import SkillToObjectSerializer -from core.services import get_views_count, get_likes_count, is_fan +from core.services import get_likes_count, get_views_count, is_fan from core.utils import get_user_online_cache_key from files.serializers import UserFileSerializer from industries.models import Industry -from projects.models import Project, Achievement, Collaborator, ProjectNews +from projects.models import Achievement, Collaborator, Project, ProjectNews from projects.validators import validate_project from vacancy.serializers import ProjectVacancyListSerializer @@ -64,7 +65,6 @@ class Meta: class ProjectDetailSerializer(serializers.ModelSerializer): - achievements = AchievementListSerializer(many=True, read_only=True) cover = UserFileSerializer(required=False) collaborators = CollaboratorSerializer( From bd241e17148f2b079f9884eb7f1059eaa3ea106e Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Fri, 4 Jul 2025 18:38:39 +0500 Subject: [PATCH 07/15] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D1=8F=20=D0=A2=D1=80?= =?UTF-8?q?=D1=8D=D0=BA,=20=D0=9D=D0=B0=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5,=20=D0=90=D0=BA=D1=82=D1=83=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C,=20=D0=A6=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C,=20=D0=9F=D1=80=D0=BE=D0=B1=D0=BB=D0=B5=D0=BC?= =?UTF-8?q?=D0=B0=20=D1=83=20=D0=9F=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=8F=D0=B7=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BF=D1=80=D0=B8=20PUT=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=81=D0=B0=D1=85=20=D0=BA=20ProjectDetail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- projects/admin.py | 4 ---- .../migrations/0025_remove_project_specialty.py | 17 +++++++++++++++++ projects/models.py | 7 ------- projects/serializers.py | 6 +++++- 4 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 projects/migrations/0025_remove_project_specialty.py diff --git a/projects/admin.py b/projects/admin.py index 70f42356..aab45816 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -19,7 +19,6 @@ class ProjectAdmin(admin.ModelAdmin): "is_company", "track", "direction", - "specialty", ) list_display_links = ( "id", @@ -28,14 +27,12 @@ class ProjectAdmin(admin.ModelAdmin): search_fields = ( "name", "track", - "specialty", ) list_filter = ( "draft", "is_company", "track", "direction", - "specialty", ) fieldsets = ( @@ -60,7 +57,6 @@ class ProjectAdmin(admin.ModelAdmin): "fields": ( "track", "direction", - "specialty", "actuality", "goal", "problem", diff --git a/projects/migrations/0025_remove_project_specialty.py b/projects/migrations/0025_remove_project_specialty.py new file mode 100644 index 00000000..47638bf0 --- /dev/null +++ b/projects/migrations/0025_remove_project_specialty.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.11 on 2025-07-04 13:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0024_project_actuality_project_direction_project_goal_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="project", + name="specialty", + ), + ] diff --git a/projects/models.py b/projects/models.py index d66cefda..2ecd8236 100644 --- a/projects/models.py +++ b/projects/models.py @@ -108,13 +108,6 @@ class Project(models.Model): verbose_name="Направление", help_text="Более общее направление деятельности проекта", ) - specialty = models.CharField( - max_length=256, - blank=True, - null=True, - verbose_name="Специальность", - help_text="Специализация проекта", - ) actuality = models.TextField( blank=True, null=True, diff --git a/projects/serializers.py b/projects/serializers.py index 95757559..141e9f79 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -76,6 +76,11 @@ class ProjectDetailSerializer(serializers.ModelSerializer): views_count = serializers.SerializerMethodField(method_name="count_views") links = serializers.SerializerMethodField() partner_programs_tags = serializers.SerializerMethodField() + track = serializers.CharField(required=False, allow_null=True, allow_blank=True) + direction = serializers.CharField(required=False, allow_null=True, allow_blank=True) + actuality = serializers.CharField(required=False, allow_null=True, allow_blank=True) + goal = serializers.CharField(required=False, allow_null=True, allow_blank=True) + problem = serializers.CharField(required=False, allow_null=True, allow_blank=True) @classmethod def get_partner_programs_tags(cls, project): @@ -132,7 +137,6 @@ class Meta: "partner_programs_tags", "track", "direction", - "specialty", "actuality", "goal", "problem", From 2708652f6857446f00e737a8d5050693882118dc Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Tue, 8 Jul 2025 17:02:37 +0500 Subject: [PATCH 08/15] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B3=D0=BB=D0=B0?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC=20=D0=B8=20=D0=9A=D0=BE?= =?UTF-8?q?=D0=BB=D0=BB=D0=B0=D0=B1=D0=BE=D1=80=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=D0=BC=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=BF=D0=BE=D0=BB=D0=B5=20Specialization;=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=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D1=83=20=D0=B8=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D1=8B;=20=D0=9F=D1=80=D0=B8=20=D0=BF=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D1=8F=D1=82=D0=B8=D0=B8=20=D0=9F=D1=80=D0=B8=D0=B3=D0=BB=D0=B0?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D1=91=D1=82=D1=81=D1=8F=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=20=D0=9A=D0=BE=D0=BB=D0=BB=D0=B0=D0=B1=D0=BE=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D1=80,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=B1=D1=83=D0=B4=D0=B5=D1=82=20=D0=B8=D0=BC=D0=B5=D1=82=D1=8C?= =?UTF-8?q?=20=D1=80=D0=BE=D0=BB=D1=8C=20=D0=B8=20=D1=81=D0=BF=D0=B5=D1=86?= =?UTF-8?q?=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=9F=D1=80=D0=B8=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- invites/admin.py | 2 +- .../migrations/0002_invite_specialization.py | 24 ++++++++++++++++++ invites/models.py | 7 ++++++ invites/serializers.py | 2 ++ invites/views.py | 5 +++- .../0026_collaborator_specialization.py | 25 +++++++++++++++++++ projects/models.py | 9 +++++++ 7 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 invites/migrations/0002_invite_specialization.py create mode 100644 projects/migrations/0026_collaborator_specialization.py diff --git a/invites/admin.py b/invites/admin.py index 4913dd7d..8148e2d3 100644 --- a/invites/admin.py +++ b/invites/admin.py @@ -5,4 +5,4 @@ @admin.register(Invite) class InviteAdmin(admin.ModelAdmin): - fields = ["project", "user", "motivational_letter", "role", "is_accepted"] + fields = ["project", "user", "motivational_letter", "role", "specialization", "is_accepted"] diff --git a/invites/migrations/0002_invite_specialization.py b/invites/migrations/0002_invite_specialization.py new file mode 100644 index 00000000..889e85a3 --- /dev/null +++ b/invites/migrations/0002_invite_specialization.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.11 on 2025-07-08 11:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("invites", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="invite", + name="specialization", + field=models.CharField( + blank=True, + default=None, + max_length=100, + null=True, + verbose_name="Специализация", + ), + ), + ] diff --git a/invites/models.py b/invites/models.py index 77e97207..3bbd82a9 100644 --- a/invites/models.py +++ b/invites/models.py @@ -27,6 +27,13 @@ class Invite(models.Model): max_length=4096, blank=True, null=True, default=None ) role = models.CharField(max_length=128, blank=True, null=True) + specialization = models.CharField( + max_length=100, + blank=True, + null=True, + default=None, + verbose_name="Специализация" + ) is_accepted = models.BooleanField(blank=False, null=True, default=None) datetime_created = models.DateTimeField( diff --git a/invites/serializers.py b/invites/serializers.py index 1e05877a..d9dd41df 100644 --- a/invites/serializers.py +++ b/invites/serializers.py @@ -21,6 +21,7 @@ class Meta: class InviteDetailSerializer(serializers.ModelSerializer[Invite]): user = UserDetailSerializer(many=False, read_only=True) project = ProjectListSerializer(many=False, read_only=True) + specialization = serializers.CharField(required=False, allow_null=True, allow_blank=True) class Meta: model = Invite @@ -30,6 +31,7 @@ class Meta: "user", "motivational_letter", "role", + "specialization", "is_accepted", "datetime_created", "datetime_updated", diff --git a/invites/views.py b/invites/views.py index 474b5d61..1214935c 100644 --- a/invites/views.py +++ b/invites/views.py @@ -49,7 +49,10 @@ def post(self, request, *args, **kwargs): return Response(status=status.HTTP_403_FORBIDDEN) # add user to project collaborators Collaborator.objects.create( - user=invite.user, project=invite.project, role=invite.role + user=invite.user, + project=invite.project, + role=invite.role, + specialization=invite.specialization, ) invite.is_accepted = True invite.save() diff --git a/projects/migrations/0026_collaborator_specialization.py b/projects/migrations/0026_collaborator_specialization.py new file mode 100644 index 00000000..03245fa6 --- /dev/null +++ b/projects/migrations/0026_collaborator_specialization.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.11 on 2025-07-08 11:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0025_remove_project_specialty"), + ] + + operations = [ + migrations.AddField( + model_name="collaborator", + name="specialization", + field=models.CharField( + blank=True, + default=None, + help_text="Направления работы участника в рамках проекта", + max_length=100, + null=True, + verbose_name="Специализация", + ), + ), + ] diff --git a/projects/models.py b/projects/models.py index 2ecd8236..81d68917 100644 --- a/projects/models.py +++ b/projects/models.py @@ -264,6 +264,7 @@ class Collaborator(models.Model): user: A ForeignKey referencing the user who is collaborating in the project. project: A ForeignKey referencing the project the user is collaborating in. role: A CharField meaning the role the user is fulfilling in the project. + specialization: A CharField indicating the user's specialization within the project. datetime_created: A DateTimeField indicating date of creation. datetime_updated: A DateTimeField indicating date of update. """ @@ -276,6 +277,14 @@ class Collaborator(models.Model): ) project = models.ForeignKey(Project, models.CASCADE, verbose_name="Проект") role = models.CharField("Роль", max_length=1024, blank=True, null=True) + specialization = models.CharField( + "Специализация", + max_length=100, + blank=True, + null=True, + default=None, + help_text="Направления работы участника в рамках проекта", + ) datetime_created = models.DateTimeField( verbose_name="Дата создания", null=False, auto_now_add=True From c49c9a330303cd972fab7c7349f6e453cedaf7e7 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Tue, 8 Jul 2025 17:04:20 +0500 Subject: [PATCH 09/15] =?UTF-8?q?=D0=9A=D0=BE=D0=B4=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B2=D0=B5=D0=B4=D1=91=D0=BD=20=D0=B2=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B8=D0=B5=20=D1=81=20?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BB=D0=B8=D1=81=D1=82=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=BC=20=D0=BE=D1=84=D0=BE=D1=80=D0=BC=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- invites/admin.py | 9 ++++++++- invites/models.py | 8 +++++--- invites/serializers.py | 4 +++- invites/views.py | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/invites/admin.py b/invites/admin.py index 8148e2d3..30527640 100644 --- a/invites/admin.py +++ b/invites/admin.py @@ -5,4 +5,11 @@ @admin.register(Invite) class InviteAdmin(admin.ModelAdmin): - fields = ["project", "user", "motivational_letter", "role", "specialization", "is_accepted"] + fields = [ + "project", + "user", + "motivational_letter", + "role", + "specialization", + "is_accepted", + ] diff --git a/invites/models.py b/invites/models.py index 3bbd82a9..42118726 100644 --- a/invites/models.py +++ b/invites/models.py @@ -1,9 +1,9 @@ from django.db import models +from django_stubs_ext.db.models import TypedModelMeta from invites.managers import InviteManager from projects.models import Project from users.models import CustomUser -from django_stubs_ext.db.models import TypedModelMeta class Invite(models.Model): @@ -32,7 +32,7 @@ class Invite(models.Model): blank=True, null=True, default=None, - verbose_name="Специализация" + verbose_name="Специализация", ) is_accepted = models.BooleanField(blank=False, null=True, default=None) @@ -46,7 +46,9 @@ class Invite(models.Model): objects = InviteManager() def __str__(self) -> str: - return f'Invite from project "{self.project.name}" to {self.user.get_full_name()}' + return ( + f'Invite from project "{self.project.name}" to {self.user.get_full_name()}' + ) class Meta(TypedModelMeta): verbose_name = "Приглашение" diff --git a/invites/serializers.py b/invites/serializers.py index d9dd41df..e0be469d 100644 --- a/invites/serializers.py +++ b/invites/serializers.py @@ -21,7 +21,9 @@ class Meta: class InviteDetailSerializer(serializers.ModelSerializer[Invite]): user = UserDetailSerializer(many=False, read_only=True) project = ProjectListSerializer(many=False, read_only=True) - specialization = serializers.CharField(required=False, allow_null=True, allow_blank=True) + specialization = serializers.CharField( + required=False, allow_null=True, allow_blank=True + ) class Meta: model = Invite diff --git a/invites/views.py b/invites/views.py index 1214935c..3563c0a6 100644 --- a/invites/views.py +++ b/invites/views.py @@ -4,7 +4,7 @@ from invites.filters import InviteFilter from invites.models import Invite -from invites.serializers import InviteListSerializer, InviteDetailSerializer +from invites.serializers import InviteDetailSerializer, InviteListSerializer from projects.models import Collaborator From 34ac1a9a0c59f02f293837686a142572815531fa Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Wed, 9 Jul 2025 15:29:59 +0500 Subject: [PATCH 10/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B1=D1=83=D0=BB=D0=B5=D0=B2=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F,=20=D1=8F=D0=B2=D1=8F=D0=BB=D1=8F=D0=B5=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=BB=D0=B8=20=D0=BE=D0=BD=20=D1=81=D1=82=D1=83?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D1=82=D0=BE=D0=BC=20=D0=BC=D0=BE=D1=81=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=D1=81=D0=BA=D0=BE=D0=B3=D0=BE=20=D0=9F=D0=BE=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D1=85=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/filters.py | 5 +++++ users/serializers.py | 1 + 2 files changed, 6 insertions(+) diff --git a/users/filters.py b/users/filters.py index 19855650..3e7fe47e 100644 --- a/users/filters.py +++ b/users/filters.py @@ -113,6 +113,11 @@ def filter_by_fullname(cls, queryset, name, value): skills__contains = filters.Filter(method="filter_by_skills") + is_mospolytech_student = filters.BooleanFilter( + field_name="is_mospolytech_student", + label="Студент Московского Политеха", + ) + class Meta: model = User fields = ( diff --git a/users/serializers.py b/users/serializers.py index fe01c8d4..0e074f9e 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -792,6 +792,7 @@ class Meta: "is_online", "birthday", "speciality", + "is_mospolytech_student", ] From 840ecfe5adf53b4afc3acc0d166db987eca2509e Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Thu, 10 Jul 2025 12:08:06 +0500 Subject: [PATCH 11/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BF=D0=BE=D0=BB=D0=B5=20=D0=A1=D0=BF?= =?UTF-8?q?=D0=B5=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B2=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=92?= =?UTF-8?q?=D0=B0=D0=BA=D0=B0=D0=BD=D1=81=D0=B8=D0=B8;=20=D0=9F=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=BD=D1=8B=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D1=80=20=D0=B8=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D1=82=D0=B8=D0=B2=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D1=8C;=20=D0=A1=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B0=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vacancy/admin.py | 1 + .../migrations/0009_vacancy_specialization.py | 20 +++++++++++++++++++ vacancy/models.py | 6 ++++++ vacancy/serializers.py | 3 +++ 4 files changed, 30 insertions(+) create mode 100644 vacancy/migrations/0009_vacancy_specialization.py diff --git a/vacancy/admin.py b/vacancy/admin.py index de42048d..a20021ee 100644 --- a/vacancy/admin.py +++ b/vacancy/admin.py @@ -15,6 +15,7 @@ class VacancySkillToObjectInline(SkillToObjectInline): class VacancyAdmin(admin.ModelAdmin): list_display = [ "role", + "specialization", "description", "project", "is_active", diff --git a/vacancy/migrations/0009_vacancy_specialization.py b/vacancy/migrations/0009_vacancy_specialization.py new file mode 100644 index 00000000..4cccf2d7 --- /dev/null +++ b/vacancy/migrations/0009_vacancy_specialization.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.11 on 2025-07-10 06:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vacancy", "0008_vacancy_salary_alter_vacancy_required_experience_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="vacancy", + name="specialization", + field=models.CharField( + blank=True, max_length=128, null=True, verbose_name="Специализация" + ), + ), + ] diff --git a/vacancy/models.py b/vacancy/models.py index e5ec2233..35bf4ae2 100644 --- a/vacancy/models.py +++ b/vacancy/models.py @@ -28,6 +28,12 @@ class Vacancy(models.Model): """ role = models.CharField(max_length=256, null=False) + specialization = models.CharField( + max_length=128, + null=True, + blank=True, + verbose_name="Специализация" + ) required_skills = GenericRelation( "core.SkillToObject", related_query_name="vacancies", diff --git a/vacancy/serializers.py b/vacancy/serializers.py index 56648c7b..30d26091 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -112,6 +112,7 @@ class Meta: fields = [ "id", "role", + "specialization", "required_skills", "required_skills_ids", "description", @@ -139,6 +140,7 @@ class Meta: fields = [ "id", "role", + "specialization", "required_skills", "description", "is_active", @@ -233,6 +235,7 @@ class Meta: fields = [ "id", "role", + "specialization", "required_skills", "required_skills_ids", "description", From 4668888a5861dadbdebd4302f8fb002b00e55c5a Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Thu, 10 Jul 2025 12:09:18 +0500 Subject: [PATCH 12/15] =?UTF-8?q?=D0=9A=D0=BE=D0=B4=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B2=D0=B5=D0=B4=D1=91=D0=BD=20=D0=B2=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D0=B8=D0=B5=20=D1=81?= =?UTF-8?q?=20=D1=81=D1=82=D0=B8=D0=BB=D0=B5=D0=B2=D1=8B=D0=BC=20=D0=BE?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vacancy/admin.py | 2 +- vacancy/filters.py | 62 ++++++++++++++++++++++-------------------- vacancy/models.py | 11 +++----- vacancy/serializers.py | 11 +++++--- vacancy/tasks.py | 10 +++---- vacancy/views.py | 8 ++++-- 6 files changed, 55 insertions(+), 49 deletions(-) diff --git a/vacancy/admin.py b/vacancy/admin.py index a20021ee..9245d43c 100644 --- a/vacancy/admin.py +++ b/vacancy/admin.py @@ -25,7 +25,7 @@ class VacancyAdmin(admin.ModelAdmin): inlines = [ VacancySkillToObjectInline, ] - readonly_fields = ('datetime_closed',) + readonly_fields = ("datetime_closed",) list_display_links = ["role"] change_list_template = "vacancies/vacancies_change_list.html" diff --git a/vacancy/filters.py b/vacancy/filters.py index 6f21c8a3..17e4159c 100644 --- a/vacancy/filters.py +++ b/vacancy/filters.py @@ -47,50 +47,52 @@ def __init__(self, *args, **kwargs): self.data = dict(self.data) self.data["is_active"] = True - def filter_by_experience(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]: - return ( - queryset - .filter(Q(required_experience__in=value) | Q(required_experience=None)) - .order_by(F("required_experience").asc(nulls_last=True)) + def filter_by_experience( + self, queryset: QuerySet[Vacancy], name, value: list[str] + ) -> QuerySet[Vacancy]: + return queryset.filter( + Q(required_experience__in=value) | Q(required_experience=None) + ).order_by(F("required_experience").asc(nulls_last=True)) + + def filter_by_schedule( + self, queryset: QuerySet[Vacancy], name, value: list[str] + ) -> QuerySet[Vacancy]: + return queryset.filter( + Q(work_schedule__in=value) | Q(work_schedule=None) + ).order_by(F("work_schedule").asc(nulls_last=True)) + + def filter_by_format( + self, queryset: QuerySet[Vacancy], name, value: list[str] + ) -> QuerySet[Vacancy]: + return queryset.filter(Q(work_format__in=value) | Q(work_format=None)).order_by( + F("work_format").asc(nulls_last=True) ) - def filter_by_schedule(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]: - return ( - queryset - .filter(Q(work_schedule__in=value) | Q(work_schedule=None)) - .order_by(F("work_schedule").asc(nulls_last=True)) - ) - - def filter_by_format(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]: - return ( - queryset - .filter(Q(work_format__in=value) | Q(work_format=None)) - .order_by(F("work_format").asc(nulls_last=True)) - ) - - def filter_by_salary_min(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]: + def filter_by_salary_min( + self, queryset: QuerySet[Vacancy], name, value: list[str] + ) -> QuerySet[Vacancy]: try: min_salary = int(value[0]) - return ( - queryset - .filter(Q(salary__gte=min_salary) | Q(salary=None)) - .order_by(F("salary").asc(nulls_last=True)) + return queryset.filter(Q(salary__gte=min_salary) | Q(salary=None)).order_by( + F("salary").asc(nulls_last=True) ) except ValueError: return queryset - def filter_by_salary_max(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]: + def filter_by_salary_max( + self, queryset: QuerySet[Vacancy], name, value: list[str] + ) -> QuerySet[Vacancy]: try: max_salary = int(value[0]) - return ( - queryset - .filter(Q(salary__lte=max_salary) | Q(salary=None)) - .order_by(F("salary").asc(nulls_last=True)) + return queryset.filter(Q(salary__lte=max_salary) | Q(salary=None)).order_by( + F("salary").asc(nulls_last=True) ) except ValueError: return queryset - def filter_by_role(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]: + def filter_by_role( + self, queryset: QuerySet[Vacancy], name, value: list[str] + ) -> QuerySet[Vacancy]: if not value: return queryset return queryset.filter(role__icontains=value[0]) diff --git a/vacancy/models.py b/vacancy/models.py index 35bf4ae2..ba433c51 100644 --- a/vacancy/models.py +++ b/vacancy/models.py @@ -1,13 +1,13 @@ from django.contrib.contenttypes.fields import GenericRelation -from django.db import models from django.core.validators import MinValueValidator +from django.db import models from django.utils import timezone +from django_stubs_ext.db.models import TypedModelMeta from files.models import UserFile from projects.models import Project -from vacancy.constants import WorkExperience, WorkSchedule, WorkFormat +from vacancy.constants import WorkExperience, WorkFormat, WorkSchedule from vacancy.managers import VacancyManager, VacancyResponseManager -from django_stubs_ext.db.models import TypedModelMeta class Vacancy(models.Model): @@ -29,10 +29,7 @@ class Vacancy(models.Model): role = models.CharField(max_length=256, null=False) specialization = models.CharField( - max_length=128, - null=True, - blank=True, - verbose_name="Специализация" + max_length=128, null=True, blank=True, verbose_name="Специализация" ) required_skills = GenericRelation( "core.SkillToObject", diff --git a/vacancy/serializers.py b/vacancy/serializers.py index 30d26091..163bf995 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -10,7 +10,7 @@ from projects.models import Project from projects.validators import validate_project from users.serializers import UserDetailSerializer -from vacancy.constants import WorkExperience, WorkSchedule, WorkFormat +from vacancy.constants import WorkExperience, WorkFormat, WorkSchedule from vacancy.models import Vacancy, VacancyResponse User = get_user_model() @@ -51,8 +51,12 @@ def validate_work_format(self, value): def to_representation(self, instance): representation = super().to_representation(instance) - representation["required_experience"] = WorkExperience.to_display(instance.required_experience) - representation["work_schedule"] = WorkSchedule.to_display(instance.work_schedule) + representation["required_experience"] = WorkExperience.to_display( + instance.required_experience + ) + representation["work_schedule"] = WorkSchedule.to_display( + instance.work_schedule + ) representation["work_format"] = WorkFormat.to_display(instance.work_format) return representation @@ -194,7 +198,6 @@ class ProjectVacancyCreateListSerializer( AbstractVacancyEnumFields, RequiredSkillsWriteSerializerMixin[Vacancy], ): - def create(self, validated_data): project = validated_data["project"] if project.leader != self.context["request"].user: diff --git a/vacancy/tasks.py b/vacancy/tasks.py index 384a71f7..9ca02605 100644 --- a/vacancy/tasks.py +++ b/vacancy/tasks.py @@ -1,16 +1,16 @@ import datetime -from mailing.typing import EmailDataToPrepare, ContextDataDict, MailDataDict -from mailing.utils import send_mass_mail, prepare_mail_data +from mailing.typing import ContextDataDict, EmailDataToPrepare, MailDataDict +from mailing.utils import prepare_mail_data, send_mass_mail from procollab.celery import app from vacancy.mapping import ( CeleryEmailParams, + EmailParamsType, + MessageTypeEnum, create_text_for_email, - message_type_to_button_text, get_link, - MessageTypeEnum, + message_type_to_button_text, message_type_to_title, - EmailParamsType, ) from vacancy.models import Vacancy diff --git a/vacancy/views.py b/vacancy/views.py index bac8de90..78994bf9 100644 --- a/vacancy/views.py +++ b/vacancy/views.py @@ -70,7 +70,9 @@ def put(self, request, *args, **kwargs): return self.update(request, *args, **kwargs) -class VacancyResponseList(mixins.ListModelMixin, mixins.CreateModelMixin, GenericAPIView): +class VacancyResponseList( + mixins.ListModelMixin, mixins.CreateModelMixin, GenericAPIView +): permission_classes = [permissions.IsAuthenticatedOrReadOnly] serializer_class = VacancyResponseListSerializer @@ -145,7 +147,9 @@ def post(self, request, pk): role_add_as: str = vacancy.role # check if this person already has a collaborator role in this project - if Collaborator.objects.filter(project=project_add_in, user=user_to_add).exists(): + if Collaborator.objects.filter( + project=project_add_in, user=user_to_add + ).exists(): return Response( "You already work for this project, you can't accept a vacancy here", status=status.HTTP_400_BAD_REQUEST, From ee36a8d05721705da5d980ea0856d4653358aec6 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Thu, 10 Jul 2025 16:40:02 +0500 Subject: [PATCH 13/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BF=D0=BE=D0=BB=D0=B5=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B5=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B2=20=D1=81=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=82=D0=BE=D1=80=20=D0=9F=D1=80=D0=B8=D0=B3=D0=BB?= =?UTF-8?q?=D0=B0=D1=88=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- invites/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invites/serializers.py b/invites/serializers.py index e0be469d..1ea64c59 100644 --- a/invites/serializers.py +++ b/invites/serializers.py @@ -14,6 +14,7 @@ class Meta: "user", "motivational_letter", "role", + "specialization", "is_accepted", ] From c8109b74b1544299fdc4e82328f4ef87504fe1aa Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Fri, 11 Jul 2025 15:38:48 +0500 Subject: [PATCH 14/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=BB=D0=B2?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BF=D0=BE=D0=BB=D0=B5=20=D1=81=D0=BF=D0=B5?= =?UTF-8?q?=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B2=20=D1=81=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B2=D0=B0=D0=BA=D0=B0=D0=BD=D1=81=D0=B8=D0=B9=20=D0=B2=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B5=20=D0=B4=D0=B5=D0=B0=D1=82=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vacancy/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vacancy/serializers.py b/vacancy/serializers.py index 163bf995..3b4cf59d 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -82,6 +82,7 @@ class Meta: fields = [ "id", "role", + "specialization", "required_skills", "description", "project", From 10191eac20c15ff9e8d6b2878011e124818d50a6 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Fri, 11 Jul 2025 17:31:19 +0500 Subject: [PATCH 15/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BD=D0=B4=D0=B0=D1=80=D1=82=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0=D1=80=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0,=20=D0=B2=20=D1=81?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B0=D0=B5=20=D0=B5=D1=81=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=B1=D0=B5=D0=B7=20=D0=B0=D0=B2=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=BA=D0=B8=20=D0=B1=D1=83=D0=B4=D0=B5=D1=82=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B1=D1=80=D0=B0=D0=BD=D0=B0=20=D1=81=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD=D0=B0=D1=8F=20=D0=B8=D0=B7=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=B4=D0=B0=D1=80=D1=82=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- projects/admin.py | 6 ++ ...tprojectcover_datetime_created_and_more.py | 64 +++++++++++++++++++ projects/models.py | 58 ++++++++--------- projects/validators.py | 3 +- 4 files changed, 100 insertions(+), 31 deletions(-) create mode 100644 projects/migrations/0027_alter_defaultprojectcover_datetime_created_and_more.py diff --git a/projects/admin.py b/projects/admin.py index aab45816..d7cbce7e 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -3,6 +3,7 @@ from projects.models import ( Achievement, Collaborator, + DefaultProjectAvatar, DefaultProjectCover, Project, ProjectLink, @@ -141,3 +142,8 @@ class DefaultProjectCoverAdmin(admin.ModelAdmin): "datetime_created", "datetime_updated", ) + + +@admin.register(DefaultProjectAvatar) +class DefaultProjectAvatarAdmin(admin.ModelAdmin): + list_display = ("id", "image", "datetime_created") diff --git a/projects/migrations/0027_alter_defaultprojectcover_datetime_created_and_more.py b/projects/migrations/0027_alter_defaultprojectcover_datetime_created_and_more.py new file mode 100644 index 00000000..0f755693 --- /dev/null +++ b/projects/migrations/0027_alter_defaultprojectcover_datetime_created_and_more.py @@ -0,0 +1,64 @@ +# Generated by Django 4.2.11 on 2025-07-11 11:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("files", "0007_auto_20230929_1727"), + ("projects", "0026_collaborator_specialization"), + ] + + operations = [ + migrations.AlterField( + model_name="defaultprojectcover", + name="datetime_created", + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name="defaultprojectcover", + name="datetime_updated", + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name="defaultprojectcover", + name="image", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="files.userfile", + ), + ), + migrations.CreateModel( + name="DefaultProjectAvatar", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("datetime_created", models.DateTimeField(auto_now_add=True)), + ("datetime_updated", models.DateTimeField(auto_now=True)), + ( + "image", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="files.userfile", + ), + ), + ], + options={ + "verbose_name": "Аватарка проекта", + "verbose_name_plural": "Аватарки проектов", + }, + ), + ] diff --git a/projects/models.py b/projects/models.py index 81d68917..117b65c7 100644 --- a/projects/models.py +++ b/projects/models.py @@ -16,54 +16,48 @@ User = get_user_model() -class DefaultProjectCover(models.Model): +class AbstractDefaultProjectImage(models.Model): """ - Default cover model for projects, is chosen randomly at project creation - - Attributes: - image: A ForeignKey referencing the image of the cover. - datetime_created: A DateTimeField indicating date of creation. - datetime_updated: A DateTimeField indicating date of update. + Абстрактная модель для хранения изображений проекта по умолчанию. """ image = models.ForeignKey( UserFile, on_delete=models.CASCADE, - related_name="default_covers", null=True, blank=True, ) + datetime_created = models.DateTimeField(auto_now_add=True) + datetime_updated = models.DateTimeField(auto_now=True) - datetime_created = models.DateTimeField( - verbose_name="Дата создания", - null=False, - auto_now_add=True, - ) - datetime_updated = models.DateTimeField( - verbose_name="Дата изменения", - null=False, - auto_now=True, - ) + class Meta: + abstract = True @classmethod - def get_random_file(cls): - # FIXME: this is not efficient, but for ~10 default covers it should be ok - return cls.objects.order_by("?").first().image + def get_random_file(cls) -> Optional[UserFile]: + if not cls.objects.exists(): + return None + obj = cls.objects.order_by("?").first() + return obj.image if obj and obj.image else None @classmethod - def get_random_file_link(cls): - # FIXME: this is not efficient, but for ~10 default covers it should be ok - return ( - cls.objects.order_by("?").first().image.link - if cls.objects.order_by("?").first().image - else None - ) + def get_random_file_link(cls) -> Optional[str]: + file = cls.get_random_file() + return file.link if file else None + +class DefaultProjectCover(AbstractDefaultProjectImage): class Meta: verbose_name = "Обложка проекта" verbose_name_plural = "Обложки проектов" +class DefaultProjectAvatar(AbstractDefaultProjectImage): + class Meta: + verbose_name = "Аватарка проекта" + verbose_name_plural = "Аватарки проектов" + + class Project(models.Model): """ Project model @@ -193,9 +187,13 @@ def __str__(self): return f"Project<{self.id}> - {self.name}" def save(self, *args, **kwargs): - """Set random cover image if `cover_image_address` blank.""" - if self.cover_image_address is None: + """Set random cover and avatar images if not provided.""" + if not self.cover_image_address: self.cover_image_address = DefaultProjectCover.get_random_file_link() + + if not self.image_address: + self.image_address = DefaultProjectAvatar.get_random_file_link() + super().save(*args, **kwargs) class Meta: diff --git a/projects/validators.py b/projects/validators.py index 8c20ca0e..f14e813c 100644 --- a/projects/validators.py +++ b/projects/validators.py @@ -4,8 +4,9 @@ def validate_project(data): if not data.get("draft"): error = {} + allowed_blank = {"image_address"} for key, value in data.items(): - if value == "" or value is None: + if (value == "" or value is None) and key not in allowed_blank: error[key] = "This field is required" if error: raise ValidationError(error)