Author SHA1 Message Date
thomas girod 6a6ed5462f Merge pull request #1491 from ae-utbm/taiste
Python 3.14, HTMX4, client-side markdown, pedagogy rework and others
2026-09-18 19:19:48 +02:00
thomas girod d914fa0658 Merge pull request #1463 from ae-utbm/taiste
Pre school year release
2026-08-25 22:37:49 +02:00
12 changed files with 91 additions and 72 deletions
+2 -14
View File
@@ -138,22 +138,10 @@ class Command(BaseCommand):
) )
def create_subscriptions(self, users: list[User]): def create_subscriptions(self, users: list[User]):
subscription_types = [
"un-semestre",
"deux-semestres",
"cursus-tronc-commun",
"cursus-branche",
]
def prepare_subscription(_user: User, start_date: date) -> Subscription: def prepare_subscription(_user: User, start_date: date) -> Subscription:
payment_method = random.choice(settings.SITH_SUBSCRIPTION_PAYMENT_METHOD)[0] payment_method = random.choice(settings.SITH_SUBSCRIPTION_PAYMENT_METHOD)[0]
subscription_type = random.choice(subscription_types) duration = random.randint(1, 4)
s = Subscription( s = Subscription(member=_user, payment_method=payment_method)
member=_user,
payment_method=payment_method,
subscription_type=subscription_type,
)
duration = settings.SITH_SUBSCRIPTIONS[subscription_type]["duration"]
s.subscription_start = s.compute_start(d=start_date, duration=duration) s.subscription_start = s.compute_start(d=start_date, duration=duration)
s.subscription_end = s.compute_end(duration) s.subscription_end = s.compute_end(duration)
return s return s
@@ -123,14 +123,14 @@ document.addEventListener("alpine:init", () => {
onRefillingSuccess(event: CustomEvent) { onRefillingSuccess(event: CustomEvent) {
if ( if (
event.type !== "htmx:after:swap" || event.type !== "htmx:after-swap" ||
event.detail.ctx.response.status !== 200 || event.detail.failed ||
event.detail.ctx.target.querySelector(".errorlist") event.detail.elt.querySelector(".errorlist")
) { ) {
return; return;
} }
this.customerBalance += Number.parseFloat( this.customerBalance += Number.parseFloat(
(event.detail.ctx.target.querySelector("#id_amount") as HTMLInputElement).value, (event.detail.target.querySelector("#id_amount") as HTMLInputElement).value,
); );
document.getElementById("selling-accordion")?.setAttribute("open", ""); document.getElementById("selling-accordion")?.setAttribute("open", "");
this.codeField?.widget.focus(); this.codeField?.widget.focus();
@@ -62,7 +62,8 @@
} }
form { form {
margin: 0; margin-top: .5rem;
margin-bottom: .5rem;
} }
} }
@@ -186,7 +186,7 @@
{% if refilling_fragment %} {% if refilling_fragment %}
<div <div
class="accordion-content" class="accordion-content"
@htmx:after:swap="onRefillingSuccess" @htmx:after-swap="onRefillingSuccess"
> >
{{ refilling_fragment }} {{ refilling_fragment }}
</div> </div>
@@ -4,8 +4,6 @@
hx-swap="outerHTML" hx-swap="outerHTML"
> >
{% csrf_token %} {% csrf_token %}
<div class="margin-bottom">
{{ form.as_p() }} {{ form.as_p() }}
</div> <input type="submit" value="{% trans %}Go{% endtrans %}"/>
<input type="submit" class="btn btn-blue" value="{% trans %}Go{% endtrans %}"/>
</form> </form>
@@ -60,7 +60,7 @@
</p> </p>
<br> <br>
{% if settings.SITH_EBOUTIC_CB_ENABLED %} {% if settings.SITH_EBOUTIC_CB_ENABLED %}
<div @htmx:after:request="fill"> <div @htmx:after-request="fill">
{{ billing_infos_form }} {{ billing_infos_form }}
</div> </div>
{% endif %} {% endif %}
+1 -1
View File
@@ -25,7 +25,7 @@
{% endfor %} {% endfor %}
</div> </div>
{% if page_obj.has_other_pages() %} {% if page_obj.has_other_pages() %}
{{ paginate_htmx(request, page_obj, paginator) }} {{ paginate_htmx(page_obj, paginator) }}
{% endif %} {% endif %}
<hr> <hr>
{% endif %} {% endif %}
-10
View File
@@ -58,13 +58,3 @@ class TestMatmatronch(TestCase):
assert list(response.context_data["object_list"]) == [] assert list(response.context_data["object_list"]) == []
assert not response.context_data["form"].is_valid() assert not response.context_data["form"].is_valid()
assert "Recherche vide" in response.context_data["form"].non_field_errors() assert "Recherche vide" in response.context_data["form"].non_field_errors()
def test_search_many_users(self):
"""Test that the pagination works when a lot of users are returned."""
baker.make(User, promo=17, _quantity=40, _bulk_create=True)
self.client.force_login(subscriber_user.make())
response = self.client.get(reverse("matmat:search", query={"promo": 17}))
assert response.status_code == 200
assert response.context_data["paginator"].count == 43
assert response.context_data["paginator"].num_pages == 3
+1
View File
@@ -20,6 +20,7 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from django.db.models import F from django.db.models import F
from django.views.generic import ListView from django.views.generic import ListView
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
+12 -1
View File
@@ -8,11 +8,22 @@ from django.utils.translation import gettext_lazy as _
from core.models import User from core.models import User
from core.utils import get_last_promo from core.utils import get_last_promo
from core.views.forms import SelectDate from core.views.forms import SelectDate, SelectDateTime
from core.views.widgets.ajax_select import AutoCompleteSelectUser from core.views.widgets.ajax_select import AutoCompleteSelectUser
from subscription.models import Subscription from subscription.models import Subscription
class SelectionDateForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["start_date"] = forms.DateTimeField(
label=_("Start date"), widget=SelectDateTime, required=True
)
self.fields["end_date"] = forms.DateTimeField(
label=_("End date"), widget=SelectDateTime, required=True
)
class SubscriptionForm(forms.ModelForm): class SubscriptionForm(forms.ModelForm):
allowed_payment_methods = ["CARD", "CASH", "AE_ACCOUNT"] allowed_payment_methods = ["CARD", "CASH", "AE_ACCOUNT"]
@@ -11,23 +11,35 @@
{% block content %} {% block content %}
<p>
<form>
{{ form.start_date.label }}<br>
{{ form.start_date }}<br><br>
{{ form.end_date.label }}<br>
{{ form.end_date }}<br>
<p><input type="submit" value="{% trans %}Go{% endtrans %}" /></p>
</form>
</p>
<canvas id="statsChart" width="400" height="200"></canvas> <canvas id="statsChart" width="400" height="200"></canvas>
<p>
{% trans %}Total subscriptions{% endtrans %} : {{ subscriptions_total.count() }}<br><br> {% trans %}Total subscriptions{% endtrans %} : {{ subscriptions_total.count() }}<br><br>
{% trans %}Subscriptions by type{% endtrans %}<br><br> {% trans %}Subscriptions by type{% endtrans %}<br><br>
{% for location in locations %} {% for location in locations %}
{{ location[1] }} : <i class="nb">{{ subscriptions_total.filter(location=location[0]).count() }}</i><br> {{ location[1] }} : <i class="nb">{{ subscriptions_total.filter(location=location[0]).count() }}</i><br>
{% endfor %} {% endfor %}
<p>
<br> <br>
<table> <table>
<thead> <tr>
<th>{% trans %}Subscription type{% endtrans %}</th> <th>{% trans %}Subscription type{% endtrans %}</th>
{% for location in locations %} {% for location in locations %}
<th>{{ location[1] }}</th> <th>{{ location[1] }}</th>
{% endfor %} {% endfor %}
<th id="graphLabel">{% trans %}Total{% endtrans %}</th> <th id="graphLabel">{% trans %}Total{% endtrans %}</th>
</thead>
{% for type in subscriptions_types %} {% for type in subscriptions_types %}
<tr> <tr>
<td><i class="types" >{{ subscriptions_types[type]['name'] }}</i></td> <td><i class="types" >{{ subscriptions_types[type]['name'] }}</i></td>
@@ -41,8 +53,9 @@
{% endfor %} {% endfor %}
</td> </td>
{% endfor %} {% endfor %}
<td class="total"><i class="nb">{{subscriptions_total_type.count()}}</i></td> <td class="total"><i class="nb">{{subscriptions_total_type.count()}}</i>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endblock %} {% endblock %}
+21 -4
View File
@@ -17,14 +17,16 @@ from django.conf import settings
from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.urls import reverse from django.urls import reverse, reverse_lazy
from django.utils.timezone import localdate from django.utils.timezone import localdate
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, TemplateView from django.views.generic import CreateView, DetailView, TemplateView
from django.views.generic.edit import FormView
from core.views import FragmentMixin, UseFragmentsMixin from core.views import FragmentMixin, UseFragmentsMixin
from core.views.group import PermissionGroupsUpdateView from core.views.group import PermissionGroupsUpdateView
from subscription.forms import ( from subscription.forms import (
SelectionDateForm,
SubscriptionExistingUserForm, SubscriptionExistingUserForm,
SubscriptionNewUserForm, SubscriptionNewUserForm,
) )
@@ -91,19 +93,34 @@ class SubscriptionPermissionView(PermissionGroupsUpdateView):
extra_context = {"object_name": _("the groups that can create subscriptions")} extra_context = {"object_name": _("the groups that can create subscriptions")}
class SubscriptionsStatsView(TemplateView): class SubscriptionsStatsView(FormView):
template_name = "subscription/stats.jinja" template_name = "subscription/stats.jinja"
form_class = SelectionDateForm
success_url = reverse_lazy("subscriptions:stats")
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
self.start_date = localdate()
self.end_date = self.start_date
if request.user.is_root or request.user.is_board_member: if request.user.is_root or request.user.is_board_member:
return super().dispatch(request, *arg, **kwargs) return super().dispatch(request, *arg, **kwargs)
raise PermissionDenied raise PermissionDenied
def post(self, request, *args, **kwargs):
self.form = self.get_form()
self.start_date = self.form["start_date"]
self.end_date = self.form["end_date"]
return super().post(request, *args, **kwargs)
def get_initial(self):
return {
"start_date": self.start_date.strftime("%Y-%m-%d %H:%M:%S"),
"end_date": self.end_date.strftime("%Y-%m-%d %H:%M:%S"),
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
today = localdate()
kwargs["subscriptions_total"] = Subscription.objects.filter( kwargs["subscriptions_total"] = Subscription.objects.filter(
subscription_end__gte=today, subscription_start__lte=today subscription_end__gte=self.end_date, subscription_start__lte=self.start_date
) )
kwargs["subscriptions_types"] = settings.SITH_SUBSCRIPTIONS kwargs["subscriptions_types"] = settings.SITH_SUBSCRIPTIONS
kwargs["payment_types"] = settings.SITH_SUBSCRIPTION_PAYMENT_METHOD kwargs["payment_types"] = settings.SITH_SUBSCRIPTION_PAYMENT_METHOD