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/.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..21a17ac --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,68 @@ +name: Lint Code + +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/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 0a472e4..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", @@ -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/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 d11e9d7..dd8a427 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 .forms import PostCreateUpdateForm, SettingsUserForm +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,66 +23,66 @@ 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", - "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_Users(self): - users = User.objects.all() - print(users) + # 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/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 a4862b0..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, @@ -54,7 +51,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 +70,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) @@ -95,8 +92,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) @@ -128,13 +123,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) @@ -166,7 +161,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) @@ -192,13 +187,16 @@ 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) 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/apps/shared/management/commands/createadmin.py b/apps/shared/management/commands/createadmin.py index 3e8c696..ae3f06f 100644 --- a/apps/shared/management/commands/createadmin.py +++ b/apps/shared/management/commands/createadmin.py @@ -1,11 +1,11 @@ import os +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")) @@ -13,9 +13,12 @@ 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) + 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 +26,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/__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/__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 9aed973..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,16 +22,15 @@ 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", - "post_count", "profiles", "created_at", "updated_at", @@ -41,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 e69de29..d8c4c7c 100644 --- a/apps/users/api_endpoints/users/User/tests.py +++ b/apps/users/api_endpoints/users/User/tests.py @@ -0,0 +1,104 @@ +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_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() + + 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.", + ) + + 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"]) + + _check_user_error_field() + _check_user_passwords_field() + _create_user() diff --git a/apps/users/api_endpoints/users/User/views.py b/apps/users/api_endpoints/users/User/views.py index bdd529e..7e7f952 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): @@ -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 eee4d15..39b78ca 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,8 @@ class UserProfileViewSet(viewsets.ModelViewSet): queryset = UserProfile.objects.all() serializer_class = UserProfileSerializer -__all__ = ("UserProfileViewSet", ) + def get_permissions(self): + return [IsAuthenticated(), IsAdminUser(), IsOwnerPermission()] + + +__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 cb76d94..4ca3941 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) @@ -58,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/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/services.py b/apps/users/services.py index 007b045..73c327a 100644 --- a/apps/users/services.py +++ b/apps/users/services.py @@ -5,7 +5,7 @@ def generate_jwt_tokens(user): 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/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/tests.py b/apps/users/tests.py index 2aba761..3b266f9 100644 --- a/apps/users/tests.py +++ b/apps/users/tests.py @@ -2,22 +2,132 @@ from django.urls import reverse from .models import User, UserProfile +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) diff --git a/apps/users/urls.py b/apps/users/urls.py index be2a414..c9766fb 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" @@ -20,5 +14,4 @@ views.UserProfilePageView.as_view(), name="user_profile", ), - ] diff --git a/apps/users/views.py b/apps/users/views.py index 22cc3c8..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) @@ -31,11 +31,15 @@ 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): template_name = "auth/login.html" @@ -44,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) @@ -70,17 +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): @@ -107,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 5cf4c8a..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,16 +9,15 @@ "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", - -] \ 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..82bd896 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 @@ -16,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 = [ @@ -33,7 +34,7 @@ ROOT_URLCONF = "core.urls" -TEMPLATES_DIRS = [os.path.join(BASE_DIR, "templates")] +TEMPLATES_DIRS = [BASE_DIR.joinpath("templates")] TEMPLATES = [ { @@ -51,6 +52,12 @@ }, ] +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR.joinpath("db.sqlite3"), + }, +} WSGI_APPLICATION = "core.wsgi.application" @@ -82,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', @@ -91,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 4601161..00b67d5 100644 --- a/core/settings/development.py +++ b/core/settings/development.py @@ -1,10 +1,3 @@ -from .base import * # noqa +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", - } -} \ No newline at end of file diff --git a/core/settings/production.py b/core/settings/production.py index eb013d8..5881584 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" @@ -12,11 +12,11 @@ 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")), "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..7546823 100644 --- a/core/wsgi.py +++ b/core/wsgi.py @@ -1,13 +1,14 @@ import os +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" -)) +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 diff --git a/media/avatars/2.jpg b/media/avatars/2.jpg index d6f7742..74e5adc 100644 Binary files a/media/avatars/2.jpg and b/media/avatars/2.jpg differ 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 0000000..ff9cc12 Binary files /dev/null and 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 differ 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 0000000..fe30061 Binary files /dev/null and b/media/avatars/financial-tops_auto_x2-e5jv3oj9.jpg differ