4 Commits

Author SHA1 Message Date
imperosol
5274f1f0f0 feat: automatic product archiving 2025-09-14 01:36:46 +02:00
thomas girod
b767079c5a Merge pull request #1167 from ae-utbm/page-n+1
Page n+1
2025-09-08 11:28:55 +02:00
imperosol
37961e437b fix: N+1 queries on PageListView 2025-09-04 17:39:17 +02:00
imperosol
b97a1a2e56 improve User.can_view and User.can_edit 2025-09-04 17:38:58 +02:00
16 changed files with 222 additions and 193 deletions

View File

@@ -25,7 +25,6 @@ from core.schemas import (
UserFamilySchema, UserFamilySchema,
UserFilterSchema, UserFilterSchema,
UserProfileSchema, UserProfileSchema,
UserSchema,
) )
from core.templatetags.renderer import markdown from core.templatetags.renderer import markdown
@@ -70,22 +69,16 @@ class MailingListController(ControllerBase):
return data return data
@api_controller("/user") @api_controller("/user", permissions=[CanAccessLookup])
class UserController(ControllerBase): class UserController(ControllerBase):
@route.get("", response=list[UserProfileSchema], permissions=[CanAccessLookup]) @route.get("", response=list[UserProfileSchema])
def fetch_profiles(self, pks: Query[set[int]]): def fetch_profiles(self, pks: Query[set[int]]):
return User.objects.filter(pk__in=pks) return User.objects.filter(pk__in=pks)
@route.get("/{int:user_id}", response=UserSchema, permissions=[CanView])
def fetch_user(self, user_id: int):
"""Fetch a single user"""
return self.get_object_or_exception(User, id=user_id)
@route.get( @route.get(
"/search", "/search",
response=PaginatedResponseSchema[UserProfileSchema], response=PaginatedResponseSchema[UserProfileSchema],
url_name="search_users", url_name="search_users",
permissions=[CanAccessLookup],
) )
@paginate(PageNumberPaginationExtra, page_size=20) @paginate(PageNumberPaginationExtra, page_size=20)
def search_users(self, filters: Query[UserFilterSchema]): def search_users(self, filters: Query[UserFilterSchema]):

View File

@@ -94,11 +94,7 @@ class Command(BaseCommand):
username=self.faker.user_name(), username=self.faker.user_name(),
first_name=self.faker.first_name(), first_name=self.faker.first_name(),
last_name=self.faker.last_name(), last_name=self.faker.last_name(),
date_of_birth=( date_of_birth=self.faker.date_of_birth(minimum_age=15, maximum_age=25),
None
if random.random() < 0.2
else self.faker.date_of_birth(minimum_age=15, maximum_age=25)
),
email=self.faker.email(), email=self.faker.email(),
phone=self.faker.phone_number(), phone=self.faker.phone_number(),
address=self.faker.address(), address=self.faker.address(),

View File

@@ -560,7 +560,7 @@ class User(AbstractUser):
"""Determine if the object is owned by the user.""" """Determine if the object is owned by the user."""
if hasattr(obj, "is_owned_by") and obj.is_owned_by(self): if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
return True return True
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id): if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group_id):
return True return True
return self.is_root return self.is_root
@@ -569,8 +569,14 @@ class User(AbstractUser):
if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self): if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
return True return True
if hasattr(obj, "edit_groups"): if hasattr(obj, "edit_groups"):
for pk in obj.edit_groups.values_list("pk", flat=True): if (
if self.is_in_group(pk=pk): hasattr(obj, "_prefetched_objects_cache")
and "edit_groups" in obj._prefetched_objects_cache
):
pks = [g.id for g in obj.edit_groups.all()]
else:
pks = list(obj.edit_groups.values_list("id", flat=True))
if any(self.is_in_group(pk=pk) for pk in pks):
return True return True
if isinstance(obj, User) and obj == self: if isinstance(obj, User) and obj == self:
return True return True
@@ -581,8 +587,17 @@ class User(AbstractUser):
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self): if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
return True return True
if hasattr(obj, "view_groups"): if hasattr(obj, "view_groups"):
for pk in obj.view_groups.values_list("pk", flat=True): # if "view_groups" has already been prefetched, use
if self.is_in_group(pk=pk): # the prefetch cache, else fetch only the ids, to make
# the query lighter.
if (
hasattr(obj, "_prefetched_objects_cache")
and "view_groups" in obj._prefetched_objects_cache
):
pks = [g.id for g in obj.view_groups.all()]
else:
pks = list(obj.view_groups.values_list("id", flat=True))
if any(self.is_in_group(pk=pk) for pk in pks):
return True return True
return self.can_edit(obj) return self.can_edit(obj)
@@ -1384,9 +1399,9 @@ class Page(models.Model):
@cached_property @cached_property
def is_club_page(self): def is_club_page(self):
club_root_page = Page.objects.filter(name=settings.SITH_CLUB_ROOT_PAGE).first() return (
return club_root_page is not None and ( self.name == settings.SITH_CLUB_ROOT_PAGE
self == club_root_page or club_root_page in self.get_parent_list() or settings.SITH_CLUB_ROOT_PAGE in [p.name for p in self.get_parent_list()]
) )
@cached_property @cached_property

View File

@@ -34,22 +34,6 @@ class SimpleUserSchema(ModelSchema):
fields = ["id", "nick_name", "first_name", "last_name"] fields = ["id", "nick_name", "first_name", "last_name"]
class UserSchema(ModelSchema):
class Meta:
model = User
fields = [
"id",
"nick_name",
"first_name",
"last_name",
"date_of_birth",
"email",
"role",
"quote",
"promo",
]
class UserProfileSchema(ModelSchema): class UserProfileSchema(ModelSchema):
"""The necessary information to show a user profile""" """The necessary information to show a user profile"""

View File

@@ -5,16 +5,12 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if page_list %}
<h3>{% trans %}Page list{% endtrans %}</h3> <h3>{% trans %}Page list{% endtrans %}</h3>
<ul> <ul>
{% for p in page_list %} {% for p in page_list %}
<li><a href="{{ p.get_absolute_url() }}">{{ p.get_display_name() }}</a></li> <li><a href="{{ p.get_absolute_url() }}">{{ p.display_name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %}
{% trans %}There is no page in this website.{% endtrans %}
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -12,7 +12,10 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import F, OuterRef, Subquery
from django.db.models.functions import Coalesce
# This file contains all the views that concern the page model # This file contains all the views that concern the page model
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
@@ -43,6 +46,20 @@ class CanEditPagePropMixin(CanEditPropMixin):
class PageListView(CanViewMixin, ListView): class PageListView(CanViewMixin, ListView):
model = Page model = Page
template_name = "core/page_list.jinja" template_name = "core/page_list.jinja"
queryset = (
Page.objects.annotate(
display_name=Coalesce(
Subquery(
PageRev.objects.filter(page=OuterRef("id"))
.order_by("-date")
.values("title")[:1]
),
F("name"),
)
)
.prefetch_related("view_groups")
.select_related("parent")
)
class PageView(CanViewMixin, DetailView): class PageView(CanViewMixin, DetailView):

View File

@@ -1,13 +1,19 @@
import math import math
from django import forms from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import ClockedSchedule, PeriodicTask
from phonenumber_field.widgets import RegionalPhoneNumberWidget from phonenumber_field.widgets import RegionalPhoneNumberWidget
from club.widgets.ajax_select import AutoCompleteSelectClub from club.widgets.ajax_select import AutoCompleteSelectClub
from core.models import User from core.models import User
from core.views.forms import NFCTextInput, SelectDate, SelectDateTime from core.views.forms import (
NFCTextInput,
SelectDate,
SelectDateTime,
)
from core.views.widgets.ajax_select import ( from core.views.widgets.ajax_select import (
AutoCompleteSelect, AutoCompleteSelect,
AutoCompleteSelectMultipleGroup, AutoCompleteSelectMultipleGroup,
@@ -158,6 +164,66 @@ class CounterEditForm(forms.ModelForm):
} }
class ProductArchiveForm(forms.Form):
"""Form for automatic product archiving."""
enabled = forms.BooleanField(
label=_("Enabled"),
widget=forms.CheckboxInput(attrs={"class": "switch"}),
required=False,
)
archive_at = forms.DateTimeField(
label=_("Date and time of archiving"), widget=SelectDateTime, required=False
)
def __init__(self, *args, product: Product, **kwargs):
self.product = product
self.instance = PeriodicTask.objects.filter(
task="counter.tasks.archive_product", args=f"[{product.id}]"
).first()
super().__init__(*args, **kwargs)
if self.instance:
self.fields["enabled"].initial = self.instance.enabled
self.fields["archive_at"].initial = self.instance.clocked.clocked_time
def clean(self):
cleaned_data = super().clean()
if cleaned_data["enabled"] is True and cleaned_data["archive_at"] is None:
raise ValidationError(
_(
"Automatic archiving cannot be enabled "
"without providing a archiving date."
)
)
def save(self):
if not self.changed_data:
return
if not self.instance:
PeriodicTask.objects.create(
task="counter.tasks.archive_product",
args=f"[{self.product.id}]",
name=f"Archive product {self.product}",
clocked=ClockedSchedule.objects.create(
clocked_time=self.cleaned_data["archive_at"]
),
enabled=self.cleaned_data["enabled"],
one_off=True,
)
return
if (
"archive_at" in self.changed_data
and self.cleaned_data["archive_at"] is None
):
self.instance.delete()
elif "archive_at" in self.changed_data:
self.instance.clocked.clocked_time = self.cleaned_data["archive_at"]
self.instance.clocked.save()
self.instance.enabled = self.cleaned_data["enabled"]
self.instance.save()
return self.instance
class ProductEditForm(forms.ModelForm): class ProductEditForm(forms.ModelForm):
error_css_class = "error" error_css_class = "error"
required_css_class = "required" required_css_class = "required"
@@ -199,22 +265,19 @@ class ProductEditForm(forms.ModelForm):
queryset=Counter.objects.all(), queryset=Counter.objects.all(),
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, instance=None, **kwargs):
super().__init__(*args, **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()
self.archive_form = ProductArchiveForm(*args, product=self.instance, **kwargs)
def is_valid(self):
return super().is_valid() and self.archive_form.is_valid()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
ret = super().save(*args, **kwargs) ret = super().save(*args, **kwargs)
if self.fields["counters"].initial: self.instance.counters.set(self.cleaned_data["counters"])
# Remove the product from all counter it was added to self.archive_form.save()
# 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 return ret

View File

@@ -445,7 +445,8 @@ class Product(models.Model):
buying_groups = list(self.buying_groups.all()) buying_groups = list(self.buying_groups.all())
if not buying_groups: if not buying_groups:
return True return True
return any(user.is_in_group(pk=group.id) for group in buying_groups) res = any(user.is_in_group(pk=group.id) for group in buying_groups)
return res
@property @property
def profit(self): def profit(self):

13
counter/tasks.py Normal file
View File

@@ -0,0 +1,13 @@
# Create your tasks here
from celery import shared_task
from counter.models import Product
@shared_task
def archive_product(product_id):
product = Product.objects.get(id=product_id)
product.archived = True
product.save()
product.counters.clear()

View File

@@ -0,0 +1,31 @@
{% 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 archiving{% endtrans %}</h3>
<p>
<em>
{%- trans trimmed -%}
Automatic archiving allows you to mark a product as archived
and remove it from all its counters at a specified time and date.
{%- endtrans -%}
</em>
</p>
<fieldset x-data="{enabled: {{ form.archive_form.enabled.initial|tojson }}}">
{{ form.archive_form.as_p() }}
</fieldset>
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form>
{% endblock %}

View File

@@ -147,7 +147,7 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
model = Product model = Product
form_class = ProductEditForm form_class = ProductEditForm
template_name = "core/create.jinja" template_name = "counter/product_form.jinja"
current_tab = "products" current_tab = "products"
@@ -157,7 +157,7 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
model = Product model = Product
form_class = ProductEditForm form_class = ProductEditForm
pk_url_kwarg = "product_id" pk_url_kwarg = "product_id"
template_name = "core/edit.jinja" template_name = "counter/product_form.jinja"
current_tab = "products" current_tab = "products"

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-02 15:56+0200\n" "POT-Creation-Date: 2025-09-14 01:35+0200\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"
@@ -561,6 +561,7 @@ msgstr ""
#: core/templates/core/user_godfathers_tree.jinja #: core/templates/core/user_godfathers_tree.jinja
#: core/templates/core/user_preferences.jinja #: core/templates/core/user_preferences.jinja
#: counter/templates/counter/cash_register_summary.jinja #: counter/templates/counter/cash_register_summary.jinja
#: counter/templates/counter/product_form.jinja
#: forum/templates/forum/reply.jinja #: forum/templates/forum/reply.jinja
#: subscription/templates/subscription/fragments/creation_form.jinja #: subscription/templates/subscription/fragments/creation_form.jinja
#: trombi/templates/trombi/comment.jinja #: trombi/templates/trombi/comment.jinja
@@ -1713,8 +1714,8 @@ msgid ""
"AE UTBM is a voluntary organisation run by UTBM students. It organises " "AE UTBM is a voluntary organisation run by UTBM students. It organises "
"student life at UTBM and manages its student facilities." "student life at UTBM and manages its student facilities."
msgstr "" msgstr ""
"L'AE UTBM est une association bénévole gérée par les étudiants de " "L'AE UTBM est une association bénévole gérée par les étudiants de l'UTBM. "
"l'UTBM. Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie." "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 #: core/templates/core/base/footer.jinja core/templates/core/base/navbar.jinja
msgid "Contacts" msgid "Contacts"
@@ -2157,10 +2158,6 @@ msgstr ""
msgid "Page history" msgid "Page history"
msgstr "Historique de la page" 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 #: core/templates/core/page_prop.jinja
msgid "Page properties" msgid "Page properties"
msgstr "Propriétés de la page" msgstr "Propriétés de la page"
@@ -2896,6 +2893,20 @@ msgstr "Cet UID est invalide"
msgid "User not found" msgid "User not found"
msgstr "Utilisateur non trouvé" msgstr "Utilisateur non trouvé"
#: counter/forms.py
msgid "Enabled"
msgstr "Activé"
#: counter/forms.py
msgid "Date and time of archiving"
msgstr "Date et heure de l'archivage"
#: counter/forms.py
msgid ""
"Automatic archiving cannot be enabled without providing a archiving date."
msgstr ""
"L'archivage automatique ne peut pas activé sans fournir une date d'archivage."
#: counter/forms.py #: counter/forms.py
msgid "" msgid ""
"Describe the product. If it's an event's click, give some insights about it, " "Describe the product. If it's an event's click, give some insights about it, "
@@ -3548,6 +3559,29 @@ msgstr ""
"votre cotisation. Si vous ne renouvelez pas votre cotisation, il n'y aura " "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." "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
#, fuzzy
#| msgid "Product state"
msgid "Product creation"
msgstr "Etat du produit"
#: counter/templates/counter/product_form.jinja
msgid "Automatic archiving"
msgstr "Archivage automatique"
#: counter/templates/counter/product_form.jinja
msgid ""
"Automatic archiving allows you to mark a product as archived and remove it "
"from all its counters at a specified time and date."
msgstr ""
"L'archivage automatique permet de marquer un produit comme archivé et de le "
"retirer de tous ses comptoirs à une heure et une date voulues."
#: counter/templates/counter/product_list.jinja #: counter/templates/counter/product_list.jinja
msgid "Product list" msgid "Product list"
msgstr "Liste des produits" msgstr "Liste des produits"
@@ -5135,10 +5169,6 @@ msgstr "Tee-shirt AE"
msgid "A user with that email address already exists" msgid "A user with that email address already exists"
msgstr "Un utilisateur avec cette adresse email existe déjà" msgstr "Un utilisateur avec cette adresse email existe déjà"
#: subscription/forms.py
msgid "This user didn't fill its birthdate yet."
msgstr "Cet utilisateur n'a pas encore renseigné sa date de naissance"
#: subscription/models.py #: subscription/models.py
msgid "Bad subscription type" msgid "Bad subscription type"
msgstr "Mauvais type de cotisation" msgstr "Mauvais type de cotisation"
@@ -5178,7 +5208,7 @@ msgid ""
"%(user)s received its new %(type)s subscription. It will be active until " "%(user)s received its new %(type)s subscription. It will be active until "
"%(end)s included." "%(end)s included."
msgstr "" msgstr ""
"%(user)s a reçu sa nouvelle cotisaton %(type)s. Elle sera active jusqu'au " "%(user)s a reçu sa nouvelle cotisaton %(type)s. Elle sert active jusqu'au "
"%(end)s inclu." "%(end)s inclu."
#: subscription/templates/subscription/fragments/creation_success.jinja #: subscription/templates/subscription/fragments/creation_success.jinja

View File

@@ -23,8 +23,8 @@ class SelectionDateForm(forms.Form):
class SubscriptionForm(forms.ModelForm): class SubscriptionForm(forms.ModelForm):
def __init__(self, *args, initial=None, **kwargs): def __init__(self, *args, **kwargs):
initial = initial or {} initial = kwargs.pop("initial", {})
if "subscription_type" not in initial: if "subscription_type" not in initial:
initial["subscription_type"] = "deux-semestres" initial["subscription_type"] = "deux-semestres"
if "payment_method" not in initial: if "payment_method" not in initial:
@@ -131,57 +131,8 @@ class SubscriptionExistingUserForm(SubscriptionForm):
"""Form to add a subscription to an existing user.""" """Form to add a subscription to an existing user."""
template_name = "subscription/forms/create_existing_user.html" template_name = "subscription/forms/create_existing_user.html"
required_css_class = "required"
birthdate = forms.fields_for_model(
User,
["date_of_birth"],
widgets={"date_of_birth": SelectDate(attrs={"hidden": True})},
help_texts={"date_of_birth": _("This user didn't fill its birthdate yet.")},
)["date_of_birth"]
class Meta: class Meta:
model = Subscription model = Subscription
fields = ["member", "subscription_type", "payment_method", "location"] fields = ["member", "subscription_type", "payment_method", "location"]
widgets = {"member": AutoCompleteSelectUser} widgets = {"member": AutoCompleteSelectUser}
field_order = [
"member",
"birthdate",
"subscription_type",
"payment_method",
"location",
]
def __init__(self, *args, initial=None, **kwargs):
super().__init__(*args, initial=initial, **kwargs)
self.fields["birthdate"].required = True
if not initial:
return
member: str | None = initial.get("member")
if member and member.isdigit():
member: User | None = User.objects.filter(id=int(member)).first()
else:
member = None
if member and member.date_of_birth:
# if there is an initial member with a birthdate,
# there is no need to ask this to the user
self.fields["birthdate"].initial = member.date_of_birth
elif member:
# if there is an initial member without a birthdate,
# then the field must be displayed
self.fields["birthdate"].widget.attrs.update({"hidden": False})
# if there is no initial member, it means that it will be
# dynamically selected using the AutoCompleteSelectUser widget.
# JS will take care of un-hiding the field if necessary
def save(self, *args, **kwargs):
if self.errors:
return super().save(*args, **kwargs)
if (
self.cleaned_data["birthdate"] is not None
and self.instance.member.date_of_birth is None
):
self.instance.member.date_of_birth = self.cleaned_data["birthdate"]
self.instance.member.save()
return super().save(*args, **kwargs)

View File

@@ -1,5 +1,3 @@
import { userFetchUser } from "#openapi";
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("existing_user_subscription_form", () => ({ Alpine.data("existing_user_subscription_form", () => ({
loading: false, loading: false,
@@ -14,24 +12,13 @@ document.addEventListener("alpine:init", () => {
}, },
async loadProfile(userId: number) { async loadProfile(userId: number) {
const birthdayInput = document.getElementById("id_birthdate") as HTMLInputElement;
if (!Number.isInteger(userId)) { if (!Number.isInteger(userId)) {
this.profileFragment = ""; this.profileFragment = "";
birthdayInput.hidden = true;
return; return;
} }
this.loading = true; this.loading = true;
const [miniProfile, userInfos] = await Promise.all([ const response = await fetch(`/user/${userId}/mini/`);
fetch(`/user/${userId}/mini/`), this.profileFragment = await response.text();
// biome-ignore lint/style/useNamingConvention: api is snake_case
userFetchUser({ path: { user_id: userId } }),
]);
this.profileFragment = await miniProfile.text();
// If the user has no birthdate yet, show the form input
// to fill this info.
// Else keep the input hidden and change its value to the user birthdate
birthdayInput.value = userInfos.data.date_of_birth;
birthdayInput.hidden = userInfos.data.date_of_birth !== null;
this.loading = false; this.loading = false;
}, },
})); }));

View File

@@ -1,14 +1,4 @@
#subscription-form form { #subscription-form form {
margin-top: 0;
.form-content {
margin-top: 0;
}
fieldset p:first-of-type, & > p:first-of-type {
margin-top: 0;
}
.form-content.existing-user { .form-content.existing-user {
max-height: 100%; max-height: 100%;
display: flex; display: flex;
@@ -23,11 +13,6 @@
* then display the user profile right in the middle of the remaining space. */ * then display the user profile right in the middle of the remaining space. */
fieldset { fieldset {
flex: 0 1 auto; flex: 0 1 auto;
p:has(input[hidden]) {
// when the input is hidden, hide the whole label+input+help text group
display: none;
}
} }
#subscription-form-user-mini-profile { #subscription-form-user-mini-profile {

View File

@@ -1,6 +1,6 @@
"""Tests focused on testing subscription creation""" """Tests focused on testing subscription creation"""
from datetime import date, timedelta from datetime import timedelta
from typing import Callable from typing import Callable
import pytest import pytest
@@ -31,26 +31,6 @@ def test_form_existing_user_valid(
): ):
"""Test `SubscriptionExistingUserForm`""" """Test `SubscriptionExistingUserForm`"""
user = user_factory() user = user_factory()
user.date_of_birth = date(year=1967, month=3, day=14)
user.save()
data = {
"member": user,
"birthdate": user.date_of_birth,
"subscription_type": "deux-semestres",
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
}
form = SubscriptionExistingUserForm(data)
assert form.is_valid()
form.save()
user.refresh_from_db()
assert user.is_subscribed
@pytest.mark.django_db
def test_form_existing_user_with_birthdate(settings: SettingsWrapper):
"""Test `SubscriptionExistingUserForm`"""
user = baker.make(User, date_of_birth=None)
data = { data = {
"member": user, "member": user,
"subscription_type": "deux-semestres", "subscription_type": "deux-semestres",
@@ -58,15 +38,11 @@ def test_form_existing_user_with_birthdate(settings: SettingsWrapper):
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0], "payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
} }
form = SubscriptionExistingUserForm(data) form = SubscriptionExistingUserForm(data)
assert not form.is_valid()
data |= {"birthdate": date(year=1967, month=3, day=14)}
form = SubscriptionExistingUserForm(data)
assert form.is_valid() assert form.is_valid()
form.save() form.save()
user.refresh_from_db() user.refresh_from_db()
assert user.is_subscribed assert user.is_subscribed
assert user.date_of_birth == date(year=1967, month=3, day=14)
@pytest.mark.django_db @pytest.mark.django_db
@@ -156,14 +132,6 @@ def test_page_access(
assert res.status_code == status_code assert res.status_code == status_code
@pytest.mark.django_db
def test_page_access_with_get_data(client: Client):
user = old_subscriber_user.make()
client.force_login(baker.make(User, is_superuser=True))
res = client.get(reverse("subscription:subscription", query={"member": user.id}))
assert res.status_code == 200
@pytest.mark.django_db @pytest.mark.django_db
def test_submit_form_existing_user(client: Client, settings: SettingsWrapper): def test_submit_form_existing_user(client: Client, settings: SettingsWrapper):
client.force_login( client.force_login(
@@ -172,12 +140,11 @@ def test_submit_form_existing_user(client: Client, settings: SettingsWrapper):
user_permissions=Permission.objects.filter(codename="add_subscription"), user_permissions=Permission.objects.filter(codename="add_subscription"),
) )
) )
user = old_subscriber_user.make(date_of_birth=date(year=1967, month=3, day=14)) user = old_subscriber_user.make()
response = client.post( response = client.post(
reverse("subscription:fragment-existing-user"), reverse("subscription:fragment-existing-user"),
{ {
"member": user.id, "member": user.id,
"birthdate": user.date_of_birth,
"subscription_type": "deux-semestres", "subscription_type": "deux-semestres",
"location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0], "location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0],
"payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0], "payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],