From e2b8b0f4edbd0477b4b256ea60da93d58869e6f9 Mon Sep 17 00:00:00 2001 From: RyanAquino Date: Tue, 9 Aug 2022 00:36:15 +0800 Subject: [PATCH] ZDL-47: Add categories model API endpoints --- authentication/permissions.py | 7 + .../tests/factories/user_factory.py | 1 + authentication/tests/test_auth_api.py | 2 +- categories/__init__.py | 0 categories/admin.py | 5 + categories/apps.py | 6 + categories/migrations/0001_initial.py | 28 ++++ categories/migrations/__init__.py | 0 categories/models.py | 5 + categories/serializers.py | 9 ++ categories/tests/__init__.py | 0 categories/tests/factories/__init__.py | 0 .../tests/factories/category_factory.py | 11 ++ categories/tests/test_categories_api.py | 131 ++++++++++++++++++ categories/urls.py | 8 ++ categories/views.py | 11 ++ conftest.py | 13 +- products/migrations/0006_product_category.py | 25 ++++ products/models.py | 5 + zadalaAPI/settings.py | 1 + zadalaAPI/urls.py | 1 + 21 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 categories/__init__.py create mode 100644 categories/admin.py create mode 100644 categories/apps.py create mode 100644 categories/migrations/0001_initial.py create mode 100644 categories/migrations/__init__.py create mode 100644 categories/models.py create mode 100644 categories/serializers.py create mode 100644 categories/tests/__init__.py create mode 100644 categories/tests/factories/__init__.py create mode 100644 categories/tests/factories/category_factory.py create mode 100644 categories/tests/test_categories_api.py create mode 100644 categories/urls.py create mode 100644 categories/views.py create mode 100644 products/migrations/0006_product_category.py diff --git a/authentication/permissions.py b/authentication/permissions.py index 26f6ab2..90b4636 100644 --- a/authentication/permissions.py +++ b/authentication/permissions.py @@ -2,6 +2,13 @@ from rest_framework import permissions +class AdminAccessPermission(permissions.BasePermission): + group = "Admin" + + def has_permission(self, request, view): + return request.method in permissions.SAFE_METHODS or request.user.is_superuser + + class CustomerAccessPermission(permissions.BasePermission): group = "Customers" diff --git a/authentication/tests/factories/user_factory.py b/authentication/tests/factories/user_factory.py index abd4fda..fbff5a8 100644 --- a/authentication/tests/factories/user_factory.py +++ b/authentication/tests/factories/user_factory.py @@ -18,6 +18,7 @@ class Meta: last_name = "account" password = PostGenerationMethodCall("set_password", "password") is_active = True + is_superuser = False auth_provider = AuthProviders.email.value date_joined = FuzzyNaiveDateTime(datetime(2022, 1, 1)) last_login = FuzzyNaiveDateTime(datetime(2022, 1, 1)) diff --git a/authentication/tests/test_auth_api.py b/authentication/tests/test_auth_api.py index a5913c8..64d6e6f 100644 --- a/authentication/tests/test_auth_api.py +++ b/authentication/tests/test_auth_api.py @@ -11,7 +11,7 @@ @pytest.mark.django_db -def test_create_superuser_command(admin_group): +def test_create_superuser_command(): """ Test admin account creation """ diff --git a/categories/__init__.py b/categories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/categories/admin.py b/categories/admin.py new file mode 100644 index 0000000..37ed2c9 --- /dev/null +++ b/categories/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from categories.models import Category + +admin.site.register(Category) diff --git a/categories/apps.py b/categories/apps.py new file mode 100644 index 0000000..bfbe430 --- /dev/null +++ b/categories/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CategoryConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "categories" diff --git a/categories/migrations/0001_initial.py b/categories/migrations/0001_initial.py new file mode 100644 index 0000000..87c3b94 --- /dev/null +++ b/categories/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.12 on 2022-08-08 16:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Category", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.TextField(unique=True)), + ], + ), + ] diff --git a/categories/migrations/__init__.py b/categories/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/categories/models.py b/categories/models.py new file mode 100644 index 0000000..721d0f5 --- /dev/null +++ b/categories/models.py @@ -0,0 +1,5 @@ +from django.db import models + + +class Category(models.Model): + name = models.TextField(unique=True) diff --git a/categories/serializers.py b/categories/serializers.py new file mode 100644 index 0000000..156d23e --- /dev/null +++ b/categories/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from categories.models import Category + + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = "__all__" diff --git a/categories/tests/__init__.py b/categories/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/categories/tests/factories/__init__.py b/categories/tests/factories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/categories/tests/factories/category_factory.py b/categories/tests/factories/category_factory.py new file mode 100644 index 0000000..41ed0f2 --- /dev/null +++ b/categories/tests/factories/category_factory.py @@ -0,0 +1,11 @@ +import factory +from factory.django import DjangoModelFactory + +from categories.models import Category + + +class CategoryFactory(DjangoModelFactory): + class Meta: + model = Category + + name = factory.Sequence(lambda n: f"Category {n}") diff --git a/categories/tests/test_categories_api.py b/categories/tests/test_categories_api.py new file mode 100644 index 0000000..ac0259a --- /dev/null +++ b/categories/tests/test_categories_api.py @@ -0,0 +1,131 @@ +import pytest +from django.test.client import MULTIPART_CONTENT + +from categories.models import Category +from categories.tests.factories.category_factory import CategoryFactory + + +@pytest.mark.django_db +def test_list_all_categories(logged_in_client): + """ + Test list all products categories with empty database + """ + response = logged_in_client.get("/v1/categories/") + + assert response.status_code == 200, response.json() + assert len(response.json()["results"]) == 0 + assert response.json()["results"] == [] + + +@pytest.mark.django_db +def test_list_all_product_categories(logged_in_client): + """ + Test list all product categories + """ + CategoryFactory() + response = logged_in_client.get("/v1/categories/") + + response_data = response.json()["results"] + + assert response.status_code == 200, response_data + assert len(response_data) == 1 + + +@pytest.mark.django_db +def test_create_product_category(admin_client): + """ + Test admin create product category + """ + category_data = {"name": "test-category-create"} + response = admin_client.post("/v1/categories/", category_data, format="json") + assert response.status_code == 201 and response.json() + assert Category.objects.count() == 1 + + +@pytest.mark.django_db +def test_retrieve_product_category(logged_in_client): + """ + Test retrieve specific product category + """ + category = CategoryFactory() + response = logged_in_client.get(f"/v1/categories/{category.id}/") + + response_data = response.json() + + assert response.status_code == 200 and response_data + assert response_data["id"] == category.id + + +@pytest.mark.django_db +def test_update_product_category(admin_client): + """ + Test update a single product category + """ + content_type = MULTIPART_CONTENT + category = CategoryFactory() + data = { + "name": "Category 1 edited", + } + data = admin_client._encode_json({} if not data else data, content_type) + encoded_data = admin_client._encode_data(data, content_type) + response = admin_client.generic( + "PUT", + f"/v1/categories/{category.id}/", + encoded_data, + content_type=content_type, + secure=False, + enctype="multipart/form-data", + ) + + response_data = response.json() + assert response.status_code == 200, response.data + assert data["name"] == response_data["name"] + + +@pytest.mark.django_db +def test_patch_product_category(admin_client): + """ + Test patch a single product property + """ + content_type = MULTIPART_CONTENT + category = CategoryFactory() + data = { + "name": "Category 1 patch edited", + } + data = admin_client._encode_json({} if not data else data, content_type) + encoded_data = admin_client._encode_data(data, content_type) + response = admin_client.generic( + "PATCH", + f"/v1/categories/{category.id}/", + encoded_data, + content_type=content_type, + secure=False, + enctype="multipart/form-data", + ) + + response_data = response.json() + assert response.status_code == 200, response.data + assert data["name"] == response_data["name"] + + +@pytest.mark.django_db +def test_delete_product_category(admin_client): + """ + Test delete specific product category + """ + category = CategoryFactory() + response = admin_client.delete(f"/v1/categories/{category.id}/") + assert response.status_code == 204 + assert Category.objects.count() == 0 + + +@pytest.mark.django_db +def test_non_safe_permission_product_category_should_raise_403(logged_in_client): + """ + Test non safe method permission access on product category + """ + category = CategoryFactory() + response = logged_in_client.delete(f"/v1/categories/{category.id}/") + assert response.status_code == 403 and response.json() == { + "detail": "You do not have permission to perform this action." + } diff --git a/categories/urls.py b/categories/urls.py new file mode 100644 index 0000000..7f3a539 --- /dev/null +++ b/categories/urls.py @@ -0,0 +1,8 @@ +from django.urls import include, path +from rest_framework import routers + +from categories.views import CategoryViewSet + +router = routers.DefaultRouter() +router.register("", CategoryViewSet) +urlpatterns = [path("", include(router.urls))] diff --git a/categories/views.py b/categories/views.py new file mode 100644 index 0000000..120b405 --- /dev/null +++ b/categories/views.py @@ -0,0 +1,11 @@ +from rest_framework.viewsets import ModelViewSet + +from authentication.permissions import AdminAccessPermission +from categories.models import Category +from categories.serializers import CategorySerializer + + +class CategoryViewSet(ModelViewSet): + permission_classes = [AdminAccessPermission] + queryset = Category.objects.all().order_by("id") + serializer_class = CategorySerializer diff --git a/conftest.py b/conftest.py index 124d320..7367c21 100644 --- a/conftest.py +++ b/conftest.py @@ -19,5 +19,14 @@ def logged_in_user(): @pytest.fixture -def admin_group(): - UserFactory.create(groups=(Group.objects.filter(name="Admins"))) +def admin_client(admin_user): + user_token = admin_user.tokens().token + return Client(HTTP_AUTHORIZATION=f"Bearer {user_token}") + + +@pytest.fixture +def admin_user(): + skip_if_no_django() + return UserFactory.create( + groups=(Group.objects.filter(name="Admins")), is_superuser=True + ) diff --git a/products/migrations/0006_product_category.py b/products/migrations/0006_product_category.py new file mode 100644 index 0000000..a13d3c0 --- /dev/null +++ b/products/migrations/0006_product_category.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.12 on 2022-08-08 16:29 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("categories", "0001_initial"), + ("products", "0005_alter_product_price"), + ] + + operations = [ + migrations.AddField( + model_name="product", + name="category", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="categories.category", + ), + ), + ] diff --git a/products/models.py b/products/models.py index 66107d4..32b27de 100644 --- a/products/models.py +++ b/products/models.py @@ -3,6 +3,8 @@ from django.db import models from django.db.models.functions import Upper +from categories.models import Category + class Product(models.Model): supplier = models.ForeignKey( @@ -16,6 +18,9 @@ class Product(models.Model): image = models.ImageField(null=True) quantity = models.IntegerField(default=1) created_at = models.DateTimeField(auto_now_add=True) + category = models.ForeignKey( + Category, on_delete=models.SET_NULL, null=True, blank=True + ) REQUIRED_FIELDS = "__all__" diff --git a/zadalaAPI/settings.py b/zadalaAPI/settings.py index e196721..d88d998 100644 --- a/zadalaAPI/settings.py +++ b/zadalaAPI/settings.py @@ -57,6 +57,7 @@ "corsheaders", "drf_yasg", "products", + "categories", "orders", "authentication", "rest_framework", diff --git a/zadalaAPI/urls.py b/zadalaAPI/urls.py index d0235ad..afd3755 100644 --- a/zadalaAPI/urls.py +++ b/zadalaAPI/urls.py @@ -39,6 +39,7 @@ urlpatterns = [ path("admin/", admin.site.urls), path("v1/products/", include("products.urls")), + path("v1/categories/", include("categories.urls")), path("v1/orders/", include("orders.urls")), path("v1/auth/", include("authentication.urls")), path("v1/social-auth/", include("social_auth.urls")),