Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PRO-341: Approving user skills #434

Merged
merged 1 commit into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ class SpecializationCategoryAdmin(admin.ModelAdmin):
"id",
"name",
)


@admin.register(SkillToObject)
class SkillToObjectAdmin(admin.ModelAdmin):
list_display = ("id", "__str__")
search_fields = ("skill__name",)
8 changes: 8 additions & 0 deletions core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ class Meta(TypedModelMeta):
verbose_name = "Ссылка на навык"
verbose_name_plural = "Ссылки на навыки"

def __str__(self):
try:
return f"{self.skill.name} - {self.content_object}"
# Possible contingencies with attributes.
except Exception:
pass
return super().__str__()


class SpecializationCategory(models.Model):
name = models.TextField()
Expand Down
19 changes: 19 additions & 0 deletions users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Investor,
UserLink,
UserEducation,
UserSkillConfirmation,
)

from core.admin import SkillToObjectInline
Expand Down Expand Up @@ -331,3 +332,21 @@ class UserEducationAdmin(admin.ModelAdmin):
list_display = ("id", "user", "organization_name", "entry_year")
list_display_links = ("id", "organization_name")
search_fields = ("user__first_name", "user__email")


@admin.register(UserSkillConfirmation)
class UserSkillConfirmationAdmin(admin.ModelAdmin):
list_display = ("id", "get_user_and_skill", "confirmed_by", "confirmed_at")
search_fields = ("skill_to_object__skill__name", "confirmed_by__first_name", "confirmed_by__last_name")
raw_id_fields = ("skill_to_object", "confirmed_by")
readonly_fields = ("confirmed_at",)

def get_user_and_skill(self, obj):
try:
user = obj.skill_to_object.content_object
skill = obj.skill_to_object.skill
return f"{user} - {skill}"
# Possible contingencies with attributes.
except Exception:
return ""
get_user_and_skill.short_description = 'User and Skill'
58 changes: 58 additions & 0 deletions users/migrations/0050_userskillconfirmation_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Generated by Django 4.2.11 on 2024-09-10 15:22

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("core", "0015_alter_skill_options_alter_skilltoobject_options"),
("users", "0049_alter_customuser_key_skills_and_more"),
]

operations = [
migrations.CreateModel(
name="UserSkillConfirmation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("confirmed_at", models.DateTimeField(auto_now_add=True)),
(
"confirmed_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="skill_confirmations",
to=settings.AUTH_USER_MODEL,
),
),
(
"skill_to_object",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="confirmations",
to="core.skilltoobject",
),
),
],
options={
"verbose_name": "Подтверждение навыка",
"verbose_name_plural": "Подтверждения навыков",
},
),
migrations.AddConstraint(
model_name="userskillconfirmation",
constraint=models.UniqueConstraint(
fields=("skill_to_object", "confirmed_by"),
name="unique_skill_confirmed_by",
),
),
]
46 changes: 46 additions & 0 deletions users/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models import QuerySet
from django.core.exceptions import ValidationError
from django_stubs_ext.db.models import TypedModelMeta
from django.contrib.contenttypes.fields import GenericRelation

Expand Down Expand Up @@ -475,3 +476,48 @@ class Meta:
def __str__(self) -> str:
return (f"{self.user.first_name}: {self.organization_name} - "
f"{self.entry_year} (id {self.id})")


class UserSkillConfirmation(models.Model):
"""
Store confirmations for skills.

Attributes:
skill_to_object: FK SkillToObject.
confirmed_by: FK CustomUser.
confirmed_at: DateTimeField.
"""
skill_to_object = models.ForeignKey(
"core.SkillToObject",
on_delete=models.CASCADE,
related_name="confirmations"
)
confirmed_by = models.ForeignKey(
CustomUser,
on_delete=models.CASCADE,
related_name="skill_confirmations"
)
confirmed_at = models.DateTimeField(auto_now_add=True)

class Meta:
constraints = [
models.UniqueConstraint(
fields=["skill_to_object", "confirmed_by"],
name="unique_skill_confirmed_by"
)
]
verbose_name = "Подтверждение навыка"
verbose_name_plural = "Подтверждения навыков"

def clean(self) -> None:
# Check if the `skill_to_object` is related to a CustomUser.
if not isinstance(self.skill_to_object.content_object, CustomUser):
raise ValidationError("Skills can only be confirmed for users.")
# Check that the user does not confirm their own skill.
if self.confirmed_by == self.skill_to_object.content_object:
raise ValidationError("User cant approve own skills.")
return super().clean()

def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
16 changes: 16 additions & 0 deletions users/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from drf_yasg import openapi


USER_PK_PARAM = openapi.Parameter(
"user_pk",
openapi.IN_PATH,
description="Id user to confirmed",
type=openapi.TYPE_INTEGER
)

SKILL_PK_PARAM = openapi.Parameter(
"skill_pk",
openapi.IN_PATH,
description="Id skill user to confirmed",
type=openapi.TYPE_INTEGER
)
96 changes: 93 additions & 3 deletions users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@
from partner_programs.models import PartnerProgram, PartnerProgramUserProfile
from projects.models import Project, Collaborator
from projects.validators import validate_project
from .models import CustomUser, Expert, Investor, Member, Mentor, UserAchievement, UserEducation
from .validators import specialization_exists_validator
from users.validators import specialization_exists_validator
from users.models import (
CustomUser,
Expert,
Investor,
Member,
Mentor,
UserAchievement,
UserEducation,
UserSkillConfirmation,
)

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

Expand Down Expand Up @@ -88,6 +97,87 @@ class Meta:
]


class UserDataConfirmationSerializer(serializers.ModelSerializer):
"""Information about the User to add to the skill confirmation information."""
v2_speciality = SpecializationSerializer()

class Meta:
model = CustomUser
fields = [
"id",
"first_name",
"last_name",
"speciality",
"v2_speciality",
"avatar",
]


class UserSkillConfirmationSerializer(serializers.ModelSerializer):
"""Represents a work that requires approval of the user's skills."""

class Meta:
model = UserSkillConfirmation
fields = [
"skill_to_object",
"confirmed_by",
]
extra_kwargs = {
"skill_to_object": {"write_only": True},
}

def validate(self, attrs):
"""User cant approve own skills."""
skill_to_object = attrs.get("skill_to_object")
confirmed_by = self.context["request"].user

if skill_to_object.content_object == confirmed_by:
raise serializers.ValidationError("User cant approve own skills.")

return attrs

def to_representation(self, instance):
"""Returns correct data about user in `confirmed_by`."""
data = super().to_representation(instance)
data.pop("skill_to_object", None)
data["confirmed_by"] = UserDataConfirmationSerializer(instance.confirmed_by).data
return data


class UserApproveSkillResponse(serializers.Serializer):
"""For swagger response presentation."""
confirmed_by = UserDataConfirmationSerializer(read_only=True)


class UserSkillsWithApprovesSerializer(SkillToObjectSerializer):
"""Added field `approves` to response about User skills."""

approves = serializers.SerializerMethodField(allow_null=True, read_only=True)

class Meta:
model = SkillToObject
fields = [
"id",
"name",
"category",
"approves",
]

def get_approves(self, obj):
"""Adds information about confirm to the skill."""
confirmations = (
UserSkillConfirmation.objects
.filter(skill_to_object=obj)
.select_related('confirmed_by')
)
return [
{
"confirmed_by": UserDataConfirmationSerializer(confirmation.confirmed_by).data,
}
for confirmation in confirmations
]


class SpecializationsSerializer(serializers.ModelSerializer[SpecializationCategory]):
specializations = SpecializationSerializer(many=True)

Expand All @@ -97,7 +187,7 @@ class Meta:


class SkillsSerializerMixin(serializers.Serializer):
skills = SkillToObjectSerializer(many=True, read_only=True)
skills = UserSkillsWithApprovesSerializer(many=True, read_only=True)


class SkillsWriteSerializerMixin(SkillsSerializerMixin):
Expand Down
2 changes: 2 additions & 0 deletions users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
UserSubscribedProjectsList,
UserSpecializationsNestedView,
UserSpecializationsInlineView,
UserSkillsApproveDeclineView,
SingleUserDataView,
RemoteViewSubscriptions,
RemoteCreatePayment,
Expand All @@ -49,6 +50,7 @@
path("users/<int:user_pk>/news/<int:pk>/", NewsDetail.as_view()),
path("users/<int:user_pk>/news/<int:pk>/set_viewed/", NewsDetailSetViewed.as_view()),
path("users/<int:user_pk>/news/<int:pk>/set_liked/", NewsDetailSetLiked.as_view()),
path("users/<int:user_pk>/approve_skill/<int:skill_pk>/", UserSkillsApproveDeclineView.as_view()),
path("users/current/", CurrentUser.as_view()),
# todo: change password view
path("users/current/programs/", CurrentUserPrograms.as_view()),
Expand Down
Loading
Loading