mirror of
https://github.com/ae-utbm/sith.git
synced 2025-12-15 12:23:21 +00:00
Compare commits
5 Commits
product-fo
...
matmat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a68f16ba9d | ||
|
|
1a99f4096e | ||
|
|
559a904e0d | ||
|
|
fca6a58c5e | ||
|
|
39c3e11d88 |
@@ -35,7 +35,7 @@ TODO : rewrite the pagination used in this template an Alpine one
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
|
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
|
||||||
<p><input type="submit" value="{% trans %}Download as csv{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p>
|
<p><input type="submit" value="{% trans %}Download as cvs{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p>
|
||||||
</form>
|
</form>
|
||||||
<p>
|
<p>
|
||||||
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>
|
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>
|
||||||
|
|||||||
@@ -211,7 +211,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i class="fa-solid fa-magnifying-glass fa-xl"></i>
|
<i class="fa-solid fa-magnifying-glass fa-xl"></i>
|
||||||
<a href="{{ url("matmat:search_clear") }}">{% trans %}Matmatronch{% endtrans %}</a>
|
<a href="{{ url("matmat:search") }}">{% trans %}Matmatronch{% endtrans %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i class="fa-solid fa-check-to-slot fa-xl"></i>
|
<i class="fa-solid fa-check-to-slot fa-xl"></i>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
@@ -8,12 +9,12 @@ from django.urls import reverse
|
|||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from haystack.query import SearchQuerySet
|
from haystack.query import SearchQuerySet
|
||||||
from ninja import FilterSchema, ModelSchema, Schema, UploadedFile
|
from ninja import FilterLookup, FilterSchema, ModelSchema, Schema, UploadedFile
|
||||||
from pydantic import AliasChoices, Field
|
from pydantic import AliasChoices, Field, field_validator
|
||||||
from pydantic_core.core_schema import ValidationInfo
|
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 get_last_promo, is_image
|
||||||
|
|
||||||
NonEmptyStr = Annotated[str, MinLen(1)]
|
NonEmptyStr = Annotated[str, MinLen(1)]
|
||||||
|
|
||||||
@@ -109,7 +110,11 @@ class GroupSchema(ModelSchema):
|
|||||||
|
|
||||||
|
|
||||||
class UserFilterSchema(FilterSchema):
|
class UserFilterSchema(FilterSchema):
|
||||||
search: Annotated[str, MinLen(1)]
|
search: Annotated[str, MinLen(1)] | None = None
|
||||||
|
role: Annotated[str, FilterLookup("role__icontains")] | None = None
|
||||||
|
department: str | None = None
|
||||||
|
promo: int | None = None
|
||||||
|
date_of_birth: datetime | None = None
|
||||||
exclude: list[int] | None = Field(
|
exclude: list[int] | None = Field(
|
||||||
None, validation_alias=AliasChoices("exclude", "exclude[]")
|
None, validation_alias=AliasChoices("exclude", "exclude[]")
|
||||||
)
|
)
|
||||||
@@ -138,6 +143,13 @@ class UserFilterSchema(FilterSchema):
|
|||||||
return Q()
|
return Q()
|
||||||
return ~Q(id__in=value)
|
return ~Q(id__in=value)
|
||||||
|
|
||||||
|
@field_validator("promo", mode="after")
|
||||||
|
@classmethod
|
||||||
|
def validate_promo(cls, value: int) -> int:
|
||||||
|
if not 0 < value <= get_last_promo():
|
||||||
|
raise ValueError(f"{value} is not a valid promo")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class MarkdownSchema(Schema):
|
class MarkdownSchema(Schema):
|
||||||
text: str
|
text: str
|
||||||
|
|||||||
@@ -143,6 +143,15 @@ form {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.fields-centered {
|
||||||
|
padding: 10px 10px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--nf-input-size) 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.helptext {
|
.helptext {
|
||||||
margin-top: .25rem;
|
margin-top: .25rem;
|
||||||
margin-bottom: .25rem;
|
margin-bottom: .25rem;
|
||||||
|
|||||||
@@ -114,15 +114,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-fields {
|
|
||||||
padding: 10px 10px 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--nf-input-size) 10px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-field {
|
&-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<details name="navbar" class="menu">
|
<details name="navbar" class="menu">
|
||||||
<summary class="head">{% trans %}Services{% endtrans %}</summary>
|
<summary class="head">{% trans %}Services{% endtrans %}</summary>
|
||||||
<ul class="content">
|
<ul class="content">
|
||||||
<li><a href="{{ url('matmat:search_clear') }}">{% trans %}Matmatronch{% endtrans %}</a></li>
|
<li><a href="{{ url('matmat:search') }}">{% trans %}Matmatronch{% endtrans %}</a></li>
|
||||||
<li><a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a></li>
|
<li><a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a></li>
|
||||||
<li><a href="{{ url('pedagogy:guide') }}">{% trans %}Pedagogy{% endtrans %}</a></li>
|
<li><a href="{{ url('pedagogy:guide') }}">{% trans %}Pedagogy{% endtrans %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -157,12 +157,12 @@
|
|||||||
{% if current_page.has_previous() %}
|
{% if current_page.has_previous() %}
|
||||||
<a
|
<a
|
||||||
{% if use_htmx -%}
|
{% if use_htmx -%}
|
||||||
hx-get="?page={{ current_page.previous_page_number() }}"
|
hx-get="?{{ querystring(page=current_page.previous_page_number()) }}"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-target="#content"
|
hx-target="#content"
|
||||||
hx-push-url="true"
|
hx-push-url="true"
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
href="?page={{ current_page.previous_page_number() }}"
|
href="?{{ querystring(page=current_page.previous_page_number()) }}"
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
>
|
>
|
||||||
<button>
|
<button>
|
||||||
@@ -180,12 +180,12 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<a
|
<a
|
||||||
{% if use_htmx -%}
|
{% if use_htmx -%}
|
||||||
hx-get="?page={{ i }}"
|
hx-get="?{{ querystring(page=i) }}"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-target="#content"
|
hx-target="#content"
|
||||||
hx-push-url="true"
|
hx-push-url="true"
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
href="?page={{ i }}"
|
href="?{{ querystring(page=i) }}"
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
>
|
>
|
||||||
<button>{{ i }}</button>
|
<button>{{ i }}</button>
|
||||||
@@ -195,12 +195,12 @@
|
|||||||
{% if current_page.has_next() %}
|
{% if current_page.has_next() %}
|
||||||
<a
|
<a
|
||||||
{% if use_htmx -%}
|
{% if use_htmx -%}
|
||||||
hx-get="?page={{ current_page.next_page_number() }}"
|
hx-get="?querystring(page=current_page.next_page_number())"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-target="#content"
|
hx-target="#content"
|
||||||
hx-push-url="true"
|
hx-push-url="true"
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
href="?page={{ current_page.next_page_number() }}"
|
href="?querystring(page=current_page.next_page_number())"
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
><button>
|
><button>
|
||||||
<i class="fa fa-caret-right"></i>
|
<i class="fa fa-caret-right"></i>
|
||||||
@@ -249,3 +249,17 @@
|
|||||||
}"></div>
|
}"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro querystring() %}
|
||||||
|
{%- for key, values in request.GET.lists() -%}
|
||||||
|
{%- if key not in kwargs -%}
|
||||||
|
{%- for value in values -%}
|
||||||
|
{{ key }}={{ value }}&
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- for key, value in kwargs.items() -%}
|
||||||
|
{{ key }}={{ value }}&
|
||||||
|
{%- endfor -%}
|
||||||
|
{% endmacro %}
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{# All fields #}
|
{# All fields #}
|
||||||
<div class="profile-fields">
|
<div class="fields-centered">
|
||||||
{%- for field in form -%}
|
{%- for field in form -%}
|
||||||
{%- if field.name in ["quote","profile_pict","avatar_pict","scrub_pict","is_viewable","forum_signature"] -%}
|
{%- if field.name in ["quote","profile_pict","avatar_pict","scrub_pict","is_viewable","forum_signature"] -%}
|
||||||
{%- continue -%}
|
{%- continue -%}
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Textareas #}
|
{# Textareas #}
|
||||||
<div class="profile-fields">
|
<div class="fields-centered">
|
||||||
{%- for field in [form.quote, form.forum_signature] -%}
|
{%- for field in [form.quote, form.forum_signature] -%}
|
||||||
<div class="profile-field">
|
<div class="profile-field">
|
||||||
{{ field.label_tag() }}
|
{{ field.label_tag() }}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from datetime import date, datetime, timezone
|
|||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import MaxValueValidator
|
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.forms import BaseModelFormSet
|
from django.forms import BaseModelFormSet
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
@@ -35,7 +34,6 @@ from counter.models import (
|
|||||||
Eticket,
|
Eticket,
|
||||||
InvoiceCall,
|
InvoiceCall,
|
||||||
Product,
|
Product,
|
||||||
ProductFormula,
|
|
||||||
Refilling,
|
Refilling,
|
||||||
ReturnableProduct,
|
ReturnableProduct,
|
||||||
ScheduledProductAction,
|
ScheduledProductAction,
|
||||||
@@ -318,6 +316,7 @@ class ProductForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
counters = forms.ModelMultipleChoiceField(
|
counters = forms.ModelMultipleChoiceField(
|
||||||
|
help_text=None,
|
||||||
label=_("Counters"),
|
label=_("Counters"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=AutoCompleteSelectMultipleCounter,
|
widget=AutoCompleteSelectMultipleCounter,
|
||||||
@@ -328,31 +327,10 @@ class ProductForm(forms.ModelForm):
|
|||||||
super().__init__(*args, instance=instance, **kwargs)
|
super().__init__(*args, instance=instance, **kwargs)
|
||||||
if self.instance.id:
|
if self.instance.id:
|
||||||
self.fields["counters"].initial = self.instance.counters.all()
|
self.fields["counters"].initial = self.instance.counters.all()
|
||||||
if hasattr(self.instance, "formula"):
|
|
||||||
self.formula_init(self.instance.formula)
|
|
||||||
self.action_formset = ScheduledProductActionFormSet(
|
self.action_formset = ScheduledProductActionFormSet(
|
||||||
*args, product=self.instance, **kwargs
|
*args, product=self.instance, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def formula_init(self, formula: ProductFormula):
|
|
||||||
"""Part of the form initialisation specific to formula products."""
|
|
||||||
self.fields["selling_price"].help_text = _(
|
|
||||||
"This product is a formula. "
|
|
||||||
"Its price cannot be greater than the price "
|
|
||||||
"of the products constituting it, which is %(price)s €"
|
|
||||||
) % {"price": formula.max_selling_price}
|
|
||||||
self.fields["special_selling_price"].help_text = _(
|
|
||||||
"This product is a formula. "
|
|
||||||
"Its special price cannot be greater than the price "
|
|
||||||
"of the products constituting it, which is %(price)s €"
|
|
||||||
) % {"price": formula.max_special_selling_price}
|
|
||||||
for key, price in (
|
|
||||||
("selling_price", formula.max_selling_price),
|
|
||||||
("special_selling_price", formula.max_special_selling_price),
|
|
||||||
):
|
|
||||||
self.fields[key].widget.attrs["max"] = price
|
|
||||||
self.fields[key].validators.append(MaxValueValidator(price))
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
@@ -371,47 +349,13 @@ class ProductForm(forms.ModelForm):
|
|||||||
return product
|
return product
|
||||||
|
|
||||||
|
|
||||||
class ProductFormulaForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = ProductFormula
|
|
||||||
fields = ["products", "result"]
|
|
||||||
widgets = {
|
|
||||||
"products": AutoCompleteSelectMultipleProduct,
|
|
||||||
"result": AutoCompleteSelectProduct,
|
|
||||||
}
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
cleaned_data = super().clean()
|
|
||||||
if cleaned_data["result"] in cleaned_data["products"]:
|
|
||||||
self.add_error(
|
|
||||||
None,
|
|
||||||
_(
|
|
||||||
"The same product cannot be at the same time "
|
|
||||||
"the result and a part of the formula."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
prices = [p.selling_price for p in cleaned_data["products"]]
|
|
||||||
special_prices = [p.special_selling_price for p in cleaned_data["products"]]
|
|
||||||
selling_price = cleaned_data["result"].selling_price
|
|
||||||
special_selling_price = cleaned_data["result"].special_selling_price
|
|
||||||
if selling_price > sum(prices) or special_selling_price > sum(special_prices):
|
|
||||||
self.add_error(
|
|
||||||
"result",
|
|
||||||
_(
|
|
||||||
"The result cannot be more expensive "
|
|
||||||
"than the total of the other products."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return cleaned_data
|
|
||||||
|
|
||||||
|
|
||||||
class ReturnableProductForm(forms.ModelForm):
|
class ReturnableProductForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ReturnableProduct
|
model = ReturnableProduct
|
||||||
fields = ["product", "returned_product", "max_return"]
|
fields = ["product", "returned_product", "max_return"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"product": AutoCompleteSelectProduct,
|
"product": AutoCompleteSelectProduct(),
|
||||||
"returned_product": AutoCompleteSelectProduct,
|
"returned_product": AutoCompleteSelectProduct(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def save(self, commit: bool = True) -> ReturnableProduct: # noqa FBT
|
def save(self, commit: bool = True) -> ReturnableProduct: # noqa FBT
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# Generated by Django 5.2.8 on 2025-11-26 11:34
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [("counter", "0035_remove_selling_is_validated_and_more")]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="ProductFormula",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"products",
|
|
||||||
models.ManyToManyField(
|
|
||||||
help_text="The products that constitute this formula.",
|
|
||||||
related_name="formulas",
|
|
||||||
to="counter.product",
|
|
||||||
verbose_name="products",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"result",
|
|
||||||
models.OneToOneField(
|
|
||||||
help_text="The formula product.",
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="counter.product",
|
|
||||||
verbose_name="result product",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -455,37 +455,6 @@ class Product(models.Model):
|
|||||||
return self.selling_price - self.purchase_price
|
return self.selling_price - self.purchase_price
|
||||||
|
|
||||||
|
|
||||||
class ProductFormula(models.Model):
|
|
||||||
products = models.ManyToManyField(
|
|
||||||
Product,
|
|
||||||
related_name="formulas",
|
|
||||||
verbose_name=_("products"),
|
|
||||||
help_text=_("The products that constitute this formula."),
|
|
||||||
)
|
|
||||||
result = models.OneToOneField(
|
|
||||||
Product,
|
|
||||||
related_name="formula",
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_("result product"),
|
|
||||||
help_text=_("The product got with the formula."),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.result.name
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def max_selling_price(self) -> float:
|
|
||||||
# iterating over all products is less efficient than doing
|
|
||||||
# a simple aggregation, but this method is likely to be used in
|
|
||||||
# coordination with `max_special_selling_price`,
|
|
||||||
# and Django caches the result of the `all` queryset.
|
|
||||||
return sum(p.selling_price for p in self.products.all())
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def max_special_selling_price(self) -> float:
|
|
||||||
return sum(p.special_selling_price for p in self.products.all())
|
|
||||||
|
|
||||||
|
|
||||||
class CounterQuerySet(models.QuerySet):
|
class CounterQuerySet(models.QuerySet):
|
||||||
def annotate_has_barman(self, user: User) -> Self:
|
def annotate_has_barman(self, user: User) -> Self:
|
||||||
"""Annotate the queryset with the `user_is_barman` field.
|
"""Annotate the queryset with the `user_is_barman` field.
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { AlertMessage } from "#core:utils/alert-message";
|
import { AlertMessage } from "#core:utils/alert-message";
|
||||||
import { BasketItem } from "#counter:counter/basket";
|
import { BasketItem } from "#counter:counter/basket";
|
||||||
import type {
|
import type { CounterConfig, ErrorMessage } from "#counter:counter/types";
|
||||||
CounterConfig,
|
|
||||||
ErrorMessage,
|
|
||||||
ProductFormula,
|
|
||||||
} from "#counter:counter/types";
|
|
||||||
import type { CounterProductSelect } from "./components/counter-product-select-index.ts";
|
import type { CounterProductSelect } from "./components/counter-product-select-index.ts";
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
@@ -51,43 +47,15 @@ document.addEventListener("alpine:init", () => {
|
|||||||
|
|
||||||
this.basket[id] = item;
|
this.basket[id] = item;
|
||||||
|
|
||||||
this.checkFormulas();
|
|
||||||
|
|
||||||
if (this.sumBasket() > this.customerBalance) {
|
if (this.sumBasket() > this.customerBalance) {
|
||||||
item.quantity = oldQty;
|
item.quantity = oldQty;
|
||||||
if (item.quantity === 0) {
|
if (item.quantity === 0) {
|
||||||
delete this.basket[id];
|
delete this.basket[id];
|
||||||
}
|
}
|
||||||
this.alertMessage.display(gettext("Not enough money"), { success: false });
|
return gettext("Not enough money");
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
checkFormulas() {
|
return "";
|
||||||
const products = new Set(
|
|
||||||
Object.keys(this.basket).map((i: string) => Number.parseInt(i)),
|
|
||||||
);
|
|
||||||
const formula: ProductFormula = config.formulas.find((f: ProductFormula) => {
|
|
||||||
return f.products.every((p: number) => products.has(p));
|
|
||||||
});
|
|
||||||
if (formula === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const product of formula.products) {
|
|
||||||
const key = product.toString();
|
|
||||||
this.basket[key].quantity -= 1;
|
|
||||||
if (this.basket[key].quantity <= 0) {
|
|
||||||
this.removeFromBasket(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.alertMessage.display(
|
|
||||||
interpolate(
|
|
||||||
gettext("Formula %(formula)s applied"),
|
|
||||||
{ formula: config.products[formula.result.toString()].name },
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
{ success: true },
|
|
||||||
);
|
|
||||||
this.addToBasket(formula.result.toString(), 1);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getBasketSize() {
|
getBasketSize() {
|
||||||
@@ -102,7 +70,14 @@ document.addEventListener("alpine:init", () => {
|
|||||||
(acc: number, cur: BasketItem) => acc + cur.sum(),
|
(acc: number, cur: BasketItem) => acc + cur.sum(),
|
||||||
0,
|
0,
|
||||||
) as number;
|
) as number;
|
||||||
return Math.round(total * 100) / 100;
|
return total;
|
||||||
|
},
|
||||||
|
|
||||||
|
addToBasketWithMessage(id: string, quantity: number) {
|
||||||
|
const message = this.addToBasket(id, quantity);
|
||||||
|
if (message.length > 0) {
|
||||||
|
this.alertMessage.display(message, { success: false });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRefillingSuccess(event: CustomEvent) {
|
onRefillingSuccess(event: CustomEvent) {
|
||||||
@@ -141,7 +116,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.finish();
|
this.finish();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.addToBasket(code, quantity);
|
this.addToBasketWithMessage(code, quantity);
|
||||||
}
|
}
|
||||||
this.codeField.widget.clear();
|
this.codeField.widget.clear();
|
||||||
this.codeField.widget.focus();
|
this.codeField.widget.focus();
|
||||||
|
|||||||
6
counter/static/bundled/counter/types.d.ts
vendored
6
counter/static/bundled/counter/types.d.ts
vendored
@@ -7,16 +7,10 @@ export interface InitialFormData {
|
|||||||
errors?: string[];
|
errors?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductFormula {
|
|
||||||
result: number;
|
|
||||||
products: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CounterConfig {
|
export interface CounterConfig {
|
||||||
customerBalance: number;
|
customerBalance: number;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
products: Record<string, Product>;
|
products: Record<string, Product>;
|
||||||
formulas: ProductFormula[];
|
|
||||||
formInitial: InitialFormData[];
|
formInitial: InitialFormData[];
|
||||||
cancelUrl: string;
|
cancelUrl: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basket-message-container {
|
.basket-error-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block
|
display: block
|
||||||
}
|
}
|
||||||
|
|
||||||
.basket-message {
|
.basket-error {
|
||||||
z-index: 10; // to get on top of tomselect
|
z-index: 10; // to get on top of tomselect
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -32,11 +32,13 @@
|
|||||||
<div id="bar-ui" x-data="counter({
|
<div id="bar-ui" x-data="counter({
|
||||||
customerBalance: {{ customer.amount }},
|
customerBalance: {{ customer.amount }},
|
||||||
products: products,
|
products: products,
|
||||||
formulas: formulas,
|
|
||||||
customerId: {{ customer.pk }},
|
customerId: {{ customer.pk }},
|
||||||
formInitial: formInitial,
|
formInitial: formInitial,
|
||||||
cancelUrl: '{{ cancel_url }}',
|
cancelUrl: '{{ cancel_url }}',
|
||||||
})">
|
})">
|
||||||
|
<noscript>
|
||||||
|
<p class="important">Javascript is required for the counter UI.</p>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
<div id="user_info">
|
<div id="user_info">
|
||||||
<h5>{% trans %}Customer{% endtrans %}</h5>
|
<h5>{% trans %}Customer{% endtrans %}</h5>
|
||||||
@@ -86,12 +88,11 @@
|
|||||||
|
|
||||||
<form x-cloak method="post" action="" x-ref="basketForm">
|
<form x-cloak method="post" action="" x-ref="basketForm">
|
||||||
|
|
||||||
<div class="basket-message-container">
|
<div class="basket-error-container">
|
||||||
<div
|
<div
|
||||||
x-cloak
|
x-cloak
|
||||||
class="alert basket-message"
|
class="alert alert-red basket-error"
|
||||||
:class="alertMessage.success ? 'alert-green' : 'alert-red'"
|
x-show="alertMessage.show"
|
||||||
x-show="alertMessage.open"
|
|
||||||
x-transition.duration.500ms
|
x-transition.duration.500ms
|
||||||
x-text="alertMessage.content"
|
x-text="alertMessage.content"
|
||||||
></div>
|
></div>
|
||||||
@@ -110,9 +111,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<button @click.prevent="addToBasket(item.product.id, -1)">-</button>
|
<button @click.prevent="addToBasketWithMessage(item.product.id, -1)">-</button>
|
||||||
<span class="quantity" x-text="item.quantity"></span>
|
<span class="quantity" x-text="item.quantity"></span>
|
||||||
<button @click.prevent="addToBasket(item.product.id, 1)">+</button>
|
<button @click.prevent="addToBasketWithMessage(item.product.id, 1)">+</button>
|
||||||
|
|
||||||
<span x-text="item.product.name"></span> :
|
<span x-text="item.product.name"></span> :
|
||||||
<span x-text="item.sum().toLocaleString(undefined, { minimumFractionDigits: 2 })">€</span>
|
<span x-text="item.sum().toLocaleString(undefined, { minimumFractionDigits: 2 })">€</span>
|
||||||
@@ -212,7 +213,7 @@
|
|||||||
<h5 class="margin-bottom">{{ category }}</h5>
|
<h5 class="margin-bottom">{{ category }}</h5>
|
||||||
<div class="row gap-2x">
|
<div class="row gap-2x">
|
||||||
{% for product in categories[category] -%}
|
{% for product in categories[category] -%}
|
||||||
<button class="card shadow" @click="addToBasket('{{ product.id }}', 1)">
|
<button class="card shadow" @click="addToBasketWithMessage('{{ product.id }}', 1)">
|
||||||
<img
|
<img
|
||||||
class="card-image"
|
class="card-image"
|
||||||
alt="image de {{ product.name }}"
|
alt="image de {{ product.name }}"
|
||||||
@@ -251,18 +252,6 @@
|
|||||||
},
|
},
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
};
|
};
|
||||||
const formulas = [
|
|
||||||
{%- for formula in formulas -%}
|
|
||||||
{
|
|
||||||
result: {{ formula.result_id }},
|
|
||||||
products: [
|
|
||||||
{%- for product in formula.products.all() -%}
|
|
||||||
{{ product.id }},
|
|
||||||
{%- endfor -%}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{%- endfor -%}
|
|
||||||
];
|
|
||||||
const formInitial = [
|
const formInitial = [
|
||||||
{%- for f in form -%}
|
{%- for f in form -%}
|
||||||
{%- if f.cleaned_data -%}
|
{%- if f.cleaned_data -%}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
{% extends "core/base.jinja" %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
{% trans %}Product formulas{% endtrans %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block additional_css %}
|
|
||||||
<link rel="stylesheet" href="{{ static("core/components/card.scss") }}">
|
|
||||||
<link rel="stylesheet" href="{{ static("counter/css/admin.scss") }}">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<main>
|
|
||||||
<h3 class="margin-bottom">{% trans %}Product formulas{% endtrans %}</h3>
|
|
||||||
<p>
|
|
||||||
<a href="{{ url('counter:product_formula_create') }}" class="btn btn-blue">
|
|
||||||
{% trans %}New formula{% endtrans %}
|
|
||||||
<i class="fa fa-plus"></i>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<ul class="product-group">
|
|
||||||
{%- for formula in object_list -%}
|
|
||||||
<li>
|
|
||||||
<a href="{{ url('counter:product_formula_edit', formula_id=formula.id) }}">
|
|
||||||
{{ formula.result.name }}
|
|
||||||
</a>
|
|
||||||
<a href="{{ url('counter:product_formula_delete', formula_id=formula.id) }}">
|
|
||||||
<i class="fa fa-trash delete-action"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{%- endfor -%}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
:disabled="csvLoading"
|
:disabled="csvLoading"
|
||||||
:aria-busy="csvLoading"
|
:aria-busy="csvLoading"
|
||||||
>
|
>
|
||||||
{% trans %}Download as csv{% endtrans %} <i class="fa fa-file-arrow-down"></i>
|
{% trans %}Download as cvs{% endtrans %} <i class="fa fa-file-arrow-down"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from counter.baker_recipes import product_recipe
|
|
||||||
from counter.forms import ProductFormulaForm
|
|
||||||
|
|
||||||
|
|
||||||
class TestFormulaForm(TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
cls.products = product_recipe.make(
|
|
||||||
selling_price=iter([1.5, 1, 1]),
|
|
||||||
special_selling_price=iter([1.4, 0.9, 0.9]),
|
|
||||||
_quantity=3,
|
|
||||||
_bulk_create=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_ok(self):
|
|
||||||
form = ProductFormulaForm(
|
|
||||||
data={
|
|
||||||
"result": self.products[0].id,
|
|
||||||
"products": [self.products[1].id, self.products[2].id],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert form.is_valid()
|
|
||||||
formula = form.save()
|
|
||||||
assert formula.result == self.products[0]
|
|
||||||
assert set(formula.products.all()) == set(self.products[1:])
|
|
||||||
|
|
||||||
def test_price_invalid(self):
|
|
||||||
self.products[0].selling_price = 2.1
|
|
||||||
self.products[0].save()
|
|
||||||
form = ProductFormulaForm(
|
|
||||||
data={
|
|
||||||
"result": self.products[0].id,
|
|
||||||
"products": [self.products[1].id, self.products[2].id],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert not form.is_valid()
|
|
||||||
assert form.errors == {
|
|
||||||
"result": [
|
|
||||||
"Le résultat ne peut pas être plus cher "
|
|
||||||
"que le total des autres produits."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_product_both_in_result_and_products(self):
|
|
||||||
form = ProductFormulaForm(
|
|
||||||
data={
|
|
||||||
"result": self.products[0].id,
|
|
||||||
"products": [self.products[0].id, self.products[1].id],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert not form.is_valid()
|
|
||||||
assert form.errors == {
|
|
||||||
"__all__": [
|
|
||||||
"Un même produit ne peut pas être à la fois "
|
|
||||||
"le résultat et un élément de la formule."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,8 @@ from pytest_django.asserts import assertNumQueries, assertRedirects
|
|||||||
from club.models import Club
|
from club.models import Club
|
||||||
from core.baker_recipes import board_user, subscriber_user
|
from core.baker_recipes import board_user, subscriber_user
|
||||||
from core.models import Group, User
|
from core.models import Group, User
|
||||||
from counter.baker_recipes import product_recipe
|
|
||||||
from counter.forms import ProductForm
|
from counter.forms import ProductForm
|
||||||
from counter.models import Product, ProductFormula, ProductType
|
from counter.models import Product, ProductType
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -94,9 +93,6 @@ class TestCreateProduct(TestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.product_type = baker.make(ProductType)
|
cls.product_type = baker.make(ProductType)
|
||||||
cls.club = baker.make(Club)
|
cls.club = baker.make(Club)
|
||||||
cls.counter_admin = baker.make(
|
|
||||||
User, groups=[Group.objects.get(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)]
|
|
||||||
)
|
|
||||||
cls.data = {
|
cls.data = {
|
||||||
"name": "foo",
|
"name": "foo",
|
||||||
"description": "bar",
|
"description": "bar",
|
||||||
@@ -120,36 +116,13 @@ class TestCreateProduct(TestCase):
|
|||||||
assert instance.name == "foo"
|
assert instance.name == "foo"
|
||||||
assert instance.selling_price == 1.0
|
assert instance.selling_price == 1.0
|
||||||
|
|
||||||
def test_form_with_product_from_formula(self):
|
|
||||||
"""Test when the edited product is a result of a formula."""
|
|
||||||
self.client.force_login(self.counter_admin)
|
|
||||||
products = product_recipe.make(
|
|
||||||
selling_price=iter([1.5, 1, 1]),
|
|
||||||
special_selling_price=iter([1.4, 0.9, 0.9]),
|
|
||||||
_quantity=3,
|
|
||||||
_bulk_create=True,
|
|
||||||
)
|
|
||||||
baker.make(ProductFormula, result=products[0], products=products[1:])
|
|
||||||
|
|
||||||
data = self.data | {"selling_price": 1.7, "special_selling_price": 1.5}
|
|
||||||
form = ProductForm(data=data, instance=products[0])
|
|
||||||
assert form.is_valid()
|
|
||||||
|
|
||||||
# it shouldn't be possible to give a price higher than the formula's products
|
|
||||||
data = self.data | {"selling_price": 2.1, "special_selling_price": 1.9}
|
|
||||||
form = ProductForm(data=data, instance=products[0])
|
|
||||||
assert not form.is_valid()
|
|
||||||
assert form.errors == {
|
|
||||||
"selling_price": [
|
|
||||||
"Assurez-vous que cette valeur est inférieure ou égale à 2.00."
|
|
||||||
],
|
|
||||||
"special_selling_price": [
|
|
||||||
"Assurez-vous que cette valeur est inférieure ou égale à 1.80."
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_view(self):
|
def test_view(self):
|
||||||
self.client.force_login(self.counter_admin)
|
self.client.force_login(
|
||||||
|
baker.make(
|
||||||
|
User,
|
||||||
|
groups=[Group.objects.get(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
|
||||||
|
)
|
||||||
|
)
|
||||||
url = reverse("counter:new_product")
|
url = reverse("counter:new_product")
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ from counter.views.admin import (
|
|||||||
CounterStatView,
|
CounterStatView,
|
||||||
ProductCreateView,
|
ProductCreateView,
|
||||||
ProductEditView,
|
ProductEditView,
|
||||||
ProductFormulaCreateView,
|
|
||||||
ProductFormulaDeleteView,
|
|
||||||
ProductFormulaEditView,
|
|
||||||
ProductFormulaListView,
|
|
||||||
ProductListView,
|
ProductListView,
|
||||||
ProductTypeCreateView,
|
ProductTypeCreateView,
|
||||||
ProductTypeEditView,
|
ProductTypeEditView,
|
||||||
@@ -120,24 +116,6 @@ urlpatterns = [
|
|||||||
ProductEditView.as_view(),
|
ProductEditView.as_view(),
|
||||||
name="product_edit",
|
name="product_edit",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"admin/formula/", ProductFormulaListView.as_view(), name="product_formula_list"
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"admin/formula/new/",
|
|
||||||
ProductFormulaCreateView.as_view(),
|
|
||||||
name="product_formula_create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"admin/formula/<int:formula_id>/edit",
|
|
||||||
ProductFormulaEditView.as_view(),
|
|
||||||
name="product_formula_edit",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"admin/formula/<int:formula_id>/delete",
|
|
||||||
ProductFormulaDeleteView.as_view(),
|
|
||||||
name="product_formula_delete",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"admin/product-type/list/",
|
"admin/product-type/list/",
|
||||||
ProductTypeListView.as_view(),
|
ProductTypeListView.as_view(),
|
||||||
|
|||||||
@@ -34,13 +34,11 @@ from counter.forms import (
|
|||||||
CloseCustomerAccountForm,
|
CloseCustomerAccountForm,
|
||||||
CounterEditForm,
|
CounterEditForm,
|
||||||
ProductForm,
|
ProductForm,
|
||||||
ProductFormulaForm,
|
|
||||||
ReturnableProductForm,
|
ReturnableProductForm,
|
||||||
)
|
)
|
||||||
from counter.models import (
|
from counter.models import (
|
||||||
Counter,
|
Counter,
|
||||||
Product,
|
Product,
|
||||||
ProductFormula,
|
|
||||||
ProductType,
|
ProductType,
|
||||||
Refilling,
|
Refilling,
|
||||||
ReturnableProduct,
|
ReturnableProduct,
|
||||||
@@ -164,49 +162,6 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
|||||||
current_tab = "products"
|
current_tab = "products"
|
||||||
|
|
||||||
|
|
||||||
class ProductFormulaListView(CounterAdminTabsMixin, PermissionRequiredMixin, ListView):
|
|
||||||
model = ProductFormula
|
|
||||||
queryset = ProductFormula.objects.select_related("result")
|
|
||||||
template_name = "counter/formula_list.jinja"
|
|
||||||
current_tab = "formulas"
|
|
||||||
permission_required = "counter.view_productformula"
|
|
||||||
|
|
||||||
|
|
||||||
class ProductFormulaCreateView(
|
|
||||||
CounterAdminTabsMixin, PermissionRequiredMixin, CreateView
|
|
||||||
):
|
|
||||||
model = ProductFormula
|
|
||||||
form_class = ProductFormulaForm
|
|
||||||
pk_url_kwarg = "formula_id"
|
|
||||||
template_name = "core/create.jinja"
|
|
||||||
current_tab = "formulas"
|
|
||||||
success_url = reverse_lazy("counter:product_formula_list")
|
|
||||||
permission_required = "counter.add_productformula"
|
|
||||||
|
|
||||||
|
|
||||||
class ProductFormulaEditView(
|
|
||||||
CounterAdminTabsMixin, PermissionRequiredMixin, UpdateView
|
|
||||||
):
|
|
||||||
model = ProductFormula
|
|
||||||
form_class = ProductFormulaForm
|
|
||||||
pk_url_kwarg = "formula_id"
|
|
||||||
template_name = "core/edit.jinja"
|
|
||||||
current_tab = "formulas"
|
|
||||||
success_url = reverse_lazy("counter:product_formula_list")
|
|
||||||
permission_required = "counter.change_productformula"
|
|
||||||
|
|
||||||
|
|
||||||
class ProductFormulaDeleteView(
|
|
||||||
CounterAdminTabsMixin, PermissionRequiredMixin, DeleteView
|
|
||||||
):
|
|
||||||
model = ProductFormula
|
|
||||||
pk_url_kwarg = "formula_id"
|
|
||||||
template_name = "core/delete_confirm.jinja"
|
|
||||||
current_tab = "formulas"
|
|
||||||
success_url = reverse_lazy("counter:product_formula_list")
|
|
||||||
permission_required = "counter.delete_productformula"
|
|
||||||
|
|
||||||
|
|
||||||
class ReturnableProductListView(
|
class ReturnableProductListView(
|
||||||
CounterAdminTabsMixin, PermissionRequiredMixin, ListView
|
CounterAdminTabsMixin, PermissionRequiredMixin, ListView
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@@ -32,7 +31,6 @@ from counter.forms import BasketForm, RefillForm
|
|||||||
from counter.models import (
|
from counter.models import (
|
||||||
Counter,
|
Counter,
|
||||||
Customer,
|
Customer,
|
||||||
ProductFormula,
|
|
||||||
ReturnableProduct,
|
ReturnableProduct,
|
||||||
Selling,
|
Selling,
|
||||||
)
|
)
|
||||||
@@ -208,13 +206,12 @@ class CounterClick(
|
|||||||
"""Add customer to the context."""
|
"""Add customer to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["products"] = self.products
|
kwargs["products"] = self.products
|
||||||
kwargs["formulas"] = ProductFormula.objects.filter(
|
kwargs["categories"] = {}
|
||||||
result__in=self.products
|
|
||||||
).prefetch_related("products")
|
|
||||||
kwargs["categories"] = defaultdict(list)
|
|
||||||
for product in kwargs["products"]:
|
for product in kwargs["products"]:
|
||||||
if product.product_type:
|
if product.product_type:
|
||||||
kwargs["categories"][product.product_type].append(product)
|
kwargs["categories"].setdefault(product.product_type, []).append(
|
||||||
|
product
|
||||||
|
)
|
||||||
kwargs["customer"] = self.customer
|
kwargs["customer"] = self.customer
|
||||||
kwargs["cancel_url"] = self.get_success_url()
|
kwargs["cancel_url"] = self.get_success_url()
|
||||||
|
|
||||||
|
|||||||
@@ -100,11 +100,6 @@ class CounterAdminTabsMixin(TabedViewMixin):
|
|||||||
"slug": "products",
|
"slug": "products",
|
||||||
"name": _("Products"),
|
"name": _("Products"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"url": reverse_lazy("counter:product_formula_list"),
|
|
||||||
"slug": "formulas",
|
|
||||||
"name": _("Formulas"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": reverse_lazy("counter:product_type_list"),
|
"url": reverse_lazy("counter:product_type_list"),
|
||||||
"slug": "product_types",
|
"slug": "product_types",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-27 14:22+0100\n"
|
"POT-Creation-Date: 2025-11-30 18:23+0100\n"
|
||||||
"PO-Revision-Date: 2016-07-18\n"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@@ -388,7 +388,7 @@ msgstr "Montrer"
|
|||||||
|
|
||||||
#: club/templates/club/club_sellings.jinja
|
#: club/templates/club/club_sellings.jinja
|
||||||
#: counter/templates/counter/product_list.jinja
|
#: counter/templates/counter/product_list.jinja
|
||||||
msgid "Download as csv"
|
msgid "Download as cvs"
|
||||||
msgstr "Télécharger en CSV"
|
msgstr "Télécharger en CSV"
|
||||||
|
|
||||||
#: club/templates/club/club_sellings.jinja
|
#: club/templates/club/club_sellings.jinja
|
||||||
@@ -2960,38 +2960,6 @@ msgstr ""
|
|||||||
"Décrivez le produit. Si c'est un click pour un évènement, donnez quelques "
|
"Décrivez le produit. Si c'est un click pour un évènement, donnez quelques "
|
||||||
"détails dessus, comme la date (en incluant l'année)."
|
"détails dessus, comme la date (en incluant l'année)."
|
||||||
|
|
||||||
#: counter/forms.py
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"This product is a formula. Its price cannot be greater than the price of the "
|
|
||||||
"products constituting it, which is %(price)s €"
|
|
||||||
msgstr ""
|
|
||||||
"Ce produit est une formule. Son prix ne peut pas être supérieur au prix des "
|
|
||||||
"produits qui la constituent, soit %(price)s €."
|
|
||||||
|
|
||||||
#: counter/forms.py
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"This product is a formula. Its special price cannot be greater than the "
|
|
||||||
"price of the products constituting it, which is %(price)s €"
|
|
||||||
msgstr ""
|
|
||||||
"Ce produit est une formule. Son prix spécial ne peut pas être supérieur au "
|
|
||||||
"prix des produits qui la constituent, soit %(price)s €."
|
|
||||||
|
|
||||||
#: counter/forms.py
|
|
||||||
msgid ""
|
|
||||||
"The same product cannot be at the same time the result and a part of the "
|
|
||||||
"formula."
|
|
||||||
msgstr ""
|
|
||||||
"Un même produit ne peut pas être à la fois le résultat et un élément de la "
|
|
||||||
"formule."
|
|
||||||
|
|
||||||
#: counter/forms.py
|
|
||||||
msgid ""
|
|
||||||
"The result cannot be more expensive than the total of the other products."
|
|
||||||
msgstr ""
|
|
||||||
"Le résultat ne peut pas être plus cher que le total des autres produits."
|
|
||||||
|
|
||||||
#: counter/forms.py
|
#: counter/forms.py
|
||||||
msgid "Refound this account"
|
msgid "Refound this account"
|
||||||
msgstr "Rembourser ce compte"
|
msgstr "Rembourser ce compte"
|
||||||
@@ -3152,18 +3120,6 @@ msgstr "produit"
|
|||||||
msgid "products"
|
msgid "products"
|
||||||
msgstr "produits"
|
msgstr "produits"
|
||||||
|
|
||||||
#: counter/models.py
|
|
||||||
msgid "The products that constitute this formula."
|
|
||||||
msgstr "Les produits qui constituent cette formule."
|
|
||||||
|
|
||||||
#: counter/models.py
|
|
||||||
msgid "result product"
|
|
||||||
msgstr "produit résultat"
|
|
||||||
|
|
||||||
#: counter/models.py
|
|
||||||
msgid "The product got with the formula."
|
|
||||||
msgstr "Le produit obtenu par la formule."
|
|
||||||
|
|
||||||
#: counter/models.py
|
#: counter/models.py
|
||||||
msgid "counter type"
|
msgid "counter type"
|
||||||
msgstr "type de comptoir"
|
msgstr "type de comptoir"
|
||||||
@@ -3584,14 +3540,6 @@ msgstr "Nouveau eticket"
|
|||||||
msgid "There is no eticket in this website."
|
msgid "There is no eticket in this website."
|
||||||
msgstr "Il n'y a pas de eticket sur ce site web."
|
msgstr "Il n'y a pas de eticket sur ce site web."
|
||||||
|
|
||||||
#: counter/templates/counter/formula_list.jinja
|
|
||||||
msgid "Product formulas"
|
|
||||||
msgstr "Formules de produits"
|
|
||||||
|
|
||||||
#: counter/templates/counter/formula_list.jinja
|
|
||||||
msgid "New formula"
|
|
||||||
msgstr "Nouvelle formule"
|
|
||||||
|
|
||||||
#: counter/templates/counter/fragments/create_student_card.jinja
|
#: counter/templates/counter/fragments/create_student_card.jinja
|
||||||
msgid "No student card registered."
|
msgid "No student card registered."
|
||||||
msgstr "Aucune carte étudiante enregistrée."
|
msgstr "Aucune carte étudiante enregistrée."
|
||||||
@@ -3931,10 +3879,6 @@ msgstr "Dernières opérations"
|
|||||||
msgid "Counter administration"
|
msgid "Counter administration"
|
||||||
msgstr "Administration des comptoirs"
|
msgstr "Administration des comptoirs"
|
||||||
|
|
||||||
#: counter/views/mixins.py
|
|
||||||
msgid "Formulas"
|
|
||||||
msgstr "Formules"
|
|
||||||
|
|
||||||
#: counter/views/mixins.py
|
#: counter/views/mixins.py
|
||||||
msgid "Product types"
|
msgid "Product types"
|
||||||
msgstr "Types de produit"
|
msgstr "Types de produit"
|
||||||
@@ -4440,6 +4384,10 @@ msgstr "Galaxie de %(user_name)s"
|
|||||||
msgid "This citizen has not yet joined the galaxy"
|
msgid "This citizen has not yet joined the galaxy"
|
||||||
msgstr "Ce citoyen n'a pas encore rejoint la galaxie"
|
msgstr "Ce citoyen n'a pas encore rejoint la galaxie"
|
||||||
|
|
||||||
|
#: matmat/forms.py
|
||||||
|
msgid "Last/First name or nickname"
|
||||||
|
msgstr "Nom de famille, prénom ou surnom"
|
||||||
|
|
||||||
#: matmat/templates/matmat/search_form.jinja
|
#: matmat/templates/matmat/search_form.jinja
|
||||||
msgid "Search user"
|
msgid "Search user"
|
||||||
msgstr "Rechercher un utilisateur"
|
msgstr "Rechercher un utilisateur"
|
||||||
@@ -4448,22 +4396,6 @@ msgstr "Rechercher un utilisateur"
|
|||||||
msgid "Results"
|
msgid "Results"
|
||||||
msgstr "Résultats"
|
msgstr "Résultats"
|
||||||
|
|
||||||
#: matmat/templates/matmat/search_form.jinja
|
|
||||||
msgid "Search by profile"
|
|
||||||
msgstr "Recherche par profil"
|
|
||||||
|
|
||||||
#: matmat/templates/matmat/search_form.jinja
|
|
||||||
msgid "Inverted search"
|
|
||||||
msgstr "Recherche inversée"
|
|
||||||
|
|
||||||
#: matmat/templates/matmat/search_form.jinja
|
|
||||||
msgid "Quick search"
|
|
||||||
msgstr "Recherche rapide"
|
|
||||||
|
|
||||||
#: matmat/views.py
|
|
||||||
msgid "Last/First name or nickname"
|
|
||||||
msgstr "Nom de famille, prénom ou surnom"
|
|
||||||
|
|
||||||
#: pedagogy/forms.py
|
#: pedagogy/forms.py
|
||||||
msgid "Do not vote"
|
msgid "Do not vote"
|
||||||
msgstr "Ne pas voter"
|
msgstr "Ne pas voter"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-26 15:45+0100\n"
|
"POT-Creation-Date: 2025-08-23 15:30+0200\n"
|
||||||
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
|
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
|
||||||
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@@ -206,10 +206,6 @@ msgstr "capture.%s"
|
|||||||
msgid "Not enough money"
|
msgid "Not enough money"
|
||||||
msgstr "Pas assez d'argent"
|
msgstr "Pas assez d'argent"
|
||||||
|
|
||||||
#: counter/static/bundled/counter/counter-click-index.ts
|
|
||||||
msgid "Formula %(formula)s applied"
|
|
||||||
msgstr "Formule %(formula)s appliquée"
|
|
||||||
|
|
||||||
#: counter/static/bundled/counter/counter-click-index.ts
|
#: counter/static/bundled/counter/counter-click-index.ts
|
||||||
msgid "You can't send an empty basket."
|
msgid "You can't send an empty basket."
|
||||||
msgstr "Vous ne pouvez pas envoyer un panier vide."
|
msgstr "Vous ne pouvez pas envoyer un panier vide."
|
||||||
@@ -266,9 +262,3 @@ msgstr "Il n'a pas été possible de modérer l'image"
|
|||||||
#: sas/static/bundled/sas/viewer-index.ts
|
#: sas/static/bundled/sas/viewer-index.ts
|
||||||
msgid "Couldn't delete picture"
|
msgid "Couldn't delete picture"
|
||||||
msgstr "Il n'a pas été possible de supprimer l'image"
|
msgstr "Il n'a pas été possible de supprimer l'image"
|
||||||
|
|
||||||
#: timetable/static/bundled/timetable/generator-index.ts
|
|
||||||
msgid ""
|
|
||||||
"Wrong timetable format. Make sure you copied if from your student folder."
|
|
||||||
msgstr ""
|
|
||||||
"Mauvais format d'emploi du temps. Assurez-vous que vous l'avez copié depuis votre dossier étudiants."
|
|
||||||
|
|||||||
48
matmat/forms.py
Normal file
48
matmat/forms.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#
|
||||||
|
# Copyright 2025
|
||||||
|
# - Maréchal <thomas.girod@utbm.fr>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
|
from core.views.forms import SelectDate
|
||||||
|
|
||||||
|
|
||||||
|
class SearchForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ["promo", "role", "department", "semester", "date_of_birth"]
|
||||||
|
widgets = {"date_of_birth": SelectDate}
|
||||||
|
|
||||||
|
name = forms.CharField(
|
||||||
|
label=_("Last/First name or nickname"), min_length=1, max_length=255
|
||||||
|
)
|
||||||
|
field_order = ["name", "promo", "role", "department", "semester", "date_of_birth"]
|
||||||
|
|
||||||
|
def __init__(self, *args, initial: dict[str, Any], **kwargs):
|
||||||
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
for key in self.fields:
|
||||||
|
self.fields[key].required = False
|
||||||
|
if key not in initial:
|
||||||
|
self.fields[key].initial = None
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from "core/macros.jinja" import user_mini_profile, paginate_jinja %}
|
{% from "core/macros.jinja" import user_mini_profile, paginate_jinja %}
|
||||||
|
{% from "core/macros.jinja" import user_mini_profile, paginate_jinja with context %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Search user{% endtrans %}
|
{% trans %}Search user{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if result_exists %}
|
{% if paginator.count > 0 %}
|
||||||
<h2>{% trans %}Results{% endtrans %}</h2>
|
<h2>{% trans %}Results{% endtrans %}</h2>
|
||||||
<div class="matmat_results">
|
<div class="matmat_results">
|
||||||
{% for user in object_list %}
|
{% for user in object_list %}
|
||||||
@@ -24,42 +25,12 @@
|
|||||||
<hr>
|
<hr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h2>{% trans %}Search user{% endtrans %}</h2>
|
<h2>{% trans %}Search user{% endtrans %}</h2>
|
||||||
<h3>{% trans %}Search by profile{% endtrans %}</h3>
|
<form action="{{ url('matmat:search') }}" method="get">
|
||||||
<form action="{{ url('matmat:search') }}" method="post" enctype="multipart/form-data">
|
<fieldset class="fields-centered">
|
||||||
{% csrf_token %}
|
{{ form }}
|
||||||
{% for field in form %}
|
</fieldset>
|
||||||
{% if field.name not in ('phone', 'quick') %}
|
<div class="fields-centered">
|
||||||
<p>
|
<input class="btn btn-blue" type="submit" value="{% trans %}Search{% endtrans %}" />
|
||||||
{{ field.errors }}
|
</div>
|
||||||
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
|
|
||||||
{{ field }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<p><input type="submit" value="{% trans %}Search{% endtrans %}" /></p>
|
|
||||||
</form>
|
|
||||||
<h3>{% trans %}Inverted search{% endtrans %}</h3>
|
|
||||||
<form action="{{ url('matmat:search_reverse') }}" method="post" enctype="multipart/form-data">
|
|
||||||
{% csrf_token %}
|
|
||||||
<p>
|
|
||||||
{{ form.phone.errors }}
|
|
||||||
<label for="{{ form.phone.id_for_label }}">{{ form.phone.label }}</label>
|
|
||||||
{{ form.phone }}
|
|
||||||
<p><input type="submit" value="{% trans %}Search{% endtrans %}" /></p>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
<h3>{% trans %}Quick search{% endtrans %}</h3>
|
|
||||||
<form action="{{ url('matmat:search_quick') }}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<p>
|
|
||||||
{{ form.quick.errors }}
|
|
||||||
<label for="{{ form.quick.id_for_label }}">{{ form.quick.label }}</label>
|
|
||||||
{{ form.quick }}
|
|
||||||
<p><input type="submit" value="{% trans %}Search{% endtrans %}" /></p>
|
|
||||||
</p>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -1 +1,35 @@
|
|||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
|
from com.models import News
|
||||||
|
from core.baker_recipes import subscriber_user
|
||||||
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class TestMatmatronch(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
News.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
users = [
|
||||||
|
baker.prepare(User, promo=17),
|
||||||
|
baker.prepare(User, promo=17),
|
||||||
|
baker.prepare(User, promo=17, department="INFO"),
|
||||||
|
baker.prepare(User, promo=18, department="INFO"),
|
||||||
|
]
|
||||||
|
cls.users = User.objects.bulk_create(users)
|
||||||
|
call_command("update_index", "core", "--remove")
|
||||||
|
|
||||||
|
def test_search(self):
|
||||||
|
self.client.force_login(subscriber_user.make())
|
||||||
|
response = self.client.get(reverse("matmat:search"))
|
||||||
|
assert response.status_code == 200
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("matmat:search", query={"promo": 17, "department": "INFO"})
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert list(response.context_data["object_list"]) == [self.users[2]]
|
||||||
|
|||||||
@@ -23,16 +23,8 @@
|
|||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from matmat.views import (
|
from matmat.views import MatmatronchView
|
||||||
SearchClearFormView,
|
|
||||||
SearchNormalFormView,
|
|
||||||
SearchQuickFormView,
|
|
||||||
SearchReverseFormView,
|
|
||||||
)
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", SearchNormalFormView.as_view(), name="search"),
|
path("", MatmatronchView.as_view(), name="search"),
|
||||||
path("reverse/", SearchReverseFormView.as_view(), name="search_reverse"),
|
|
||||||
path("quick/", SearchQuickFormView.as_view(), name="search_quick"),
|
|
||||||
path("clear/", SearchClearFormView.as_view(), name="search_clear"),
|
|
||||||
]
|
]
|
||||||
|
|||||||
195
matmat/views.py
195
matmat/views.py
@@ -20,191 +20,44 @@
|
|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
from ast import literal_eval
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.http.response import HttpResponseRedirect
|
from django.views.generic import ListView
|
||||||
from django.urls import reverse
|
from django.views.generic.edit import FormMixin
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.views.generic import ListView, View
|
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
|
||||||
from django.views.generic.edit import FormView
|
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
|
||||||
|
|
||||||
from core.auth.mixins import FormerSubscriberMixin
|
from core.auth.mixins import FormerSubscriberMixin
|
||||||
from core.models import User
|
from core.models import User, UserQuerySet
|
||||||
from core.schemas import UserFilterSchema
|
from core.schemas import UserFilterSchema
|
||||||
from core.views.forms import SelectDate
|
from matmat.forms import SearchForm
|
||||||
|
|
||||||
# Enum to select search type
|
|
||||||
|
|
||||||
|
|
||||||
class SearchType(Enum):
|
class MatmatronchView(FormerSubscriberMixin, FormMixin, ListView):
|
||||||
NORMAL = 1
|
|
||||||
REVERSE = 2
|
|
||||||
QUICK = 3
|
|
||||||
|
|
||||||
|
|
||||||
# Custom form
|
|
||||||
|
|
||||||
|
|
||||||
class SearchForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = User
|
model = User
|
||||||
fields = [
|
paginate_by = 20
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"nick_name",
|
|
||||||
"role",
|
|
||||||
"department",
|
|
||||||
"semester",
|
|
||||||
"promo",
|
|
||||||
"date_of_birth",
|
|
||||||
"phone",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"date_of_birth": SelectDate,
|
|
||||||
"phone": RegionalPhoneNumberWidget,
|
|
||||||
}
|
|
||||||
|
|
||||||
quick = forms.CharField(label=_("Last/First name or nickname"), max_length=255)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for key in self.fields:
|
|
||||||
self.fields[key].required = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cleaned_data_json(self):
|
|
||||||
data = self.cleaned_data
|
|
||||||
for key, val in data.items():
|
|
||||||
if key in ("date_of_birth", "phone") and val is not None:
|
|
||||||
data[key] = str(val)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# Views
|
|
||||||
|
|
||||||
|
|
||||||
class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
|
|
||||||
model = User
|
|
||||||
ordering = ["-id"]
|
|
||||||
paginate_by = 12
|
|
||||||
template_name = "matmat/search_form.jinja"
|
template_name = "matmat/search_form.jinja"
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
self.form_class = kwargs["form"]
|
|
||||||
self.search_type = kwargs["search_type"]
|
|
||||||
self.session = request.session
|
|
||||||
self.last_search = self.session.get("matmat_search_result", str([]))
|
|
||||||
self.last_search = literal_eval(self.last_search)
|
|
||||||
self.valid_form = kwargs.get("valid_form")
|
|
||||||
|
|
||||||
self.init_query = self.model.objects
|
|
||||||
self.can_see_hidden = True
|
|
||||||
if not (request.user.is_board_member or request.user.is_root):
|
|
||||||
self.can_see_hidden = False
|
|
||||||
self.init_query = self.init_query.filter(is_viewable=True)
|
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
return self.get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
self.object = None
|
|
||||||
kwargs = super().get_context_data(**kwargs)
|
|
||||||
kwargs["form"] = self.form_class
|
|
||||||
kwargs["result_exists"] = self.result_exists
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
q = self.init_query
|
|
||||||
if self.valid_form is not None:
|
|
||||||
if self.search_type == SearchType.REVERSE:
|
|
||||||
q = q.filter(phone=self.valid_form["phone"]).all()
|
|
||||||
elif self.search_type == SearchType.QUICK:
|
|
||||||
if self.valid_form["quick"].strip():
|
|
||||||
q = list(
|
|
||||||
UserFilterSchema(search=self.valid_form["quick"])
|
|
||||||
.filter(User.objects.viewable_by(self.request.user))
|
|
||||||
.order_by(F("last_login").desc(nulls_last=True))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
q = []
|
|
||||||
else:
|
|
||||||
search_dict = {}
|
|
||||||
for key, value in self.valid_form.items():
|
|
||||||
if key not in ("phone", "quick") and not (
|
|
||||||
value == "" or value is None
|
|
||||||
):
|
|
||||||
search_dict[key + "__icontains"] = value
|
|
||||||
q = q.filter(**search_dict).all()
|
|
||||||
else:
|
|
||||||
q = q.filter(pk__in=self.last_search).all()
|
|
||||||
if isinstance(q, list):
|
|
||||||
self.result_exists = len(q) > 0
|
|
||||||
else:
|
|
||||||
self.result_exists = q.exists()
|
|
||||||
self.last_search = []
|
|
||||||
for user in q:
|
|
||||||
self.last_search.append(user.id)
|
|
||||||
self.session["matmat_search_result"] = str(self.last_search)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
class SearchFormView(FormerSubscriberMixin, FormView):
|
|
||||||
"""Allows users to search inside the user list."""
|
|
||||||
|
|
||||||
form_class = SearchForm
|
form_class = SearchForm
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
self.session = request.session
|
|
||||||
self.init_query = User.objects
|
|
||||||
kwargs["form"] = self.get_form()
|
|
||||||
kwargs["search_type"] = self.search_type
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
view = SearchFormListView.as_view()
|
self.form = self.get_form()
|
||||||
return view(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
form = self.get_form()
|
|
||||||
view = SearchFormListView.as_view()
|
|
||||||
if form.is_valid():
|
|
||||||
kwargs["valid_form"] = form.clean()
|
|
||||||
request.session["matmat_search_form"] = form.cleaned_data_json
|
|
||||||
return view(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
init = self.session.get("matmat_search_form", {})
|
return self.request.GET
|
||||||
if not init:
|
|
||||||
init["department"] = ""
|
|
||||||
return init
|
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
res = super().get_form_kwargs()
|
||||||
|
if self.request.GET:
|
||||||
|
res["data"] = self.request.GET
|
||||||
|
return res
|
||||||
|
|
||||||
class SearchNormalFormView(SearchFormView):
|
def get_queryset(self) -> UserQuerySet:
|
||||||
search_type = SearchType.NORMAL
|
if not self.form.is_valid():
|
||||||
|
return User.objects.none()
|
||||||
|
data = self.form.cleaned_data
|
||||||
|
data["search"] = data.get("name")
|
||||||
|
filters = UserFilterSchema(**{key: val for key, val in data.items() if val})
|
||||||
|
qs = User.objects.viewable_by(self.request.user).select_related("profile_pict")
|
||||||
|
return filters.filter(qs).order_by(F("last_login").desc(nulls_last=True))
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
class SearchReverseFormView(SearchFormView):
|
return super().get_context_data(form=self.form, **kwargs)
|
||||||
search_type = SearchType.REVERSE
|
|
||||||
|
|
||||||
|
|
||||||
class SearchQuickFormView(SearchFormView):
|
|
||||||
search_type = SearchType.QUICK
|
|
||||||
|
|
||||||
|
|
||||||
class SearchClearFormView(FormerSubscriberMixin, View):
|
|
||||||
"""Clear SearchFormView and redirect to it."""
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
super().dispatch(request, *args, **kwargs)
|
|
||||||
if "matmat_search_form" in request.session:
|
|
||||||
request.session.pop("matmat_search_form")
|
|
||||||
if "matmat_search_result" in request.session:
|
|
||||||
request.session.pop("matmat_search_result")
|
|
||||||
return HttpResponseRedirect(reverse("matmat:search"))
|
|
||||||
|
|||||||
Reference in New Issue
Block a user