From 4946122f2e58664afe6e409f535c5221365cb24f Mon Sep 17 00:00:00 2001 From: ITQ Date: Mon, 4 Mar 2024 16:37:16 +0300 Subject: [PATCH] Added posts creation and posts detail --- solution/pulse/api/posts/__init__.py | 0 solution/pulse/api/posts/apps.py | 6 +++ .../api/posts/migrations/0001_initial.py | 29 ++++++++++++++ .../pulse/api/posts/migrations/__init__.py | 0 solution/pulse/api/posts/models.py | 27 +++++++++++++ solution/pulse/api/posts/permissions.py | 17 +++++++++ solution/pulse/api/posts/serializers.py | 38 +++++++++++++++++++ solution/pulse/api/posts/urls.py | 16 ++++++++ solution/pulse/api/posts/views.py | 35 +++++++++++++++++ solution/pulse/api/urls.py | 1 + solution/pulse/api/users/models.py | 8 ++++ solution/pulse/pulse/settings.py | 3 ++ 12 files changed, 180 insertions(+) create mode 100644 solution/pulse/api/posts/__init__.py create mode 100644 solution/pulse/api/posts/apps.py create mode 100644 solution/pulse/api/posts/migrations/0001_initial.py create mode 100644 solution/pulse/api/posts/migrations/__init__.py create mode 100644 solution/pulse/api/posts/models.py create mode 100644 solution/pulse/api/posts/permissions.py create mode 100644 solution/pulse/api/posts/serializers.py create mode 100644 solution/pulse/api/posts/urls.py create mode 100644 solution/pulse/api/posts/views.py diff --git a/solution/pulse/api/posts/__init__.py b/solution/pulse/api/posts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/pulse/api/posts/apps.py b/solution/pulse/api/posts/apps.py new file mode 100644 index 0000000..713e60f --- /dev/null +++ b/solution/pulse/api/posts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PostsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "api.posts" diff --git a/solution/pulse/api/posts/migrations/0001_initial.py b/solution/pulse/api/posts/migrations/0001_initial.py new file mode 100644 index 0000000..05e63ef --- /dev/null +++ b/solution/pulse/api/posts/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.10 on 2024-03-03 17:48 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('content', models.TextField(max_length=1000)), + ('tags', models.JSONField()), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='users.profile')), + ('dislikes', models.ManyToManyField(blank=True, related_name='disliked_posts', to='users.profile')), + ('likes', models.ManyToManyField(blank=True, related_name='liked_posts', to='users.profile')), + ], + ), + ] diff --git a/solution/pulse/api/posts/migrations/__init__.py b/solution/pulse/api/posts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/pulse/api/posts/models.py b/solution/pulse/api/posts/models.py new file mode 100644 index 0000000..36d42cc --- /dev/null +++ b/solution/pulse/api/posts/models.py @@ -0,0 +1,27 @@ +import uuid + +from django.db import models + +from api.users.models import Profile + + +class Post(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + content = models.TextField(max_length=1000) + author = models.ForeignKey( + Profile, + on_delete=models.CASCADE, + related_name="posts", + ) + tags = models.JSONField() + # ruff: noqa: N815 + createdAt = models.DateTimeField(auto_now_add=True) + likes = models.ManyToManyField( + Profile, related_name="liked_posts", blank=True + ) + dislikes = models.ManyToManyField( + Profile, related_name="disliked_posts", blank=True + ) + + def __str__(self): + return self.content diff --git a/solution/pulse/api/posts/permissions.py b/solution/pulse/api/posts/permissions.py new file mode 100644 index 0000000..ee40832 --- /dev/null +++ b/solution/pulse/api/posts/permissions.py @@ -0,0 +1,17 @@ +from rest_framework import status +from rest_framework.permissions import BasePermission + + +class CanAccessPost(BasePermission): + message = "You do not have permission to access this post." + status_code = status.HTTP_404_NOT_FOUND + + def has_object_permission(self, request, view, obj): + if ( + obj.author.isPublic + or obj.author.check_for_friendship(request.user) + or obj.author == request.user + ): + return True + + return False diff --git a/solution/pulse/api/posts/serializers.py b/solution/pulse/api/posts/serializers.py new file mode 100644 index 0000000..8de68f2 --- /dev/null +++ b/solution/pulse/api/posts/serializers.py @@ -0,0 +1,38 @@ +from django.conf import settings +from rest_framework import serializers + +from api.posts.models import Post + + +class PostSerializer(serializers.ModelSerializer): + # ruff: noqa: N815 + author = serializers.ReadOnlyField(source="author.username") + likesCount = serializers.SerializerMethodField() + dislikesCount = serializers.SerializerMethodField() + + class Meta: + model = Post + fields = [ + "id", + "content", + "author", + "tags", + "createdAt", + "likesCount", + "dislikesCount", + ] + read_only_fields = ["id", "createdAt", "likesCount", "dislikesCount"] + + # ruff: noqa: N802 + def get_likesCount(self, obj): + return obj.likes.count() + + def get_dislikesCount(self, obj): + return obj.dislikes.count() + + def validate_tags(self, value): + for tag in value: + if len(tag) > settings.MAX_TAG_LENGTH: + error = "Each tag must be 20 characters or fewer." + raise serializers.ValidationError(error) + return value diff --git a/solution/pulse/api/posts/urls.py b/solution/pulse/api/posts/urls.py new file mode 100644 index 0000000..6e55be5 --- /dev/null +++ b/solution/pulse/api/posts/urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +import api.posts.views + +urlpatterns = [ + path( + "/create", + api.posts.views.CreatePostApiView.as_view(), + name="create-post", + ), + path( + "/", + api.posts.views.PostDetailApiView.as_view(), + name="post-detail", + ), +] diff --git a/solution/pulse/api/posts/views.py b/solution/pulse/api/posts/views.py new file mode 100644 index 0000000..b854efa --- /dev/null +++ b/solution/pulse/api/posts/views.py @@ -0,0 +1,35 @@ +from rest_framework import status +from rest_framework.exceptions import NotFound, ValidationError +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from api.posts.models import Post +from api.posts.permissions import CanAccessPost +from api.posts.serializers import PostSerializer + + +class CreatePostApiView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + serializer = PostSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(author=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + raise ValidationError(serializer.errors) + + +class PostDetailApiView(APIView): + permission_classes = [IsAuthenticated, CanAccessPost] + + def get(self, request, post_id): + try: + post = Post.objects.get(id=post_id) + self.check_object_permissions(request, post) + serializer = PostSerializer(post) + return Response(serializer.data) + except Post.DoesNotExist: + raise NotFound( + {"detail": "Post not found."}, + ) from None diff --git a/solution/pulse/api/urls.py b/solution/pulse/api/urls.py index dcfc123..c2dcb60 100644 --- a/solution/pulse/api/urls.py +++ b/solution/pulse/api/urls.py @@ -3,5 +3,6 @@ from django.urls import include, path urlpatterns = [ path("ping", include("api.ping.urls")), path("countries", include("api.countries.urls")), + path("posts", include("api.posts.urls")), path("", include("api.users.urls")), ] diff --git a/solution/pulse/api/users/models.py b/solution/pulse/api/users/models.py index 9e7a0c8..d33a3ab 100644 --- a/solution/pulse/api/users/models.py +++ b/solution/pulse/api/users/models.py @@ -51,6 +51,14 @@ class Profile(models.Model): def check_for_friendship(self, user): return self.friends.filter(pk=user.pk).exists() + def like_post(self, post): + self.disliked_posts.remove(post) + return self.liked_posts.add(post) + + def dislike_post(self, post): + self.liked_posts.remove(post) + return self.disliked_posts.add(post) + @classmethod def check_unique(cls, user_id, validated_data): errors = {} diff --git a/solution/pulse/pulse/settings.py b/solution/pulse/pulse/settings.py index ff16111..fe67680 100644 --- a/solution/pulse/pulse/settings.py +++ b/solution/pulse/pulse/settings.py @@ -29,6 +29,7 @@ INSTALLED_APPS = [ "api.ping.apps.PingConfig", "api.countries.apps.CountriesConfig", "api.users.apps.UsersConfig", + "api.posts.apps.PostsConfig", ] MIDDLEWARE = [ @@ -124,6 +125,8 @@ REST_FRAMEWORK = { APPEND_SLASH = False +MAX_TAG_LENGTH = 20 + if DEBUG: INSTALLED_APPS.insert(0, "debug_toolbar") MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")