diff --git a/.idea/BlogProject.iml b/.idea/BlogProject.iml index 92de0e4..8a24ab6 100644 --- a/.idea/BlogProject.iml +++ b/.idea/BlogProject.iml @@ -16,7 +16,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index a6c6116..c0ac767 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/Blog/admin.py b/Blog/admin.py index 133200a..06d6bc8 100644 --- a/Blog/admin.py +++ b/Blog/admin.py @@ -1,4 +1,3 @@ -from django.contrib import admin from django.contrib import admin from .models import Post, Comment diff --git a/Blog/models.py b/Blog/models.py index aa6aae7..4523881 100644 --- a/Blog/models.py +++ b/Blog/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.auth.models import User + class Post(models.Model): title = models.CharField(max_length=255) content = models.TextField() diff --git a/Blog/serializer.py b/Blog/serializer.py new file mode 100644 index 0000000..75cb3ec --- /dev/null +++ b/Blog/serializer.py @@ -0,0 +1,62 @@ +from Blog.models import Post, Comment +from rest_framework import serializers +from django.contrib.auth.models import User + +class UserRegistrationSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = User + fields = ['email', 'first_name', 'last_name', 'password'] + + def validate_email(self, value): + # Кастомна валідація для формату електронної пошти + if not value.endswith('@gmail.com'): + raise serializers.ValidationError('Електронна пошта повинна закінчуватися на @gmail.com.') + return value + + def validate_password(self, value): + # Кастомна валідація для паролю (наприклад, довжина паролю) + if len(value) < 8: + raise serializers.ValidationError('Пароль повинен бути не менше 8 символів.') + return value + + def validate(self, data): + # Перевірка, чи користувач з таким ім'ям вже існує + username = data.get('email') + if User.objects.filter(username=username).exists(): + raise serializers.ValidationError('Користувач з таким іменем вже існує.') + return data + def create(self, validated_data): + validated_data['username'] = validated_data['email'] + user = User.objects.create_user(**validated_data) + return user + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('username', 'email', 'password') + + def validate_email(self, value): + # Кастомна валідація для формату електронної пошти + if not value.endswith('@gmail.com'): + raise serializers.ValidationError('Електронна пошта повинна закінчуватися на @gmail.com.') + return value + + def validate_username(self, value): + # Кастомна валідація для унікальності імені користувача + if User.objects.filter(username=value).exists(): + raise serializers.ValidationError('Користувач з таким іменем вже існує.') + return value + + +class PostSerializer(serializers.ModelSerializer): + class Meta: + model = Post + fields = ['title', 'content', 'author', 'publ_date', 'category'] + +class CommentSerializer(serializers.ModelSerializer): + class Meta: + model = Comment + fields = ['post', 'author_of_the_comment', 'content_of_the_comment', 'date_of_creation'] + diff --git a/Blog/tests.py b/Blog/tests.py index 7ce503c..2596f16 100644 --- a/Blog/tests.py +++ b/Blog/tests.py @@ -1,3 +1,89 @@ from django.test import TestCase +from rest_framework.test import APITestCase + +from .models import User +from .serializer import UserRegistrationSerializer + + +class UserRegistrationAPITests(APITestCase): + + def setUp(self): + self.valid_data = { + 'email': 'test@gmail.com', + 'first_name': 'Test', + 'last_name': 'User', + 'password': '12345678', + } + + def test_registration_success(self): + response = self.client.post('/api/blog/register/', data=self.valid_data) + self.assertEqual(response.status_code, 201) + + user = User.objects.get(email='test@gmail.com') + self.assertEqual(user.first_name, 'Test') + self.assertEqual(user.last_name, 'User') + self.assertTrue(user.check_password('12345678')) + + def test_registration_with_invalid_email(self): + invalid_data = self.valid_data.copy() + invalid_data['email'] = 'test' + response = self.client.post('/api/blog/register/', data=invalid_data) + self.assertEqual(response.status_code, 400) + + # Перевірка наявності 'email' у відповіді + self.assertIn('email', response.json()) + + # Перевірка тексту помилки, використовуючи 'in' + expected_error = 'Enter a valid email address.' + actual_error = response.json()['email'][0] + self.assertIn(expected_error, actual_error) + + def test_registration_with_too_short_password(self): + invalid_data = self.valid_data.copy() + invalid_data['password'] = '123' + response = self.client.post('/api/blog/register/', data=invalid_data) + self.assertEqual(response.status_code, 400) + + # Перевірка наявності 'password' у відповіді + self.assertIn('password', response.json()) + + # Перевірка тексту помилки + self.assertEqual(response.json()['password'][0], 'Пароль повинен бути не менше 8 символів.') + + def test_registration_with_existing_username(self): + self.client.post('/api/blog/register/', data=self.valid_data) + response = self.client.post('/api/blog/register/', data=self.valid_data) + self.assertEqual(response.status_code, 400) + + # Перевірка наявності 'non_field_errors' у відповіді + self.assertIn('non_field_errors', response.json()) + + # Перевірка тексту помилки для 'non_field_errors' + expected_error = 'Користувач з таким іменем вже існує.' + actual_error = response.json()['non_field_errors'][0] + self.assertIn(expected_error, actual_error) + + def test_registration_with_non_gmail_email(self): + invalid_data = self.valid_data.copy() + invalid_data['email'] = 'test@yahoo.com' + response = self.client.post('/api/blog/register/', data=invalid_data) + self.assertEqual(response.status_code, 400) + + # Перевірка наявності 'email' у відповіді + self.assertIn('email', response.json()) + + # Перевірка тексту помилки для 'email' + expected_error = 'Електронна пошта повинна закінчуватися на @gmail.com.' + actual_error = response.json()['email'][0] + self.assertIn(expected_error, actual_error) + + def test_registration_with_valid_email(self): + valid_data = self.valid_data.copy() + valid_data['email'] = 'test@gmail.com' + response = self.client.post('/api/blog/register/', data=valid_data) + self.assertEqual(response.status_code, 201) + + # Перевірка, що користувач був успішно створений + user = User.objects.get(email='test@gmail.com') + self.assertEqual(user.email, 'test@gmail.com') -# Create your tests here. diff --git a/Blog/urls.py b/Blog/urls.py new file mode 100644 index 0000000..0daab44 --- /dev/null +++ b/Blog/urls.py @@ -0,0 +1,20 @@ +from django.urls import path, include, re_path +from . import views +from .views import register_user + +from rest_framework.urlpatterns import format_suffix_patterns + +urlpatterns = [ + path('post/', views.PostList.as_view()), + path('post//', views.PostDetail.as_view()), + path('comment/', views.CommentList.as_view()), + path('comment//', views.CommentDetail.as_view()), + path('drf-auth/', include('rest_framework.urls')), #в гуглі + path('register/', register_user, name='register_user'), # в postman + #path('auth/', include('djoser.urls')), + #path('auth/', include('djoser.urls.authtoken')), + + +] + +urlpatterns = format_suffix_patterns(urlpatterns) \ No newline at end of file diff --git a/Blog/views.py b/Blog/views.py index 91ea44a..e0e8740 100644 --- a/Blog/views.py +++ b/Blog/views.py @@ -1,3 +1,179 @@ -from django.shortcuts import render +from drf_yasg.utils import swagger_auto_schema +from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAdminUser +from .models import Post, Comment +from .serializer import PostSerializer, CommentSerializer +from django.http import Http404 +from rest_framework.views import APIView +from drf_yasg import openapi +from rest_framework.response import Response +from rest_framework import status +from rest_framework.decorators import api_view +from .serializer import UserRegistrationSerializer + +@api_view(['POST']) +def register_user(request): + serializer = UserRegistrationSerializer(data=request.data) + + if serializer.is_valid(): + serializer.save() + return Response({'message': 'User registered successfully'}, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class PostList(APIView): + permission_classes = [IsAuthenticatedOrReadOnly] + + @swagger_auto_schema( + operation_description="Get a list of posts", + responses={200: openapi.Response('List of posts', PostSerializer(many=True))} + ) + def get(self, request, format=None): + post = Post.objects.all() + serializer = PostSerializer(post, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Create a new post", + request_body=PostSerializer, + responses={201: 'Created', 400: 'Bad Request'} + ) + + def post(self, request, format=None): + serializer = PostSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class PostDetail(APIView): + permission_classes = [IsAdminUser] + + def get_object(self, pk): + try: + return Post.objects.get(pk=pk) + except Post.DoesNotExist: + raise Http404 + + @swagger_auto_schema( + operation_description="Get details of a specific post", + manual_parameters=[ + openapi.Parameter('pk', openapi.IN_PATH, description="Post ID", type=openapi.TYPE_INTEGER), + ], + responses={200: openapi.Response('Post details', PostSerializer)} + ) + + def get(self, request, pk, format=None): + post = self.get_object(pk) + serializer = PostSerializer(post) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Update details of a specific post", + manual_parameters=[ + openapi.Parameter('pk', openapi.IN_PATH, description="Post ID", type=openapi.TYPE_INTEGER), + ], + request_body=PostSerializer, + responses={200: 'Updated', 400: 'Bad Request'} + ) + + def put(self, request, pk, format=None): + post = self.get_object(pk) + serializer = PostSerializer(post, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_description="Delete a specific post", + manual_parameters=[ + openapi.Parameter('pk', openapi.IN_PATH, description="Post ID", type=openapi.TYPE_INTEGER), + ], + responses={204: 'No Content'} + ) + + def delete(self, request, pk, format=None): + post = self.get_object(pk) + post.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class CommentList(APIView): + permission_classes = [IsAuthenticatedOrReadOnly] + + @swagger_auto_schema( + operation_description="Get a list of comments", + responses={200: openapi.Response('List of comments', CommentSerializer(many=True))} + ) + + def get(self, request, format=None): + comment = Comment.objects.all() + serializer = CommentSerializer(comment, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Create a new comment", + request_body=CommentSerializer, + responses={201: 'Created', 400: 'Bad Request'} + ) + + def post(self, request, format=None): + serializer = CommentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class CommentDetail(APIView): + permission_classes = [IsAdminUser] + def get_object(self, pk): + try: + return Comment.objects.get(pk=pk) + except Comment.DoesNotExist: + raise Http404 + + @swagger_auto_schema( + operation_description="Get details of a specific comment", + manual_parameters=[ + openapi.Parameter('pk', openapi.IN_PATH, description="Comment ID", type=openapi.TYPE_INTEGER), + ], + responses={200: openapi.Response('Comment details', CommentSerializer)} + ) + + def get(self, request, pk, format=None): + comment = self.get_object(pk) + serializer = CommentSerializer(comment) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Update details of a specific comment", + manual_parameters=[ + openapi.Parameter('pk', openapi.IN_PATH, description="Comment ID", type=openapi.TYPE_INTEGER), + ], + request_body=CommentSerializer, + responses={200: 'Updated', 400: 'Bad Request'} + + ) + + def put(self, request, pk, format=None): + comment = self.get_object(pk) + serializer = CommentSerializer(comment, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_description="Delete a specific comment", + manual_parameters=[ + openapi.Parameter('pk', openapi.IN_PATH, description="Comment ID", type=openapi.TYPE_INTEGER), + ], + responses={204: 'No Content'} + ) + + def delete(self, request, pk, format=None): + comment = self.get_object(pk) + comment.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + -# Create your views here. diff --git a/BlogProject/settings.py b/BlogProject/settings.py index 9bf9d7d..1e13200 100644 --- a/BlogProject/settings.py +++ b/BlogProject/settings.py @@ -1,14 +1,3 @@ -""" -Django settings for BlogProject project. - -Generated by 'django-admin startproject' using Django 4.2.6. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.2/ref/settings/ -""" from pathlib import Path @@ -38,6 +27,11 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'Blog', + 'rest_framework', + 'rest_framework.authtoken', + 'drf_yasg', + 'djoser', + ] MIDDLEWARE = [ @@ -123,3 +117,13 @@ # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework_simplejwt.authentication.JWTAuthentication', + + ] +} \ No newline at end of file diff --git a/BlogProject/urls.py b/BlogProject/urls.py index ce890c3..3a4e885 100644 --- a/BlogProject/urls.py +++ b/BlogProject/urls.py @@ -1,22 +1,12 @@ -""" -URL configuration for BlogProject project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin -from django.urls import path +from django.urls import path, re_path, include +from .yasg import urlpatterns as doc_urls + urlpatterns = [ path('admin/', admin.site.urls), + path('api/', include([ + path('blog/', include('Blog.urls')), + ])), ] +urlpatterns += doc_urls \ No newline at end of file diff --git a/BlogProject/yasg.py b/BlogProject/yasg.py new file mode 100644 index 0000000..b5588d9 --- /dev/null +++ b/BlogProject/yasg.py @@ -0,0 +1,21 @@ +from django.urls import re_path, path +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="Snippets API", + default_version='v1', + description="Test description", + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + +urlpatterns = [ + path('swagger/', schema_view.without_ui(cache_timeout=0), name='schema-json'), + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), +] \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index ae05419..b8c1143 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..26b2513 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,33 @@ +asgiref==3.7.2 +certifi==2023.11.17 +cffi==1.16.0 +charset-normalizer==3.3.2 +cryptography==41.0.7 +defusedxml==0.8.0rc2 +Django==4.2.6 +django-templated-mail==1.1.1 +djangorestframework==3.14.0 +djangorestframework-simplejwt==5.3.0 +djoser==2.2.2 +drf-yasg==1.21.7 +factory-boy==3.3.0 +Faker==20.1.0 +idna==3.6 +inflection==0.5.1 +oauthlib==3.2.2 +packaging==23.2 +pycparser==2.21 +PyJWT==2.8.0 +python-dateutil==2.8.2 +python3-openid==3.2.0 +pytz==2023.3.post1 +PyYAML==6.0.1 +requests==2.31.0 +requests-oauthlib==1.3.1 +six==1.16.0 +social-auth-app-django==5.4.0 +social-auth-core==4.5.1 +sqlparse==0.4.4 +typing_extensions==4.8.0 +uritemplate==4.1.1 +urllib3==2.1.0