Skip to content

Commit d8b1bd9

Browse files
authored
Merge pull request #239 from PROCOLLAB-github/feature/subscribes
add subscribe and subscribers routes
2 parents 1534b4a + aedc5da commit d8b1bd9

6 files changed

Lines changed: 140 additions & 2 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2.3 on 2023-11-14 20:07
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11+
("projects", "0020_project_cover_defaultprojectcover"),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name="project",
17+
name="subscribers",
18+
field=models.ManyToManyField(
19+
related_name="subscribed_projects",
20+
to=settings.AUTH_USER_MODEL,
21+
verbose_name="Подписчики",
22+
),
23+
),
24+
]

projects/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ class Project(models.Model):
107107
blank=True,
108108
)
109109

110+
subscribers = models.ManyToManyField(
111+
User, verbose_name="Подписчики", related_name="subscribed_projects"
112+
)
113+
110114
datetime_created = models.DateTimeField(
111115
verbose_name="Дата создания", null=False, auto_now_add=True
112116
)

projects/serializers.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from django.contrib.auth import get_user_model
22
from rest_framework import serializers
3-
3+
from django.core.cache import cache
44
from core.fields import CustomListField
55
from core.services import get_views_count, get_likes_count, is_fan
6+
from core.utils import get_user_online_cache_key
67
from files.serializers import UserFileSerializer
78
from industries.models import Industry
89
from projects.models import Project, Achievement, Collaborator, ProjectNews
@@ -275,3 +276,26 @@ class Meta:
275276
"is_user_liked",
276277
"files",
277278
]
279+
280+
281+
class ProjectSubscribersListSerializer(serializers.ModelSerializer):
282+
is_online = serializers.SerializerMethodField()
283+
284+
def get_is_online(self, user: User) -> bool:
285+
request = self.context.get("request")
286+
if request and request.user.is_authenticated and request.user.id == user.id:
287+
return True
288+
289+
cache_key = get_user_online_cache_key(user)
290+
is_online = cache.get(cache_key, False)
291+
return is_online
292+
293+
class Meta:
294+
model = User
295+
fields = [
296+
"id",
297+
"first_name",
298+
"last_name",
299+
"avatar",
300+
"is_online",
301+
]

projects/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
ProjectVacancyResponses,
1313
ProjectRecommendedUsers,
1414
SetLikeOnProject,
15+
ProjectSubscribe,
16+
ProjectUnsubscribe,
17+
ProjectSubscribers,
1518
)
1619

1720
app_name = "projects"
@@ -20,6 +23,9 @@
2023
path("", ProjectList.as_view()),
2124
path("<int:pk>/like/", SetLikeOnProject.as_view()),
2225
path("<int:project_pk>/news/", NewsList.as_view()),
26+
path("<int:project_pk>/subscribe/", ProjectSubscribe.as_view()),
27+
path("<int:project_pk>/unsubscribe/", ProjectUnsubscribe.as_view()),
28+
path("<int:project_pk>/subscribers/", ProjectSubscribers.as_view()),
2329
path("<int:project_pk>/news/<int:pk>/", NewsDetail.as_view()),
2430
path("<int:project_pk>/news/<int:pk>/set_viewed/", NewsDetailSetViewed.as_view()),
2531
path("<int:project_pk>/news/<int:pk>/set_liked/", NewsDetailSetLiked.as_view()),

projects/views.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import logging
2+
13
from django.contrib.auth import get_user_model
24
from django.db.models import Q
35
from django_filters import rest_framework as filters
6+
from drf_yasg import openapi
7+
from drf_yasg.utils import swagger_auto_schema
48
from rest_framework import generics, permissions, status
9+
from rest_framework.exceptions import NotFound
510
from rest_framework.permissions import IsAuthenticated
611
from rest_framework.response import Response
712
from rest_framework.views import APIView
@@ -33,12 +38,15 @@
3338
ProjectCollaboratorSerializer,
3439
ProjectNewsListSerializer,
3540
ProjectNewsDetailSerializer,
41+
ProjectSubscribersListSerializer,
3642
)
3743
from users.models import LikesOnProject
3844
from users.serializers import UserListSerializer
3945
from vacancy.models import VacancyResponse
4046
from vacancy.serializers import VacancyResponseListSerializer
4147

48+
logger = logging.getLogger()
49+
4250
User = get_user_model()
4351

4452

@@ -381,3 +389,75 @@ def post(self, request, *args, **kwargs):
381389
return Response(status=status.HTTP_200_OK)
382390
except ProjectNews.DoesNotExist:
383391
return Response(status=status.HTTP_404_NOT_FOUND)
392+
393+
394+
class ProjectSubscribers(APIView):
395+
permission_classes = [IsAuthenticated]
396+
397+
@swagger_auto_schema(
398+
responses={
399+
200: openapi.Response(
400+
"List of project subscribers", ProjectSubscribersListSerializer(many=True)
401+
)
402+
}
403+
)
404+
def get(self, request, *args, **kwargs):
405+
try:
406+
project = Project.objects.get(pk=self.kwargs["project_pk"])
407+
except Project.DoesNotExist:
408+
raise NotFound
409+
subscribers = ProjectSubscribersListSerializer(
410+
project.subscribers.all(), many=True
411+
).data
412+
return Response(subscribers, status=status.HTTP_200_OK)
413+
414+
415+
class ProjectSubscribe(APIView):
416+
permission_classes = [IsAuthenticated]
417+
418+
def post(self, request, project_pk):
419+
try:
420+
project = Project.objects.get(pk=project_pk)
421+
except Project.DoesNotExist:
422+
raise NotFound
423+
try:
424+
project.subscribers.add(request.user)
425+
except Exception:
426+
return Response(
427+
{
428+
"detail": f"User {request.user.id} is not part of project {project.pk}."
429+
},
430+
status=status.HTTP_400_BAD_REQUEST,
431+
)
432+
433+
logger.info(f"User {request.user.id} subscribed to project {project_pk}")
434+
435+
return Response(
436+
{"detail": "Subscriber was successfully added"}, status=status.HTTP_200_OK
437+
)
438+
439+
440+
class ProjectUnsubscribe(APIView):
441+
permission_classes = [IsAuthenticated]
442+
443+
def post(self, request, project_pk):
444+
try:
445+
project = Project.objects.get(pk=project_pk)
446+
except Project.DoesNotExist:
447+
raise NotFound
448+
try:
449+
project.subscribers.remove(request.user)
450+
# todo: add more specific error here
451+
except Exception:
452+
return Response(
453+
{
454+
"detail": f"User {request.user.id} is not part of project {project.pk}."
455+
},
456+
status=status.HTTP_400_BAD_REQUEST,
457+
)
458+
459+
logger.info(f"User {request.user.id} unsubscribed to project {project_pk}")
460+
461+
return Response(
462+
{"detail": "Subscriber was successfully removed"}, status=status.HTTP_200_OK
463+
)

users/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,12 @@ class CustomUser(AbstractUser):
9696
verbose_name="Стадия онбординга",
9797
help_text="0, 1, 2 - номера стадий онбординга, null(пустое) - онбординг пройден",
9898
)
99+
99100
verification_date = models.DateField(
100101
null=True,
101102
blank=True,
102103
verbose_name="Дата верификации",
103104
)
104-
105105
datetime_updated = models.DateTimeField(auto_now=True)
106106
datetime_created = models.DateTimeField(auto_now_add=True)
107107

0 commit comments

Comments
 (0)