mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 15:51:19 +00:00
Add ProductTypeController
This commit is contained in:
parent
483670e798
commit
c79c251ba7
@ -14,22 +14,31 @@
|
|||||||
#
|
#
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from ninja import Query
|
from ninja import Query
|
||||||
from ninja_extra import ControllerBase, api_controller, paginate, route
|
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||||
from ninja_extra.pagination import PageNumberPaginationExtra
|
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||||
from ninja_extra.schemas import PaginatedResponseSchema
|
from ninja_extra.schemas import PaginatedResponseSchema
|
||||||
|
|
||||||
from core.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoot
|
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 (
|
from counter.schemas import (
|
||||||
CounterFilterSchema,
|
CounterFilterSchema,
|
||||||
CounterSchema,
|
CounterSchema,
|
||||||
ProductFilterSchema,
|
ProductFilterSchema,
|
||||||
ProductSchema,
|
ProductSchema,
|
||||||
|
ProductTypeSchema,
|
||||||
|
ReorderProductTypeSchema,
|
||||||
SimpleProductSchema,
|
SimpleProductSchema,
|
||||||
SimplifiedCounterSchema,
|
SimplifiedCounterSchema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
IsCounterAdmin = (
|
||||||
|
IsRoot
|
||||||
|
| IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||||
|
| IsInGroup(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_controller("/counter")
|
@api_controller("/counter")
|
||||||
class CounterController(ControllerBase):
|
class CounterController(ControllerBase):
|
||||||
@ -80,11 +89,7 @@ class ProductController(ControllerBase):
|
|||||||
@route.get(
|
@route.get(
|
||||||
"/search/detailed",
|
"/search/detailed",
|
||||||
response=PaginatedResponseSchema[ProductSchema],
|
response=PaginatedResponseSchema[ProductSchema],
|
||||||
permissions=[
|
permissions=[IsCounterAdmin],
|
||||||
IsRoot
|
|
||||||
| IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
|
||||||
| IsInGroup(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
|
||||||
],
|
|
||||||
url_name="search_products_detailed",
|
url_name="search_products_detailed",
|
||||||
)
|
)
|
||||||
@paginate(PageNumberPaginationExtra, page_size=50)
|
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||||
@ -100,3 +105,40 @@ class ProductController(ControllerBase):
|
|||||||
"name",
|
"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)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated, Self
|
||||||
|
|
||||||
from annotated_types import MinLen
|
from annotated_types import MinLen
|
||||||
from django.urls import reverse
|
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 club.schemas import ClubSchema
|
||||||
from core.schemas import GroupSchema, SimpleUserSchema
|
from core.schemas import GroupSchema, SimpleUserSchema
|
||||||
@ -29,11 +30,36 @@ class SimplifiedCounterSchema(ModelSchema):
|
|||||||
|
|
||||||
|
|
||||||
class ProductTypeSchema(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:
|
class Meta:
|
||||||
model = ProductType
|
model = ProductType
|
||||||
fields = ["id", "name"]
|
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 SimpleProductSchema(ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
@ -57,7 +83,7 @@ class ProductSchema(ModelSchema):
|
|||||||
|
|
||||||
buying_groups: list[GroupSchema]
|
buying_groups: list[GroupSchema]
|
||||||
club: ClubSchema
|
club: ClubSchema
|
||||||
product_type: ProductTypeSchema | None
|
product_type: SimpleProductTypeSchema | None
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
91
counter/tests/test_product_type.py
Normal file
91
counter/tests/test_product_type.py
Normal 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
|
Loading…
Reference in New Issue
Block a user