Add ProductTypeController

This commit is contained in:
imperosol 2024-12-16 19:46:34 +01:00
parent 483670e798
commit c79c251ba7
3 changed files with 168 additions and 9 deletions

View File

@ -14,22 +14,31 @@
#
from django.conf import settings
from django.db.models import F
from django.shortcuts import get_object_or_404
from ninja import Query
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema
from core.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoot
from counter.models import Counter, Product
from counter.models import Counter, Product, ProductType
from counter.schemas import (
CounterFilterSchema,
CounterSchema,
ProductFilterSchema,
ProductSchema,
ProductTypeSchema,
ReorderProductTypeSchema,
SimpleProductSchema,
SimplifiedCounterSchema,
)
IsCounterAdmin = (
IsRoot
| IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)
| IsInGroup(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
)
@api_controller("/counter")
class CounterController(ControllerBase):
@ -80,11 +89,7 @@ class ProductController(ControllerBase):
@route.get(
"/search/detailed",
response=PaginatedResponseSchema[ProductSchema],
permissions=[
IsRoot
| IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)
| IsInGroup(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
],
permissions=[IsCounterAdmin],
url_name="search_products_detailed",
)
@paginate(PageNumberPaginationExtra, page_size=50)
@ -100,3 +105,40 @@ class ProductController(ControllerBase):
"name",
)
)
@api_controller("/product-type", permissions=[IsCounterAdmin])
class ProductTypeController(ControllerBase):
@route.get("", response=list[ProductTypeSchema], url_name="fetch-product-types")
def fetch_all(self):
return ProductType.objects.order_by("order")
@route.patch("/{type_id}/move")
def reorder(self, type_id: int, other_id: Query[ReorderProductTypeSchema]):
"""Change the order of a product type.
To use this route, give either the id of the product type
this one should be above of,
of the id of the product type this one should be below of.
Order affects the display order of the product types.
Examples:
```
GET /api/counter/product-type
=> [<1: type A>, <2: type B>, <3: type C>]
PATCH /api/counter/product-type/3/move?below=1
GET /api/counter/product-type
=> [<1: type A>, <3: type C>, <2: type B>]
```
"""
product_type: ProductType = self.get_object_or_exception(
ProductType, pk=type_id
)
other = get_object_or_404(ProductType, pk=other_id.above or other_id.below)
if other_id.below is not None:
product_type.below(other)
else:
product_type.above(other)

View File

@ -1,8 +1,9 @@
from typing import Annotated
from typing import Annotated, Self
from annotated_types import MinLen
from django.urls import reverse
from ninja import Field, FilterSchema, ModelSchema
from ninja import Field, FilterSchema, ModelSchema, Schema
from pydantic import model_validator
from club.schemas import ClubSchema
from core.schemas import GroupSchema, SimpleUserSchema
@ -29,11 +30,36 @@ class SimplifiedCounterSchema(ModelSchema):
class ProductTypeSchema(ModelSchema):
class Meta:
model = ProductType
fields = ["id", "name", "description", "comment", "icon", "order"]
url: str
@staticmethod
def resolve_url(obj: ProductType) -> str:
return reverse("counter:producttype_edit", kwargs={"type_id": obj.id})
class SimpleProductTypeSchema(ModelSchema):
class Meta:
model = ProductType
fields = ["id", "name"]
class ReorderProductTypeSchema(Schema):
below: int | None = None
above: int | None = None
@model_validator(mode="after")
def validate_exclusive(self) -> Self:
if self.below is None and self.above is None:
raise ValueError("Either 'below' or 'above' must be set.")
if self.below is not None and self.above is not None:
raise ValueError("Only one of 'below' or 'above' must be set.")
return self
class SimpleProductSchema(ModelSchema):
class Meta:
model = Product
@ -57,7 +83,7 @@ class ProductSchema(ModelSchema):
buying_groups: list[GroupSchema]
club: ClubSchema
product_type: ProductTypeSchema | None
product_type: SimpleProductTypeSchema | None
url: str
@staticmethod

View File

@ -0,0 +1,91 @@
import pytest
from django.conf import settings
from django.test import Client
from django.urls import reverse
from model_bakery import baker, seq
from ninja_extra.testing import TestClient
from core.baker_recipes import board_user, subscriber_user
from core.models import RealGroup, User
from counter.api import ProductTypeController
from counter.models import ProductType
@pytest.fixture
def product_types(db) -> list[ProductType]:
"""All existing product types, ordered by their `order` field"""
# delete product types that have been created in the `populate` command
ProductType.objects.all().delete()
return baker.make(ProductType, _quantity=5, order=seq(0))
@pytest.mark.django_db
def test_fetch_product_types(product_types: list[ProductType]):
"""Test that the API returns the right products in the right order"""
client = TestClient(ProductTypeController)
response = client.get("")
assert response.status_code == 200
assert [i["id"] for i in response.json()] == [t.id for t in product_types]
@pytest.mark.django_db
def test_move_below_product_type(product_types: list[ProductType]):
"""Test that moving a product below another works"""
client = TestClient(ProductTypeController)
response = client.patch(
f"/{product_types[-1].id}/move", query={"below": product_types[0].id}
)
assert response.status_code == 200
new_order = [i["id"] for i in client.get("").json()]
assert new_order == [
product_types[0].id,
product_types[-1].id,
*[t.id for t in product_types[1:-1]],
]
@pytest.mark.django_db
def test_move_above_product_type(product_types: list[ProductType]):
"""Test that moving a product above another works"""
client = TestClient(ProductTypeController)
response = client.patch(
f"/{product_types[1].id}/move", query={"above": product_types[0].id}
)
assert response.status_code == 200
new_order = [i["id"] for i in client.get("").json()]
assert new_order == [
product_types[1].id,
product_types[0].id,
*[t.id for t in product_types[2:]],
]
@pytest.mark.django_db
@pytest.mark.parametrize(
("user_factory", "status_code"),
[
(lambda: baker.make(User, is_superuser=True), 200),
(subscriber_user.make, 403),
(board_user.make, 403),
(
lambda: baker.make(
User,
groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
),
200,
),
(
lambda: baker.make(
User,
groups=[
RealGroup.objects.get(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
],
),
200,
),
],
)
def test_controller_permissions(client: Client, user_factory, status_code):
client.force_login(user_factory())
response = client.get(reverse("api:fetch-product-types"))
assert response.status_code == status_code