mirror of
https://github.com/ae-utbm/sith.git
synced 2025-09-16 04:53:48 +00:00
Compare commits
1 Commits
auto-archi
...
fix-depend
Author | SHA1 | Date | |
---|---|---|---|
|
b58da0ea30 |
2
.github/auto_assign.yml
vendored
2
.github/auto_assign.yml
vendored
@@ -6,7 +6,7 @@ addAssignees: author
|
||||
|
||||
# A list of team reviewers to be added to pull requests (GitHub team slug)
|
||||
reviewers:
|
||||
- ae-utbm/sith-3-developers
|
||||
- ae-utbm/developpeurs
|
||||
|
||||
# Number of reviewers has no impact on GitHub teams
|
||||
# Set 0 to add all the reviewers (default: 0)
|
||||
|
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -16,7 +16,16 @@ multi-ecosystem-groups:
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "uv"
|
||||
patterns: ["*"]
|
||||
multi-ecosystem-group: "common"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
patterns: ["*"]
|
||||
multi-ecosystem-group: "common"
|
||||
groups:
|
||||
# npm supports production and development groups, but not uv
|
||||
# cf. https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#dependency-type-groups
|
||||
main-deps:
|
||||
dependency-type: "production"
|
||||
dev-deps:
|
||||
dependency-type: "development"
|
||||
|
@@ -153,8 +153,10 @@ form {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.row > label {
|
||||
margin: unset;
|
||||
.row {
|
||||
label {
|
||||
margin: unset;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------- LABEL
|
||||
|
@@ -115,7 +115,7 @@ class SelectUser(TextInput):
|
||||
|
||||
def validate_future_timestamp(value: date | datetime):
|
||||
if value <= now():
|
||||
raise ValidationError(_("Ensure this timestamp is set in the future"))
|
||||
raise ValueError(_("Ensure this timestamp is set in the future"))
|
||||
|
||||
|
||||
class FutureDateTimeField(forms.DateTimeField):
|
||||
|
137
counter/forms.py
137
counter/forms.py
@@ -1,21 +1,13 @@
|
||||
import json
|
||||
import math
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.forms.formsets import DELETION_FIELD_NAME, BaseFormSet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_celery_beat.models import ClockedSchedule
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
|
||||
from club.widgets.ajax_select import AutoCompleteSelectClub
|
||||
from core.models import User
|
||||
from core.views.forms import (
|
||||
FutureDateTimeField,
|
||||
NFCTextInput,
|
||||
SelectDate,
|
||||
SelectDateTime,
|
||||
)
|
||||
from core.views.forms import NFCTextInput, SelectDate, SelectDateTime
|
||||
from core.views.widgets.ajax_select import (
|
||||
AutoCompleteSelect,
|
||||
AutoCompleteSelectMultipleGroup,
|
||||
@@ -30,9 +22,7 @@ from counter.models import (
|
||||
Product,
|
||||
Refilling,
|
||||
ReturnableProduct,
|
||||
ScheduledProductAction,
|
||||
StudentCard,
|
||||
get_product_actions,
|
||||
)
|
||||
from counter.widgets.ajax_select import (
|
||||
AutoCompleteSelectMultipleCounter,
|
||||
@@ -168,110 +158,6 @@ class CounterEditForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ScheduledProductActionForm(forms.Form):
|
||||
"""Form for automatic product archiving.
|
||||
|
||||
The `save` method will update or create tasks using celery-beat.
|
||||
"""
|
||||
|
||||
required_css_class = "required"
|
||||
prefix = "product-action-form"
|
||||
|
||||
task = forms.fields_for_model(
|
||||
ScheduledProductAction,
|
||||
["task"],
|
||||
widgets={"task": forms.RadioSelect(choices=get_product_actions)},
|
||||
labels={"task": _("Action")},
|
||||
help_texts={"task": ""},
|
||||
)["task"]
|
||||
trigger_at = FutureDateTimeField(
|
||||
label=_("Date and time of action"), widget=SelectDateTime
|
||||
)
|
||||
counters = forms.ModelMultipleChoiceField(
|
||||
label=_("New counters"),
|
||||
help_text=_("The selected counters will replace the current ones"),
|
||||
required=False,
|
||||
widget=AutoCompleteSelectMultipleCounter,
|
||||
queryset=Counter.objects.all(),
|
||||
)
|
||||
|
||||
def __init__(self, *args, product: Product, **kwargs):
|
||||
self.product = product
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
if not self.changed_data:
|
||||
return
|
||||
task = self.cleaned_data["task"]
|
||||
instance = ScheduledProductAction.objects.filter(
|
||||
product=self.product, task=task
|
||||
).first()
|
||||
if not instance:
|
||||
instance = ScheduledProductAction(
|
||||
product=self.product,
|
||||
clocked=ClockedSchedule.objects.create(
|
||||
clocked_time=self.cleaned_data["trigger_at"]
|
||||
),
|
||||
)
|
||||
elif "trigger_at" in self.changed_data:
|
||||
instance.clocked.clocked_time = self.cleaned_data["trigger_at"]
|
||||
instance.clocked.save()
|
||||
instance.task = task
|
||||
instance.kwargs = json.dumps(
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"counters": [c.id for c in self.cleaned_data["counters"]],
|
||||
}
|
||||
)
|
||||
instance.name = f"{self.cleaned_data['task']} - {self.product}"
|
||||
instance.enabled = True
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def delete(self):
|
||||
instance = ScheduledProductAction.objects.get(
|
||||
product=self.product, task=self.cleaned_data["task"]
|
||||
)
|
||||
# if the clocked object linked to the task has no other task,
|
||||
# delete it and let the task be deleted in cascade,
|
||||
# else delete only the task.
|
||||
if instance.clocked.periodictask_set.count() > 1:
|
||||
return instance.clocked.delete()
|
||||
return instance.delete()
|
||||
|
||||
|
||||
class BaseScheduledProductActionFormSet(BaseFormSet):
|
||||
def __init__(self, *args, product: Product, **kwargs):
|
||||
initial = [
|
||||
{
|
||||
"task": action.task,
|
||||
"trigger_at": action.clocked.clocked_time,
|
||||
"counters": json.loads(action.kwargs).get("counters"),
|
||||
}
|
||||
for action in product.scheduled_actions.filter(enabled=True)
|
||||
]
|
||||
kwargs["initial"] = initial
|
||||
kwargs["form_kwargs"] = {"product": product}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
for form in self.forms:
|
||||
if form.cleaned_data.get(DELETION_FIELD_NAME, False):
|
||||
form.delete()
|
||||
else:
|
||||
form.save()
|
||||
|
||||
|
||||
ScheduledProductActionFormSet = forms.formset_factory(
|
||||
ScheduledProductActionForm,
|
||||
formset=BaseScheduledProductActionFormSet,
|
||||
absolute_max=None,
|
||||
can_delete=True,
|
||||
can_delete_extra=False,
|
||||
extra=2,
|
||||
)
|
||||
|
||||
|
||||
class ProductEditForm(forms.ModelForm):
|
||||
error_css_class = "error"
|
||||
required_css_class = "required"
|
||||
@@ -313,21 +199,22 @@ class ProductEditForm(forms.ModelForm):
|
||||
queryset=Counter.objects.all(),
|
||||
)
|
||||
|
||||
def __init__(self, *args, instance=None, **kwargs):
|
||||
super().__init__(*args, instance=instance, **kwargs)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.id:
|
||||
self.fields["counters"].initial = self.instance.counters.all()
|
||||
self.action_formset = ScheduledProductActionFormSet(
|
||||
*args, product=self.instance, **kwargs
|
||||
)
|
||||
|
||||
def is_valid(self):
|
||||
return super().is_valid() and self.action_formset.is_valid()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
ret = super().save(*args, **kwargs)
|
||||
self.instance.counters.set(self.cleaned_data["counters"])
|
||||
self.action_formset.save()
|
||||
if self.fields["counters"].initial:
|
||||
# Remove the product from all counter it was added to
|
||||
# It will then only be added to selected counters
|
||||
for counter in self.fields["counters"].initial:
|
||||
counter.products.remove(self.instance)
|
||||
counter.save()
|
||||
for counter in self.cleaned_data["counters"]:
|
||||
counter.products.add(self.instance)
|
||||
counter.save()
|
||||
return ret
|
||||
|
||||
|
||||
|
@@ -1,43 +0,0 @@
|
||||
# Generated by Django 5.2.3 on 2025-09-14 11:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("counter", "0031_alter_counter_options"),
|
||||
("django_celery_beat", "0019_alter_periodictasks_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ScheduledProductAction",
|
||||
fields=[
|
||||
(
|
||||
"periodictask_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="django_celery_beat.periodictask",
|
||||
),
|
||||
),
|
||||
(
|
||||
"product",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="scheduled_actions",
|
||||
to="counter.product",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Product scheduled action",
|
||||
"ordering": ["-clocked"],
|
||||
},
|
||||
bases=("django_celery_beat.periodictask",),
|
||||
),
|
||||
]
|
@@ -34,7 +34,6 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from django_countries.fields import CountryField
|
||||
from ordered_model.models import OrderedModel
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
@@ -446,8 +445,7 @@ class Product(models.Model):
|
||||
buying_groups = list(self.buying_groups.all())
|
||||
if not buying_groups:
|
||||
return True
|
||||
res = any(user.is_in_group(pk=group.id) for group in buying_groups)
|
||||
return res
|
||||
return any(user.is_in_group(pk=group.id) for group in buying_groups)
|
||||
|
||||
@property
|
||||
def profit(self):
|
||||
@@ -1364,30 +1362,3 @@ class ReturnableProductBalance(models.Model):
|
||||
f"return balance of {self.customer} "
|
||||
f"for {self.returnable.product_id} : {self.balance}"
|
||||
)
|
||||
|
||||
|
||||
def get_product_actions():
|
||||
return [
|
||||
("counter.tasks.archive_product", _("Archiving")),
|
||||
("counter.tasks.change_counters", _("Counters change")),
|
||||
]
|
||||
|
||||
|
||||
class ScheduledProductAction(PeriodicTask):
|
||||
"""Extension of celery-beat tasks dedicated to perform actions on Product."""
|
||||
|
||||
product = models.ForeignKey(
|
||||
Product, related_name="scheduled_actions", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Product scheduled action")
|
||||
ordering = ["-clocked"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._meta.get_field("task").choices = get_product_actions()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.one_off = True # A product action should occur one time only
|
||||
return super().save(*args, **kwargs)
|
||||
|
@@ -1,20 +0,0 @@
|
||||
# Create your tasks here
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
from counter.models import Counter, Product
|
||||
|
||||
|
||||
@shared_task
|
||||
def archive_product(*, product_id: int):
|
||||
product = Product.objects.get(id=product_id)
|
||||
product.archived = True
|
||||
product.save()
|
||||
product.counters.clear()
|
||||
|
||||
|
||||
@shared_task
|
||||
def change_counters(*, product_id: int, counters: list[int]):
|
||||
product = Product.objects.get(id=product_id)
|
||||
counters = Counter.objects.filter(id__in=counters)
|
||||
product.counters.set(counters)
|
@@ -1,55 +0,0 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
{% if object %}
|
||||
<h2>{% trans name=object %}Edit product {{ name }}{% endtrans %}</h2>
|
||||
{% else %}
|
||||
<h2>{% trans %}Product creation{% endtrans %}</h2>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p() }}
|
||||
|
||||
<br />
|
||||
|
||||
<h3>{% trans %}Automatic actions{% endtrans %}</h3>
|
||||
|
||||
<p class="margin-bottom">
|
||||
<em>
|
||||
{%- trans trimmed -%}
|
||||
Automatic actions allows to schedule product changes
|
||||
ahead of time.
|
||||
{%- endtrans -%}
|
||||
</em>
|
||||
</p>
|
||||
|
||||
{{ form.action_formset.management_form }}
|
||||
{% for action_form in form.action_formset.forms %}
|
||||
<fieldset x-data="{action: '{{ action_form.task.initial }}'}">
|
||||
<div class="row gap-2x margin-bottom">
|
||||
<div>
|
||||
{{ action_form.task.errors }}
|
||||
{{ action_form.task.label_tag() }}
|
||||
{{ action_form.task|add_attr("x-model=action") }}
|
||||
</div>
|
||||
<div>
|
||||
{{ action_form.trigger_at.as_field_group() }}
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="action==='counter.tasks.change_counters'" class="margin-bottom">
|
||||
{{ action_form.counters.as_field_group() }}
|
||||
</div>
|
||||
{% if action_form.DELETE %}
|
||||
<div class="row gap">
|
||||
{{ action_form.DELETE.as_field_group() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% if not loop.last %}
|
||||
<hr class="margin-bottom">
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
|
||||
</form>
|
||||
{% endblock %}
|
@@ -147,7 +147,7 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
|
||||
|
||||
model = Product
|
||||
form_class = ProductEditForm
|
||||
template_name = "counter/product_form.jinja"
|
||||
template_name = "core/create.jinja"
|
||||
current_tab = "products"
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
||||
model = Product
|
||||
form_class = ProductEditForm
|
||||
pk_url_kwarg = "product_id"
|
||||
template_name = "counter/product_form.jinja"
|
||||
template_name = "core/edit.jinja"
|
||||
current_tab = "products"
|
||||
|
||||
|
||||
|
@@ -6,7 +6,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-14 17:09+0200\n"
|
||||
"POT-Creation-Date: 2025-09-01 18:18+0200\n"
|
||||
"PO-Revision-Date: 2016-07-18\n"
|
||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||
@@ -117,7 +117,7 @@ msgstr "S'abonner"
|
||||
msgid "Remove"
|
||||
msgstr "Retirer"
|
||||
|
||||
#: club/forms.py counter/forms.py pedagogy/templates/pedagogy/moderation.jinja
|
||||
#: club/forms.py pedagogy/templates/pedagogy/moderation.jinja
|
||||
msgid "Action"
|
||||
msgstr "Action"
|
||||
|
||||
@@ -561,7 +561,6 @@ msgstr ""
|
||||
#: core/templates/core/user_godfathers_tree.jinja
|
||||
#: core/templates/core/user_preferences.jinja
|
||||
#: counter/templates/counter/cash_register_summary.jinja
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
#: forum/templates/forum/reply.jinja
|
||||
#: subscription/templates/subscription/fragments/creation_form.jinja
|
||||
#: trombi/templates/trombi/comment.jinja
|
||||
@@ -1714,8 +1713,8 @@ msgid ""
|
||||
"AE UTBM is a voluntary organisation run by UTBM students. It organises "
|
||||
"student life at UTBM and manages its student facilities."
|
||||
msgstr ""
|
||||
"L'AE UTBM est une association bénévole gérée par les étudiants de l'UTBM. "
|
||||
"Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie."
|
||||
"L'AE UTBM est une association bénévole gérée par les étudiants de "
|
||||
"l'UTBM. Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie."
|
||||
|
||||
#: core/templates/core/base/footer.jinja core/templates/core/base/navbar.jinja
|
||||
msgid "Contacts"
|
||||
@@ -2158,6 +2157,10 @@ msgstr ""
|
||||
msgid "Page history"
|
||||
msgstr "Historique de la page"
|
||||
|
||||
#: core/templates/core/page_list.jinja
|
||||
msgid "There is no page in this website."
|
||||
msgstr "Il n'y a pas de page sur ce site web."
|
||||
|
||||
#: core/templates/core/page_prop.jinja
|
||||
msgid "Page properties"
|
||||
msgstr "Propriétés de la page"
|
||||
@@ -2893,18 +2896,6 @@ msgstr "Cet UID est invalide"
|
||||
msgid "User not found"
|
||||
msgstr "Utilisateur non trouvé"
|
||||
|
||||
#: counter/forms.py
|
||||
msgid "Date and time of action"
|
||||
msgstr "Date et heure de l'action"
|
||||
|
||||
#: counter/forms.py
|
||||
msgid "New counters"
|
||||
msgstr "Nouveaux comptoirs"
|
||||
|
||||
#: counter/forms.py
|
||||
msgid "The selected counters will replace the current ones"
|
||||
msgstr "Les comptoirs sélectionnés remplaceront les comptoirs actuels"
|
||||
|
||||
#: counter/forms.py
|
||||
msgid ""
|
||||
"Describe the product. If it's an event's click, give some insights about it, "
|
||||
@@ -3239,18 +3230,6 @@ msgid "The returnable product cannot be the same as the returned one"
|
||||
msgstr ""
|
||||
"Le produit consigné ne peut pas être le même que le produit de déconsigne"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "Archiving"
|
||||
msgstr "Archivage"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "Counters change"
|
||||
msgstr "Changement des comptoirs"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "Product scheduled action"
|
||||
msgstr "Actions sur produit planifiées"
|
||||
|
||||
#: counter/templates/counter/activity.jinja
|
||||
#, python-format
|
||||
msgid "%(counter_name)s activity"
|
||||
@@ -3569,25 +3548,6 @@ msgstr ""
|
||||
"votre cotisation. Si vous ne renouvelez pas votre cotisation, il n'y aura "
|
||||
"aucune conséquence autre que le retrait de l'argent de votre compte."
|
||||
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
#, python-format
|
||||
msgid "Edit product %(name)s"
|
||||
msgstr "Édition du produit %(name)s"
|
||||
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
msgid "Product creation"
|
||||
msgstr "Création de produit"
|
||||
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
msgid "Automatic actions"
|
||||
msgstr "Actions automatiques"
|
||||
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
msgid "Automatic actions allows to schedule product changes ahead of time."
|
||||
msgstr ""
|
||||
"Les actions automatiques vous permettent de planifier des modifications du "
|
||||
"produit à l'avance."
|
||||
|
||||
#: counter/templates/counter/product_list.jinja
|
||||
msgid "Product list"
|
||||
msgstr "Liste des produits"
|
||||
|
Reference in New Issue
Block a user