Skip to content

Commit

Permalink
Merge pull request #405 from PROCOLLAB-github/dev
Browse files Browse the repository at this point in the history
merge from dev
  • Loading branch information
sh1nkey authored Jul 24, 2024
2 parents d40ddd1 + 979c18a commit 49b00f9
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 36 deletions.
9 changes: 9 additions & 0 deletions projects/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework import status
from rest_framework.exceptions import APIException
from django.utils.translation import gettext_lazy as _


class CollaboratorDoesNotExist(APIException):
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
default_detail = _("Not found.")
default_code = "not_found"
8 changes: 8 additions & 0 deletions projects/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
ProjectSubscribe,
ProjectUnsubscribe,
ProjectSubscribers,
SwitchLeaderRole,
LeaveProject,
SwitchLeaderRole,
)

app_name = "projects"
Expand All @@ -30,6 +33,11 @@
path("<int:project_pk>/news/<int:pk>/set_viewed/", NewsDetailSetViewed.as_view()),
path("<int:project_pk>/news/<int:pk>/set_liked/", NewsDetailSetLiked.as_view()),
path("<int:pk>/collaborators/", ProjectCollaborators.as_view()),
path("<int:project_pk>/collaborators/leave/", LeaveProject.as_view()),
path(
"<int:project_pk>/collaborators/<int:user_to_leader_pk>/switch-leader/",
SwitchLeaderRole.as_view(),
),
path("<int:pk>/", ProjectDetail.as_view()),
path("<int:pk>/recommended_users", ProjectRecommendedUsers.as_view()),
path("count/", ProjectCountView.as_view()),
Expand Down
204 changes: 191 additions & 13 deletions projects/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import logging
from typing import Annotated

from django.contrib.auth import get_user_model
from django.db.models import Q
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.db.models import Q, QuerySet
from django_filters import rest_framework as filters
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
Expand All @@ -15,14 +18,15 @@
from core.serializers import SetLikedSerializer
from core.services import add_view, set_like
from partner_programs.models import PartnerProgram, PartnerProgramUserProfile
from projects.exceptions import CollaboratorDoesNotExist
from projects.filters import ProjectFilter
from projects.constants import VERBOSE_STEPS
from projects.helpers import (
get_recommended_users,
check_related_fields_update,
update_partner_program,
)
from projects.models import Project, Achievement, ProjectNews
from projects.models import Project, Achievement, ProjectNews, Collaborator
from projects.pagination import ProjectNewsPagination, ProjectsPagination
from projects.permissions import (
IsProjectLeaderOrReadOnlyForNonDrafts,
Expand All @@ -43,7 +47,7 @@
from users.models import LikesOnProject
from users.serializers import UserListSerializer
from vacancy.models import VacancyResponse
from vacancy.serializers import VacancyResponseListSerializer
from vacancy.serializers import VacancyResponseFullFileInfoListSerializer

logger = logging.getLogger()

Expand Down Expand Up @@ -247,15 +251,48 @@ def post(self, request, pk: int):
return Response(status=200)

def delete(self, request, pk: int):
"""delete collaborators from the project"""
m2m_manager = self.get_object().collaborators
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
collaborators = serializer.validated_data["collaborators"]
for user in collaborators:
# note: doesn't raise an error when we try to delete someone who isn't a collaborator
m2m_manager.remove(user)
return Response(status=200)
"""delete collaborator from project"""
requested_collab_id: int = int(self.request.query_params.get("id"))

project_id, leader_id = self._project_data(pk)
existing_collab_id = self._collabs_queryset(
project_id, requested_collab_id, leader_id
)

if leader_id == requested_collab_id:
return Response(
{
"error": f"User with id: {leader_id} is a leader of a project. "
f"Be careful not to delete yourself from a project!"
},
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
if not existing_collab_id:
return Response(
{
"error": f"User with id: {requested_collab_id} are not part of this project."
},
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
)

existing_collab_id.delete()
return Response(status=204)

def _project_data(
self, project_pk: int
) -> tuple[Annotated[int, "ID проекта"], Annotated[int, "ID лидера проекта"]]:
project = get_object_or_404(
Project.objects.select_related("leader"), id=project_pk
)
return project.id, project.leader.id

@staticmethod
def _collabs_queryset(project_id: int, requested_id: int, leader_id: int) -> QuerySet:
return Collaborator.objects.exclude(
user__id=leader_id
).get( # чтоб случайно лидер сам себя не удалил
user__id=requested_id, project__id=project_id
)


class ProjectSteps(APIView):
Expand All @@ -281,7 +318,7 @@ class AchievementDetail(generics.RetrieveUpdateDestroyAPIView):


class ProjectVacancyResponses(generics.GenericAPIView):
serializer_class = VacancyResponseListSerializer
serializer_class = VacancyResponseFullFileInfoListSerializer
permission_classes = [IsAuthenticated]

def get_queryset(self):
Expand Down Expand Up @@ -461,3 +498,144 @@ def post(self, request, project_pk):
return Response(
{"detail": "Subscriber was successfully removed"}, status=status.HTTP_200_OK
)



class SwitchLeaderRole(generics.GenericAPIView):
permission_classes = [IsProjectLeader]
queryset = Project.objects.all().select_related("leader")

def _get_new_leader(self, user_id: int, project: Project) -> Collaborator:
try:
return Collaborator.objects.select_related("user").get(
user_id=user_id, project=project
)
except ObjectDoesNotExist:
raise CollaboratorDoesNotExist(
f"""Collaborator with user_id: {user_id} does not exist. Either user_id is not correct, or project_id
is not correct, or try adding this user to a project (as collaborator) before making them a leader. """
)

def patch(self, request, pk: int):
project = self.get_object()

new_leader_id = int(request.data["new_leader_id"])
new_leader = self._get_new_leader(new_leader_id, project)

if project.leader.id == new_leader_id:
return Response(
{"error": "User is already a leader of a project"},
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
)

project.leader = new_leader.user
project.save()
return Response(status=204)


class LeaveProject(generics.GenericAPIView):
permission_classes = [IsAuthenticated]

def delete(self, request, project_pk: int) -> Response:
current_user_id = self.request.user.id
collaborator = get_object_or_404(
Collaborator.objects.all(),
project_id=project_pk,
user_id=current_user_id,
)
project = Project.objects.select_related("leader").get(id=project_pk)
if project.leader.id == current_user_id:
return Response(
{
"error": "You can't leave if you are a leader of a project. "
"Please, switch leadership!"
},
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
collaborator.delete()
return Response(status=204)


class DeleteProjectCollaborators(generics.GenericAPIView):
permission_classes = [IsProjectLeader]

def _project_data(
self, project_pk: int
) -> tuple[Annotated[int, "ID проекта"], Annotated[int, "ID лидера проекта"]]:
project = get_object_or_404(
Project.objects.select_related("leader"), id=project_pk
)
return project.id, project.leader.id

@staticmethod
def _collabs_queryset(project_id: int, requested_id: int, leader_id: int) -> QuerySet:
return Collaborator.objects.exclude(
user__id=leader_id
).get( # чтоб случайно лидер сам себя не удалил
user__id=requested_id, project__id=project_id
)

def delete(self, request, project_pk: int) -> Response:
requested_collab_id: int = int(self.request.query_params.get("id"))

project_id, leader_id = self._project_data(project_pk)
existing_collab_id = self._collabs_queryset(
project_id, requested_collab_id, leader_id
)

if leader_id == requested_collab_id:
return Response(
{
"error": f"User with id: {leader_id} is a leader of a project. "
f"Be careful not to delete yourself from a project!"
},
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
if not existing_collab_id:
return Response(
{
"error": f"User with id: {requested_collab_id} are not part of this project."
},
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
)

existing_collab_id.delete()
return Response(status=204)


class SwitchLeaderRole(generics.GenericAPIView):
permission_classes = [IsProjectLeader]
queryset = Project.objects.all().select_related("leader")

@staticmethod
def _get_new_leader(user_id: int, project: Project) -> Collaborator:
try:
return Collaborator.objects.select_related("user").get(
user_id=user_id, project=project
)
except ObjectDoesNotExist:
raise CollaboratorDoesNotExist(
f"""Collaborator with user_id: {user_id} does not exist. Either user_id is not correct, or project_id
is not correct, or try adding this user to a project (as collaborator) before making them a leader. """
)

@staticmethod
def _get_project(project_pk: int) -> Project:
return get_object_or_404(Project.objects.all(), id=project_pk)

def patch(self, request, project_pk: int, user_to_leader_pk: int) -> Response:
project = self._get_project(project_pk)

new_leader_id = user_to_leader_pk

if project.leader.id == new_leader_id:
return Response(
{"error": "User is already a leader of a project"},
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
)

new_leader = self._get_new_leader(new_leader_id, project)

project.leader = new_leader.user
project.save()
return Response(status=204)
2 changes: 2 additions & 0 deletions users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class CustomUser(AbstractUser):
speciality: CharField instance the user's specialty.
datetime_updated: A DateTimeField indicating date of update.
datetime_created: A DateTimeField indicating date of creation.
dataset_migration_applied: A BooleanField indicating based on
the `v2_speciality` and `skills`.
"""

ADMIN = ADMIN
Expand Down
16 changes: 16 additions & 0 deletions users/signals.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.core.mail import EmailMultiAlternatives
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.template.loader import render_to_string
Expand All @@ -25,6 +26,21 @@ def create_or_update_user_types(sender, instance, created, **kwargs):
instance.save()


@receiver(post_save, sender=CustomUser)
def update_dataset_migration_applied(sender, instance, **kwargs):
"""Update the `dataset_migration_applied` attribute based on the presence of `v2_speciality` and `skills`."""

def update_migration():
dataset_migration_applied = bool(instance.v2_speciality and instance.skills.exists())
if instance.dataset_migration_applied != dataset_migration_applied:
CustomUser.objects.filter(pk=instance.pk).update(
dataset_migration_applied=dataset_migration_applied
)

# Delayed execution until transaction completes.
transaction.on_commit(update_migration)


@receiver(reset_password_token_created)
def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs):
reset_password_url = (
Expand Down
1 change: 1 addition & 0 deletions vacancy/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class VacancyAdmin(admin.ModelAdmin):
inlines = [
VacancySkillToObjectInline,
]
readonly_fields = ('datetime_closed',)
list_display_links = ["role"]

change_list_template = "vacancies/vacancies_change_list.html"
Expand Down
39 changes: 23 additions & 16 deletions vacancy/migrations/0003_migrate_old_required_skills_to_new.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
# Generated by Django 4.2.3 on 2024-03-02 22:32
from django.contrib.contenttypes.models import ContentType

"""
The migration is irrelevant and breaks other migrations,
providing access to the database before formation.
The edits described in this migration have already been done manually.
"""

# from django.contrib.contenttypes.models import ContentType
from django.db import migrations
from core.models import Skill, SkillToObject
from vacancy.models import Vacancy
# from core.models import Skill, SkillToObject
# from vacancy.models import 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=ContentType.objects.get_for_model(Vacancy), object_id=vacancy.id
)
# 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=ContentType.objects.get_for_model(Vacancy), object_id=vacancy.id
# )


def reverse(apps, schema_editor):
SkillToObject.objects.filter(content_type=ContentType.objects.get_for_model(Vacancy)).delete()
# def reverse(apps, schema_editor):
# SkillToObject.objects.filter(content_type=ContentType.objects.get_for_model(Vacancy)).delete()


class Migration(migrations.Migration):
Expand All @@ -29,5 +36,5 @@ class Migration(migrations.Migration):
]

operations = [
migrations.RunPython(migrate_required_skills, reverse_code=reverse),
# migrations.RunPython(migrate_required_skills, reverse_code=reverse),
]
Loading

0 comments on commit 49b00f9

Please sign in to comment.