Skip to content

Commit

Permalink
Merge pull request #362 from PROCOLLAB-github/dev-to-master
Browse files Browse the repository at this point in the history
Dev
sh1nkey authored May 28, 2024
2 parents 9268298 + fccbe87 commit f56d7a6
Showing 19 changed files with 182 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dev-ci.yml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ on:
branches:
- dev
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
4 changes: 2 additions & 2 deletions core/fields.py
Original file line number Diff line number Diff line change
@@ -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)
33 changes: 17 additions & 16 deletions core/models.py
Original file line number Diff line number Diff line change
@@ -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<name={self.name},id={self.id}>"

class Meta:
class Meta(TypedModelMeta):
verbose_name = "Навык"
verbose_name_plural = "Навыки"
ordering = ["id", "category", "name"]
@@ -160,18 +161,18 @@ 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 = "Ссылки на навыки"


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 = "Специализации"

5 changes: 3 additions & 2 deletions invites/models.py
Original file line number Diff line number Diff line change
@@ -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"]
4 changes: 2 additions & 2 deletions invites/serializers.py
Original file line number Diff line number Diff line change
@@ -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)

8 changes: 6 additions & 2 deletions news/managers.py
Original file line number Diff line number Diff line change
@@ -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", [])
10 changes: 5 additions & 5 deletions news/mixins.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion news/models.py
Original file line number Diff line number Diff line change
@@ -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"]
69 changes: 67 additions & 2 deletions news/serializers.py
Original file line number Diff line number Diff line change
@@ -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()
13 changes: 7 additions & 6 deletions news/views.py
Original file line number Diff line number Diff line change
@@ -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,15 +58,15 @@ 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}
return Response(NewsDetailSerializer(news, context=context).data)
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"))
8 changes: 5 additions & 3 deletions procollab/settings.py
Original file line number Diff line number Diff line change
@@ -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:
12 changes: 10 additions & 2 deletions project_rates/views.py
Original file line number Diff line number Diff line change
@@ -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":
9 changes: 9 additions & 0 deletions users/permissions.py
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 16 additions & 0 deletions users/serializers.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions users/urls.py
Original file line number Diff line number Diff line change
@@ -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()),
]
13 changes: 12 additions & 1 deletion users/views.py
Original file line number Diff line number Diff line change
@@ -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"])]
3 changes: 2 additions & 1 deletion vacancy/filters.py
Original file line number Diff line number Diff line change
@@ -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],
9 changes: 5 additions & 4 deletions vacancy/models.py
Original file line number Diff line number Diff line change
@@ -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"]
16 changes: 8 additions & 8 deletions vacancy/serializers.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit f56d7a6

Please sign in to comment.