diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index ab2c2e27..9958aaa9 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -5,7 +5,7 @@ on: branches: - dev workflow_dispatch: - + jobs: deploy: runs-on: ubuntu-latest diff --git a/core/fields.py b/core/fields.py index 878fa92f..6dadd909 100644 --- a/core/fields.py +++ b/core/fields.py @@ -2,8 +2,8 @@ class CustomListField(serializers.ListField): - def to_representation(self, data): + def to_representation(self, data) -> list: return [value.strip() for value in data.split(",") if value.strip()] - def to_internal_value(self, data): + def to_internal_value(self, data) -> str: return ",".join(data) diff --git a/core/models.py b/core/models.py index 221a1a60..e2580fe6 100644 --- a/core/models.py +++ b/core/models.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models import Model +from django_stubs_ext.db.models import TypedModelMeta User = get_user_model() @@ -23,7 +24,7 @@ class Link(Model): object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") - class Meta: + class Meta(TypedModelMeta): unique_together = ( "link", "content_type", @@ -32,7 +33,7 @@ class Meta: verbose_name = "Ссылка" verbose_name_plural = "Ссылки" - def __str__(self): + def __str__(self) -> str: return f"Link for {self.content_object} - {self.link}" @@ -54,7 +55,7 @@ class Like(Model): object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") - class Meta: + class Meta(TypedModelMeta): unique_together = ( "user", "content_type", @@ -63,7 +64,7 @@ class Meta: verbose_name = "Лайк" verbose_name_plural = "Лайки" - def __str__(self): + def __str__(self) -> str: return f"Like<{self.user} - {self.content_object}>" @@ -88,7 +89,7 @@ class View(Model): object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") - class Meta: + class Meta(TypedModelMeta): unique_together = ( "user", "content_type", @@ -97,7 +98,7 @@ class Meta: verbose_name = "Просмотр" verbose_name_plural = "Просмотры" - def __str__(self): + def __str__(self) -> str: return f"View<{self.user} - {self.content_object}>" @@ -108,10 +109,10 @@ class SkillCategory(models.Model): name = models.CharField(max_length=256, null=False) - def __str__(self): + def __str__(self) -> str: return self.name - class Meta: + class Meta(TypedModelMeta): verbose_name = "Категория навыка" verbose_name_plural = "Категории навыков" ordering = ["name"] @@ -129,13 +130,13 @@ class Skill(models.Model): related_name="skills", ) - def __str__(self): + def __str__(self) -> str: return self.name - def __repr__(self): + def __repr__(self) -> str: return f"Skill" - class Meta: + class Meta(TypedModelMeta): verbose_name = "Навык" verbose_name_plural = "Навыки" ordering = ["id", "category", "name"] @@ -160,7 +161,7 @@ class SkillToObject(models.Model): object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") - class Meta: + class Meta(TypedModelMeta): verbose_name = "Ссылка на навык" verbose_name_plural = "Ссылки на навыки" @@ -168,10 +169,10 @@ class Meta: class SpecializationCategory(models.Model): name = models.TextField() - def __str__(self): + def __str__(self) -> str: return self.name - class Meta: + class Meta(TypedModelMeta): verbose_name = "Категория специализации" verbose_name_plural = "Категории специализаций" @@ -182,10 +183,10 @@ class Specialization(models.Model): SpecializationCategory, related_name="specializations", on_delete=models.CASCADE ) - def __str__(self): + def __str__(self) -> str: return self.name - class Meta: + class Meta(TypedModelMeta): verbose_name = "Специализация" verbose_name_plural = "Специализации" diff --git a/invites/models.py b/invites/models.py index 36f4c72e..77e97207 100644 --- a/invites/models.py +++ b/invites/models.py @@ -3,6 +3,7 @@ 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): @@ -37,10 +38,10 @@ class Invite(models.Model): objects = InviteManager() - def __str__(self): + def __str__(self) -> str: return f'Invite from project "{self.project.name}" to {self.user.get_full_name()}' - class Meta: + class Meta(TypedModelMeta): verbose_name = "Приглашение" verbose_name_plural = "Приглашения" ordering = ["-datetime_created"] diff --git a/invites/serializers.py b/invites/serializers.py index 91a336c7..1e05877a 100644 --- a/invites/serializers.py +++ b/invites/serializers.py @@ -5,7 +5,7 @@ from users.serializers import UserDetailSerializer -class InviteListSerializer(serializers.ModelSerializer): +class InviteListSerializer(serializers.ModelSerializer[Invite]): class Meta: model = Invite fields = [ @@ -18,7 +18,7 @@ class Meta: ] -class InviteDetailSerializer(serializers.ModelSerializer): +class InviteDetailSerializer(serializers.ModelSerializer[Invite]): user = UserDetailSerializer(many=False, read_only=True) project = ProjectListSerializer(many=False, read_only=True) diff --git a/news/managers.py b/news/managers.py index 339af533..21b3ec7b 100644 --- a/news/managers.py +++ b/news/managers.py @@ -1,13 +1,17 @@ from django.contrib.contenttypes.models import ContentType from django.db import models +from django.db.models.query import QuerySet +import typing +if typing.TYPE_CHECKING: + from news.models import News class NewsManager(models.Manager): - def get_news(self, obj): + def get_news(self, obj: models.Model) -> QuerySet["News"]: obj_type = ContentType.objects.get_for_model(obj) return self.get_queryset().filter(content_type=obj_type, object_id=obj.pk) - def add_news(self, obj, **kwargs): + def add_news(self, obj: models.Model, **kwargs) -> "News": obj_type = ContentType.objects.get_for_model(obj) kwargs = dict(kwargs) files = kwargs.pop("files", []) diff --git a/news/mixins.py b/news/mixins.py index 1e5a33e5..c0c6fd99 100644 --- a/news/mixins.py +++ b/news/mixins.py @@ -1,5 +1,5 @@ from django.contrib.auth import get_user_model - +from django.db.models.query import QuerySet from news.models import News from partner_programs.models import PartnerProgram from projects.models import Project @@ -12,7 +12,7 @@ class NewsQuerysetMixin: Mixin for getting queryset for news """ - def get_queryset_for_project(self): + def get_queryset_for_project(self) -> QuerySet[News]: """Returns queryset of news for project""" project_pk = self.kwargs.get("project_pk") try: @@ -25,7 +25,7 @@ def get_queryset_for_project(self): text="", content_type__model="project" ) - def get_queryset_for_program(self): + def get_queryset_for_program(self) -> QuerySet[News]: """Returns queryset of news for partner program""" partnerprogram_pk = self.kwargs.get("partnerprogram_pk") try: @@ -35,7 +35,7 @@ def get_queryset_for_program(self): return News.objects.none() return News.objects.get_news(obj=program) - def get_queryset_for_user(self): + def get_queryset_for_user(self) -> QuerySet[News]: """Returns queryset of news for user""" user_pk = self.kwargs.get("user_pk") try: @@ -45,7 +45,7 @@ def get_queryset_for_user(self): return News.objects.none() return News.objects.get_news(obj=user) - def get_queryset(self): + def get_queryset(self) -> QuerySet[News] | None: """Chooses what queryset to return - for project, program or user""" if self.kwargs.get("project_pk") is not None: return self.get_queryset_for_project() diff --git a/news/models.py b/news/models.py index 334bccef..f32cdc04 100644 --- a/news/models.py +++ b/news/models.py @@ -6,6 +6,7 @@ from core.models import Like, View from files.models import UserFile from news.managers import NewsManager +from django_stubs_ext.db.models import TypedModelMeta class News(models.Model): @@ -43,7 +44,7 @@ class News(models.Model): objects = NewsManager() - class Meta: + class Meta(TypedModelMeta): verbose_name = "Новость" verbose_name_plural = "Новости" ordering = ["-datetime_created"] diff --git a/news/serializers.py b/news/serializers.py index 66ab8dc9..d23345d0 100644 --- a/news/serializers.py +++ b/news/serializers.py @@ -10,7 +10,7 @@ User = get_user_model() -class NewsListCreateSerializer(serializers.ModelSerializer): +class NewsListCreateSerializer(serializers.ModelSerializer[News]): class Meta: model = News fields = [ @@ -19,7 +19,7 @@ class Meta: ] -class NewsListSerializer(serializers.ModelSerializer): +class NewsListSerializer(serializers.ModelSerializer[News]): views_count = serializers.SerializerMethodField() likes_count = serializers.SerializerMethodField() name = serializers.SerializerMethodField() @@ -60,6 +60,71 @@ class Meta: ] + +class NewsFeedListSerializer(serializers.ModelSerializer): + name = serializers.SerializerMethodField() + image_address = serializers.SerializerMethodField() + is_user_liked = serializers.SerializerMethodField() + files = UserFileSerializer(many=True) + views_count = serializers.SerializerMethodField() + likes_count = serializers.SerializerMethodField() + content_object = serializers.SerializerMethodField() + type_model = serializers.SerializerMethodField() + + def get_type_model(self, obj) -> str: + model_type = CONTENT_OBJECT_MAPPING[obj.content_type.model] + if obj.text != "" and model_type == "project": + return "news" + return model_type + + def get_content_object(self, obj) -> dict: + type_model = obj.content_type.model + if obj.text != "" and self.get_type_model(obj) == "project": + type_model = "news" + serializer = CONTENT_OBJECT_SERIALIZER_MAPPING[type_model](obj.content_object) + return serializer.data + + def get_views_count(self, obj): + return get_views_count(obj) + + def get_likes_count(self, obj): + return get_likes_count(obj) + + def get_name(self, obj): + if obj.content_type.model == CustomUser.__name__.lower(): + return f"{obj.content_object.first_name} {obj.content_object.last_name}" + elif obj.text != "" and obj.content_type.model == Project.__name__.lower(): + return f"{obj.content_object.name}" + + def get_image_address(self, obj): + return NewsMapping.get_image_address(obj.content_object) + + def get_is_user_liked(self, obj): + user = self.context.get("user") + if user: + return is_fan(obj, user) + return False + + class Meta: + model = News + fields = [ + "id", + "name", + "image_address", + "text", + "datetime_created", + "views_count", + "likes_count", + "files", + "is_user_liked", + "content_object", + "type_model", + ] + read_only_fields = ["views_count", "likes_count", "type_model"] + + + + class NewsDetailSerializer(serializers.ModelSerializer): views_count = serializers.SerializerMethodField() likes_count = serializers.SerializerMethodField() diff --git a/news/views.py b/news/views.py index fc43ce95..f29e1599 100644 --- a/news/views.py +++ b/news/views.py @@ -3,6 +3,7 @@ from rest_framework import generics, status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.request import Request from core.serializers import SetViewedSerializer, SetLikedSerializer from core.services import add_view, set_like @@ -25,7 +26,7 @@ class NewsList(NewsQuerysetMixin, generics.ListCreateAPIView): permission_classes = [IsNewsCreatorOrReadOnly] pagination_class = NewsPagination - def post(self, request, *args, **kwargs): + def post(self, request: Request, *args, **kwargs) -> Response: serializer = NewsListCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.validated_data @@ -46,7 +47,7 @@ def post(self, request, *args, **kwargs): # creating partner program news, not implemented yet, return 400 return Response(status=status.HTTP_400_BAD_REQUEST) - def get(self, request, *args, **kwargs): + def get(self, request: Request, *args, **kwargs) -> Response: news = self.paginate_queryset(self.get_queryset()) context = {"user": request.user} serializer = NewsListSerializer(news, context=context, many=True) @@ -57,7 +58,7 @@ class NewsDetail(NewsQuerysetMixin, generics.RetrieveUpdateDestroyAPIView): serializer_class = NewsDetailSerializer permission_classes = [IsNewsCreatorOrReadOnly] - def get(self, request, *args, **kwargs): + def get(self, request: Request, *args, **kwargs) -> Response: try: news = self.get_queryset().get(pk=self.kwargs["pk"]) context = {"user": request.user} @@ -65,7 +66,7 @@ def get(self, request, *args, **kwargs): except News.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) - def update(self, request, *args, **kwargs): + def update(self, request: Request, *args, **kwargs) -> Response: try: news = self.get_queryset().get(pk=self.kwargs["pk"]) context = {"user": request.user} @@ -82,7 +83,7 @@ class NewsDetailSetViewed(NewsQuerysetMixin, generics.CreateAPIView): serializer_class = SetViewedSerializer permission_classes = [IsAuthenticated] - def post(self, request, *args, **kwargs): + def post(self, request: Request, *args, **kwargs) -> Response: try: news = self.get_queryset().get(pk=self.kwargs["pk"]) add_view(news, request.user) @@ -95,7 +96,7 @@ class NewsDetailSetLiked(NewsQuerysetMixin, generics.CreateAPIView): serializer_class = SetLikedSerializer permission_classes = [IsAuthenticated] - def post(self, request, *args, **kwargs): + def post(self, request: Request, *args, **kwargs) -> Response: try: news = self.get_queryset().get(pk=self.kwargs["pk"]) set_like(news, request.user, request.data.get("is_liked")) diff --git a/procollab/settings.py b/procollab/settings.py index 1d54ce63..91246db5 100644 --- a/procollab/settings.py +++ b/procollab/settings.py @@ -34,6 +34,7 @@ "https://www.procollab.ru", "https://app.procollab.ru", "https://dev.procollab.ru", + "https://www.procollab.ru", ] ALLOWED_HOSTS = [ @@ -162,12 +163,12 @@ REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.IsAuthenticated", + "users.permissions.CustomIsAuthenticated", ], "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework_simplejwt.authentication.JWTAuthentication", "rest_framework.authentication.BasicAuthentication", - "rest_framework.authentication.SessionAuthentication", + # "rest_framework.authentication.SessionAuthentication",S ], "DEFAULT_RENDERER_CLASSES": [ "rest_framework.renderers.JSONRenderer", @@ -251,7 +252,7 @@ # Internationalization -LANGUAGE_CODE = "ru-ru" +LANGUAGE_CODE = "en-en" TIME_ZONE = "Europe/Moscow" @@ -294,6 +295,7 @@ "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), + "TOKEN_OBTAIN_SERIALIZER": "users.serializers.CustomObtainPairSerializer", } if DEBUG: diff --git a/project_rates/views.py b/project_rates/views.py index 00ea77d2..e76fb9e4 100644 --- a/project_rates/views.py +++ b/project_rates/views.py @@ -16,6 +16,7 @@ ProjectScoreCreateSerializer, ProjectScoreGetSerializer, ) +from users.models import Expert from users.permissions import IsExpert, IsExpertPost User = get_user_model() @@ -33,6 +34,9 @@ def get_needed_data(self) -> tuple[dict, list[int]]: criteria_to_get = [ criterion["criterion_id"] for criterion in data ] # is needed for validation later + + Expert.objects.get(user__id=user_id, programs__criterias__id=criteria_to_get[0]) + for criterion in data: criterion["user"] = user_id criterion["project"] = project_id @@ -57,7 +61,11 @@ def create(self, request, *args, **kwargs) -> Response: ) return Response({"success": True}, status=status.HTTP_201_CREATED) - + except Expert.DoesNotExist: + return Response( + {"error": "you have no permission to rate this program"}, + status=status.HTTP_403_FORBIDDEN, + ) except Exception as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -83,7 +91,7 @@ def get_querysets_dict(self) -> dict[str, QuerySet]: def serialize_querysets(self) -> list[dict]: return serialize_project_criterias(self.get_querysets_dict()) - def get(self, request, *args, **kwargs): + def get(self, request, *args, **kwargs) -> Response: serialized_data = self.serialize_querysets() if self.request.query_params.get("scored") == "true": diff --git a/users/permissions.py b/users/permissions.py index a5471156..d5f4f976 100644 --- a/users/permissions.py +++ b/users/permissions.py @@ -38,3 +38,12 @@ class IsExpertPost(BasePermission): def has_permission(self, request, view): return True if request.user.user_type == 3 else False + + +class CustomIsAuthenticated(BasePermission): + def has_permission(self, request, view): + if ( + hasattr(view, "authentication_off") and view.authentication_off + ): # Проверка наличия и значения атрибута + return True + return bool(request.user and request.user.is_authenticated) diff --git a/users/serializers.py b/users/serializers.py index 401ec7d4..ce5a41c2 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -12,6 +12,8 @@ from .models import CustomUser, Expert, Investor, Member, Mentor, UserAchievement from .validators import specialization_exists_validator +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer + class AchievementListSerializer(serializers.ModelSerializer[UserAchievement]): class Meta: @@ -466,3 +468,17 @@ def is_valid(self, *, raise_exception=False): def validate(self, data): super().validate(data) return validate_project(data) + + +class UserCloneDataSerializer(serializers.ModelSerializer): + class Meta: + model = CustomUser + fields = "__all__" + + +class CustomObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + token = super().get_token(user) + token["email"] = user.email + return token diff --git a/users/urls.py b/users/urls.py index e496b6f3..088644c3 100644 --- a/users/urls.py +++ b/users/urls.py @@ -23,6 +23,7 @@ UserSubscribedProjectsList, UserSpecializationsNestedView, UserSpecializationsInlineView, + SingleUserDataView, ) app_name = "users" @@ -73,4 +74,6 @@ "reset_password/", include("django_rest_passwordreset.urls", namespace="password_reset"), ), + # for skills + path("users/clone-data", SingleUserDataView.as_view()), ] diff --git a/users/views.py b/users/views.py index 43f7f699..57e33be8 100644 --- a/users/views.py +++ b/users/views.py @@ -2,9 +2,10 @@ from django.apps import apps from django.conf import settings from django.contrib.auth import get_user_model + from django.db import transaction from django.db.models import Q -from django.shortcuts import redirect +from django.shortcuts import redirect, get_object_or_404 from django_filters import rest_framework as filters from rest_framework import status, permissions from rest_framework import exceptions @@ -57,6 +58,7 @@ UserSubscribedProjectsSerializer, SpecializationsSerializer, SpecializationSerializer, + UserCloneDataSerializer, ) from .filters import UserFilter, SpecializationFilter from .pagination import UsersPagination @@ -408,3 +410,12 @@ class UserSpecializationsInlineView(ListAPIView): def get_queryset(self): return Specialization.objects.all() + + +class SingleUserDataView(ListAPIView): + serializer_class = UserCloneDataSerializer + permissions = [AllowAny] + authentication_off = True + + def get_queryset(self) -> User: + return [get_object_or_404(User, email=self.request.data["email"])] diff --git a/vacancy/filters.py b/vacancy/filters.py index 44c4d928..eb6b5db7 100644 --- a/vacancy/filters.py +++ b/vacancy/filters.py @@ -1,9 +1,10 @@ from django_filters import rest_framework as filters from vacancy.models import Vacancy +from django.db.models import QuerySet -def project_id_filter(queryset, name, value): +def project_id_filter(queryset, name, value) -> QuerySet: return queryset.filter( **{ "project_id": value[0], diff --git a/vacancy/models.py b/vacancy/models.py index 3c6e9b76..7a6a8e69 100644 --- a/vacancy/models.py +++ b/vacancy/models.py @@ -3,6 +3,7 @@ from projects.models import Project from vacancy.managers import VacancyManager, VacancyResponseManager +from django_stubs_ext.db.models import TypedModelMeta class Vacancy(models.Model): @@ -51,10 +52,10 @@ def get_required_skills(self): required_skills.append(sto.skill) return required_skills - def __str__(self): + def __str__(self) -> str: return f"Vacancy<{self.id}> - {self.role}" - class Meta: + class Meta(TypedModelMeta): verbose_name = "Вакансия" verbose_name_plural = "Вакансии" ordering = ["-datetime_created"] @@ -100,10 +101,10 @@ class VacancyResponse(models.Model): objects = VacancyResponseManager() - def __str__(self): + def __str__(self) -> str: return f"VacancyResponse<{self.id}> - {self.user} - {self.vacancy}" - class Meta: + class Meta(TypedModelMeta): verbose_name = "Отклик на вакансию" verbose_name_plural = "Отклик на вакансии" ordering = ["-datetime_created"] diff --git a/vacancy/serializers.py b/vacancy/serializers.py index 5979946e..2a1f5554 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -21,7 +21,7 @@ class RequiredSkillsWriteSerializerMixin(RequiredSkillsSerializerMixin): ) -class ProjectForVacancySerializer(serializers.ModelSerializer): +class ProjectForVacancySerializer(serializers.ModelSerializer[Project]): class Meta: model = Project fields = [ @@ -33,7 +33,7 @@ class Meta: class VacancyDetailSerializer( - serializers.ModelSerializer, RequiredSkillsWriteSerializerMixin + serializers.ModelSerializer, RequiredSkillsWriteSerializerMixin[Vacancy] ): project = ProjectForVacancySerializer(many=False, read_only=True) @@ -53,7 +53,7 @@ class Meta: read_only_fields = ["project"] -class VacancyListSerializer(serializers.ModelSerializer, RequiredSkillsSerializerMixin): +class VacancyListSerializer(serializers.ModelSerializer, RequiredSkillsSerializerMixin[Vacancy]): class Meta: model = Vacancy fields = [ @@ -69,7 +69,7 @@ class Meta: class ProjectVacancyListSerializer( - serializers.ModelSerializer, RequiredSkillsSerializerMixin + serializers.ModelSerializer, RequiredSkillsSerializerMixin[Vacancy] ): class Meta: model = Vacancy @@ -84,7 +84,7 @@ class Meta: class ProjectVacancyCreateListSerializer( - serializers.ModelSerializer, RequiredSkillsWriteSerializerMixin + serializers.ModelSerializer, RequiredSkillsWriteSerializerMixin[Vacancy] ): def create(self, validated_data): project = validated_data["project"] @@ -129,7 +129,7 @@ class Meta: ] -class VacancyResponseListSerializer(serializers.ModelSerializer): +class VacancyResponseListSerializer(serializers.ModelSerializer[VacancyResponse]): is_approved = serializers.BooleanField(read_only=True) user = UserDetailSerializer(read_only=True) user_id = serializers.IntegerField(write_only=True) @@ -170,7 +170,7 @@ def create(self, validated_data): return vacancy_response -class VacancyResponseDetailSerializer(serializers.ModelSerializer): +class VacancyResponseDetailSerializer(serializers.ModelSerializer[VacancyResponse]): user = UserDetailSerializer(many=False, read_only=True) vacancy = VacancyListSerializer(many=False, read_only=True) is_approved = serializers.BooleanField(read_only=True) @@ -188,5 +188,5 @@ class Meta: ] -class VacancyResponseAcceptSerializer(VacancyResponseDetailSerializer): +class VacancyResponseAcceptSerializer(VacancyResponseDetailSerializer[VacancyResponse]): is_approved = serializers.BooleanField(required=True, read_only=False)