From 139d34bc5aa36d605bbb83d66948dd67c6dc3705 Mon Sep 17 00:00:00 2001 From: Igor Kuzmenkov <74809945+igorduino@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:26:12 +0300 Subject: [PATCH 01/22] Fix possible migration errors because of 0008 core migration Signed-off-by: igor kuzmenkov --- core/migrations/0011_auto_20240128_2214.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/migrations/0011_auto_20240128_2214.py b/core/migrations/0011_auto_20240128_2214.py index 110710c0..4c55f884 100644 --- a/core/migrations/0011_auto_20240128_2214.py +++ b/core/migrations/0011_auto_20240128_2214.py @@ -137,17 +137,15 @@ def fill_specializations(apps, schema_editor): for spec, category in SPECIALIZATIONS: spec = spec.strip() category = category.strip() - if category not in categories_set: - category_obj = SpecializationCategory.objects.create(name=category) - category_obj.save() + if (category not in categories_set) and not SpecializationCategory.objects.filter(name=category).exists(): + SpecializationCategory.objects.create(name=category) categories_set.add(category) if spec not in specs_set: try: category_obj = SpecializationCategory.objects.get(name=category) specs_set.add(spec) - spec_obj = Specialization.objects.create(name=spec, category=category_obj) - spec_obj.save() + Specialization.objects.create(name=spec, category=category_obj) except Exception as error: print('An error during migration:', error) From 9ec5a9c5a03b6342717bc50a6245048f4b607b5a Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Thu, 1 Feb 2024 16:29:12 +0300 Subject: [PATCH 02/22] Add skills from dataset migration Signed-off-by: igor kuzmenkov --- .../0013_add_skills_from_dataset.py | 103 ++++++++++++++++++ .../0014_migrate_key_skills_to_skills.py | 31 ++++++ users/models.py | 17 ++- 3 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 core/migrations/0013_add_skills_from_dataset.py create mode 100644 core/migrations/0014_migrate_key_skills_to_skills.py diff --git a/core/migrations/0013_add_skills_from_dataset.py b/core/migrations/0013_add_skills_from_dataset.py new file mode 100644 index 00000000..75ff601f --- /dev/null +++ b/core/migrations/0013_add_skills_from_dataset.py @@ -0,0 +1,103 @@ +# Generated by Django 4.2.3 on 2024-02-01 09:12 + +from django.db import migrations +from core.models import SkillCategory, Skill + +skills = {'Маркетинг': ['Ведение социальных сетей', 'Рилсмейкер', 'Копирайтер', 'Рерайтер', 'Email-кампании', + 'Аналитика рекламных кампаний', 'Аналитика влияния социальных сетей', 'Инфлюенс-маркетинг', + 'Аудиторская аналитика', 'Оптимизация для мобильных устройств', 'Таргетирование', + 'Мобильный маркетинг', 'Анализ и оптимизация контент-стратегий', 'Звукорежиссура', 'MS Office', + 'Копирайтинг', 'Создание контент-плана', 'SEO', 'SMM', 'Реклама', 'e-mail маркетинг', + 'Создание воронок', 'Digital маркетинг', 'SWOT', 'web Аналитика', 'PEST', '5W анализ', 'crm', + 'Холодные продажи', 'Продажи', 'Разработка стратегий продвижения', + 'Разработка стратегий выхода на рынок', 'Наружная реклама', 'Ценообразование'], + 'Нейросети': ['Midjourney', 'Word Embeddings', 'GANs', 'NLP', 'YOLO', 'DeepDream', 'LightGBM', 'U-Net', 'GPT', + 'Transfer Learning', 'bert', 'AutoML', 'Apache Spark', 'Apache Airflow', 'Kafka', 'ML', + 'catboost', 'pandas', 'scikit-learn', 'TensorRT', 'Tensorflow', 'Numpy', 'ETL', 'CV', 'PyTorch', + 'Keras', 'Matplotlib', 'Prophet', 'Hadoop', 'SARIMA', 'Spark', 'LSTM'], + 'Дизайн': ['Design thinking', 'Power point', 'Слайд-мейкинг', 'Motion design', 'AR', 'VR', 'VR/AR', + 'Дизайн игр', 'readymag', 'Видеомонтаж', 'Adobe', 'Презентации', 'Видео', 'Фотография', + 'Рилсмейкер', 'Прототипирование UI-UX', 'Работа с цветокоррекцией', 'UX проектирование', + 'Иллюстрации Procreate', 'Adobe After Effects', 'JTBD', 'Lightroom', 'QT', 'UX', 'UI', 'FIGMA', + 'Photoshop', 'Adobe illustrator', 'Inkscape', 'Adobe InDesign', 'GIMP', 'Graphic', 'Типографика', + 'Дизайн упаковки продукции', 'Дизайн мобильных приложений', 'Визуализация данных', 'PainNET', + 'Sketch', 'Paint3D', 'Создание презентации', 'Создание лендингов', 'CJM', 'Web-design', 'Canva', + 'WIX', 'Tilda', 'UserFlow', 'Blender', 'Magicavoxel'], + 'Soft skills': ['Вербальная и письменная коммуникация', 'Вовлеченность', 'Педагогика', + 'Разрешение конфликтов', 'Латеральное мышление', 'Быстрая обучаемость', 'Поиск информации', + 'Китайским (A2-B1)', 'Немецким А2', 'Основы бизнеса', 'Руководство', 'Нестандартное мышление', + 'Креативность', 'Эрудиция', 'Урбанизм', 'Внимательность', 'Английский язык (B2)', + 'Знание немецкого языка', 'Активность', 'Испанский', 'Финансовый учет', 'Самостоятельность', + 'Социальное предпринимательство', 'Этика', 'Сбор данных', 'Деловые переговоры', 'Research', + 'Целеустремленность', 'Ответственность', 'Коммуникабельность', 'Спикер', + 'Навык работы в команде', 'Критическое мышление', 'Решение проблем', + 'Адаптивность и гибкость', 'Умение обучаться и готовность к обучению', 'Организация времени', + 'Умение принимать решения', 'Эмоциональный интеллект', 'Стрессоустойчивость', 'Толерантность', + 'Эмпатия и умение понимать других', 'Управление конфликтами', 'Лидерские качества', + 'Тактичность', 'Мотивация', 'Самоорганизация', 'Ответственность', 'Дипломатичность', + 'Умение убеждать', 'Активный слушатель', 'Усидчивость', 'Пунктуальность', + 'Умение находить общий язык', 'Творческое мышление', 'Самоменеджмент', 'Настойчивость', + 'Автономность', 'Исполнительность', 'Скоропечатание', 'Восприятие критики', 'Красноречие', + 'Риторика', 'Умение работать с обратной связью', 'Профессиональная этика', 'Смекалка', + 'Дисциплина', 'Умение учиться на своих ошибках', 'Умение благодарить', + 'Оптимизм и позитивное мышление', 'Умение дать и принять комплимент', 'Навык делегирования', + 'Умение проводить встречи и презентации', 'Многозадачность', 'Публичные выступления', + 'Тайм-менеджмент', 'Лидерство', 'Работа в команде'], + 'Инженерия': ['1С', 'Написание протоколов', 'SaaS', 'LeoECS', 'KiCad', 'Ansys', 'Eagle', + 'Информационная безопасность', 'Построение интернет сетей', 'Lightroom', 'Unreal Engine 4', + 'Unreal Engine 5', 'Unity', 'Unity3D', 'Rockwell Automation', 'Paspberry Pi', 'Blender', + 'Развитие транспортных систем городов', '1С:Предприятие', '3ds Max', 'Revit', 'ArchiCAD', + 'Autodesk Maya', 'Autodesk AutoCAD', 'DesignSpark Mechanical', 'SketchUP', + 'Autodesk Fusion 360', 'Cinema 4D', 'Houdini', 'ZBrush', 'SculptGL', 'Wings 3D', 'FreeCAD', + 'Sweet Home 3D', 'LEGO Digital Designer', '3D Slash', 'Autodesk Tinkercad', + 'Autodesk Meshmixer', 'Autodesk ReCap Pro', 'Конструирование', 'Компас-3D', 'Solid Works', + 'Autodesk inventor', 'Робототехника', 'Работа с проектной документацией'], + 'Менеджмент': ['Комьюнити-менеджмент', 'Адаптивность продуктов и проектов', + 'Организация работы проектной группы', 'Аналитика', 'Мотивирование команды и отдельных людей', + 'Разработка стратегии', 'Глубинное интервью', 'Управленческие компетенции', 'QA', + 'Презентации проекта', 'Стратегия развития продукта', 'Формирование бэклога', + 'Ведение проекта', 'Анализ конкурентов', 'SOLID', 'Анализ рынка', 'Ideation', 'а', 'Use Case', + 'A/B тестирование', 'Unit-экономика', 'Диаграммы Ганта', 'Создание бизнес-модели', 'EduNet', + 'CustDev', 'ClickUp', 'Создание карты эмпатии', 'Trello', 'Jira', 'Confluence', 'XMind', + 'Bitrix', 'AmoCRM', 'Контроль команды', 'Координация команды', 'Организация команды', + 'Планирование задач', 'Разработка ТЗ', 'Мотивация команды', 'Управление процессами', + 'Управление задачами', 'Agile', 'SCRUM', 'Kanban', 'Введение документооборота', + 'Риск-менеджмент', 'Управление бюджетами и затратами', 'Ведение переговоров', + 'Расстановка приоритетов', 'Критическое мышление', 'Адаптивность', 'Lean', 'BPMN', + 'Управление изменениями в организации', 'R&D', 'Управление качеством', + 'Знание внедрения программ лояльности', 'Инновационное мышление и внедрение новых технологий', + 'Waterfall', 'Six Sigma', 'PRINCE2', 'Growth-hacking', 'Miro', 'HR'], + 'Back-end': ['Java', 'Docker', 'PHP', 'Ruby', 'Golang', 'Aiogram', 'Python', 'C++', 'C#', '1C', 'SQL', + 'MYSQL', 'PostgreSQL', 'Yii2', 'FastApi', 'ASP.NET', 'Scala', 'Swagger', 'Redis', 'Linux', + 'Django', 'Rust', 'Spring Framework', 'Flask', 'Express.js', 'Laravel', 'Ruby on Rails', + 'Fiber Framework', 'CakePHP', 'Play Framework', 'NestJS', 'GitHub', 'Flutter', 'GitHub Actions', + 'Django REST', 'Celery', 'Kotlin', 'PyTest', 'MongoDB', 'Postman', 'Dart', 'Apache Kafka', + 'RabbitMQ', 'PyQT', 'DRF', 'MsSQL Server', 'Git', 'ООП', 'Google Cloud Storage', 'Telegram боты', + 'NOSQL', 'grpc', 'vba', 'Directium', 'Kubernetes'], + 'Front-end': ['Agile', 'Bubble', 'Creatium', 'Glide', 'Гугл форма', 'XML', 'No code', 'Web components', + 'RxJS', 'Redux', 'D3.js', 'Node.js', 'Angular', 'Html', 'CSS', 'SCSS', 'JavaScript', 'NextJS', + 'VueJS', 'ReactJS', 'Tailwind', 'TypeScript', 'Nginx', 'JQuery', 'EmberJS', 'BackboneJS', + 'Elastic Search', 'Semantic-UI', 'BCPB', 'Foundation', 'Svelte', 'Preact', 'mjml', 'Swift']} + + +def fill_skills(apps, schema_editor): + for category_name, skills_list in skills.items(): + category = SkillCategory.objects.create(name=category_name) + + for skill_name in skills_list: + Skill.objects.create(name=skill_name, category=category) + + +def reverse(apps, schema_editor): + Skill.objects.all().delete() + SkillCategory.objects.all().delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0012_alter_specialization_options_and_more"), + ] + + operations = [ + migrations.RunPython(fill_skills, reverse_code=reverse), + ] diff --git a/core/migrations/0014_migrate_key_skills_to_skills.py b/core/migrations/0014_migrate_key_skills_to_skills.py new file mode 100644 index 00000000..a2909441 --- /dev/null +++ b/core/migrations/0014_migrate_key_skills_to_skills.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.3 on 2024-02-01 09:34 + +from django.db import migrations +from core.models import Skill, SkillToObject, SkillCategory +from users.models import CustomUser + + +def migrate_key_skills_to_skills(apps, schema_editor): + for user in CustomUser.objects.all(): + if user.key_skills: + for skill_name in user.key_skills: + skill = Skill.objects.filter(name__iexact=skill_name).first() + if skill: + SkillToObject.objects.get_or_create( + skill=skill, content_object=CustomUser, object_id=user.id + ) + + +def reverse(apps, schema_editor): + SkillToObject.objects.filter(content_object=CustomUser).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0013_add_skills_from_dataset"), + ("users", "0044_auto_20240128_2236") + ] + + operations = [ + migrations.RunPython(migrate_key_skills_to_skills, reverse_code=reverse), + ] diff --git a/users/models.py b/users/models.py index a86669d0..980ccc90 100644 --- a/users/models.py +++ b/users/models.py @@ -2,6 +2,7 @@ from django.db import models from django.db.models import QuerySet from django_stubs_ext.db.models import TypedModelMeta +from django.contrib.contenttypes.fields import GenericRelation from users.constants import ( ADMIN, @@ -76,7 +77,14 @@ class CustomUser(AbstractUser): patronymic = models.CharField( max_length=255, validators=[user_name_validator], null=True, blank=True ) - key_skills = models.CharField(max_length=512, null=True, blank=True) + key_skills = models.CharField( + max_length=512, null=True, blank=True + ) # to be deprecated in future + skills = GenericRelation( + "core.SkillToObject", + related_query_name="users", + ) + avatar = models.URLField(null=True, blank=True) birthday = models.DateField( validators=[user_birthday_validator], @@ -125,7 +133,7 @@ def calculate_ordering_score(self) -> int: score = 0 if self.avatar: score += 10 - if self.key_skills: + if self.skills_count > 0: score += 7 if self.about_me: score += 6 @@ -145,9 +153,6 @@ def get_project_chats(self) -> QuerySet: user_project_ids = self.collaborations.all().values_list("project_id", flat=True) return ProjectChat.objects.filter(project__in=user_project_ids) - def get_key_skills(self) -> list[str]: - return [skill.strip() for skill in self.key_skills.split(",") if skill.strip()] - def get_full_name(self) -> str: return f"{self.first_name} {self.last_name}" @@ -157,7 +162,7 @@ def __str__(self) -> str: class Meta(TypedModelMeta): verbose_name = "Пользователь" verbose_name_plural = "Пользователи" - # order by count of fields inputted, like avatar, key_skills, about_me, etc. + # order by count of fields inputted, like avatar, skills, about_me, etc. # first show users with all fields inputted, then with 1 field inputted, etc. ordering = ["-ordering_score", "id"] From d5feb3838d7ff213b3dad4deae363a85964eb040 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Thu, 1 Feb 2024 16:36:47 +0300 Subject: [PATCH 03/22] Add skills and skills categories to the django admin Signed-off-by: igor kuzmenkov --- core/admin.py | 35 ++++++++++++++++++++++++++++++++++- core/models.py | 3 +++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/core/admin.py b/core/admin.py index bb184111..76387792 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,5 +1,13 @@ from django.contrib import admin -from core.models import Like, View, Link, Specialization, SpecializationCategory +from core.models import ( + Like, + View, + Link, + Specialization, + SpecializationCategory, + Skill, + SkillCategory, +) @admin.register(Like) @@ -20,6 +28,31 @@ class LinkAdmin(admin.ModelAdmin): list_display_links = ("id", "link", "content_type", "object_id", "content_object") +@admin.register(Skill) +class SkillAdmin(admin.ModelAdmin): + list_display = ( + "id", + "name", + "category", + ) + list_display_links = ( + "id", + "name", + ) + + +@admin.register(SkillCategory) +class SkillCategoryAdmin(admin.ModelAdmin): + list_display = ( + "id", + "name", + ) + list_display_links = ( + "id", + "name", + ) + + @admin.register(Specialization) class SpecializationAdmin(admin.ModelAdmin): list_display = ( diff --git a/core/models.py b/core/models.py index 00b55315..e0ff5e3e 100644 --- a/core/models.py +++ b/core/models.py @@ -108,6 +108,9 @@ class SkillCategory(models.Model): name = models.CharField(max_length=256, null=False) + def __str__(self): + return self.name + class Meta: verbose_name = "Категория навыка" verbose_name_plural = "Категории навыков" From 71c4e227fde1d9ae8c2a725e0147afafbb65f6b8 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sat, 2 Mar 2024 23:07:07 +0300 Subject: [PATCH 04/22] Add new skills to Admin, upd skills_count Signed-off-by: igor kuzmenkov --- core/models.py | 3 +++ users/admin.py | 12 ++++++++++++ users/models.py | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/core/models.py b/core/models.py index 55372dbb..055dcb9e 100644 --- a/core/models.py +++ b/core/models.py @@ -129,6 +129,9 @@ class Skill(models.Model): related_name="skills", ) + def __str__(self): + return self.name + class Meta: verbose_name = "Навык" verbose_name_plural = "Навыки" diff --git a/users/admin.py b/users/admin.py index 49ecba7c..fff87aec 100644 --- a/users/admin.py +++ b/users/admin.py @@ -15,6 +15,14 @@ UserLink, ) +from django.contrib.contenttypes.admin import GenericTabularInline +from core.models import SkillToObject + + +class SkillToObjectInline(GenericTabularInline): + model = SkillToObject + extra = 1 + @admin.register(CustomUser) class CustomUserAdmin(admin.ModelAdmin): @@ -111,6 +119,10 @@ class CustomUserAdmin(admin.ModelAdmin): "v2_speciality__name", ) + inlines = [ + SkillToObjectInline, + ] + readonly_fields = ("ordering_score",) change_form_template = "users/admin/users_change_form.html" diff --git a/users/models.py b/users/models.py index 22307165..45e9f3ec 100644 --- a/users/models.py +++ b/users/models.py @@ -123,6 +123,10 @@ class CustomUser(AbstractUser): objects = CustomUserManager() + @property + def skills_count(self): + return self.skills.count() + def calculate_ordering_score(self) -> int: """ Calculate ordering score of the user, e.g. how full their profile is. From a95d14b23f76555630f0bb2f4c1850def9d61dc1 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 01:21:41 +0300 Subject: [PATCH 05/22] Update serializers for new skills Signed-off-by: igor kuzmenkov --- core/serializers.py | 20 +++++++++++++ events/serializers.py | 7 +++-- projects/serializers.py | 6 ++-- users/serializers.py | 62 +++++++++++++++++++++++++++++++---------- 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index c93960cb..3ad36571 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from .models import SkillToObject class SetLikedSerializer(serializers.Serializer): @@ -7,3 +8,22 @@ class SetLikedSerializer(serializers.Serializer): class SetViewedSerializer(serializers.Serializer): is_viewed = serializers.BooleanField() + + +class SkillSerializer(serializers.ModelSerializer): + id = serializers.SerializerMethodField() + name = serializers.SerializerMethodField() + category = serializers.SerializerMethodField() + + class Meta: + model = SkillToObject + fields = ["id", "name", "category"] + + def get_id(self, obj): + return obj.skill.id + + def get_name(self, obj): + return obj.skill.name + + def get_category(self, obj): + return obj.skill.category.name diff --git a/events/serializers.py b/events/serializers.py index ec43f6d8..dfda5bbd 100644 --- a/events/serializers.py +++ b/events/serializers.py @@ -3,9 +3,10 @@ from rest_framework import serializers from taggit.serializers import TaggitSerializer, TagListSerializerField +from core.serializers import SkillSerializer from core.utils import get_user_online_cache_key from events.models import Event -from users.serializers import MemberSerializer, KeySkillsField +from users.serializers import MemberSerializer USER = get_user_model() @@ -55,7 +56,7 @@ class Meta: class RegisteredUserListSerializer(serializers.ModelSerializer): member = MemberSerializer(required=False) - key_skills = KeySkillsField(required=False) + skills = SkillSerializer(many=True, read_only=True) is_online = serializers.SerializerMethodField() @classmethod @@ -72,7 +73,7 @@ class Meta: "first_name", "last_name", "patronymic", - "key_skills", + "skills", "avatar", "speciality", "birthday", diff --git a/projects/serializers.py b/projects/serializers.py index 0d7ca62f..e9f74846 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from rest_framework import serializers from django.core.cache import cache -from core.fields import CustomListField +from core.serializers import SkillSerializer from core.services import get_views_count, get_likes_count, is_fan from core.utils import get_user_online_cache_key from files.serializers import UserFileSerializer @@ -41,7 +41,7 @@ class CollaboratorSerializer(serializers.ModelSerializer): first_name = serializers.CharField(source="user.first_name") last_name = serializers.CharField(source="user.last_name") avatar = serializers.CharField(source="user.avatar") - key_skills = CustomListField(child=serializers.CharField(), source="user.key_skills") + skills = SkillSerializer(many=True, read_only=True, source="user.skills") class Meta: model = Collaborator @@ -50,7 +50,7 @@ class Meta: "first_name", "last_name", "role", - "key_skills", + "skills", "avatar", ] diff --git a/users/serializers.py b/users/serializers.py index 64e7b48e..483d6b2c 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,8 +1,11 @@ +from django.contrib.contenttypes.models import ContentType from django.forms.models import model_to_dict +from django.http import Http404 from rest_framework import serializers from django.core.cache import cache -from core.models import SpecializationCategory, Specialization +from core.serializers import SkillSerializer +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 projects.models import Project, Collaborator @@ -18,14 +21,6 @@ class Meta: ref_name = "Users" -class KeySkillsField(serializers.Field): - def to_representation(self, value): - return [skill.strip() for skill in value.split(",") if skill.strip()] - - def to_internal_value(self, data): - return ",".join(data) - - class CustomListField(serializers.ListField): # костыль def to_representation(self, data): @@ -174,7 +169,6 @@ class UserDetailSerializer(serializers.ModelSerializer[CustomUser]): expert = ExpertSerializer(required=False) mentor = MentorSerializer(required=False) achievements = AchievementListSerializer(required=False, many=True) - key_skills = KeySkillsField(required=False) links = serializers.SerializerMethodField() is_online = serializers.SerializerMethodField() projects = serializers.SerializerMethodField() @@ -182,6 +176,10 @@ class UserDetailSerializer(serializers.ModelSerializer[CustomUser]): v2_speciality_id = serializers.IntegerField( write_only=True, validators=[specialization_exists_validator] ) + skills = SkillSerializer(many=True, read_only=True) + skills_ids = serializers.ListField( + child=serializers.IntegerField(), write_only=True, required=False + ) def get_projects(self, user: CustomUser): return UserProjectsSerializer( @@ -213,7 +211,8 @@ class Meta: "first_name", "last_name", "patronymic", - "key_skills", + "skills", + "skills_ids", "birthday", "speciality", "v2_speciality", @@ -302,13 +301,31 @@ def update(self, instance, validated_data): new_user_type.save() setattr(instance, attr, value) + if attr == "skills_ids": + instance.skills.all().delete() + + for skill_id in value: + skill = Skill.objects.filter(id=skill_id).first() + if not skill: + raise Http404("Такого навыка не существует") + + SkillToObject.objects.create( + skill=skill, + content_type=ContentType.objects.get_for_model(CustomUser), + object_id=instance.id, + ) + instance.save() + return instance class UserListSerializer(serializers.ModelSerializer[CustomUser]): member = MemberSerializer(required=False) - key_skills = KeySkillsField(required=False) + skills = SkillSerializer(many=True, read_only=True) + skills_ids = serializers.ListField( + child=serializers.IntegerField(), write_only=True, required=False + ) is_online = serializers.SerializerMethodField() def get_is_online(self, user: CustomUser) -> bool: @@ -325,6 +342,20 @@ def create(self, validated_data) -> CustomUser: user.set_password(validated_data["password"]) user.save() + if "skills_ids" in validated_data: + for skill_id in validated_data["skills_ids"]: + skill = Skill.objects.filter(id=skill_id).first() + if not skill: + raise Http404("Такого навыка не существует") + + SkillToObject.objects.create( + skill=skill, + content_type=ContentType.objects.get_for_model(CustomUser), + object_id=user.id, + ) + + user.save() + return user class Meta: @@ -336,7 +367,8 @@ class Meta: "first_name", "last_name", "patronymic", - "key_skills", + "skills", + "skills_ids", "avatar", "speciality", "birthday", @@ -355,6 +387,8 @@ class Meta: class UserFeedSerializer(serializers.ModelSerializer): + skills = SkillSerializer(many=True, read_only=True) + class Meta: model = CustomUser fields = [ @@ -364,7 +398,7 @@ class Meta: "first_name", "last_name", "patronymic", - "key_skills", + "skills", "speciality", ] From fe652983e932af88189be1c655c027217f7f989d Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 01:50:05 +0300 Subject: [PATCH 06/22] fix key_skills migration Signed-off-by: igor kuzmenkov --- core/migrations/0014_migrate_key_skills_to_skills.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/migrations/0014_migrate_key_skills_to_skills.py b/core/migrations/0014_migrate_key_skills_to_skills.py index a2909441..a1b18ad6 100644 --- a/core/migrations/0014_migrate_key_skills_to_skills.py +++ b/core/migrations/0014_migrate_key_skills_to_skills.py @@ -1,18 +1,19 @@ # Generated by Django 4.2.3 on 2024-02-01 09:34 - +from django.contrib.contenttypes.models import ContentType from django.db import migrations -from core.models import Skill, SkillToObject, SkillCategory +from core.models import Skill, SkillToObject from users.models import CustomUser def migrate_key_skills_to_skills(apps, schema_editor): for user in CustomUser.objects.all(): if user.key_skills: - for skill_name in user.key_skills: + for skill_name in user.key_skills.lower().split(','): + skill_name = skill_name.strip() skill = Skill.objects.filter(name__iexact=skill_name).first() if skill: SkillToObject.objects.get_or_create( - skill=skill, content_object=CustomUser, object_id=user.id + skill=skill, content_type=ContentType.objects.get_for_model(CustomUser), object_id=user.id ) From beb7d7f31e9880087ed7ee61338f43611e34c974 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 01:55:04 +0300 Subject: [PATCH 07/22] fix key_skills reverse migration Signed-off-by: igor kuzmenkov --- core/migrations/0014_migrate_key_skills_to_skills.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/migrations/0014_migrate_key_skills_to_skills.py b/core/migrations/0014_migrate_key_skills_to_skills.py index a1b18ad6..1087e21a 100644 --- a/core/migrations/0014_migrate_key_skills_to_skills.py +++ b/core/migrations/0014_migrate_key_skills_to_skills.py @@ -5,6 +5,9 @@ from users.models import CustomUser +custom_user_content_type = ContentType.objects.get_for_model(CustomUser) + + def migrate_key_skills_to_skills(apps, schema_editor): for user in CustomUser.objects.all(): if user.key_skills: @@ -13,12 +16,12 @@ def migrate_key_skills_to_skills(apps, schema_editor): skill = Skill.objects.filter(name__iexact=skill_name).first() if skill: SkillToObject.objects.get_or_create( - skill=skill, content_type=ContentType.objects.get_for_model(CustomUser), object_id=user.id + skill=skill, content_type=custom_user_content_type, object_id=user.id ) def reverse(apps, schema_editor): - SkillToObject.objects.filter(content_object=CustomUser).delete() + SkillToObject.objects.filter(content_type=custom_user_content_type).delete() class Migration(migrations.Migration): From d78c542f249f1bf059538f20374ae3c72418c98f Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 02:48:40 +0300 Subject: [PATCH 08/22] Skills serializers mixin refactor Signed-off-by: igor kuzmenkov --- users/serializers.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/users/serializers.py b/users/serializers.py index 483d6b2c..51c49680 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -93,6 +93,16 @@ class Meta: fields = ["id", "name", "specializations"] +class SkillsSerializerMixin(serializers.Serializer): + skills = SkillSerializer(many=True, read_only=True) + + +class SkillsWriteSerializerMixin(SkillsSerializerMixin): + skills_ids = serializers.ListField( + child=serializers.IntegerField(), write_only=True, required=False + ) + + class UserProjectsSerializer(serializers.ModelSerializer[Project]): short_description = serializers.SerializerMethodField() views_count = serializers.SerializerMethodField() @@ -163,7 +173,9 @@ class Meta: read_only_fields = ["leader", "collaborator"] -class UserDetailSerializer(serializers.ModelSerializer[CustomUser]): +class UserDetailSerializer( + serializers.ModelSerializer[CustomUser], SkillsWriteSerializerMixin +): member = MemberSerializer(required=False) investor = InvestorSerializer(required=False) expert = ExpertSerializer(required=False) @@ -176,10 +188,6 @@ class UserDetailSerializer(serializers.ModelSerializer[CustomUser]): v2_speciality_id = serializers.IntegerField( write_only=True, validators=[specialization_exists_validator] ) - skills = SkillSerializer(many=True, read_only=True) - skills_ids = serializers.ListField( - child=serializers.IntegerField(), write_only=True, required=False - ) def get_projects(self, user: CustomUser): return UserProjectsSerializer( @@ -320,12 +328,10 @@ def update(self, instance, validated_data): return instance -class UserListSerializer(serializers.ModelSerializer[CustomUser]): +class UserListSerializer( + serializers.ModelSerializer[CustomUser], SkillsWriteSerializerMixin +): member = MemberSerializer(required=False) - skills = SkillSerializer(many=True, read_only=True) - skills_ids = serializers.ListField( - child=serializers.IntegerField(), write_only=True, required=False - ) is_online = serializers.SerializerMethodField() def get_is_online(self, user: CustomUser) -> bool: @@ -386,9 +392,7 @@ class Meta: } -class UserFeedSerializer(serializers.ModelSerializer): - skills = SkillSerializer(many=True, read_only=True) - +class UserFeedSerializer(serializers.ModelSerializer, SkillsSerializerMixin): class Meta: model = CustomUser fields = [ From a25a513cca8348c6a9b2669fd43adb037c2250e8 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 03:01:38 +0300 Subject: [PATCH 09/22] refactor skills exceptions handling in user serializers Signed-off-by: igor kuzmenkov --- users/serializers.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/users/serializers.py b/users/serializers.py index 51c49680..1faba34c 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,6 +1,5 @@ from django.contrib.contenttypes.models import ContentType from django.forms.models import model_to_dict -from django.http import Http404 from rest_framework import serializers from django.core.cache import cache @@ -313,9 +312,10 @@ def update(self, instance, validated_data): instance.skills.all().delete() for skill_id in value: - skill = Skill.objects.filter(id=skill_id).first() - if not skill: - raise Http404("Такого навыка не существует") + try: + skill = Skill.objects.get(id=skill_id) + except Skill.DoesNotExist: + raise serializers.ValidationError("Skill does not exist") SkillToObject.objects.create( skill=skill, @@ -350,9 +350,10 @@ def create(self, validated_data) -> CustomUser: if "skills_ids" in validated_data: for skill_id in validated_data["skills_ids"]: - skill = Skill.objects.filter(id=skill_id).first() - if not skill: - raise Http404("Такого навыка не существует") + try: + skill = Skill.objects.get(id=skill_id) + except Skill.DoesNotExist: + raise serializers.ValidationError("Skill does not exist") SkillToObject.objects.create( skill=skill, From 15329f6750b4766fa135f46401bdbb82e073df20 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 03:14:35 +0300 Subject: [PATCH 10/22] vacancy required skills migration Signed-off-by: igor kuzmenkov --- ...ired_skills_vacancy_required_skills_old.py | 18 ++++++++++ ...0003_migrate_old_required_skills_to_new.py | 34 +++++++++++++++++++ vacancy/models.py | 10 ++++-- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 vacancy/migrations/0002_rename_required_skills_vacancy_required_skills_old.py create mode 100644 vacancy/migrations/0003_migrate_old_required_skills_to_new.py diff --git a/vacancy/migrations/0002_rename_required_skills_vacancy_required_skills_old.py b/vacancy/migrations/0002_rename_required_skills_vacancy_required_skills_old.py new file mode 100644 index 00000000..d9fa9c4a --- /dev/null +++ b/vacancy/migrations/0002_rename_required_skills_vacancy_required_skills_old.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2024-03-02 22:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("vacancy", "0001_initial"), + ] + + operations = [ + migrations.RenameField( + model_name="vacancy", + old_name="required_skills", + new_name="required_skills_old", + ), + ] diff --git a/vacancy/migrations/0003_migrate_old_required_skills_to_new.py b/vacancy/migrations/0003_migrate_old_required_skills_to_new.py new file mode 100644 index 00000000..17ccd256 --- /dev/null +++ b/vacancy/migrations/0003_migrate_old_required_skills_to_new.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.3 on 2024-03-02 22:32 +from django.contrib.contenttypes.models import ContentType +from django.db import migrations +from core.models import Skill, SkillToObject +from vacancy.models import Vacancy + +vacancy_content_type = ContentType.objects.get_for_model(Vacancy) + + +def migrate_required_skills(apps, schema_editor): + for vacancy in Vacancy.objects.all(): + if vacancy.required_skills_old: + for skill_name in vacancy.required_skills_old.lower().split(','): + skill_name = skill_name.strip() + skill = Skill.objects.filter(name__iexact=skill_name).first() + if skill: + SkillToObject.objects.get_or_create( + skill=skill, content_type=vacancy_content_type, object_id=vacancy.id + ) + + +def reverse(apps, schema_editor): + SkillToObject.objects.filter(content_type=vacancy_content_type).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("vacancy", "0002_rename_required_skills_vacancy_required_skills_old"), + ("core", "0013_add_skills_from_dataset") + ] + + operations = [ + migrations.RunPython(migrate_required_skills, reverse_code=reverse), + ] diff --git a/vacancy/models.py b/vacancy/models.py index 1932d75f..c56814d5 100644 --- a/vacancy/models.py +++ b/vacancy/models.py @@ -1,3 +1,4 @@ +from django.contrib.contenttypes.fields import GenericRelation from django.db import models from projects.models import Project @@ -10,7 +11,8 @@ class Vacancy(models.Model): Attributes: role: A CharField title of the vacancy. - required_skills: A CharField required skills for the vacancy. + required_skills_old: A CharField required skills for the vacancy. + required_skills: A GenericRelation ша required skills for the vacancy. description: A TextField description of the vacancy. project: A ForeignKey referring to the Company model. is_active: A boolean indicating if Vacancy is active. @@ -19,7 +21,11 @@ class Vacancy(models.Model): """ role = models.CharField(max_length=256, null=False) - required_skills = models.TextField(blank=True) + required_skills_old = models.TextField(blank=True) # to be deprecated in future + required_skills = GenericRelation( + "core.SkillToObject", + related_query_name="vacancies", + ) description = models.TextField(blank=True) project = models.ForeignKey( Project, From a8477585bfc1f859081ffb0bb552b54be5c33234 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 03:16:41 +0300 Subject: [PATCH 11/22] vacancy required skills admin, minor user skills admin update Signed-off-by: igor kuzmenkov --- core/admin.py | 10 ++++++++++ core/models.py | 4 ++++ users/admin.py | 8 +------- vacancy/admin.py | 9 ++++++++- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/admin.py b/core/admin.py index 76387792..c4d37f1e 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,4 +1,6 @@ from django.contrib import admin +from django.contrib.contenttypes.admin import GenericStackedInline + from core.models import ( Like, View, @@ -7,9 +9,17 @@ SpecializationCategory, Skill, SkillCategory, + SkillToObject, ) +class SkillToObjectInline(GenericStackedInline): + model = SkillToObject + extra = 1 + verbose_name = "Навык" + verbose_name_plural = "Навыки" + + @admin.register(Like) class LikeAdmin(admin.ModelAdmin): list_display = ("id", "user", "content_type", "object_id", "content_object") diff --git a/core/models.py b/core/models.py index 055dcb9e..9fdd28d5 100644 --- a/core/models.py +++ b/core/models.py @@ -157,6 +157,10 @@ class SkillToObject(models.Model): object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") + class Meta: + verbose_name = "Навык" + verbose_name_plural = "Навыки" + class SpecializationCategory(models.Model): name = models.TextField() diff --git a/users/admin.py b/users/admin.py index fff87aec..3d6e1ec5 100644 --- a/users/admin.py +++ b/users/admin.py @@ -15,13 +15,7 @@ UserLink, ) -from django.contrib.contenttypes.admin import GenericTabularInline -from core.models import SkillToObject - - -class SkillToObjectInline(GenericTabularInline): - model = SkillToObject - extra = 1 +from core.admin import SkillToObjectInline @admin.register(CustomUser) diff --git a/vacancy/admin.py b/vacancy/admin.py index d76d5db1..5e97aa2f 100644 --- a/vacancy/admin.py +++ b/vacancy/admin.py @@ -1,19 +1,26 @@ from django.contrib import admin +from core.admin import SkillToObjectInline from vacancy.models import Vacancy, VacancyResponse +class VacancySkillToObjectInline(SkillToObjectInline): + verbose_name_plural = "Необходимые навыки" + + @admin.register(Vacancy) class VacancyAdmin(admin.ModelAdmin): list_display = [ "role", - "required_skills", "description", "project", "is_active", "datetime_created", "datetime_updated", ] + inlines = [ + VacancySkillToObjectInline, + ] list_display_links = ["role"] From 45a9278815ef882b2c8847a0781b4e6beb42b41e Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 04:10:04 +0300 Subject: [PATCH 12/22] Update get_recommended_users for new required skills Signed-off-by: igor kuzmenkov --- projects/helpers.py | 12 ++++++------ users/models.py | 6 ++++++ vacancy/models.py | 6 ++++++ vacancy/serializers.py | 16 +++++++++++++--- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/projects/helpers.py b/projects/helpers.py index 28d39a84..4a660f5f 100644 --- a/projects/helpers.py +++ b/projects/helpers.py @@ -11,21 +11,21 @@ def get_recommended_users(project: Project) -> list[User]: """ - Searches for users by matching their key_skills and vacancies required_skills + Searches for users by matching their skills and vacancies required_skills """ - # fixme: store key_skills and required_skills more convenient, not just as a string all_needed_skills = set() for vacancy in project.vacancies.all(): - all_needed_skills.update(set(vacancy.required_skills.lower().split(","))) + all_needed_skills.update(set(vacancy.get_required_skills())) recommended_users = [] for user in User.objects.get_members(): - if user == project.leader or not user.key_skills: + if user == project.leader or user.skills_count < 1: continue - skills = set(user.key_skills.lower().split(",")) - if skills.intersection(all_needed_skills): + user_skills = set(user.get_skills()) + print(user_skills, all_needed_skills) + if user_skills.intersection(all_needed_skills): recommended_users.append(user) # get some random users diff --git a/users/models.py b/users/models.py index 45e9f3ec..2e1f3efe 100644 --- a/users/models.py +++ b/users/models.py @@ -127,6 +127,12 @@ class CustomUser(AbstractUser): def skills_count(self): return self.skills.count() + def get_skills(self): + skills = [] + for sto in self.skills.all(): + skills.append(sto.skill) + return skills + def calculate_ordering_score(self) -> int: """ Calculate ordering score of the user, e.g. how full their profile is. diff --git a/vacancy/models.py b/vacancy/models.py index c56814d5..743b5479 100644 --- a/vacancy/models.py +++ b/vacancy/models.py @@ -45,6 +45,12 @@ class Vacancy(models.Model): objects = VacancyManager() + def get_required_skills(self): + required_skills = [] + for sto in self.required_skills.all(): + required_skills.append(sto.skill) + return required_skills + def __str__(self): return f"Vacancy<{self.id}> - {self.role}" diff --git a/vacancy/serializers.py b/vacancy/serializers.py index 18f1f1d2..f5114155 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -1,15 +1,22 @@ from django.contrib.auth import get_user_model from rest_framework import serializers +from core.serializers import SkillSerializer from projects.models import Project -from users.serializers import UserDetailSerializer, CustomListField +from users.serializers import UserDetailSerializer from vacancy.models import Vacancy, VacancyResponse User = get_user_model() class RequiredSkillsSerializerMixin(serializers.Serializer): - required_skills = CustomListField(child=serializers.CharField()) + required_skills = SkillSerializer(many=True, read_only=True) + + +class RequiredSkillsWriteSerializerMixin(RequiredSkillsSerializerMixin): + required_skills_ids = serializers.ListField( + child=serializers.IntegerField(), write_only=True, required=False + ) class ProjectForVacancySerializer(serializers.ModelSerializer): @@ -23,7 +30,9 @@ class Meta: ] -class VacancyDetailSerializer(serializers.ModelSerializer, RequiredSkillsSerializerMixin): +class VacancyDetailSerializer( + serializers.ModelSerializer, RequiredSkillsWriteSerializerMixin +): project = ProjectForVacancySerializer(many=False, read_only=True) class Meta: @@ -32,6 +41,7 @@ class Meta: "id", "role", "required_skills", + "required_skills_ids", "description", "project", "is_active", From fb8754edcd5fc998285d82610b4d837f18db1c71 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 04:43:03 +0300 Subject: [PATCH 13/22] feat skills filter for users Signed-off-by: igor kuzmenkov --- users/filters.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/users/filters.py b/users/filters.py index 3633e99b..718a616d 100644 --- a/users/filters.py +++ b/users/filters.py @@ -1,4 +1,5 @@ from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django_filters import rest_framework as filters @@ -19,11 +20,10 @@ class UserFilter(filters.FilterSet): Parameters to filter by: first_name (str), last_name (str), patronymic (str), city (str), region (str), organization (str), about_me__contains (str), - key_skills__contains (str), useful_to_project__contains (str) + useful_to_project__contains (str) Examples: ?first_name=test equals to .filter(first_name='test') - ?key_skills__contains=yawning equals to .filter(key_skills__containing='yawning') ?user_type=1 equals to .filter(user_type=1) To check what user_types there are & what id they are, see CustomUser.VERBOSE_USER_TYPES @@ -47,6 +47,25 @@ def filter_by_partner_program(cls, queryset, name, value): except PartnerProgram.DoesNotExist: return User.objects.none() + @classmethod + def filter_by_skills(cls, queryset, name, skills_string): + skill_names = [ + skill.strip() for skill in skills_string.split(",") if skill.strip() + ] + + user_content_type = ContentType.objects.get_for_model(queryset.model) + + skills_filter = Q() + for skill_name in skill_names: + skills_filter |= Q( + skills__skill__name__icontains=skill_name, + skills__content_type=user_content_type, + ) + + filtered_queryset = queryset.filter(skills_filter).distinct() + + return filtered_queryset + @classmethod def filter_age__gte(cls, queryset, name, value): return filter_age(queryset, value, MAX_AGE_VALUE) @@ -74,9 +93,6 @@ def filter_by_fullname(cls, queryset, name, value): ) about_me__contains = filters.Filter(field_name="about_me", lookup_expr="contains") - key_skills__icontains = filters.Filter( - field_name="key_skills", lookup_expr="icontains" - ) speciality__icontains = filters.Filter( field_name="speciality", lookup_expr="icontains" ) @@ -95,6 +111,8 @@ def filter_by_fullname(cls, queryset, name, value): age__gte = filters.Filter(method="filter_age__gte") age__lte = filters.Filter(method="filter_age__lte") + skills__contains = filters.Filter(method="filter_by_skills") + class Meta: model = User fields = ( From f0e11d0a7c19e25b254d3277c2e43b03c6afba9e Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 15:08:48 +0300 Subject: [PATCH 14/22] feat serialize skill with category as object Signed-off-by: igor kuzmenkov --- core/serializers.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 3ad36571..21864afb 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import SkillToObject +from .models import SkillToObject, SkillCategory class SetLikedSerializer(serializers.Serializer): @@ -10,20 +10,20 @@ class SetViewedSerializer(serializers.Serializer): is_viewed = serializers.BooleanField() +class SkillCategorySerializer(serializers.ModelSerializer[SkillCategory]): + class Meta: + model = SkillCategory + fields = [ + "id", + "name", + ] + + class SkillSerializer(serializers.ModelSerializer): - id = serializers.SerializerMethodField() - name = serializers.SerializerMethodField() - category = serializers.SerializerMethodField() + id = serializers.IntegerField(source="skill.id") + name = serializers.CharField(source="skill.name") + category = SkillCategorySerializer(source="skill.category") class Meta: model = SkillToObject fields = ["id", "name", "category"] - - def get_id(self, obj): - return obj.skill.id - - def get_name(self, obj): - return obj.skill.name - - def get_category(self, obj): - return obj.skill.category.name From 5becb2db23defdbbce9ee3de71f56f0915c08bd4 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Sun, 3 Mar 2024 17:41:48 +0300 Subject: [PATCH 15/22] feat inline and nested skills routes Signed-off-by: igor kuzmenkov --- core/filters.py | 11 +++++++++++ core/models.py | 2 +- core/serializers.py | 19 ++++++++++++++++++- core/urls.py | 13 +++++++++++++ core/views.py | 34 ++++++++++++++++++++++++++++++++++ events/serializers.py | 4 ++-- procollab/urls.py | 1 + projects/serializers.py | 4 ++-- users/serializers.py | 4 ++-- vacancy/serializers.py | 4 ++-- 10 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 core/filters.py create mode 100644 core/urls.py create mode 100644 core/views.py diff --git a/core/filters.py b/core/filters.py new file mode 100644 index 00000000..233ef245 --- /dev/null +++ b/core/filters.py @@ -0,0 +1,11 @@ +from django_filters import rest_framework as filters + +from core.models import Skill + + +class SkillFilter(filters.FilterSet): + name__icontains = filters.Filter(field_name="name", lookup_expr="icontains") + + class Meta: + model = Skill + fields = ("name",) diff --git a/core/models.py b/core/models.py index 9fdd28d5..675b1ccc 100644 --- a/core/models.py +++ b/core/models.py @@ -135,7 +135,7 @@ def __str__(self): class Meta: verbose_name = "Навык" verbose_name_plural = "Навыки" - ordering = ["category", "name"] + ordering = ["id", "category", "name"] class SkillToObject(models.Model): diff --git a/core/serializers.py b/core/serializers.py index 21864afb..d516d81c 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers -from .models import SkillToObject, SkillCategory + +from .models import SkillToObject, SkillCategory, Skill class SetLikedSerializer(serializers.Serializer): @@ -20,6 +21,14 @@ class Meta: class SkillSerializer(serializers.ModelSerializer): + category = SkillCategorySerializer() + + class Meta: + model = Skill + fields = ["id", "name", "category"] + + +class STOSerializer(serializers.ModelSerializer): id = serializers.IntegerField(source="skill.id") name = serializers.CharField(source="skill.name") category = SkillCategorySerializer(source="skill.category") @@ -27,3 +36,11 @@ class SkillSerializer(serializers.ModelSerializer): class Meta: model = SkillToObject fields = ["id", "name", "category"] + + +class SkillsSerializer(serializers.ModelSerializer[SkillCategory]): + skills = SkillSerializer(many=True) + + class Meta: + model = SkillCategory + fields = ["id", "name", "skills"] diff --git a/core/urls.py b/core/urls.py new file mode 100644 index 00000000..86599c86 --- /dev/null +++ b/core/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from core.views import ( + SkillsNestedView, + SkillsInlineView, +) + +app_name = "core" + +urlpatterns = [ + path("skills/nested/", SkillsNestedView.as_view()), + path("skills/inline/", SkillsInlineView.as_view()), +] diff --git a/core/views.py b/core/views.py new file mode 100644 index 00000000..2913fd6d --- /dev/null +++ b/core/views.py @@ -0,0 +1,34 @@ +from django_filters import rest_framework as filters +from rest_framework import status +from rest_framework.generics import ( + GenericAPIView, + ListAPIView, +) +from rest_framework.response import Response + +from core.filters import SkillFilter +from core.models import SkillCategory, Skill +from core.pagination import Pagination +from core.serializers import ( + SkillsSerializer, + SkillSerializer, +) + + +class SkillsNestedView(GenericAPIView): + serializer_class = SkillsSerializer + queryset = SkillCategory.objects.all() + + def get(self, request): + data = self.serializer_class(self.get_queryset(), many=True).data + return Response(status=status.HTTP_200_OK, data=data) + + +class SkillsInlineView(ListAPIView): + serializer_class = SkillSerializer + pagination_class = Pagination + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = SkillFilter + + def get_queryset(self): + return Skill.objects.all() diff --git a/events/serializers.py b/events/serializers.py index dfda5bbd..080a1244 100644 --- a/events/serializers.py +++ b/events/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from taggit.serializers import TaggitSerializer, TagListSerializerField -from core.serializers import SkillSerializer +from core.serializers import STOSerializer from core.utils import get_user_online_cache_key from events.models import Event from users.serializers import MemberSerializer @@ -56,7 +56,7 @@ class Meta: class RegisteredUserListSerializer(serializers.ModelSerializer): member = MemberSerializer(required=False) - skills = SkillSerializer(many=True, read_only=True) + skills = STOSerializer(many=True, read_only=True) is_online = serializers.SerializerMethodField() @classmethod diff --git a/procollab/urls.py b/procollab/urls.py index 25c9d7de..4637c30a 100644 --- a/procollab/urls.py +++ b/procollab/urls.py @@ -42,6 +42,7 @@ path("news/", include("news.urls", namespace="news")), path("projects/", include("projects.urls", namespace="projects")), path("vacancies/", include("vacancy.urls", namespace="vacancies")), + path("core/", include("core.urls", namespace="core")), path("invites/", include("invites.urls", namespace="invites")), path("auth/", include(("users.urls", "users"), namespace="users")), path("chats/", include("chats.urls", namespace="chats")), diff --git a/projects/serializers.py b/projects/serializers.py index e9f74846..bea5f33e 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from rest_framework import serializers from django.core.cache import cache -from core.serializers import SkillSerializer +from core.serializers import STOSerializer from core.services import get_views_count, get_likes_count, is_fan from core.utils import get_user_online_cache_key from files.serializers import UserFileSerializer @@ -41,7 +41,7 @@ class CollaboratorSerializer(serializers.ModelSerializer): first_name = serializers.CharField(source="user.first_name") last_name = serializers.CharField(source="user.last_name") avatar = serializers.CharField(source="user.avatar") - skills = SkillSerializer(many=True, read_only=True, source="user.skills") + skills = STOSerializer(many=True, read_only=True, source="user.skills") class Meta: model = Collaborator diff --git a/users/serializers.py b/users/serializers.py index 1faba34c..e9e8a61c 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from django.core.cache import cache -from core.serializers import SkillSerializer +from core.serializers import STOSerializer from core.models import SpecializationCategory, Specialization, Skill, SkillToObject from core.services import get_views_count from core.utils import get_user_online_cache_key @@ -93,7 +93,7 @@ class Meta: class SkillsSerializerMixin(serializers.Serializer): - skills = SkillSerializer(many=True, read_only=True) + skills = STOSerializer(many=True, read_only=True) class SkillsWriteSerializerMixin(SkillsSerializerMixin): diff --git a/vacancy/serializers.py b/vacancy/serializers.py index f5114155..5cc8140c 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from rest_framework import serializers -from core.serializers import SkillSerializer +from core.serializers import STOSerializer from projects.models import Project from users.serializers import UserDetailSerializer from vacancy.models import Vacancy, VacancyResponse @@ -10,7 +10,7 @@ class RequiredSkillsSerializerMixin(serializers.Serializer): - required_skills = SkillSerializer(many=True, read_only=True) + required_skills = STOSerializer(many=True, read_only=True) class RequiredSkillsWriteSerializerMixin(RequiredSkillsSerializerMixin): From 8dffad65782e8eb564e42290380a2e80659e50d1 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Mon, 4 Mar 2024 16:59:43 +0300 Subject: [PATCH 16/22] remove redundant print Signed-off-by: igor kuzmenkov --- projects/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/helpers.py b/projects/helpers.py index 4a660f5f..df049b84 100644 --- a/projects/helpers.py +++ b/projects/helpers.py @@ -24,7 +24,7 @@ def get_recommended_users(project: Project) -> list[User]: continue user_skills = set(user.get_skills()) - print(user_skills, all_needed_skills) + if user_skills.intersection(all_needed_skills): recommended_users.append(user) From 2a19a3eedf3ff7d05c6c6cc844792cf658f3eda3 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Mon, 4 Mar 2024 18:56:44 +0300 Subject: [PATCH 17/22] fix contenttypes migration dependency Signed-off-by: igor kuzmenkov --- core/migrations/0014_migrate_key_skills_to_skills.py | 10 ++++------ .../0003_migrate_old_required_skills_to_new.py | 7 +++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/core/migrations/0014_migrate_key_skills_to_skills.py b/core/migrations/0014_migrate_key_skills_to_skills.py index 1087e21a..e1773a90 100644 --- a/core/migrations/0014_migrate_key_skills_to_skills.py +++ b/core/migrations/0014_migrate_key_skills_to_skills.py @@ -5,9 +5,6 @@ from users.models import CustomUser -custom_user_content_type = ContentType.objects.get_for_model(CustomUser) - - def migrate_key_skills_to_skills(apps, schema_editor): for user in CustomUser.objects.all(): if user.key_skills: @@ -16,18 +13,19 @@ def migrate_key_skills_to_skills(apps, schema_editor): skill = Skill.objects.filter(name__iexact=skill_name).first() if skill: SkillToObject.objects.get_or_create( - skill=skill, content_type=custom_user_content_type, object_id=user.id + skill=skill, content_type=ContentType.objects.get_for_model(CustomUser), object_id=user.id ) def reverse(apps, schema_editor): - SkillToObject.objects.filter(content_type=custom_user_content_type).delete() + SkillToObject.objects.filter(content_type=ContentType.objects.get_for_model(CustomUser)).delete() class Migration(migrations.Migration): dependencies = [ + ('contenttypes', '0001_initial'), ("core", "0013_add_skills_from_dataset"), - ("users", "0044_auto_20240128_2236") + ("users", "0044_auto_20240128_2236"), ] operations = [ diff --git a/vacancy/migrations/0003_migrate_old_required_skills_to_new.py b/vacancy/migrations/0003_migrate_old_required_skills_to_new.py index 17ccd256..613bda04 100644 --- a/vacancy/migrations/0003_migrate_old_required_skills_to_new.py +++ b/vacancy/migrations/0003_migrate_old_required_skills_to_new.py @@ -4,8 +4,6 @@ from core.models import Skill, SkillToObject from vacancy.models import Vacancy -vacancy_content_type = ContentType.objects.get_for_model(Vacancy) - def migrate_required_skills(apps, schema_editor): for vacancy in Vacancy.objects.all(): @@ -15,16 +13,17 @@ def migrate_required_skills(apps, schema_editor): skill = Skill.objects.filter(name__iexact=skill_name).first() if skill: SkillToObject.objects.get_or_create( - skill=skill, content_type=vacancy_content_type, object_id=vacancy.id + skill=skill, content_type=ContentType.objects.get_for_model(Vacancy), object_id=vacancy.id ) def reverse(apps, schema_editor): - SkillToObject.objects.filter(content_type=vacancy_content_type).delete() + SkillToObject.objects.filter(content_type=ContentType.objects.get_for_model(Vacancy)).delete() class Migration(migrations.Migration): dependencies = [ + ('contenttypes', '0001_initial'), ("vacancy", "0002_rename_required_skills_vacancy_required_skills_old"), ("core", "0013_add_skills_from_dataset") ] From b7089d90df53a948e4e4ae237ac8379f7e0ba210 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Mon, 4 Mar 2024 21:57:10 +0300 Subject: [PATCH 18/22] Handle required_skills_ids in vacancy routes start working on refactoring logic to serializers Signed-off-by: igor kuzmenkov --- users/serializers.py | 2 -- vacancy/serializers.py | 30 +++++++++++++++++++- vacancy/views.py | 62 ++++++++++++++++++++++++++++++++---------- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/users/serializers.py b/users/serializers.py index e9e8a61c..83a4f827 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -361,8 +361,6 @@ def create(self, validated_data) -> CustomUser: object_id=user.id, ) - user.save() - return user class Meta: diff --git a/vacancy/serializers.py b/vacancy/serializers.py index 5cc8140c..f4e71eb8 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -1,6 +1,8 @@ from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType from rest_framework import serializers +from core.models import Skill, SkillToObject from core.serializers import STOSerializer from projects.models import Project from users.serializers import UserDetailSerializer @@ -82,14 +84,40 @@ class Meta: class ProjectVacancyCreateListSerializer( - serializers.ModelSerializer, RequiredSkillsSerializerMixin + serializers.ModelSerializer, RequiredSkillsWriteSerializerMixin ): + def create(self, validated_data): + project = validated_data["project"] + if project.leader != self.context["request"].user: + raise serializers.ValidationError("You are not the leader of the project") + + required_skills_ids = validated_data.pop("required_skills_ids") + + vacancy = Vacancy.objects.create(**validated_data) + + for skill_id in required_skills_ids: + try: + skill = Skill.objects.get(id=skill_id) + except Skill.DoesNotExist: + raise serializers.ValidationError( + f"Skill with id {skill_id} does not exist" + ) + + SkillToObject.objects.create( + skill=skill, + content_type=ContentType.objects.get_for_model(Vacancy), + object_id=vacancy.id, + ) + + return vacancy + class Meta: model = Vacancy fields = [ "id", "role", "required_skills", + "required_skills_ids", "description", "project", "is_active", diff --git a/vacancy/views.py b/vacancy/views.py index 8b6936af..6adacbee 100644 --- a/vacancy/views.py +++ b/vacancy/views.py @@ -1,9 +1,11 @@ +from django.contrib.contenttypes.models import ContentType from django_filters import rest_framework as filters from rest_framework import generics, mixins, permissions, status from rest_framework.generics import GenericAPIView from rest_framework.response import Response -from projects.models import Collaborator +from core.models import Skill, SkillToObject +from projects.models import Collaborator from vacancy.filters import VacancyFilter from vacancy.models import Vacancy, VacancyResponse from vacancy.permissions import ( @@ -27,33 +29,63 @@ class VacancyList(generics.ListCreateAPIView): filter_backends = (filters.DjangoFilterBackend,) filterset_class = VacancyFilter - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - if serializer.validated_data["project"].leader != request.user: - # additional check that the user is the vacancy's project's leader - return Response(status=status.HTTP_403_FORBIDDEN) - self.perform_create(serializer) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - class VacancyDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Vacancy.objects.get_vacancy_for_detail_view() serializer_class = VacancyDetailSerializer permission_classes = [IsVacancyProjectLeader] + def patch(self, request, *args, **kwargs): + vacancy = self.get_object() + if "required_skills_ids" in request.data: + vacancy.required_skills.all().delete() + + for skill_id in request.data["required_skills_ids"]: + try: + skill = Skill.objects.get(id=skill_id) + except Skill.DoesNotExist: + return Response( + f"Skill with id {skill_id} does not exist", + status=status.HTTP_400_BAD_REQUEST, + ) + + SkillToObject.objects.create( + skill=skill, + content_type=ContentType.objects.get_for_model(Vacancy), + object_id=vacancy.id, + ) + + return self.partial_update(request, *args, **kwargs) + def put(self, request, *args, **kwargs): """updating the vacancy""" + vacancy = self.get_object() + if not request.data.get("is_active"): # automatically declining every vacancy response if the vacancy is not active - vacancy = self.get_object() vacancy_requests = VacancyResponse.objects.filter( vacancy=vacancy, is_approved=None ) - for vacancy_request in vacancy_requests: - vacancy_request.is_approved = False - vacancy_request.save() + vacancy_requests.update(is_approved=False) + + if request.data.get("required_skills_ids"): + vacancy.required_skills.all().delete() + + for skill_id in request.data["required_skills_ids"]: + try: + skill = Skill.objects.get(id=skill_id) + except Skill.DoesNotExist: + return Response( + f"Skill with id {skill_id} does not exist", + status=status.HTTP_400_BAD_REQUEST, + ) + + SkillToObject.objects.get_or_create( + skill=skill, + content_type=ContentType.objects.get_for_model(Vacancy), + object_id=vacancy.id, + ) + return self.update(request, *args, **kwargs) From 99d8dacb228145bd9315d4099699115989b8d873 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Mon, 4 Mar 2024 22:01:07 +0300 Subject: [PATCH 19/22] Update vacancy creation test for new required_skills from dataset Signed-off-by: igor kuzmenkov --- vacancy/tests.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/vacancy/tests.py b/vacancy/tests.py index 79bb43d7..b15e2928 100644 --- a/vacancy/tests.py +++ b/vacancy/tests.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + from django.test import TestCase from rest_framework.test import APIRequestFactory, force_authenticate @@ -23,7 +25,7 @@ def setUp(self): self.user_project_owner = self.user_create() self.vacancy_create_data = { "role": "Test", - "required_skills": ["Test"], + "required_skills_ids": [1, 15], "description": "Test", "is_active": True, "project": Project.objects.create( @@ -42,7 +44,25 @@ def test_vacancy_creation(self): self.assertEqual(response.status_code, 201) self.assertEqual(response.data["role"], "Test") - self.assertEqual(response.data["required_skills"], ["Test"]) + self.assertEqual( + response.data["required_skills"], + [ + OrderedDict( + [ + ("id", 1), + ("name", "Ведение социальных сетей"), + ("category", OrderedDict([("id", 1), ("name", "Маркетинг")])), + ] + ), + OrderedDict( + [ + ("id", 15), + ("name", "MS Office"), + ("category", OrderedDict([("id", 1), ("name", "Маркетинг")])), + ] + ), + ], + ) self.assertEqual(response.data["description"], "Test") self.assertEqual(response.data["is_active"], True) self.assertEqual(response.data["project"], self.vacancy_create_data["project"]) From 54b286217ade87418ce36c4cf785df815f8cd90f Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Mon, 4 Mar 2024 22:25:20 +0300 Subject: [PATCH 20/22] minor cosmetic refactors Signed-off-by: igor kuzmenkov --- core/models.py | 7 +++++-- core/serializers.py | 2 +- events/serializers.py | 4 ++-- projects/helpers.py | 2 +- projects/serializers.py | 4 ++-- users/models.py | 5 +---- users/serializers.py | 4 ++-- vacancy/serializers.py | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/models.py b/core/models.py index 675b1ccc..221a1a60 100644 --- a/core/models.py +++ b/core/models.py @@ -132,6 +132,9 @@ class Skill(models.Model): def __str__(self): return self.name + def __repr__(self): + return f"Skill" + class Meta: verbose_name = "Навык" verbose_name_plural = "Навыки" @@ -158,8 +161,8 @@ class SkillToObject(models.Model): content_object = GenericForeignKey("content_type", "object_id") class Meta: - verbose_name = "Навык" - verbose_name_plural = "Навыки" + verbose_name = "Ссылка на навык" + verbose_name_plural = "Ссылки на навыки" class SpecializationCategory(models.Model): diff --git a/core/serializers.py b/core/serializers.py index d516d81c..6fff7685 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -28,7 +28,7 @@ class Meta: fields = ["id", "name", "category"] -class STOSerializer(serializers.ModelSerializer): +class SkillToObjectSerializer(serializers.ModelSerializer): id = serializers.IntegerField(source="skill.id") name = serializers.CharField(source="skill.name") category = SkillCategorySerializer(source="skill.category") diff --git a/events/serializers.py b/events/serializers.py index 080a1244..47094d23 100644 --- a/events/serializers.py +++ b/events/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from taggit.serializers import TaggitSerializer, TagListSerializerField -from core.serializers import STOSerializer +from core.serializers import SkillToObjectSerializer from core.utils import get_user_online_cache_key from events.models import Event from users.serializers import MemberSerializer @@ -56,7 +56,7 @@ class Meta: class RegisteredUserListSerializer(serializers.ModelSerializer): member = MemberSerializer(required=False) - skills = STOSerializer(many=True, read_only=True) + skills = SkillToObjectSerializer(many=True, read_only=True) is_online = serializers.SerializerMethodField() @classmethod diff --git a/projects/helpers.py b/projects/helpers.py index df049b84..3b37a20c 100644 --- a/projects/helpers.py +++ b/projects/helpers.py @@ -25,7 +25,7 @@ def get_recommended_users(project: Project) -> list[User]: user_skills = set(user.get_skills()) - if user_skills.intersection(all_needed_skills): + if all_needed_skills & user_skills: recommended_users.append(user) # get some random users diff --git a/projects/serializers.py b/projects/serializers.py index bea5f33e..fb6f159c 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from rest_framework import serializers from django.core.cache import cache -from core.serializers import STOSerializer +from core.serializers import SkillToObjectSerializer from core.services import get_views_count, get_likes_count, is_fan from core.utils import get_user_online_cache_key from files.serializers import UserFileSerializer @@ -41,7 +41,7 @@ class CollaboratorSerializer(serializers.ModelSerializer): first_name = serializers.CharField(source="user.first_name") last_name = serializers.CharField(source="user.last_name") avatar = serializers.CharField(source="user.avatar") - skills = STOSerializer(many=True, read_only=True, source="user.skills") + skills = SkillToObjectSerializer(many=True, read_only=True, source="user.skills") class Meta: model = Collaborator diff --git a/users/models.py b/users/models.py index 2e1f3efe..6732a840 100644 --- a/users/models.py +++ b/users/models.py @@ -128,10 +128,7 @@ def skills_count(self): return self.skills.count() def get_skills(self): - skills = [] - for sto in self.skills.all(): - skills.append(sto.skill) - return skills + return [sto.skill for sto in self.skills.all()] def calculate_ordering_score(self) -> int: """ diff --git a/users/serializers.py b/users/serializers.py index 83a4f827..b5039401 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from django.core.cache import cache -from core.serializers import STOSerializer +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 @@ -93,7 +93,7 @@ class Meta: class SkillsSerializerMixin(serializers.Serializer): - skills = STOSerializer(many=True, read_only=True) + skills = SkillToObjectSerializer(many=True, read_only=True) class SkillsWriteSerializerMixin(SkillsSerializerMixin): diff --git a/vacancy/serializers.py b/vacancy/serializers.py index f4e71eb8..2b1007cf 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from core.models import Skill, SkillToObject -from core.serializers import STOSerializer +from core.serializers import SkillToObjectSerializer from projects.models import Project from users.serializers import UserDetailSerializer from vacancy.models import Vacancy, VacancyResponse @@ -12,7 +12,7 @@ class RequiredSkillsSerializerMixin(serializers.Serializer): - required_skills = STOSerializer(many=True, read_only=True) + required_skills = SkillToObjectSerializer(many=True, read_only=True) class RequiredSkillsWriteSerializerMixin(RequiredSkillsSerializerMixin): From 5477e04227933c20246a7b18d4abf85d642e56ed Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Mon, 4 Mar 2024 22:27:10 +0300 Subject: [PATCH 21/22] docs typo Signed-off-by: igor kuzmenkov --- vacancy/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vacancy/models.py b/vacancy/models.py index 743b5479..3c6e9b76 100644 --- a/vacancy/models.py +++ b/vacancy/models.py @@ -11,8 +11,8 @@ class Vacancy(models.Model): Attributes: role: A CharField title of the vacancy. - required_skills_old: A CharField required skills for the vacancy. - required_skills: A GenericRelation ша required skills for the vacancy. + required_skills_old: A CharField required skills for the vacancy (to be deprecated). + required_skills: A GenericRelation of required skills for the vacancy. description: A TextField description of the vacancy. project: A ForeignKey referring to the Company model. is_active: A boolean indicating if Vacancy is active. From 5421c87a0706872e2b7b6f75dc0cfc5a37355352 Mon Sep 17 00:00:00 2001 From: igor kuzmenkov Date: Mon, 4 Mar 2024 22:36:00 +0300 Subject: [PATCH 22/22] minor refactor Signed-off-by: igor kuzmenkov --- users/serializers.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/users/serializers.py b/users/serializers.py index b5039401..401ec7d4 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -348,18 +348,17 @@ def create(self, validated_data) -> CustomUser: user.set_password(validated_data["password"]) user.save() - if "skills_ids" in validated_data: - for skill_id in validated_data["skills_ids"]: - try: - skill = Skill.objects.get(id=skill_id) - except Skill.DoesNotExist: - raise serializers.ValidationError("Skill does not exist") - - SkillToObject.objects.create( - skill=skill, - content_type=ContentType.objects.get_for_model(CustomUser), - object_id=user.id, - ) + for skill_id in validated_data.get("skills_ids", []): + try: + skill = Skill.objects.get(id=skill_id) + except Skill.DoesNotExist: + raise serializers.ValidationError("Skill does not exist") + + SkillToObject.objects.create( + skill=skill, + content_type=ContentType.objects.get_for_model(CustomUser), + object_id=user.id, + ) return user