diff --git a/projects/managers.py b/projects/managers.py index 0cddea5a..3bc8642d 100644 --- a/projects/managers.py +++ b/projects/managers.py @@ -42,17 +42,21 @@ def get_projects_for_list_view(self): ) def get_user_projects_for_list_view(self): - return self.get_queryset().prefetch_related( - Prefetch( - "industry", - queryset=Industry.objects.only("name").all(), - ), - Prefetch( - "leader", - queryset=CustomUser.objects.only("id").all(), - ), - Prefetch("collaborator_set"), - ).distinct() + return ( + self.get_queryset() + .prefetch_related( + Prefetch( + "industry", + queryset=Industry.objects.only("name").all(), + ), + Prefetch( + "leader", + queryset=CustomUser.objects.only("id").all(), + ), + Prefetch("collaborator_set"), + ) + .distinct() + ) def get_projects_for_detail_view(self): return ( @@ -66,6 +70,9 @@ def check_if_owns_any_projects(self, user) -> bool: # I don't think this should work but the function has no usages, so I'll let it be return user.leader_projects.exists() + def get_projects_from_list_of_ids(self, ids): + return self.get_queryset().filter(id__in=ids) + class AchievementManager(Manager): def get_achievements_for_list_view(self): diff --git a/projects/migrations/0010_project_views_count.py b/projects/migrations/0010_project_views_count.py new file mode 100644 index 00000000..5366db7f --- /dev/null +++ b/projects/migrations/0010_project_views_count.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2023-01-21 14:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0009_remove_project_short_description"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="views_count", + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/projects/models.py b/projects/models.py index 1e29e4df..c6762325 100644 --- a/projects/models.py +++ b/projects/models.py @@ -65,6 +65,12 @@ class Project(models.Model): objects = ProjectManager() + views_count = models.PositiveIntegerField(default=0) + + def increment_views_count(self): + self.views_count += 1 + self.save() + def get_short_description(self) -> Optional[str]: return self.description[:90] if self.description else None diff --git a/projects/serializers.py b/projects/serializers.py index cdd710a8..5d9f7e26 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -4,6 +4,7 @@ from industries.models import Industry from projects.models import Project, Achievement, Collaborator from projects.validators import validate_project +from users.models import LikesOnProject from vacancy.serializers import ProjectVacancyListSerializer @@ -65,6 +66,7 @@ class ProjectDetailSerializer(serializers.ModelSerializer): vacancies = ProjectVacancyListSerializer(many=True, read_only=True) short_description = serializers.SerializerMethodField() industry_id = serializers.IntegerField(required=False) + likes_count = serializers.SerializerMethodField(method_name="count_likes") def validate(self, data): super().validate(data) @@ -74,6 +76,9 @@ def validate(self, data): def get_short_description(cls, project): return project.get_short_description() + def count_likes(self, project): + return LikesOnProject.objects.filter(project=project, is_liked=True).count() + def update(self, instance, validated_data): instance = super().update(instance, validated_data) instance.save() @@ -99,12 +104,20 @@ class Meta: "vacancies", "datetime_created", "datetime_updated", + "views_count", + "likes_count", + ] + read_only_fields = [ + "leader", + "views_count", + "datetime_created", + "datetime_updated", ] - read_only_fields = ["leader"] class ProjectListSerializer(serializers.ModelSerializer): collaborators = serializers.SerializerMethodField(method_name="get_collaborators") + likes_count = serializers.SerializerMethodField(method_name="count_likes") collaborator_count = serializers.SerializerMethodField( method_name="get_collaborator_count" ) @@ -120,6 +133,9 @@ def get_short_description(cls, project): def get_collaborator_count(cls, obj): return len(obj.collaborator_set.all()) + def count_likes(self, obj): + return LikesOnProject.objects.filter(project=obj, is_liked=True).count() + def get_collaborators(self, obj): max_collaborator_count = 4 return CollaboratorSerializer( @@ -142,6 +158,7 @@ class Meta: "collaborators", "vacancies", "datetime_created", + "likes_count", ] read_only_fields = [ diff --git a/projects/urls.py b/projects/urls.py index 124bc9a2..8e155866 100644 --- a/projects/urls.py +++ b/projects/urls.py @@ -9,12 +9,14 @@ ProjectCollaborators, ProjectCountView, ProjectVacancyResponses, + SetLikeOnProject, ) app_name = "projects" urlpatterns = [ path("", ProjectList.as_view()), + path("/like/", SetLikeOnProject.as_view()), path("/collaborators/", ProjectCollaborators.as_view()), path("/", ProjectDetail.as_view()), path("count/", ProjectCountView.as_view()), diff --git a/projects/views.py b/projects/views.py index 1ca9470f..48160e3f 100644 --- a/projects/views.py +++ b/projects/views.py @@ -20,6 +20,7 @@ AchievementDetailSerializer, ProjectCollaboratorSerializer, ) +from users.models import LikesOnProject from vacancy.models import VacancyResponse from vacancy.serializers import VacancyResponseListSerializer @@ -79,6 +80,12 @@ class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = [HasInvolvementInProjectOrReadOnly] serializer_class = ProjectDetailSerializer + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + instance.increment_views_count() + serializer = self.get_serializer(instance) + return Response(serializer.data) + def put(self, request, pk, **kwargs): # bootleg version of updating achievements via project if request.data.get("achievements") is not None: @@ -100,6 +107,29 @@ def put(self, request, pk, **kwargs): return super(ProjectDetail, self).put(request, pk) +class SetLikeOnProject(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + """ + Set like on project + + --- + + Args: + request: + pk - project id + + Returns: + Response + + """ + project = Project.objects.get(pk=pk) + LikesOnProject.objects.toggle_like(request.user, project) + + return Response(ProjectListSerializer(project).data) + + class ProjectCountView(generics.GenericAPIView): queryset = Project.objects.get_projects_for_count_view() serializer_class = ProjectListSerializer diff --git a/users/managers.py b/users/managers.py index bbd6ea6c..bc94c02a 100644 --- a/users/managers.py +++ b/users/managers.py @@ -60,3 +60,21 @@ def get_achievements_for_detail_view(self): .select_related("user") .only("id", "title", "status", "user") ) + + +class LikesOnProjectManager(Manager): + def get_likes_for_list_view(self): + return ( + self.get_queryset() + .select_related("user") + .only("id", "user__id", "project__id") + ) + + def get_or_create(self, user, project): + return super().get_or_create(user=user, project=project) + + def toggle_like(self, user, project): + like, created = self.get_or_create(user=user, project=project) + if not created: + like.toggle_like() + return like diff --git a/users/migrations/0027_likesonproject.py b/users/migrations/0027_likesonproject.py new file mode 100644 index 00000000..e465fc3e --- /dev/null +++ b/users/migrations/0027_likesonproject.py @@ -0,0 +1,52 @@ +# Generated by Django 4.1.3 on 2023-01-21 14:45 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0010_project_views_count"), + ("users", "0026_remove_member_preferred_industries_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="LikesOnProject", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("like", models.BooleanField(default=True)), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="likes", + to="projects.project", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="likes_on_projects", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Лайк на проект", + "verbose_name_plural": "Лайки на проекты", + "unique_together": {("user", "project")}, + }, + ), + ] diff --git a/users/migrations/0028_rename_like_likesonproject_is_liked.py b/users/migrations/0028_rename_like_likesonproject_is_liked.py new file mode 100644 index 00000000..1923857e --- /dev/null +++ b/users/migrations/0028_rename_like_likesonproject_is_liked.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2023-01-21 18:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0027_likesonproject"), + ] + + operations = [ + migrations.RenameField( + model_name="likesonproject", + old_name="like", + new_name="is_liked", + ), + ] diff --git a/users/models.py b/users/models.py index f51e4cb2..43c754c8 100644 --- a/users/models.py +++ b/users/models.py @@ -12,7 +12,11 @@ VERBOSE_ROLE_TYPES, VERBOSE_USER_TYPES, ) -from users.managers import CustomUserManager, UserAchievementManager +from users.managers import ( + CustomUserManager, + UserAchievementManager, + LikesOnProjectManager, +) from users.validators import user_birthday_validator @@ -151,6 +155,45 @@ class Meta: abstract = True +class LikesOnProject(models.Model): + """ + LikesOnProject model + + This model is used to store the user's likes on projects. + + Attributes: + user: ForeignKey instance of user. + project: ForeignKey instance of project. + """ + + is_liked = models.BooleanField(default=True) + + user = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + related_name="likes_on_projects", + ) + project = models.ForeignKey( + "projects.Project", + on_delete=models.CASCADE, + related_name="likes", + ) + + objects = LikesOnProjectManager() + + def toggle_like(self): + self.is_liked = not self.is_liked + self.save() + + def __str__(self): + return f"LikesOnProject<{self.id}>" + + class Meta: + verbose_name = "Лайк на проект" + verbose_name_plural = "Лайки на проекты" + unique_together = ("user", "project") + + class Member(models.Model): """ Member model diff --git a/users/urls.py b/users/urls.py index 51a1fffe..ac24bdf5 100644 --- a/users/urls.py +++ b/users/urls.py @@ -14,6 +14,7 @@ UserTypesView, VerifyEmail, LogoutView, + LikedProjectList, ) app_name = "users" @@ -24,6 +25,7 @@ ), # this url actually returns mentors, experts and investors path("users/", UserList.as_view()), path("users/projects/", UserProjectsList.as_view()), + path("users/liked/", LikedProjectList.as_view()), path("users/roles/", UserAdditionalRolesView.as_view()), path("users/types/", UserTypesView.as_view()), path("users//", UserDetail.as_view()), diff --git a/users/views.py b/users/views.py index 8f98299a..78674cd1 100644 --- a/users/views.py +++ b/users/views.py @@ -27,7 +27,7 @@ from core.utils import Email from projects.serializers import ProjectListSerializer from users.helpers import VERBOSE_ROLE_TYPES, VERBOSE_USER_TYPES -from users.models import UserAchievement +from users.models import UserAchievement, LikesOnProject from users.permissions import IsAchievementOwnerOrReadOnly from users.serializers import ( AchievementDetailSerializer, @@ -38,7 +38,6 @@ UserListSerializer, VerifyEmailSerializer, ) - from .filters import UserFilter User = get_user_model() @@ -82,6 +81,18 @@ def post(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) +class LikedProjectList(ListAPIView): + serializer_class = ProjectListSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + projects_ids_list = LikesOnProject.objects.filter( + user=self.request.user, is_liked=True + ).values_list("project", flat=True) + + return Project.objects.get_projects_from_list_of_ids(projects_ids_list) + + class UserAdditionalRolesView(APIView): permission_classes = [AllowAny]