From 16ae46be1c12b8b73c5e564c3d08564d9e9e857d Mon Sep 17 00:00:00 2001 From: Akromjon <152626511+RustamovAkrom@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:37:46 +0500 Subject: [PATCH 01/10] create users app tests --- apps/users/tests.py | 121 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 4 deletions(-) diff --git a/apps/users/tests.py b/apps/users/tests.py index 2aba761..1c3519b 100644 --- a/apps/users/tests.py +++ b/apps/users/tests.py @@ -2,22 +2,135 @@ from django.urls import reverse from .models import User, UserProfile +from .services import generate_jwt_tokens +from .forms import RegisterForm, LoginForm + +from rest_framework_simplejwt.tokens import RefreshToken, AccessToken class TestUsers(TestCase): + def setUp(self): + self.username = "Admin" + self.is_active=True + self.email = "admin@example.com", + self.password = "password" + + user = User.objects.create( + username=self.username, + is_active=self.is_active, + email=self.email + ) + user.set_password(self.password) + user.save() + + self.user = user + + def test_user_creating_profile_exists(self): + profile = UserProfile.objects.filter(pk=self.user.profiles.pk) + self.assertTrue(profile.exists()) def test_user_register_page(self): response = self.client.get(reverse("users:register")) - self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "auth/register.html") self.assertContains(response, "Register") - # self.assertRedirects(response, reverse("users:login")) def test_user_login_page(self): response = self.client.get(reverse("users:login")) - self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "auth/login.html") self.assertContains(response, "Login") - # self.assertRedirects(response, reverse("blog:home")) \ No newline at end of file + + def test_user_logout_page(self): + data = { + "username": self.username, + "password": self.password, + } + # Authorization + self.client.post(reverse("users:login"), data=data) + + response = self.client.get(reverse("users:logout")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "auth/logout.html") + self.assertContains(response, "Logout") + + def test_user_profile_page(self): + response = self.client.get(reverse("users:user_profile", kwargs={"username": self.username})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "blog/profile.html") + self.assertContains(response, self.user.username) + + def test_RegisterForm(self): + form_data = { + "username": "Admin1", + "password1": "password1", + "password2": "password1", + "email": "admin1@example.com", + } + form = RegisterForm(data=form_data) + self.assertTrue(form.is_valid()) + + def test_LoginForm(self): + form_data = { + "username": "Admin1", + "password": "password1", + } + form = LoginForm(data=form_data) + self.assertTrue(form.is_valid()) + + def test_registration(self): + data = { + "username": "Admin1", + "password1": "password1", + "password2": "password1", + "email": "admin1@example.com", + } + response = self.client.post(reverse("users:register"), data=data) + self.assertEqual(response.status_code, 302) # Redirecting + self.assertRedirects(response, reverse("users:login")) + + def test_authorization(self): + data = { + "username": self.username, + "password": self.password, + } + + response = self.client.post(reverse("users:login"), data=data) + + refresh_token = response.client.cookies.get("refresh_token").value + access_token = response.client.cookies.get("access_token").value + + self.assertEqual(response.status_code, 302) # Redirecting + self.assertRedirects(response, reverse("blog:home")) + + access_token = AccessToken(access_token) + refresh_token = RefreshToken(refresh_token) + + user_id = access_token["user_id"] + user_in_db = User.objects.filter(id=user_id) + + user_in_refresh_id = refresh_token.for_user(user_in_db.first()).access_token["user_id"] + + self.assertTrue(user_in_db.exists()) + self.assertEqual(self.user.id, user_in_db.first().id) + self.assertEqual(self.user.id, user_in_refresh_id) + + def test_logout(self): + data = { + "username": self.username, + "password": self.password + } + # Authorization + response = self.client.post(reverse("users:login"), data=data) + + refresh_token_before = response.client.cookies.get("refresh_token").value + access_token_before = response.client.cookies.get("access_token").value + + response = self.client.post(reverse("users:logout")) + + refresh_token_after = response.client.cookies.get("refresh_token").value + access_token_after = response.client.cookies.get("access_token").value + + self.assertNotEqual(refresh_token_before, refresh_token_after) + self.assertNotEqual(access_token_before, access_token_after) + From f4c8adec5cd8e87f3f12975bb6f7c2e90e29a670 Mon Sep 17 00:00:00 2001 From: Akromjon <152626511+RustamovAkrom@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:38:12 +0500 Subject: [PATCH 02/10] configure --- apps/blog/tests.py | 66 +++++++++++++++++++----------------------- apps/blog/views.py | 2 -- apps/users/models.py | 2 -- apps/users/services.py | 4 +-- apps/users/signals.py | 2 +- apps/users/views.py | 9 ++++-- 6 files changed, 39 insertions(+), 46 deletions(-) diff --git a/apps/blog/tests.py b/apps/blog/tests.py index d11e9d7..b276ff8 100644 --- a/apps/blog/tests.py +++ b/apps/blog/tests.py @@ -1,17 +1,21 @@ +from datetime import datetime + from django.test import TestCase -from .models import User, Post +from apps.users.models import User +from apps.blog.models import Post from django.urls import reverse -from .forms import PostCreateForm, RegisterForm +from apps.users.forms import RegisterForm +from .forms import PostCreateUpdateForm +from .choices import StatusChoice class TestBlog(TestCase): def setUp(self): user = User.objects.create( - first_name="Saydula", - last_name="Polatov", - username="Polat", + first_name="Admin", + last_name="Adminovich", + username="Admin", email="saydulapolatov456@gmail.com", - avatar="avatars/fitnes.png", ) user.set_password("2007") @@ -19,53 +23,47 @@ def setUp(self): self.user = user post = Post.objects.create( - title="Olamnig uygonishi", - content="content1", - publisher_at="2024-02-06", - author=user, + title="PostTitle1d", + description="PostDescription1", + content="PostContent1", + publisher_at=datetime.now().strftime("%Y-%m-%d"), + author=self.user, is_active=True, ) self.post = post - def test_home(self): - response = self.client.get(reverse("home")) + def test_home_page(self): + response = self.client.get(reverse("blog:home")) self.assertEqual(response.status_code, 200) - def test_About_page(self): - response = self.client.get(reverse("about")) + def test_about_page(self): + response = self.client.get(reverse("blog:about")) self.assertEqual(response.status_code, 200) - def test_Login_page(self): - response = self.client.get(reverse("login")) - print(response.headers) + def test_contacts_page(self): + response = self.client.get(reverse("blog:contacts")) + self.assertEqual(response.status_code, 200) def test_post_detil(self): - response = self.client.get(reverse("post_detail", kwargs={"id": self.post.id})) + response = self.client.get(reverse("blog:post_detail", kwargs={"slug": self.post.slug})) self.assertContains(response, self.post.title) self.assertContains(response, self.post.content) - self.assertContains(response, self.post.author.first_name) - self.assertContains(response, self.post.author.last_name) + self.assertContains(response, self.post.author.username) self.assertTrue(self.post.is_active, "True") - def test_forms(self): + def test_PostCreateUpdateForm_forms(self): form_data = { - "title": "postTietle1", - "content": "postContent1", - "publisher_at": "2024-02-05", + "title": "PostTitle1", + "description": "PostDescription1", + "content": "PostContent1", "is_active": True, "author": self.user, + "status": StatusChoice.PUBLISHED.value } - form = PostCreateForm(data=form_data) + form = PostCreateUpdateForm(data=form_data) self.assertTrue(form.is_valid()) - def test_objects_firstname_and_lastname(self): - author = User.objects.get(id=1) - post_count = author.is_active - object_name = f"{author.first_name}, {author.last_name}|Is active: {post_count}" - - print(object_name) - def test_register_form(self): user_create_form_data = { "username": "Saydula", @@ -78,7 +76,3 @@ def test_register_form(self): } form = RegisterForm(data=user_create_form_data) self.assertTrue(form.is_valid()) - - def test_Users(self): - users = User.objects.all() - print(users) diff --git a/apps/blog/views.py b/apps/blog/views.py index a4862b0..d0ac8fb 100644 --- a/apps/blog/views.py +++ b/apps/blog/views.py @@ -95,8 +95,6 @@ def get_context_data(self, **kwargs): kwargs["form"] = PostCreateUpdateForm() kwargs["title"] = "Post Create" return super().get_context_data(**kwargs) - # def get(self, request): - # return render(request, self.template_name, {"form": PostCreateUpdateForm()}) def post(self, request): form = PostCreateUpdateForm(request.POST) diff --git a/apps/users/models.py b/apps/users/models.py index cb76d94..c0fafb1 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -4,8 +4,6 @@ from apps.shared.models import TimestempedAbstractModel from .utils import processor_iamge -from PIL import Image - class User(TimestempedAbstractModel, AbstractUser): email = models.EmailField(_("email address"), unique=True) diff --git a/apps/users/services.py b/apps/users/services.py index 007b045..e74e5e2 100644 --- a/apps/users/services.py +++ b/apps/users/services.py @@ -1,8 +1,8 @@ from django.http import HttpResponse, HttpRequest -from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework_simplejwt.tokens import RefreshToken, AccessToken -def generate_jwt_tokens(user): +def generate_jwt_tokens(user) -> tuple[RefreshToken, AccessToken]: refresh = RefreshToken.for_user(user) access_token = refresh.access_token access_token['user_id'] = user.id diff --git a/apps/users/signals.py b/apps/users/signals.py index 40bf3a2..23b6dca 100644 --- a/apps/users/signals.py +++ b/apps/users/signals.py @@ -6,6 +6,6 @@ @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: - print(f"Creating UserProfile to User({instance})") + # print(f"Creating UserProfile to User({instance})") UserProfile.objects.create(user=instance) diff --git a/apps/users/views.py b/apps/users/views.py index 22cc3c8..46dd15e 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -31,10 +31,14 @@ def post(self, request): if form.is_valid(): form.save() messages.success(request, "User succesfully registered") - return redirect(reverse_lazy("users:login")) + response = redirect(reverse_lazy("users:login")) + response.status_code = 302 + return response messages.warning(request, "Invalid registration fields!") - return redirect(reverse_lazy("auth:register")) + response = redirect(reverse_lazy("users:register")) + response.status_code = 400 + return response class LoginPageView(CustomHtmxMixin, View): @@ -72,7 +76,6 @@ def post(self, request): return redirect(reverse_lazy("users:login")) - class LogoutPageView(CustomHtmxMixin, LoginRequiredMixin, View): template_name = "auth/logout.html" From 73d935a2d7579cb2e1f94f714faac4301ec5c3c3 Mon Sep 17 00:00:00 2001 From: Akromjon <152626511+RustamovAkrom@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:05:45 +0500 Subject: [PATCH 03/10] fix: change tests --- apps/blog/forms.py | 7 +--- apps/blog/tests.py | 33 +++++++++++------- apps/blog/views.py | 3 ++ media/avatars/2.jpg | Bin 21776 -> 21706 bytes ...ence_room_setting_with_modern_lightin.webp | Bin 0 -> 15794 bytes .../financial-tops_auto_x2-e5jv3oj9.jpg | Bin 0 -> 4427 bytes 6 files changed, 25 insertions(+), 18 deletions(-) create mode 100644 media/avatars/DALLE_2024-09-27_14.30.15_-_A_young_man_with_short_hair_and_glasses_wearing_a_formal_business_suit._The_background_is_a_professional_conference_room_setting_with_modern_lightin.webp create mode 100644 media/avatars/financial-tops_auto_x2-e5jv3oj9.jpg diff --git a/apps/blog/forms.py b/apps/blog/forms.py index 0a472e4..e92fa76 100644 --- a/apps/blog/forms.py +++ b/apps/blog/forms.py @@ -52,7 +52,7 @@ class Meta: class SettingsUserForm(forms.ModelForm): class Meta: model = User - fields = ("first_name", "last_name", "username", "email") + fields = ("first_name", "last_name", "email") first_name = forms.CharField( widget=forms.TextInput( @@ -66,11 +66,6 @@ class Meta: ), required=False, ) - username = forms.CharField( - widget=forms.TextInput( - attrs=default_attrs("username", "Username..."), - ) - ) email = forms.EmailField( widget=forms.EmailInput(attrs=default_attrs("email", "Email...")) ) diff --git a/apps/blog/tests.py b/apps/blog/tests.py index b276ff8..e9ef7c6 100644 --- a/apps/blog/tests.py +++ b/apps/blog/tests.py @@ -1,11 +1,12 @@ from datetime import datetime from django.test import TestCase +from django.core.files.uploadedfile import UploadedFile + from apps.users.models import User from apps.blog.models import Post from django.urls import reverse -from apps.users.forms import RegisterForm -from .forms import PostCreateUpdateForm +from .forms import PostCreateUpdateForm, SettingsUserForm, SettingsUserProfileForm from .choices import StatusChoice @@ -64,15 +65,23 @@ def test_PostCreateUpdateForm_forms(self): form = PostCreateUpdateForm(data=form_data) self.assertTrue(form.is_valid()) - def test_register_form(self): - user_create_form_data = { - "username": "Saydula", - "first_name": "Sayid", - "last_name": "Movlonov", - "password1": "Sayid", - "password2": "Sayid", - "email": "sayidmovlonov34@gmail.com", - "avatar": "C:/Users/user/Pictures/Screenshots homevork/Снимок экрана 2024-02-08 193246.png", + def test_SettingsUserForm(self): + form_data = { + "first_name": "Admin1", + "last_name": "Adminovna", + "email": "admin@example.com", } - form = RegisterForm(data=user_create_form_data) + form = SettingsUserForm(data=form_data, instance=self.user) self.assertTrue(form.is_valid()) + + # def test_SettingsUserProfileForm(self): + # form_files = { + # "avatar": "media/avatars/default/logo.png", + # } + # form_data = { + # "bio": "settings user profile bio ...", + # } + # form = SettingsUserProfileForm(data=form_data, files=form_files) + # print(form.get_context()) + # print(form.errors.get_context()) + # self.assertTrue(form.is_valid()) diff --git a/apps/blog/views.py b/apps/blog/views.py index d0ac8fb..74a6a9d 100644 --- a/apps/blog/views.py +++ b/apps/blog/views.py @@ -197,6 +197,9 @@ def get(self, request): return render(request, self.template_name, context) def post(self, request): + print(request.FILES) + print(request.POST) + user = get_object_or_404(User, pk=request.user.pk) user_form = SettingsUserForm(data=request.POST, instance=user) user_profile_form = SettingsUserProfileForm( diff --git a/media/avatars/2.jpg b/media/avatars/2.jpg index d6f7742fb727a2ed8f69736605bb56646e4ad6e7..74e5adcc828c6b0ea23090c2f3c85a853367b797 100644 GIT binary patch delta 19762 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 JQAGfM|JfA2-tYhb delta 19835 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 z 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 J6j4AQ|Jl~w>_Gqk diff --git a/media/avatars/DALLE_2024-09-27_14.30.15_-_A_young_man_with_short_hair_and_glasses_wearing_a_formal_business_suit._The_background_is_a_professional_conference_room_setting_with_modern_lightin.webp b/media/avatars/DALLE_2024-09-27_14.30.15_-_A_young_man_with_short_hair_and_glasses_wearing_a_formal_business_suit._The_background_is_a_professional_conference_room_setting_with_modern_lightin.webp new file mode 100644 index 0000000000000000000000000000000000000000..ff9cc12c9456a6e6dd0593190208e54d77df8a8e GIT binary patch literal 15794 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 literal 0 HcmV?d00001 diff --git a/media/avatars/financial-tops_auto_x2-e5jv3oj9.jpg b/media/avatars/financial-tops_auto_x2-e5jv3oj9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe30061c4975b0fc99999fc3cdebbddef3d80474 GIT binary patch literal 4427 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 l(}KKC$IGR49mD`| Date: Tue, 19 Nov 2024 16:02:27 +0500 Subject: [PATCH 04/10] fix: change for tests --- apps/users/api_endpoints/__init__.py | 4 +- .../api_endpoints/users/User/serializers.py | 1 - apps/users/api_endpoints/users/User/tests.py | 70 +++++++++++++++++++ apps/users/api_endpoints/users/User/views.py | 2 +- .../api_endpoints/users/UserProfile/views.py | 5 ++ .../users/User => }/permissions.py | 0 apps/users/urls.py | 8 +-- 7 files changed, 79 insertions(+), 11 deletions(-) rename apps/users/{api_endpoints/users/User => }/permissions.py (100%) diff --git a/apps/users/api_endpoints/__init__.py b/apps/users/api_endpoints/__init__.py index 97e8f45..04407ab 100644 --- a/apps/users/api_endpoints/__init__.py +++ b/apps/users/api_endpoints/__init__.py @@ -4,5 +4,5 @@ router = routers.DefaultRouter() -router.register("user", UserViewSet) -router.register("user_profile", UserProfileViewSet) +router.register("user", UserViewSet, basename="user") +router.register("user_profile", UserProfileViewSet, basename="user_profile") diff --git a/apps/users/api_endpoints/users/User/serializers.py b/apps/users/api_endpoints/users/User/serializers.py index 9aed973..7e0de97 100644 --- a/apps/users/api_endpoints/users/User/serializers.py +++ b/apps/users/api_endpoints/users/User/serializers.py @@ -31,7 +31,6 @@ class Meta: "is_active", "is_superuser", "is_staff", - "post_count", "profiles", "created_at", "updated_at", diff --git a/apps/users/api_endpoints/users/User/tests.py b/apps/users/api_endpoints/users/User/tests.py index e69de29..3aa53ee 100644 --- a/apps/users/api_endpoints/users/User/tests.py +++ b/apps/users/api_endpoints/users/User/tests.py @@ -0,0 +1,70 @@ +from rest_framework.test import APITestCase +from rest_framework_simplejwt.tokens import RefreshToken, AccessToken + +from django.urls import reverse + +from apps.users.models import User + +# Authorizations +# self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.token}") + +class UsersApiTestCase(APITestCase): + def setUp(self) -> None: + self.username = "Admin" + self.email = "admin@example.com" + self.password = "password" + + user = User.objects.create( + username=self.username, + email=self.email, + ) + user.set_password(self.password) + user.save() + self.user = user + refresh = RefreshToken.for_user(self.user) + self.token = str(refresh.access_token) + return super().setUp() + + def test_api_tokens(self): + data = { + "username": "Admin", + "password": "password", + } + response = self.client.post(reverse("token_obtain_pair"), data=data) + tokens = dict(response.json()) + refresh = tokens.get("refresh") + access = tokens.get("access") + + # Test jwt access token + jwt_access = AccessToken(access) + self.assertEqual(self.user.id, jwt_access["user_id"]) + + # Test jwt refresh token + jwt_refresh = RefreshToken(refresh) + self.assertEqual(self.user.id, jwt_refresh.for_user(self.user).access_token["user_id"]) + + self.assertEqual(response.status_code, 200) + + # def test_api_user_create(self): + # url = "/api/v1/users/user/" + # def check_user(): + # data = { + # "username": "Admin2", + # "email": "admin2@example.com", + # "password": "password", + # "password_confirm": "password" + # } + + # invalid_data = { + # "username": self.username, + # "email": self.email, + # "password": self.password, + # "password_confirm": self.password + # } + + # response = self.client.post(url, data=valid_data) + + + # print(response.status_code) + # print(response.json()) + diff --git a/apps/users/api_endpoints/users/User/views.py b/apps/users/api_endpoints/users/User/views.py index bdd529e..4b1884d 100644 --- a/apps/users/api_endpoints/users/User/views.py +++ b/apps/users/api_endpoints/users/User/views.py @@ -3,7 +3,7 @@ from apps.users.models import User from .serializers import UserSerializer -from .permissions import IsOwnerPermission +from apps.users.permissions import IsOwnerPermission class UserViewSet(viewsets.ModelViewSet): diff --git a/apps/users/api_endpoints/users/UserProfile/views.py b/apps/users/api_endpoints/users/UserProfile/views.py index eee4d15..018437e 100644 --- a/apps/users/api_endpoints/users/UserProfile/views.py +++ b/apps/users/api_endpoints/users/UserProfile/views.py @@ -1,6 +1,8 @@ from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated, IsAdminUser from apps.users.models import UserProfile +from apps.users.permissions import IsOwnerPermission from .serializers import UserProfileSerializer @@ -8,4 +10,7 @@ class UserProfileViewSet(viewsets.ModelViewSet): queryset = UserProfile.objects.all() serializer_class = UserProfileSerializer + def get_permissions(self): + return [IsAuthenticated(), IsAdminUser(), IsOwnerPermission()] + __all__ = ("UserProfileViewSet", ) diff --git a/apps/users/api_endpoints/users/User/permissions.py b/apps/users/permissions.py similarity index 100% rename from apps/users/api_endpoints/users/User/permissions.py rename to apps/users/permissions.py diff --git a/apps/users/urls.py b/apps/users/urls.py index be2a414..a1ce7f9 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -1,13 +1,7 @@ -from django.urls import path, include +from django.urls import path -from .api_endpoints import router from . import views -from rest_framework_simplejwt.views import ( - TokenObtainPairView, - TokenRefreshView -) - app_name = "users" From ff1b1234a36d0a93a441568facd903071a022d3d Mon Sep 17 00:00:00 2001 From: Akromjon <152626511+RustamovAkrom@users.noreply.github.com> Date: Tue, 19 Nov 2024 17:08:07 +0500 Subject: [PATCH 05/10] create User api tests --- apps/users/api_endpoints/users/User/tests.py | 66 ++++++++++++++------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/apps/users/api_endpoints/users/User/tests.py b/apps/users/api_endpoints/users/User/tests.py index 3aa53ee..4beaf57 100644 --- a/apps/users/api_endpoints/users/User/tests.py +++ b/apps/users/api_endpoints/users/User/tests.py @@ -44,27 +44,53 @@ def test_api_tokens(self): self.assertEqual(self.user.id, jwt_refresh.for_user(self.user).access_token["user_id"]) self.assertEqual(response.status_code, 200) + + def test_api_user_create(self): + url = "/api/v1/users/user/" + + def _check_user_error_field(): + data = { + "username": self.username, + "email": self.email, + "password": self.password, + "password_confirm": self.password + } + response = self.client.post(url, data=data) + response_data = response.json() - # def test_api_user_create(self): - # url = "/api/v1/users/user/" - # def check_user(): - # data = { - # "username": "Admin2", - # "email": "admin2@example.com", - # "password": "password", - # "password_confirm": "password" - # } + self.assertEqual(response.status_code, 400) + self.assertEqual(response_data['username'][0], 'A user with that username already exists.') + self.assertEqual(response_data['email'][0], 'User with this email address already exists.') - # invalid_data = { - # "username": self.username, - # "email": self.email, - # "password": self.password, - # "password_confirm": self.password - # } - - # response = self.client.post(url, data=valid_data) - + def _check_user_passwords_field(): + data = { + "username": "Admin2", + "email": "admin2@example.com", + "password": "password", + "password_confirm": "error password" + } + response = self.client.post(url, data=data) + response_data = response.json() + + self.assertEqual(response_data['password'][0], 'Password didnt match!') + + def _create_user(): + data = { + "username": "Admin2", + "email": "admin2@example.com", + "password": "password", + "password_confirm": "password" + } + response = self.client.post(url, data=data) + response_data = response.json() + + self.assertEqual(response.status_code, 201) + self.assertEqual(response_data['username'], data["username"]) + self.assertEqual(response_data['email'], data["email"]) + self.assertEqual(response_data['id'], 2) + self.assertEqual(response_data['profiles']['id'], response_data['id']) - # print(response.status_code) - # print(response.json()) + _check_user_error_field() + _check_user_passwords_field() + _create_user() From 0a711360f0edda3668560a31ebbe7b858ce010dc Mon Sep 17 00:00:00 2001 From: Akromjon <152626511+RustamovAkrom@users.noreply.github.com> Date: Tue, 19 Nov 2024 18:29:27 +0500 Subject: [PATCH 06/10] add linter workflows --- .github/workflows/django.yml | 18 +++++----- .github/workflows/lint.yml | 68 ++++++++++++++++++++++++++++++++++++ core/config/apps.py | 1 - 3 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 9885e8b..f93321f 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -1,4 +1,4 @@ -name: Django CI +name: Django Test on: push: @@ -20,23 +20,21 @@ jobs: steps: - name: Check out the repository - uses: actions/checkout@v4 # This is an action, so it uses 'uses' + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 # This is an action, so it uses 'uses' + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install OpenSSL - run: sudo apt-get install -y openssl # This is a command, so it uses 'run' + run: sudo apt-get install -y openssl - name: Generate private and public keys run: | - # Очистка старых файлов перед созданием новых rm -rf security mkdir -p security - # Генерация приватного и публичного ключа openssl genpkey -algorithm RSA -out security/private_key.pem -pkeyopt rsa_keygen_bits:2048 openssl rsa -pubout -in security/private_key.pem -out security/public_key.pem @@ -53,17 +51,17 @@ jobs: - name: Check if .env has been updated - run: cat .env # This is a command, so it uses 'run' + run: cat .env - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt # This is a command, so it uses 'run' + pip install -r requirements.txt - name: Run Tests - run: python manage.py test # This is a command, so it uses 'run' + run: python manage.py test - name: Clean up keys (Optional) run: | rm -rf security - echo "Keys removed after use." # This is a command, so it uses 'run' + echo "Keys removed after use." diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..089a8c5 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,68 @@ +name: Django Test + +on: + push: + branches: + - "main" + - "master" + pull_request: + branches: + - "main" + - "master" + +jobs: + + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8, 3.9] + + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install OpenSSL + run: sudo apt-get install -y openssl + + - name: Generate private and public keys + run: | + rm -rf security + mkdir -p security + + openssl genpkey -algorithm RSA -out security/private_key.pem -pkeyopt rsa_keygen_bits:2048 + openssl rsa -pubout -in security/private_key.pem -out security/public_key.pem + + echo "Private key saved as security/private_key.pem" + echo "Public key saved as security/public_key.pem" + + - name: Create .env file + run: | + touch .env + echo PRIVATE_KEY_PATH=$(pwd)/security/private_key.pem >> .env + echo PUBLIC_KEY_PATH=$(pwd)/security/public_key.pem >> .env + DJANGO_SETTINGS_MODULE=core.settings.development >> .env + echo DATABASE_ENVIRON=sqlite3 >> .env + + + - name: Check if .env has been updated + run: cat .env + + - name: Install Dependencies + run: | + pip install flake8 + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Lint Code + run: flake8 . + + - name: Clean up keys (Optional) + run: | + rm -rf security + echo "Keys removed after use." diff --git a/core/config/apps.py b/core/config/apps.py index 5cf4c8a..2ecf521 100644 --- a/core/config/apps.py +++ b/core/config/apps.py @@ -21,5 +21,4 @@ "rest_framework", "rest_framework_simplejwt", "rest_framework_simplejwt.token_blacklist", - ] \ No newline at end of file From 289d559f5b9862cb4e661dc6985f4a86d43d225a Mon Sep 17 00:00:00 2001 From: Akromjon <152626511+RustamovAkrom@users.noreply.github.com> Date: Tue, 19 Nov 2024 18:31:10 +0500 Subject: [PATCH 07/10] lint code --- .flake8 | 2 +- apps/blog/api_endpoints/__init__.py | 10 +- apps/blog/api_endpoints/blog/Post/__init__.py | 2 +- .../api_endpoints/blog/Post/serializer.py | 4 +- apps/blog/api_endpoints/blog/Post/views.py | 9 +- .../blog/PostComment/__init__.py | 2 +- .../api_endpoints/blog/PostComment/views.py | 9 +- .../blog/PostCommentLike/__init__.py | 2 +- .../blog/PostCommentLike/serializers.py | 2 +- .../blog/PostCommentLike/views.py | 3 +- .../blog/PostDislike/__init__.py | 2 +- .../api_endpoints/blog/PostDislike/views.py | 10 +- .../api_endpoints/blog/PostLike/__init__.py | 2 +- .../blog/api_endpoints/blog/PostLike/views.py | 11 +- apps/blog/api_endpoints/blog/__init__.py | 10 +- apps/blog/choices.py | 4 +- apps/blog/forms.py | 2 +- apps/blog/migrations/0001_initial.py | 124 +++++++++--- apps/blog/migrations/0002_initial.py | 92 ++++++--- apps/blog/models.py | 12 +- apps/blog/tests.py | 6 +- apps/blog/utils.py | 11 +- apps/blog/views.py | 20 +- .../shared/management/commands/createadmin.py | 6 +- apps/shared/mixins.py | 4 +- .../api_endpoints/users/User/__init__.py | 2 +- .../api_endpoints/users/User/serializers.py | 13 +- apps/users/api_endpoints/users/User/tests.py | 42 ++-- apps/users/api_endpoints/users/User/views.py | 17 +- .../users/UserProfile/__init__.py | 2 +- .../api_endpoints/users/UserProfile/views.py | 3 +- apps/users/api_endpoints/users/__init__.py | 4 +- apps/users/apps.py | 2 +- apps/users/authentication.py | 6 +- apps/users/forms.py | 18 +- apps/users/middleware.py | 10 +- apps/users/migrations/0001_initial.py | 180 +++++++++++++++--- apps/users/models.py | 2 +- apps/users/services.py | 8 +- apps/users/tests.py | 36 ++-- apps/users/urls.py | 1 - apps/users/views.py | 17 +- core/config/__init__.py | 6 +- core/config/apps.py | 2 +- core/config/jwt.py | 12 +- core/config/rest_framework.py | 6 +- core/settings/base.py | 3 +- core/settings/development.py | 4 +- core/settings/production.py | 4 +- core/urls.py | 2 +- core/wsgi.py | 8 +- manage.py | 11 +- 52 files changed, 517 insertions(+), 265 deletions(-) diff --git a/.flake8 b/.flake8 index 45ce446..2ee413c 100644 --- a/.flake8 +++ b/.flake8 @@ -15,7 +15,7 @@ exclude = __pycache__, migrations, static, - env, # your virtual environment directory + venv, # your virtual environment directory # Enable checking for complexity max-complexity = 10 diff --git a/apps/blog/api_endpoints/__init__.py b/apps/blog/api_endpoints/__init__.py index 18d8020..2d02e4a 100644 --- a/apps/blog/api_endpoints/__init__.py +++ b/apps/blog/api_endpoints/__init__.py @@ -1,16 +1,18 @@ from rest_framework import routers from .blog import ( - PostViewSet, - PostCommentViewSet, + PostViewSet, + PostCommentViewSet, PostCommentLikeViewSet, PostLikeViewSet, - PostDislikeViewSet + PostDislikeViewSet, ) router = routers.DefaultRouter() router.register("post", PostViewSet, basename="post") router.register("post_comment", PostCommentViewSet, basename="post-comment") -router.register("post_comment_like", PostCommentLikeViewSet, basename="post-comment-like") +router.register( + "post_comment_like", PostCommentLikeViewSet, basename="post-comment-like" +) router.register("post_like", PostLikeViewSet, basename="post-like") router.register("post_dislike", PostDislikeViewSet, basename="post-dislike") diff --git a/apps/blog/api_endpoints/blog/Post/__init__.py b/apps/blog/api_endpoints/blog/Post/__init__.py index 1da4c1c..360d6f8 100644 --- a/apps/blog/api_endpoints/blog/Post/__init__.py +++ b/apps/blog/api_endpoints/blog/Post/__init__.py @@ -1 +1 @@ -from .views import * # noqa \ No newline at end of file +from .views import * # noqa diff --git a/apps/blog/api_endpoints/blog/Post/serializer.py b/apps/blog/api_endpoints/blog/Post/serializer.py index 084acb3..4b409ed 100644 --- a/apps/blog/api_endpoints/blog/Post/serializer.py +++ b/apps/blog/api_endpoints/blog/Post/serializer.py @@ -8,7 +8,7 @@ class Meta: model = Post fields = [ "id", - "title", + "title", "get_absolute_url", "status", "description", @@ -21,4 +21,4 @@ class Meta: "watching", "created_at", "updated_at", - ] \ No newline at end of file + ] diff --git a/apps/blog/api_endpoints/blog/Post/views.py b/apps/blog/api_endpoints/blog/Post/views.py index e589730..37fd00f 100644 --- a/apps/blog/api_endpoints/blog/Post/views.py +++ b/apps/blog/api_endpoints/blog/Post/views.py @@ -7,10 +7,11 @@ class PostViewSet(viewsets.ModelViewSet): queryset = Post.published.all().order_by("-created_at") serializer_class = PostSerializer - + def get_permissions(self): - if self.action in ['list', 'retrieve']: + if self.action in ["list", "retrieve"]: return [permissions.AllowAny()] return [permissions.IsAuthenticated()] - -__all__ = ("PostViewSet", ) + + +__all__ = ("PostViewSet",) diff --git a/apps/blog/api_endpoints/blog/PostComment/__init__.py b/apps/blog/api_endpoints/blog/PostComment/__init__.py index 1da4c1c..360d6f8 100644 --- a/apps/blog/api_endpoints/blog/PostComment/__init__.py +++ b/apps/blog/api_endpoints/blog/PostComment/__init__.py @@ -1 +1 @@ -from .views import * # noqa \ No newline at end of file +from .views import * # noqa diff --git a/apps/blog/api_endpoints/blog/PostComment/views.py b/apps/blog/api_endpoints/blog/PostComment/views.py index 83ac244..f1a6082 100644 --- a/apps/blog/api_endpoints/blog/PostComment/views.py +++ b/apps/blog/api_endpoints/blog/PostComment/views.py @@ -9,8 +9,9 @@ class PostCommentViewSet(viewsets.ModelViewSet): serializer_class = PostCommentSerializer def get_permissions(self): - if self.action in ['list', 'retrieve']: + if self.action in ["list", "retrieve"]: return [permissions.AllowAny()] - return [permissions.IsAuthenticated()] - -__all__ = ("PostCommentViewSet", ) + return [permissions.IsAuthenticated()] + + +__all__ = ("PostCommentViewSet",) diff --git a/apps/blog/api_endpoints/blog/PostCommentLike/__init__.py b/apps/blog/api_endpoints/blog/PostCommentLike/__init__.py index 1da4c1c..360d6f8 100644 --- a/apps/blog/api_endpoints/blog/PostCommentLike/__init__.py +++ b/apps/blog/api_endpoints/blog/PostCommentLike/__init__.py @@ -1 +1 @@ -from .views import * # noqa \ No newline at end of file +from .views import * # noqa diff --git a/apps/blog/api_endpoints/blog/PostCommentLike/serializers.py b/apps/blog/api_endpoints/blog/PostCommentLike/serializers.py index 623310e..b280ee3 100644 --- a/apps/blog/api_endpoints/blog/PostCommentLike/serializers.py +++ b/apps/blog/api_endpoints/blog/PostCommentLike/serializers.py @@ -18,7 +18,7 @@ class Meta: class PostCommentLikeSerializer(serializers.ModelSerializer): user = MiniPostCommentLikeUserSerializer(read_only=True) comment = MiniPostCommentLikePostCommentSerializer(read_only=True) - + class Meta: model = PostCommentLike fields = ["id", "user", "comment"] diff --git a/apps/blog/api_endpoints/blog/PostCommentLike/views.py b/apps/blog/api_endpoints/blog/PostCommentLike/views.py index 8c9c3f4..d59c2ae 100644 --- a/apps/blog/api_endpoints/blog/PostCommentLike/views.py +++ b/apps/blog/api_endpoints/blog/PostCommentLike/views.py @@ -9,4 +9,5 @@ class PostCommentLikeViewSet(viewsets.ModelViewSet): serializer_class = PostCommentLikeSerializer permission_classes = [permissions.IsAuthenticated] -__all__ = ("PostCommentLikeViewSet", ) + +__all__ = ("PostCommentLikeViewSet",) diff --git a/apps/blog/api_endpoints/blog/PostDislike/__init__.py b/apps/blog/api_endpoints/blog/PostDislike/__init__.py index 1da4c1c..360d6f8 100644 --- a/apps/blog/api_endpoints/blog/PostDislike/__init__.py +++ b/apps/blog/api_endpoints/blog/PostDislike/__init__.py @@ -1 +1 @@ -from .views import * # noqa \ No newline at end of file +from .views import * # noqa diff --git a/apps/blog/api_endpoints/blog/PostDislike/views.py b/apps/blog/api_endpoints/blog/PostDislike/views.py index e17cb1f..3a67441 100644 --- a/apps/blog/api_endpoints/blog/PostDislike/views.py +++ b/apps/blog/api_endpoints/blog/PostDislike/views.py @@ -21,12 +21,14 @@ def create(self, request, *args, **kwargs): existing_dislike = PostDislike.objects.filter(post=post, user=user) if existing_dislike.exists(): existing_dislike.delete() - return response.Response({"message": "Dislike removed"}, status=status.HTTP_200_OK) - + return response.Response( + {"message": "Dislike removed"}, status=status.HTTP_200_OK + ) + dislike = PostDislike.objects.create(post=post, user=user) serializer = self.get_serializer(dislike) return response.Response(serializer.data, status=status.HTTP_201_CREATED) - -__all__ = ("PostDislikeViewSet", ) + +__all__ = ("PostDislikeViewSet",) diff --git a/apps/blog/api_endpoints/blog/PostLike/__init__.py b/apps/blog/api_endpoints/blog/PostLike/__init__.py index 1da4c1c..360d6f8 100644 --- a/apps/blog/api_endpoints/blog/PostLike/__init__.py +++ b/apps/blog/api_endpoints/blog/PostLike/__init__.py @@ -1 +1 @@ -from .views import * # noqa \ No newline at end of file +from .views import * # noqa diff --git a/apps/blog/api_endpoints/blog/PostLike/views.py b/apps/blog/api_endpoints/blog/PostLike/views.py index 98de276..38080fe 100644 --- a/apps/blog/api_endpoints/blog/PostLike/views.py +++ b/apps/blog/api_endpoints/blog/PostLike/views.py @@ -10,7 +10,7 @@ class PostLikeViewSet(viewsets.ModelViewSet): queryset = PostLike.objects.all() serializer_class = PostLikeSerializer permission_classes = [permissions.IsAuthenticated] - + def create(self, request, *args, **kwargs): post_id = request.data.get("post") post = get_object_or_404(Post, id=post_id) @@ -21,11 +21,14 @@ def create(self, request, *args, **kwargs): existing_like = PostLike.objects.filter(post=post, user=user) if existing_like.exists(): existing_like.delete() - return response.Response({"message": "Like removed"}, status=status.HTTP_200_OK) - + return response.Response( + {"message": "Like removed"}, status=status.HTTP_200_OK + ) + like = PostLike.objects.create(post=post, user=user) serializer = self.get_serializer(like) return response.Response(serializer.data, status=status.HTTP_201_CREATED) -__all__ = ("PostLikeViewSet", ) + +__all__ = ("PostLikeViewSet",) diff --git a/apps/blog/api_endpoints/blog/__init__.py b/apps/blog/api_endpoints/blog/__init__.py index b689173..64b8805 100644 --- a/apps/blog/api_endpoints/blog/__init__.py +++ b/apps/blog/api_endpoints/blog/__init__.py @@ -1,5 +1,5 @@ -from .Post import * # noqa -from .PostComment import * # noqa -from .PostCommentLike import * # noqa -from .PostDislike import * # noqa -from .PostLike import * # noqa +from .Post import * # noqa +from .PostComment import * # noqa +from .PostCommentLike import * # noqa +from .PostDislike import * # noqa +from .PostLike import * # noqa diff --git a/apps/blog/choices.py b/apps/blog/choices.py index d008ee8..7fc6735 100644 --- a/apps/blog/choices.py +++ b/apps/blog/choices.py @@ -2,5 +2,5 @@ class StatusChoice(TextChoices): - DRAFT = 'df', 'Draft' - PUBLISHED = 'pb', 'Published' + DRAFT = "df", "Draft" + PUBLISHED = "pb", "Published" diff --git a/apps/blog/forms.py b/apps/blog/forms.py index e92fa76..165e892 100644 --- a/apps/blog/forms.py +++ b/apps/blog/forms.py @@ -4,7 +4,7 @@ from .models import Post -default_attrs = lambda name, placeholder: { # noqa :E731 +default_attrs = lambda name, placeholder: { # noqa :E731 "name": name, "placeholder": placeholder, "class": "form-control", diff --git a/apps/blog/migrations/0001_initial.py b/apps/blog/migrations/0001_initial.py index 860719a..6b05da4 100644 --- a/apps/blog/migrations/0001_initial.py +++ b/apps/blog/migrations/0001_initial.py @@ -7,59 +7,125 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Post', + name="Post", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('title', models.CharField(db_index=True, max_length=120, verbose_name='title')), - ('slug', models.SlugField(max_length=255, unique=True, verbose_name='slug')), - ('status', models.CharField(choices=[('df', 'Draft'), ('pb', 'Published')], default='df', max_length=2, verbose_name='status')), - ('description', models.CharField(blank=True, max_length=300, null=True, verbose_name='description')), - ('content', models.TextField(verbose_name='content')), - ('publisher_at', models.DateField(verbose_name='publisher at')), - ('is_active', models.BooleanField(default=True, verbose_name='active')), - ('watching', models.BigIntegerField(default=0, verbose_name='watching')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "title", + models.CharField( + db_index=True, max_length=120, verbose_name="title" + ), + ), + ( + "slug", + models.SlugField(max_length=255, unique=True, verbose_name="slug"), + ), + ( + "status", + models.CharField( + choices=[("df", "Draft"), ("pb", "Published")], + default="df", + max_length=2, + verbose_name="status", + ), + ), + ( + "description", + models.CharField( + blank=True, + max_length=300, + null=True, + verbose_name="description", + ), + ), + ("content", models.TextField(verbose_name="content")), + ("publisher_at", models.DateField(verbose_name="publisher at")), + ("is_active", models.BooleanField(default=True, verbose_name="active")), + ( + "watching", + models.BigIntegerField(default=0, verbose_name="watching"), + ), ], options={ - 'verbose_name': 'Post', - 'verbose_name_plural': 'Posts', - 'db_table': 'posts', + "verbose_name": "Post", + "verbose_name_plural": "Posts", + "db_table": "posts", }, ), migrations.CreateModel( - name='PostComment', + name="PostComment", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('message', models.TextField(verbose_name='message')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("message", models.TextField(verbose_name="message")), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='PostCommentLike', + name="PostCommentLike", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ], ), migrations.CreateModel( - name='PostDislike', + name="PostDislike", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ], ), migrations.CreateModel( - name='PostLike', + name="PostLike", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ], ), ] diff --git a/apps/blog/migrations/0002_initial.py b/apps/blog/migrations/0002_initial.py index fab4b8f..b54a979 100644 --- a/apps/blog/migrations/0002_initial.py +++ b/apps/blog/migrations/0002_initial.py @@ -10,54 +10,90 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('blog', '0001_initial'), + ("blog", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddField( - model_name='post', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL), + model_name="post", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="posts", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='postcomment', - name='post', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_comments', to='blog.post'), + model_name="postcomment", + name="post", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="post_comments", + to="blog.post", + ), ), migrations.AddField( - model_name='postcomment', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_comments', to=settings.AUTH_USER_MODEL), + model_name="postcomment", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="post_comments", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='postcommentlike', - name='comment', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_comment_likes', to='blog.postcomment'), + model_name="postcommentlike", + name="comment", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="post_comment_likes", + to="blog.postcomment", + ), ), migrations.AddField( - model_name='postcommentlike', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_comment_likes', to=settings.AUTH_USER_MODEL), + model_name="postcommentlike", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="post_comment_likes", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='postdislike', - name='post', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_dislikes', to='blog.post'), + model_name="postdislike", + name="post", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="post_dislikes", + to="blog.post", + ), ), migrations.AddField( - model_name='postdislike', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_dislikes', to=settings.AUTH_USER_MODEL), + model_name="postdislike", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="post_dislikes", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='postlike', - name='post', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_likes', to='blog.post'), + model_name="postlike", + name="post", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="post_likes", + to="blog.post", + ), ), migrations.AddField( - model_name='postlike', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_likes', to=settings.AUTH_USER_MODEL), + model_name="postlike", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="post_likes", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/apps/blog/models.py b/apps/blog/models.py index 620cab2..de7d0ef 100644 --- a/apps/blog/models.py +++ b/apps/blog/models.py @@ -14,12 +14,14 @@ class Post(TimestempedAbstractModel): title = models.CharField(_("title"), max_length=120, db_index=True) slug = models.SlugField(_("slug"), max_length=255, unique=True, db_index=True) status = models.CharField( - _("status"), - max_length=2, - choices=StatusChoice.choices, - default=StatusChoice.DRAFT.value + _("status"), + max_length=2, + choices=StatusChoice.choices, + default=StatusChoice.DRAFT.value, + ) + description = models.CharField( + _("description"), max_length=300, blank=True, null=True ) - description = models.CharField(_("description"), max_length=300, blank=True, null=True) content = models.TextField(_("content")) publisher_at = models.DateField(_("publisher at")) is_active = models.BooleanField(_("active"), default=True) diff --git a/apps/blog/tests.py b/apps/blog/tests.py index e9ef7c6..219a865 100644 --- a/apps/blog/tests.py +++ b/apps/blog/tests.py @@ -46,7 +46,9 @@ def test_contacts_page(self): self.assertEqual(response.status_code, 200) def test_post_detil(self): - response = self.client.get(reverse("blog:post_detail", kwargs={"slug": self.post.slug})) + response = self.client.get( + reverse("blog:post_detail", kwargs={"slug": self.post.slug}) + ) self.assertContains(response, self.post.title) self.assertContains(response, self.post.content) self.assertContains(response, self.post.author.username) @@ -59,7 +61,7 @@ def test_PostCreateUpdateForm_forms(self): "content": "PostContent1", "is_active": True, "author": self.user, - "status": StatusChoice.PUBLISHED.value + "status": StatusChoice.PUBLISHED.value, } form = PostCreateUpdateForm(data=form_data) diff --git a/apps/blog/utils.py b/apps/blog/utils.py index afca9f0..40c3a10 100644 --- a/apps/blog/utils.py +++ b/apps/blog/utils.py @@ -12,11 +12,12 @@ def get_search_model_queryset( return model_queryset search_query = model_queryset.filter( - Q(title__icontains=search_query) | Q(description__icontains=search_query) | Q(content__icontains=search_query) + Q(title__icontains=search_query) + | Q(description__icontains=search_query) + | Q(content__icontains=search_query) ) - - return search_query + return search_query def get_pagination_obj(model_queryset: QuerySet, page: int = 1, size: int = 4) -> Page: @@ -24,10 +25,10 @@ def get_pagination_obj(model_queryset: QuerySet, page: int = 1, size: int = 4) - try: page_obj = paginator.page(page) - + except PageNotAnInteger: page_obj = paginator.page(1) - + except EmptyPage: page_obj = paginator.page(paginator.num_pages) diff --git a/apps/blog/views.py b/apps/blog/views.py index 74a6a9d..970adbb 100644 --- a/apps/blog/views.py +++ b/apps/blog/views.py @@ -54,7 +54,7 @@ def get_context_data(self, **kwargs): class AboutPageView(CustomHtmxMixin, TemplateView): template_name = "blog/about.html" - + def get_context_data(self, **kwargs): kwargs["title"] = "About" return super().get_context_data(**kwargs) @@ -73,16 +73,16 @@ class PostDetailPageView(CustomHtmxMixin, View): def get(self, request, slug): post = get_object_or_404(Post, slug=slug) - + post_comments = post.post_comments.all().order_by("-created_at") post.watching += 1 post.save() context = { "title": post.title, - "post": post, + "post": post, "post_comments": post_comments, - "template_htmx": self.template_htmx + "template_htmx": self.template_htmx, } return render(request, self.template_name, context) @@ -126,13 +126,13 @@ class PostUpdateView(CustomHtmxMixin, LoginRequiredMixin, TemplateView): def get(self, request, slug): post = get_object_or_404(Post, slug=slug, is_active=True) - + form = PostCreateUpdateForm(instance=post) context = { "title": "Update " + post.title, "template_htmx": self.template_htmx, - "form": form, - "post": post + "form": form, + "post": post, } return render(request, self.template_name, context) @@ -164,7 +164,7 @@ def get(self, request): context = { "title": "My posts", "template_htmx": self.template_htmx, - "posts": posts.order_by("-created_at") + "posts": posts.order_by("-created_at"), } return render(request, self.template_name, context) @@ -190,8 +190,8 @@ def get(self, request): context = { "title": "Settings", "template_htmx": self.template_htmx, - "user_form": user_form, - "user_profile_form": user_profile_form + "user_form": user_form, + "user_profile_form": user_profile_form, } return render(request, self.template_name, context) diff --git a/apps/shared/management/commands/createadmin.py b/apps/shared/management/commands/createadmin.py index 3e8c696..bed2df5 100644 --- a/apps/shared/management/commands/createadmin.py +++ b/apps/shared/management/commands/createadmin.py @@ -1,6 +1,7 @@ import os from dotenv import load_dotenv + load_dotenv() from django.contrib.auth import get_user_model @@ -16,6 +17,7 @@ class Command(BaseCommand): def handle(self, *args, **options): User = get_user_model() self.create_superuser(User, ADMIN_USERNAME, ADMIN_EMAIL, ADMIN_PASSWORD) + def create_superuser(self, User, username, email, password): if not User.objects.filter(username=username).exists(): User.objects.create_superuser(username, email, password) @@ -23,6 +25,4 @@ def create_superuser(self, User, username, email, password): self.style.SUCCESS(f"Superuser {username} created successfully.") ) else: - self.stdout.write( - self.style.ERROR(f"Superuser {username} already exists.") - ) + self.stdout.write(self.style.ERROR(f"Superuser {username} already exists.")) diff --git a/apps/shared/mixins.py b/apps/shared/mixins.py index 4067366..00bf3e1 100644 --- a/apps/shared/mixins.py +++ b/apps/shared/mixins.py @@ -28,7 +28,7 @@ def _wrapped_view(request: HttpRequest, *args, **kwargs): return render(request, htmx_template_name, response.context_data) return render(request, template_name, response.context_data) return response + return _wrapped_view - return decorator - \ No newline at end of file + return decorator diff --git a/apps/users/api_endpoints/users/User/__init__.py b/apps/users/api_endpoints/users/User/__init__.py index 1da4c1c..360d6f8 100644 --- a/apps/users/api_endpoints/users/User/__init__.py +++ b/apps/users/api_endpoints/users/User/__init__.py @@ -1 +1 @@ -from .views import * # noqa \ No newline at end of file +from .views import * # noqa diff --git a/apps/users/api_endpoints/users/User/serializers.py b/apps/users/api_endpoints/users/User/serializers.py index 7e0de97..1f51159 100644 --- a/apps/users/api_endpoints/users/User/serializers.py +++ b/apps/users/api_endpoints/users/User/serializers.py @@ -12,7 +12,7 @@ class Meta: "bio", ] - + class UserSerializer(serializers.ModelSerializer): profiles = MiniUserProfileSerializer(read_only=True) password = serializers.CharField(write_only=True, required=True) @@ -22,13 +22,13 @@ class Meta: model = User fields = [ "id", - "first_name", - "last_name", + "first_name", + "last_name", "username", - "email", + "email", "password", "password_confirm", - "is_active", + "is_active", "is_superuser", "is_staff", "profiles", @@ -40,10 +40,9 @@ class Meta: def validate(self, attrs): "Confirmation passwords" - if attrs['password'] != attrs['password_confirm']: + if attrs["password"] != attrs["password_confirm"]: raise serializers.ValidationError({"password": "Password didnt match!"}) return attrs - def create(self, validated_data): "Save a hashed password" diff --git a/apps/users/api_endpoints/users/User/tests.py b/apps/users/api_endpoints/users/User/tests.py index 4beaf57..d8c4c7c 100644 --- a/apps/users/api_endpoints/users/User/tests.py +++ b/apps/users/api_endpoints/users/User/tests.py @@ -8,12 +8,13 @@ # Authorizations # self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.token}") + class UsersApiTestCase(APITestCase): def setUp(self) -> None: self.username = "Admin" self.email = "admin@example.com" self.password = "password" - + user = User.objects.create( username=self.username, email=self.email, @@ -24,7 +25,7 @@ def setUp(self) -> None: refresh = RefreshToken.for_user(self.user) self.token = str(refresh.access_token) return super().setUp() - + def test_api_tokens(self): data = { "username": "Admin", @@ -41,55 +42,62 @@ def test_api_tokens(self): # Test jwt refresh token jwt_refresh = RefreshToken(refresh) - self.assertEqual(self.user.id, jwt_refresh.for_user(self.user).access_token["user_id"]) + self.assertEqual( + self.user.id, jwt_refresh.for_user(self.user).access_token["user_id"] + ) self.assertEqual(response.status_code, 200) - + def test_api_user_create(self): url = "/api/v1/users/user/" - + def _check_user_error_field(): data = { "username": self.username, "email": self.email, "password": self.password, - "password_confirm": self.password + "password_confirm": self.password, } response = self.client.post(url, data=data) response_data = response.json() self.assertEqual(response.status_code, 400) - self.assertEqual(response_data['username'][0], 'A user with that username already exists.') - self.assertEqual(response_data['email'][0], 'User with this email address already exists.') + self.assertEqual( + response_data["username"][0], + "A user with that username already exists.", + ) + self.assertEqual( + response_data["email"][0], + "User with this email address already exists.", + ) def _check_user_passwords_field(): data = { "username": "Admin2", "email": "admin2@example.com", "password": "password", - "password_confirm": "error password" + "password_confirm": "error password", } response = self.client.post(url, data=data) response_data = response.json() - self.assertEqual(response_data['password'][0], 'Password didnt match!') - + self.assertEqual(response_data["password"][0], "Password didnt match!") + def _create_user(): data = { "username": "Admin2", "email": "admin2@example.com", "password": "password", - "password_confirm": "password" + "password_confirm": "password", } response = self.client.post(url, data=data) response_data = response.json() self.assertEqual(response.status_code, 201) - self.assertEqual(response_data['username'], data["username"]) - self.assertEqual(response_data['email'], data["email"]) - self.assertEqual(response_data['id'], 2) - self.assertEqual(response_data['profiles']['id'], response_data['id']) - + self.assertEqual(response_data["username"], data["username"]) + self.assertEqual(response_data["email"], data["email"]) + self.assertEqual(response_data["id"], 2) + self.assertEqual(response_data["profiles"]["id"], response_data["id"]) _check_user_error_field() _check_user_passwords_field() diff --git a/apps/users/api_endpoints/users/User/views.py b/apps/users/api_endpoints/users/User/views.py index 4b1884d..7e7f952 100644 --- a/apps/users/api_endpoints/users/User/views.py +++ b/apps/users/api_endpoints/users/User/views.py @@ -12,12 +12,17 @@ class UserViewSet(viewsets.ModelViewSet): authentication_classes = [authentication.JWTAuthentication] def get_permissions(self): - if self.action == 'create': + if self.action == "create": return [permissions.AllowAny()] - - if self.action in ['retrieve', 'update', 'partial_update', 'destroy']: - return [permissions.IsAuthenticated(), permissions.IsAdminUser, IsOwnerPermission()] + + if self.action in ["retrieve", "update", "partial_update", "destroy"]: + return [ + permissions.IsAuthenticated(), + permissions.IsAdminUser, + IsOwnerPermission(), + ] return [permissions.IsAuthenticated()] - -__all__ = ("UserViewSet", ) + + +__all__ = ("UserViewSet",) diff --git a/apps/users/api_endpoints/users/UserProfile/__init__.py b/apps/users/api_endpoints/users/UserProfile/__init__.py index 1da4c1c..360d6f8 100644 --- a/apps/users/api_endpoints/users/UserProfile/__init__.py +++ b/apps/users/api_endpoints/users/UserProfile/__init__.py @@ -1 +1 @@ -from .views import * # noqa \ No newline at end of file +from .views import * # noqa diff --git a/apps/users/api_endpoints/users/UserProfile/views.py b/apps/users/api_endpoints/users/UserProfile/views.py index 018437e..39b78ca 100644 --- a/apps/users/api_endpoints/users/UserProfile/views.py +++ b/apps/users/api_endpoints/users/UserProfile/views.py @@ -13,4 +13,5 @@ class UserProfileViewSet(viewsets.ModelViewSet): def get_permissions(self): return [IsAuthenticated(), IsAdminUser(), IsOwnerPermission()] -__all__ = ("UserProfileViewSet", ) + +__all__ = ("UserProfileViewSet",) diff --git a/apps/users/api_endpoints/users/__init__.py b/apps/users/api_endpoints/users/__init__.py index 09010fe..ca17025 100644 --- a/apps/users/api_endpoints/users/__init__.py +++ b/apps/users/api_endpoints/users/__init__.py @@ -1,2 +1,2 @@ -from .User import * # noqa -from .UserProfile import * # noqa \ No newline at end of file +from .User import * # noqa +from .UserProfile import * # noqa diff --git a/apps/users/apps.py b/apps/users/apps.py index e5b0b44..62b5078 100644 --- a/apps/users/apps.py +++ b/apps/users/apps.py @@ -6,6 +6,6 @@ class UsersConfig(AppConfig): name = "apps.users" def ready(self) -> None: - import apps.users.signals # noqa + import apps.users.signals # noqa return super().ready() diff --git a/apps/users/authentication.py b/apps/users/authentication.py index 385fdda..da1bdbc 100644 --- a/apps/users/authentication.py +++ b/apps/users/authentication.py @@ -12,15 +12,15 @@ class JWTAdminAuthentication(ModelBackend): def authenticate(self, request): # get access token in cookies - access_token = request.COOKIES.get('access_token') + access_token = request.COOKIES.get("access_token") if not access_token: return None - + try: # Decode token token = AccessToken(access_token) - user_id = token['user_id'] + user_id = token["user_id"] user = User.objects.get(id=user_id) return (user, None) except (TokenError, ObjectDoesNotExist): diff --git a/apps/users/forms.py b/apps/users/forms.py index 317c5bf..f219ae0 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -12,8 +12,8 @@ class LoginForm(forms.Form): ), error_messages={ "required": "Username is required!", - 'max_length': "Username is too lang, max length is 150 charecters." - } + "max_length": "Username is too lang, max length is 150 charecters.", + }, ) password = forms.CharField( max_length=60, @@ -22,8 +22,8 @@ class LoginForm(forms.Form): ), error_messages={ "required": "Password is required!", - "max_length": "Password is to long, max length is 60 charecters." - } + "max_length": "Password is to long, max length is 60 charecters.", + }, ) @@ -32,7 +32,6 @@ class RegisterForm(forms.ModelForm): label="Password", max_length=28, widget=forms.PasswordInput(attrs={"id": "password", "type": "password"}), - ) password2 = forms.CharField( label="Password (Confirm)", @@ -46,7 +45,7 @@ def clean_password2(self): if password1 and password2 and password1 != password2: raise ValidationError("Passwords must be match!") return password2 - + def save(self, commit=True): user = super().save(commit=False) @@ -61,4 +60,9 @@ def __init__(self, *args, **kwargs): class Meta: model = User - fields = ("username", "password1", "password2", "email", ) + fields = ( + "username", + "password1", + "password2", + "email", + ) diff --git a/apps/users/middleware.py b/apps/users/middleware.py index f9998a9..656cb40 100644 --- a/apps/users/middleware.py +++ b/apps/users/middleware.py @@ -10,23 +10,23 @@ class JWTAuthMiddleware(MiddlewareMixin): def process_request(self, request): - if request.path.startswith('/admin'): + if request.path.startswith("/admin"): return - + access_token = request.COOKIES.get("access_token") if not access_token: request.user = AnonymousUser() return - + cached_user = cache.get(access_token) if cached_user: request.user = cached_user return - + try: token = AccessToken(access_token) - user_id = token['user_id'] + user_id = token["user_id"] user = User.objects.get(id=user_id) cache.set(access_token, user, timeout=60 * 15) diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py index c2e6c61..248e3b4 100644 --- a/apps/users/migrations/0001_initial.py +++ b/apps/users/migrations/0001_initial.py @@ -13,52 +13,170 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "email", + models.EmailField( + max_length=254, unique=True, verbose_name="email address" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'User', - 'verbose_name_plural': 'Users', - 'db_table': 'users', + "verbose_name": "User", + "verbose_name_plural": "Users", + "db_table": "users", }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='UserProfile', + name="UserProfile", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('avatar', models.ImageField(blank=True, default='avatars/default/logo.png', max_length=250, null=True, upload_to='avatars/', verbose_name='avatar')), - ('bio', models.CharField(blank=True, max_length=170, null=True, verbose_name='bio')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profiles', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "avatar", + models.ImageField( + blank=True, + default="avatars/default/logo.png", + max_length=250, + null=True, + upload_to="avatars/", + verbose_name="avatar", + ), + ), + ( + "bio", + models.CharField( + blank=True, max_length=170, null=True, verbose_name="bio" + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="profiles", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'User Profile', - 'verbose_name_plural': 'User Profiles', - 'db_table': 'user_profiles', + "verbose_name": "User Profile", + "verbose_name_plural": "User Profiles", + "db_table": "user_profiles", }, ), ] diff --git a/apps/users/models.py b/apps/users/models.py index c0fafb1..4ca3941 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -56,5 +56,5 @@ def __str__(self) -> str: def save(self, *args, **kwargs): super().save(*args, **kwargs) - + processor_iamge(self.avatar.path) diff --git a/apps/users/services.py b/apps/users/services.py index e74e5e2..46cc7ef 100644 --- a/apps/users/services.py +++ b/apps/users/services.py @@ -5,7 +5,7 @@ def generate_jwt_tokens(user) -> tuple[RefreshToken, AccessToken]: refresh = RefreshToken.for_user(user) access_token = refresh.access_token - access_token['user_id'] = user.id + access_token["user_id"] = user.id access_token = str(access_token) refresh_token = str(refresh) @@ -25,7 +25,9 @@ def get_jwt_login_response(response: HttpResponse, user) -> HttpResponse: return response -def get_jwt_logout_response(response: HttpResponse, request: HttpRequest) -> HttpResponse: +def get_jwt_logout_response( + response: HttpResponse, request: HttpRequest +) -> HttpResponse: # Get refresh token for cookie refresh_token = request.COOKIES.get("refresh_token") @@ -36,7 +38,7 @@ def get_jwt_logout_response(response: HttpResponse, request: HttpRequest) -> Htt token.blacklist() except Exception: pass - + # Delete token in cookies response.delete_cookie("access_token") response.delete_cookie("refresh_token") diff --git a/apps/users/tests.py b/apps/users/tests.py index 1c3519b..bbc2591 100644 --- a/apps/users/tests.py +++ b/apps/users/tests.py @@ -11,14 +11,12 @@ class TestUsers(TestCase): def setUp(self): self.username = "Admin" - self.is_active=True - self.email = "admin@example.com", + self.is_active = True + self.email = ("admin@example.com",) self.password = "password" user = User.objects.create( - username=self.username, - is_active=self.is_active, - email=self.email + username=self.username, is_active=self.is_active, email=self.email ) user.set_password(self.password) user.save() @@ -55,7 +53,9 @@ def test_user_logout_page(self): self.assertContains(response, "Logout") def test_user_profile_page(self): - response = self.client.get(reverse("users:user_profile", kwargs={"username": self.username})) + response = self.client.get( + reverse("users:user_profile", kwargs={"username": self.username}) + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "blog/profile.html") self.assertContains(response, self.user.username) @@ -69,7 +69,7 @@ def test_RegisterForm(self): } form = RegisterForm(data=form_data) self.assertTrue(form.is_valid()) - + def test_LoginForm(self): form_data = { "username": "Admin1", @@ -86,21 +86,21 @@ def test_registration(self): "email": "admin1@example.com", } response = self.client.post(reverse("users:register"), data=data) - self.assertEqual(response.status_code, 302) # Redirecting + self.assertEqual(response.status_code, 302) # Redirecting self.assertRedirects(response, reverse("users:login")) - + def test_authorization(self): data = { "username": self.username, "password": self.password, } - + response = self.client.post(reverse("users:login"), data=data) refresh_token = response.client.cookies.get("refresh_token").value access_token = response.client.cookies.get("access_token").value - - self.assertEqual(response.status_code, 302) # Redirecting + + self.assertEqual(response.status_code, 302) # Redirecting self.assertRedirects(response, reverse("blog:home")) access_token = AccessToken(access_token) @@ -108,18 +108,17 @@ def test_authorization(self): user_id = access_token["user_id"] user_in_db = User.objects.filter(id=user_id) - - user_in_refresh_id = refresh_token.for_user(user_in_db.first()).access_token["user_id"] + + user_in_refresh_id = refresh_token.for_user(user_in_db.first()).access_token[ + "user_id" + ] self.assertTrue(user_in_db.exists()) self.assertEqual(self.user.id, user_in_db.first().id) self.assertEqual(self.user.id, user_in_refresh_id) def test_logout(self): - data = { - "username": self.username, - "password": self.password - } + data = {"username": self.username, "password": self.password} # Authorization response = self.client.post(reverse("users:login"), data=data) @@ -133,4 +132,3 @@ def test_logout(self): self.assertNotEqual(refresh_token_before, refresh_token_after) self.assertNotEqual(access_token_before, access_token_after) - diff --git a/apps/users/urls.py b/apps/users/urls.py index a1ce7f9..c9766fb 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -14,5 +14,4 @@ views.UserProfilePageView.as_view(), name="user_profile", ), - ] diff --git a/apps/users/views.py b/apps/users/views.py index 46dd15e..2350f1c 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -20,7 +20,7 @@ def get(self, request): context = { "title": "Registration", "template_htmx": self.template_htmx, - "form": RegisterForm() + "form": RegisterForm(), } return render(request, self.template_name, context) @@ -39,7 +39,7 @@ def post(self, request): response = redirect(reverse_lazy("users:register")) response.status_code = 400 return response - + class LoginPageView(CustomHtmxMixin, View): template_name = "auth/login.html" @@ -48,7 +48,7 @@ def get(self, request): context = { "title": "Login", "template_htmx": self.template_htmx, - "form": LoginForm() + "form": LoginForm(), } return render(request, self.template_name, context) @@ -74,16 +74,13 @@ def post(self, request): messages.error(request, "Invalid username or password.") return redirect(reverse_lazy("users:login")) - + class LogoutPageView(CustomHtmxMixin, LoginRequiredMixin, View): template_name = "auth/logout.html" def get(self, request): - context = { - "title": "Logout", - "template_htmx": self.template_htmx - } + context = {"title": "Logout", "template_htmx": self.template_htmx} return render(request, self.template_name, context) def post(self, request): @@ -110,7 +107,7 @@ def get(self, request, username): context = { "title": str(user), "template_htmx": self.template_htmx, - "posts": posts, - "user": user + "posts": posts, + "user": user, } return render(request, self.template_name, context) diff --git a/core/config/__init__.py b/core/config/__init__.py index f1508eb..6cc4c3b 100644 --- a/core/config/__init__.py +++ b/core/config/__init__.py @@ -1,3 +1,3 @@ -from .apps import * # noqa -from .jwt import * # noqa -from .rest_framework import * # noqa +from .apps import * # noqa +from .jwt import * # noqa +from .rest_framework import * # noqa diff --git a/core/config/apps.py b/core/config/apps.py index 2ecf521..61082ce 100644 --- a/core/config/apps.py +++ b/core/config/apps.py @@ -21,4 +21,4 @@ "rest_framework", "rest_framework_simplejwt", "rest_framework_simplejwt.token_blacklist", -] \ No newline at end of file +] diff --git a/core/config/jwt.py b/core/config/jwt.py index 719a020..dba6029 100644 --- a/core/config/jwt.py +++ b/core/config/jwt.py @@ -6,14 +6,14 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent -load_dotenv(os.path.join(BASE_DIR, '.env')) +load_dotenv(os.path.join(BASE_DIR, ".env")) with open(str(os.getenv("PRIVATE_KEY_PATH", "security/private_key")), "r") as f: PRIVATE_KEY = f.read() with open(str(os.getenv("PUBLIC_KEY_PATH", "security/public_key.pem")), "r") as f: PUBLIC_KEY = f.read() - + SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(days=7), @@ -21,9 +21,9 @@ "ROTATE_REFRESH_TOKENS": True, "BLACKLIST_AFTER_ROTATION": True, "UPDATE_LAST_LOGIN": False, - "ALGORITHM": "RS256", # SH256 old, new RS256 - "SIGNING_KEY": PRIVATE_KEY, # private key - "VERIFYING_KEY": PUBLIC_KEY, # public key + "ALGORITHM": "RS256", # SH256 old, new RS256 + "SIGNING_KEY": PRIVATE_KEY, # private key + "VERIFYING_KEY": PUBLIC_KEY, # public key "AUDIENCE": None, "ISSUER": None, "JWK_URL": None, @@ -39,4 +39,4 @@ "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", "SLIDING_TOKEN_LIFETIME": timedelta(days=7), "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=31), -} \ No newline at end of file +} diff --git a/core/config/rest_framework.py b/core/config/rest_framework.py index f9cdb5f..9aac977 100644 --- a/core/config/rest_framework.py +++ b/core/config/rest_framework.py @@ -2,7 +2,5 @@ "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", ), - "DEFAULT_PERMISSION_CLASSES": ( - "rest_framework.permissions.AllowAny", - ) -} \ No newline at end of file + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",), +} diff --git a/core/settings/base.py b/core/settings/base.py index 8331ec1..9ef9376 100644 --- a/core/settings/base.py +++ b/core/settings/base.py @@ -2,9 +2,10 @@ from pathlib import Path -from core.config import * # noqa +from core.config import * # noqa from dotenv import load_dotenv + load_dotenv() BASE_DIR = Path(__file__).resolve().parent.parent.parent diff --git a/core/settings/development.py b/core/settings/development.py index 4601161..96882c3 100644 --- a/core/settings/development.py +++ b/core/settings/development.py @@ -1,4 +1,4 @@ -from .base import * # noqa +from .base import * # noqa EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" @@ -7,4 +7,4 @@ "ENGINE": f"django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } -} \ No newline at end of file +} diff --git a/core/settings/production.py b/core/settings/production.py index eb013d8..adf9b66 100644 --- a/core/settings/production.py +++ b/core/settings/production.py @@ -1,6 +1,6 @@ import os -from .base import * # noqa +from .base import * # noqa EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_HOST = "smtp.gmail.com" @@ -17,6 +17,6 @@ "USER": str(os.getenv("DATABASE_USER")), "PASSWORD": str(os.getenv("DATABASE_PASSWORD")), "HOST": str(os.getenv("DATABASE_HOST")), - "PORT": int(os.getenv("DATABASE_PORT")) + "PORT": int(os.getenv("DATABASE_PORT")), } } diff --git a/core/urls.py b/core/urls.py index e0a56d7..2eb33a0 100644 --- a/core/urls.py +++ b/core/urls.py @@ -40,7 +40,7 @@ path("api/token", TokenObtainPairView.as_view(), name="token_obtain_pair"), path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("api/v1/users/", include(users_api_router.urls)), - path("api/v1/blogs/", include(blog_api_router.urls)) + path("api/v1/blogs/", include(blog_api_router.urls)), ] if settings.DEBUG: diff --git a/core/wsgi.py b/core/wsgi.py index 646bdc6..8ba2346 100644 --- a/core/wsgi.py +++ b/core/wsgi.py @@ -1,13 +1,15 @@ import os from dotenv import load_dotenv + load_dotenv() from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv( - "DJANGO_SETTINGS_MODULE", "core.settings.development" -)) +os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", + os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.development"), +) application = get_wsgi_application() diff --git a/manage.py b/manage.py index cdce1ac..b67adc1 100644 --- a/manage.py +++ b/manage.py @@ -4,14 +4,17 @@ import sys from dotenv import load_dotenv + load_dotenv() + def main(): """Run administrative tasks.""" - - os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv( - "DJANGO_SETTINGS_MODULE", "core.settings.development" - )) + + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", + os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.development"), + ) try: from django.core.management import execute_from_command_line From a020808e03dbe9f7f38eff2575415759f6324686 Mon Sep 17 00:00:00 2001 From: Akromjon <152626511+RustamovAkrom@users.noreply.github.com> Date: Tue, 19 Nov 2024 18:39:57 +0500 Subject: [PATCH 08/10] lint code dev --- apps/blog/tests.py | 3 +-- apps/blog/views.py | 5 +---- apps/shared/management/commands/createadmin.py | 9 +++++---- apps/users/tests.py | 1 - core/config/apps.py | 5 ++--- core/settings/base.py | 16 +++++++++++----- core/settings/development.py | 7 ------- core/settings/production.py | 2 +- core/wsgi.py | 5 ++--- 9 files changed, 23 insertions(+), 30 deletions(-) diff --git a/apps/blog/tests.py b/apps/blog/tests.py index 219a865..dd8a427 100644 --- a/apps/blog/tests.py +++ b/apps/blog/tests.py @@ -1,12 +1,11 @@ from datetime import datetime from django.test import TestCase -from django.core.files.uploadedfile import UploadedFile from apps.users.models import User from apps.blog.models import Post from django.urls import reverse -from .forms import PostCreateUpdateForm, SettingsUserForm, SettingsUserProfileForm +from .forms import PostCreateUpdateForm, SettingsUserForm from .choices import StatusChoice diff --git a/apps/blog/views.py b/apps/blog/views.py index 970adbb..03bb904 100644 --- a/apps/blog/views.py +++ b/apps/blog/views.py @@ -1,17 +1,14 @@ import datetime -from functools import wraps -from typing import Any from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import TemplateView, DeleteView from django.contrib import messages -from django.http import HttpRequest from django.views import View from django.urls import reverse from apps.users.models import User -from apps.shared.mixins import CustomHtmxMixin, render_htmx_or_default +from apps.shared.mixins import CustomHtmxMixin from .models import Post from .forms import ( PostCreateUpdateForm, diff --git a/apps/shared/management/commands/createadmin.py b/apps/shared/management/commands/createadmin.py index bed2df5..ae3f06f 100644 --- a/apps/shared/management/commands/createadmin.py +++ b/apps/shared/management/commands/createadmin.py @@ -1,12 +1,11 @@ import os -from dotenv import load_dotenv +from django.contrib.auth import get_user_model +from django.core.management import BaseCommand +from dotenv import load_dotenv load_dotenv() -from django.contrib.auth import get_user_model -from django.core.management.base import BaseCommand - ADMIN_USERNAME = str(os.getenv("ADMIN_USERNAME")) ADMIN_PASSWORD = str(os.getenv("ADMIN_PASSWORD")) @@ -14,6 +13,8 @@ class Command(BaseCommand): + help = "Create superuser" + def handle(self, *args, **options): User = get_user_model() self.create_superuser(User, ADMIN_USERNAME, ADMIN_EMAIL, ADMIN_PASSWORD) diff --git a/apps/users/tests.py b/apps/users/tests.py index bbc2591..3b266f9 100644 --- a/apps/users/tests.py +++ b/apps/users/tests.py @@ -2,7 +2,6 @@ from django.urls import reverse from .models import User, UserProfile -from .services import generate_jwt_tokens from .forms import RegisterForm, LoginForm from rest_framework_simplejwt.tokens import RefreshToken, AccessToken diff --git a/core/config/apps.py b/core/config/apps.py index 61082ce..83de4a9 100644 --- a/core/config/apps.py +++ b/core/config/apps.py @@ -1,5 +1,4 @@ DEFAULT_APPS = [ - # Default apps "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -10,14 +9,14 @@ "django.contrib.staticfiles", "django.contrib.postgres", ] + PROJECT_APPS = [ - # Project apps "apps.shared.apps.SharedConfig", "apps.blog.apps.BlogConfig", "apps.users.apps.UsersConfig", ] + THIRD_PARTY_APPS = [ - # Third party apps "rest_framework", "rest_framework_simplejwt", "rest_framework_simplejwt.token_blacklist", diff --git a/core/settings/base.py b/core/settings/base.py index 9ef9376..82bd896 100644 --- a/core/settings/base.py +++ b/core/settings/base.py @@ -17,7 +17,7 @@ ALLOWED_HOSTS = str(os.getenv("ALLOWED_HOSTS")).split(",") -INSTALLED_APPS = DEFAULT_APPS + PROJECT_APPS + THIRD_PARTY_APPS +INSTALLED_APPS = DEFAULT_APPS + PROJECT_APPS + THIRD_PARTY_APPS # NOQA MIDDLEWARE = [ @@ -34,7 +34,7 @@ ROOT_URLCONF = "core.urls" -TEMPLATES_DIRS = [os.path.join(BASE_DIR, "templates")] +TEMPLATES_DIRS = [BASE_DIR.joinpath("templates")] TEMPLATES = [ { @@ -52,6 +52,12 @@ }, ] +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR.joinpath("db.sqlite3"), + }, +} WSGI_APPLICATION = "core.wsgi.application" @@ -83,8 +89,8 @@ LOGIN_REDIRECT_URL = "/" STATIC_URL = "static/" -STATIC_ROOT = BASE_DIR / "staticfiles" -STATICFILES_DIRS = [BASE_DIR / "static"] +STATIC_ROOT = BASE_DIR.joinpath("staticfiles") +STATICFILES_DIRS = [BASE_DIR.joinpath("static")] # AUTHENTICATION_BACKENDS = ( # 'apps.users.authentication.JWTAdminAuthentication', @@ -92,7 +98,7 @@ # ) MEDIA_URL = "media/" -MEDIA_ROOT = BASE_DIR / "media/" +MEDIA_ROOT = BASE_DIR.joinpath("media/") DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/core/settings/development.py b/core/settings/development.py index 96882c3..00b67d5 100644 --- a/core/settings/development.py +++ b/core/settings/development.py @@ -1,10 +1,3 @@ from .base import * # noqa EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -DATABASES = { - "default": { - "ENGINE": f"django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} diff --git a/core/settings/production.py b/core/settings/production.py index adf9b66..5881584 100644 --- a/core/settings/production.py +++ b/core/settings/production.py @@ -12,7 +12,7 @@ DATABASES = { "default": { - "ENGINE": f"django.db.backends.postgresql", + "ENGINE": "django.db.backends.postgresql", "NAME": str(os.getenv("DATABASE_NAME")), "USER": str(os.getenv("DATABASE_USER")), "PASSWORD": str(os.getenv("DATABASE_PASSWORD")), diff --git a/core/wsgi.py b/core/wsgi.py index 8ba2346..7546823 100644 --- a/core/wsgi.py +++ b/core/wsgi.py @@ -1,11 +1,10 @@ import os -from dotenv import load_dotenv +from django.core.wsgi import get_wsgi_application +from dotenv import load_dotenv load_dotenv() -from django.core.wsgi import get_wsgi_application - os.environ.setdefault( "DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.development"), From a7716d6968dad3c7a75b4eaa06cf9090428ad6cb Mon Sep 17 00:00:00 2001 From: Akromjon <152626511+RustamovAkrom@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:11:19 +0500 Subject: [PATCH 09/10] fix: change error python version --- apps/users/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/users/services.py b/apps/users/services.py index 46cc7ef..39898b4 100644 --- a/apps/users/services.py +++ b/apps/users/services.py @@ -2,7 +2,7 @@ from rest_framework_simplejwt.tokens import RefreshToken, AccessToken -def generate_jwt_tokens(user) -> tuple[RefreshToken, AccessToken]: +def generate_jwt_tokens(user): refresh = RefreshToken.for_user(user) access_token = refresh.access_token access_token["user_id"] = user.id From 1ceccb5bf8b3a0dd659e890c3988d341dc4c693b Mon Sep 17 00:00:00 2001 From: Akromjon <152626511+RustamovAkrom@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:14:39 +0500 Subject: [PATCH 10/10] fix: change acction --- .github/workflows/lint.yml | 2 +- apps/users/services.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 089a8c5..21a17ac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Django Test +name: Lint Code on: push: diff --git a/apps/users/services.py b/apps/users/services.py index 39898b4..73c327a 100644 --- a/apps/users/services.py +++ b/apps/users/services.py @@ -1,5 +1,5 @@ from django.http import HttpResponse, HttpRequest -from rest_framework_simplejwt.tokens import RefreshToken, AccessToken +from rest_framework_simplejwt.tokens import RefreshToken def generate_jwt_tokens(user):