mirror of
https://github.com/ae-utbm/sith.git
synced 2025-11-23 05:07:00 +00:00
Compare commits
1 Commits
update-nin
...
fix/produc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60bd3a42b |
@@ -1,16 +1,18 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
|
from annotated_types import MinLen
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from ninja import FilterLookup, FilterSchema, ModelSchema
|
from ninja import Field, FilterSchema, ModelSchema
|
||||||
|
|
||||||
from club.models import Club, Membership
|
from club.models import Club, Membership
|
||||||
from core.schemas import NonEmptyStr, SimpleUserSchema
|
from core.schemas import SimpleUserSchema
|
||||||
|
|
||||||
|
|
||||||
class ClubSearchFilterSchema(FilterSchema):
|
class ClubSearchFilterSchema(FilterSchema):
|
||||||
search: Annotated[NonEmptyStr | None, FilterLookup("name__icontains")] = None
|
search: Annotated[str, MinLen(1)] | None = Field(None, q="name__icontains")
|
||||||
is_active: bool | None = None
|
is_active: bool | None = None
|
||||||
parent_id: int | None = None
|
parent_id: int | None = None
|
||||||
|
parent_name: str | None = Field(None, q="parent__name__icontains")
|
||||||
exclude_ids: set[int] | None = None
|
exclude_ids: set[int] | None = None
|
||||||
|
|
||||||
def filter_exclude_ids(self, value: set[int] | None):
|
def filter_exclude_ids(self, value: set[int] | None):
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from ninja import FilterLookup, FilterSchema, ModelSchema
|
from ninja import FilterSchema, ModelSchema
|
||||||
from ninja_extra import service_resolver
|
from ninja_extra import service_resolver
|
||||||
from ninja_extra.context import RouteContext
|
from ninja_extra.context import RouteContext
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
from club.schemas import ClubProfileSchema
|
from club.schemas import ClubProfileSchema
|
||||||
from com.models import News, NewsDate
|
from com.models import News, NewsDate
|
||||||
@@ -11,12 +11,12 @@ from core.markdown import markdown
|
|||||||
|
|
||||||
|
|
||||||
class NewsDateFilterSchema(FilterSchema):
|
class NewsDateFilterSchema(FilterSchema):
|
||||||
before: Annotated[datetime | None, FilterLookup("end_date__lt")] = None
|
before: datetime | None = Field(None, q="end_date__lt")
|
||||||
after: Annotated[datetime | None, FilterLookup("start_date__gt")] = None
|
after: datetime | None = Field(None, q="start_date__gt")
|
||||||
club_id: Annotated[int | None, FilterLookup("news__club_id")] = None
|
club_id: int | None = Field(None, q="news__club_id")
|
||||||
news_id: int | None = None
|
news_id: int | None = None
|
||||||
is_published: Annotated[bool | None, FilterLookup("news__is_published")] = None
|
is_published: bool | None = Field(None, q="news__is_published")
|
||||||
title: Annotated[str | None, FilterLookup("news__title__icontains")] = None
|
title: str | None = Field(None, q="news__title__icontains")
|
||||||
|
|
||||||
|
|
||||||
class NewsSchema(ModelSchema):
|
class NewsSchema(ModelSchema):
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ from pydantic_core.core_schema import ValidationInfo
|
|||||||
from core.models import Group, QuickUploadImage, SithFile, User
|
from core.models import Group, QuickUploadImage, SithFile, User
|
||||||
from core.utils import is_image
|
from core.utils import is_image
|
||||||
|
|
||||||
NonEmptyStr = Annotated[str, MinLen(1)]
|
|
||||||
|
|
||||||
|
|
||||||
class UploadedImage(UploadedFile):
|
class UploadedImage(UploadedFile):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -235,6 +235,19 @@ class ScheduledProductActionForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
return super().clean()
|
return super().clean()
|
||||||
|
|
||||||
|
def set_product(self, product: Product):
|
||||||
|
"""Set the product to which this form's instance is linked.
|
||||||
|
|
||||||
|
When this form is linked to a ProductForm in the case of a product's creation,
|
||||||
|
the product doesn't exist yet, so saving this form as is will result
|
||||||
|
in having `{"product_id": null}` in the action kwargs.
|
||||||
|
For the creation to be useful, it may be needed to inject the newly created
|
||||||
|
product into this form, before saving the latter.
|
||||||
|
"""
|
||||||
|
self.product = product
|
||||||
|
kwargs = json.loads(self.instance.kwargs) | {"product_id": self.product.id}
|
||||||
|
self.instance.kwargs = json.dumps(kwargs)
|
||||||
|
|
||||||
|
|
||||||
class BaseScheduledProductActionFormSet(BaseModelFormSet):
|
class BaseScheduledProductActionFormSet(BaseModelFormSet):
|
||||||
def __init__(self, *args, product: Product, **kwargs):
|
def __init__(self, *args, product: Product, **kwargs):
|
||||||
@@ -321,11 +334,19 @@ class ProductForm(forms.ModelForm):
|
|||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
return super().is_valid() and self.action_formset.is_valid()
|
return super().is_valid() and self.action_formset.is_valid()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs) -> Product:
|
||||||
ret = super().save(*args, **kwargs)
|
product = super().save(*args, **kwargs)
|
||||||
self.instance.counters.set(self.cleaned_data["counters"])
|
product.counters.set(self.cleaned_data["counters"])
|
||||||
|
for form in self.action_formset:
|
||||||
|
# if it's a creation, the product given in the formset
|
||||||
|
# wasn't a persisted instance.
|
||||||
|
# So if we tried to persist the scheduled actions in the current state,
|
||||||
|
# they would be linked to no product, thus be completely useless
|
||||||
|
# To make it work, we have to replace
|
||||||
|
# the initial product with a persisted one
|
||||||
|
form.set_product(product)
|
||||||
self.action_formset.save()
|
self.action_formset.save()
|
||||||
return ret
|
return product
|
||||||
|
|
||||||
|
|
||||||
class ReturnableProductForm(forms.ModelForm):
|
class ReturnableProductForm(forms.ModelForm):
|
||||||
@@ -369,7 +390,6 @@ class EticketForm(forms.ModelForm):
|
|||||||
class CloseCustomerAccountForm(forms.Form):
|
class CloseCustomerAccountForm(forms.Form):
|
||||||
user = forms.ModelChoiceField(
|
user = forms.ModelChoiceField(
|
||||||
label=_("Refound this account"),
|
label=_("Refound this account"),
|
||||||
help_text=None,
|
|
||||||
required=True,
|
required=True,
|
||||||
widget=AutoCompleteSelectUser,
|
widget=AutoCompleteSelectUser,
|
||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Annotated, Self
|
from typing import Annotated, Self
|
||||||
|
|
||||||
|
from annotated_types import MinLen
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from ninja import FilterLookup, FilterSchema, ModelSchema, Schema
|
from ninja import Field, FilterSchema, ModelSchema, Schema
|
||||||
from pydantic import model_validator
|
from pydantic import model_validator
|
||||||
|
|
||||||
from club.schemas import SimpleClubSchema
|
from club.schemas import SimpleClubSchema
|
||||||
from core.schemas import GroupSchema, NonEmptyStr, SimpleUserSchema
|
from core.schemas import GroupSchema, SimpleUserSchema
|
||||||
from counter.models import Counter, Product, ProductType
|
from counter.models import Counter, Product, ProductType
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class CounterSchema(ModelSchema):
|
|||||||
|
|
||||||
|
|
||||||
class CounterFilterSchema(FilterSchema):
|
class CounterFilterSchema(FilterSchema):
|
||||||
search: Annotated[NonEmptyStr | None, FilterLookup("name__icontains")] = None
|
search: Annotated[str, MinLen(1)] = Field(None, q="name__icontains")
|
||||||
|
|
||||||
|
|
||||||
class SimplifiedCounterSchema(ModelSchema):
|
class SimplifiedCounterSchema(ModelSchema):
|
||||||
@@ -92,18 +93,18 @@ class ProductSchema(ModelSchema):
|
|||||||
|
|
||||||
|
|
||||||
class ProductFilterSchema(FilterSchema):
|
class ProductFilterSchema(FilterSchema):
|
||||||
search: Annotated[
|
search: Annotated[str, MinLen(1)] | None = Field(
|
||||||
NonEmptyStr | None, FilterLookup(["name__icontains", "code__icontains"])
|
None, q=["name__icontains", "code__icontains"]
|
||||||
] = None
|
)
|
||||||
is_archived: Annotated[bool | None, FilterLookup("archived")] = None
|
is_archived: bool | None = Field(None, q="archived")
|
||||||
buying_groups: Annotated[set[int] | None, FilterLookup("buying_groups__in")] = None
|
buying_groups: set[int] | None = Field(None, q="buying_groups__in")
|
||||||
product_type: Annotated[set[int] | None, FilterLookup("product_type__in")] = None
|
product_type: set[int] | None = Field(None, q="product_type__in")
|
||||||
club: Annotated[set[int] | None, FilterLookup("club__in")] = None
|
club: set[int] | None = Field(None, q="club__in")
|
||||||
counter: Annotated[set[int] | None, FilterLookup("counters__in")] = None
|
counter: set[int] | None = Field(None, q="counters__in")
|
||||||
|
|
||||||
|
|
||||||
class SaleFilterSchema(FilterSchema):
|
class SaleFilterSchema(FilterSchema):
|
||||||
before: Annotated[datetime | None, FilterLookup("date__lt")] = None
|
before: datetime | None = Field(None, q="date__lt")
|
||||||
after: Annotated[datetime | None, FilterLookup("date__gt")] = None
|
after: datetime | None = Field(None, q="date__gt")
|
||||||
counters: Annotated[set[int] | None, FilterLookup("counter__in")] = None
|
counters: set[int] | None = Field(None, q="counter__in")
|
||||||
products: Annotated[set[int] | None, FilterLookup("product__in")] = None
|
products: set[int] | None = Field(None, q="product__in")
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ from model_bakery import baker
|
|||||||
|
|
||||||
from core.models import Group, User
|
from core.models import Group, User
|
||||||
from counter.baker_recipes import counter_recipe, product_recipe
|
from counter.baker_recipes import counter_recipe, product_recipe
|
||||||
from counter.forms import ScheduledProductActionForm, ScheduledProductActionFormSet
|
from counter.forms import (
|
||||||
from counter.models import ScheduledProductAction
|
ProductForm,
|
||||||
|
ScheduledProductActionForm,
|
||||||
|
ScheduledProductActionFormSet,
|
||||||
|
)
|
||||||
|
from counter.models import Product, ScheduledProductAction
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -34,6 +38,39 @@ def test_edit_product(client: Client):
|
|||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_actions_alongside_product():
|
||||||
|
"""The form should work when the product and the actions are created alongside."""
|
||||||
|
# non-persisted instance
|
||||||
|
product: Product = product_recipe.prepare(_save_related=True)
|
||||||
|
trigger_at = now() + timedelta(minutes=10)
|
||||||
|
form = ProductForm(
|
||||||
|
data={
|
||||||
|
"name": "foo",
|
||||||
|
"description": "bar",
|
||||||
|
"product_type": product.product_type_id,
|
||||||
|
"club": product.club_id,
|
||||||
|
"code": "FOO",
|
||||||
|
"purchase_price": 1.0,
|
||||||
|
"selling_price": 1.0,
|
||||||
|
"special_selling_price": 1.0,
|
||||||
|
"limit_age": 0,
|
||||||
|
"form-TOTAL_FORMS": "2",
|
||||||
|
"form-INITIAL_FORMS": "0",
|
||||||
|
"form-0-task": "counter.tasks.archive_product",
|
||||||
|
"form-0-trigger_at": trigger_at,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert form.is_valid()
|
||||||
|
product = form.save()
|
||||||
|
action = ScheduledProductAction.objects.last()
|
||||||
|
assert action.clocked.clocked_time == trigger_at
|
||||||
|
assert action.enabled is True
|
||||||
|
assert action.one_off is True
|
||||||
|
assert action.task == "counter.tasks.archive_product"
|
||||||
|
assert action.kwargs == json.dumps({"product_id": product.id})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestProductActionForm:
|
class TestProductActionForm:
|
||||||
def test_single_form_archive(self):
|
def test_single_form_archive(self):
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from typing import Annotated, Literal
|
from typing import Literal
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import html
|
from django.utils import html
|
||||||
from haystack.query import SearchQuerySet
|
from haystack.query import SearchQuerySet
|
||||||
from ninja import FilterLookup, FilterSchema, ModelSchema, Schema
|
from ninja import FilterSchema, ModelSchema, Schema
|
||||||
from pydantic import AliasPath, ConfigDict, Field, TypeAdapter
|
from pydantic import AliasPath, ConfigDict, Field, TypeAdapter
|
||||||
from pydantic.alias_generators import to_camel
|
from pydantic.alias_generators import to_camel
|
||||||
|
|
||||||
@@ -114,14 +114,13 @@ class UvSchema(ModelSchema):
|
|||||||
|
|
||||||
|
|
||||||
class UvFilterSchema(FilterSchema):
|
class UvFilterSchema(FilterSchema):
|
||||||
search: Annotated[str | None, FilterLookup("code__icontains")] = None
|
search: str | None = Field(None, q="code__icontains")
|
||||||
semester: set[Literal["AUTUMN", "SPRING"]] | None = None
|
semester: set[Literal["AUTUMN", "SPRING"]] | None = None
|
||||||
credit_type: Annotated[
|
credit_type: set[Literal["CS", "TM", "EC", "OM", "QC"]] | None = Field(
|
||||||
set[Literal["CS", "TM", "EC", "OM", "QC"]] | None,
|
None, q="credit_type__in"
|
||||||
FilterLookup("credit_type__in"),
|
)
|
||||||
] = None
|
|
||||||
language: str = "FR"
|
language: str = "FR"
|
||||||
department: Annotated[set[str] | None, FilterLookup("department__in")] = None
|
department: set[str] | None = Field(None, q="department__in")
|
||||||
|
|
||||||
def filter_search(self, value: str | None) -> Q:
|
def filter_search(self, value: str | None) -> Q:
|
||||||
"""Special filter for the search text.
|
"""Special filter for the search text.
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ license = { text = "GPL-3.0-only" }
|
|||||||
requires-python = "<4.0,>=3.12"
|
requires-python = "<4.0,>=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django>=5.2.8,<6.0.0",
|
"django>=5.2.8,<6.0.0",
|
||||||
"django-ninja>=1.5.0,<6.0.0",
|
"django-ninja>=1.4.5,<2.0.0",
|
||||||
"django-ninja-extra>=0.30.6",
|
"django-ninja-extra>=0.30.2,<1.0.0",
|
||||||
"Pillow>=12.0.0,<13.0.0",
|
"Pillow>=12.0.0,<13.0.0",
|
||||||
"mistune>=3.1.4,<4.0.0",
|
"mistune>=3.1.4,<4.0.0",
|
||||||
"django-jinja<3.0.0,>=2.11.0",
|
"django-jinja<3.0.0,>=2.11.0",
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
|
from annotated_types import MinLen
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from ninja import FilterLookup, FilterSchema, ModelSchema, Schema
|
from ninja import FilterSchema, ModelSchema, Schema
|
||||||
from pydantic import Field, NonNegativeInt
|
from pydantic import Field, NonNegativeInt
|
||||||
|
|
||||||
from core.schemas import NonEmptyStr, SimpleUserSchema, UserProfileSchema
|
from core.schemas import SimpleUserSchema, UserProfileSchema
|
||||||
from sas.models import Album, Picture, PictureModerationRequest
|
from sas.models import Album, Picture, PictureModerationRequest
|
||||||
|
|
||||||
|
|
||||||
class AlbumFilterSchema(FilterSchema):
|
class AlbumFilterSchema(FilterSchema):
|
||||||
search: Annotated[NonEmptyStr | None, FilterLookup("name__icontains")] = None
|
search: Annotated[str, MinLen(1)] | None = Field(None, q="name__icontains")
|
||||||
before_date: Annotated[datetime | None, FilterLookup("event_date__lte")] = None
|
before_date: datetime | None = Field(None, q="event_date__lte")
|
||||||
after_date: Annotated[datetime | None, FilterLookup("event_date__gte")] = None
|
after_date: datetime | None = Field(None, q="event_date__gte")
|
||||||
parent_id: Annotated[int | None, FilterLookup("parent_id")] = None
|
parent_id: int | None = Field(None, q="parent_id")
|
||||||
|
|
||||||
|
|
||||||
class SimpleAlbumSchema(ModelSchema):
|
class SimpleAlbumSchema(ModelSchema):
|
||||||
@@ -59,12 +60,10 @@ class AlbumAutocompleteSchema(ModelSchema):
|
|||||||
|
|
||||||
|
|
||||||
class PictureFilterSchema(FilterSchema):
|
class PictureFilterSchema(FilterSchema):
|
||||||
before_date: Annotated[datetime | None, FilterLookup("date__lte")] = None
|
before_date: datetime | None = Field(None, q="date__lte")
|
||||||
after_date: Annotated[datetime | None, FilterLookup("date__gte")] = None
|
after_date: datetime | None = Field(None, q="date__gte")
|
||||||
users_identified: Annotated[
|
users_identified: set[int] | None = Field(None, q="people__user_id__in")
|
||||||
set[int] | None, FilterLookup("people__user_id__in")
|
album_id: int | None = Field(None, q="parent_id")
|
||||||
] = None
|
|
||||||
album_id: Annotated[int | None, FilterLookup("parent_id")] = None
|
|
||||||
|
|
||||||
|
|
||||||
class PictureSchema(ModelSchema):
|
class PictureSchema(ModelSchema):
|
||||||
|
|||||||
Reference in New Issue
Block a user