From 70f5ae4f9c01318ee33e00772a03cdefff173d30 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 18 Nov 2024 18:00:31 +0100 Subject: [PATCH 1/9] Move subscription forms to `subscription/forms.py` --- subscription/forms.py | 88 +++++++++++++++++++++++++++++++++++++++++++ subscription/views.py | 85 +---------------------------------------- 2 files changed, 90 insertions(+), 83 deletions(-) create mode 100644 subscription/forms.py diff --git a/subscription/forms.py b/subscription/forms.py new file mode 100644 index 00000000..f627a6b7 --- /dev/null +++ b/subscription/forms.py @@ -0,0 +1,88 @@ +import random + +from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +from core.models import User +from core.views.forms import SelectDate, SelectDateTime +from core.views.widgets.select import AutoCompleteSelectUser +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 Meta: + model = Subscription + fields = ["member", "subscription_type", "payment_method", "location"] + widgets = {"member": AutoCompleteSelectUser} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["member"].required = False + self.fields |= forms.fields_for_model( + User, + fields=["first_name", "last_name", "email", "date_of_birth"], + widgets={"date_of_birth": SelectDate}, + ) + + def clean_member(self): + subscriber = self.cleaned_data.get("member") + if subscriber: + subscriber = User.objects.filter(id=subscriber.id).first() + return subscriber + + def clean(self): + cleaned_data = super().clean() + if ( + cleaned_data.get("member") is None + and "last_name" not in self.errors.as_data() + and "first_name" not in self.errors.as_data() + and "email" not in self.errors.as_data() + and "date_of_birth" not in self.errors.as_data() + ): + self.errors.pop("member", None) + if self.errors: + return cleaned_data + if User.objects.filter(email=cleaned_data.get("email")).first() is not None: + self.add_error( + "email", + ValidationError(_("A user with that email address already exists")), + ) + else: + u = User( + last_name=self.cleaned_data.get("last_name"), + first_name=self.cleaned_data.get("first_name"), + email=self.cleaned_data.get("email"), + date_of_birth=self.cleaned_data.get("date_of_birth"), + ) + u.generate_username() + u.set_password(str(random.randrange(1000000, 10000000))) + u.save() + cleaned_data["member"] = u + elif cleaned_data.get("member") is not None: + self.errors.pop("last_name", None) + self.errors.pop("first_name", None) + self.errors.pop("email", None) + self.errors.pop("date_of_birth", None) + if cleaned_data.get("member") is None: + # This should be handled here, + # but it is done in the Subscription model's clean method + # TODO investigate why! + raise ValidationError( + _( + "You must either choose an existing " + "user or create a new one properly" + ) + ) + return cleaned_data diff --git a/subscription/views.py b/subscription/views.py index 9a39e7b0..a6ecc294 100644 --- a/subscription/views.py +++ b/subscription/views.py @@ -17,95 +17,14 @@ import secrets from django import forms from django.conf import settings -from django.core.exceptions import PermissionDenied, ValidationError +from django.core.exceptions import PermissionDenied from django.urls import reverse_lazy -from django.utils.translation import gettext_lazy as _ from django.views.generic.edit import CreateView, FormView -from core.models import User -from core.views.forms import SelectDate, SelectDateTime -from core.views.widgets.select import AutoCompleteSelectUser +from subscription.forms import SelectionDateForm, SubscriptionForm 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 Meta: - model = Subscription - fields = ["member", "subscription_type", "payment_method", "location"] - widgets = {"member": AutoCompleteSelectUser} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["member"].required = False - self.fields |= forms.fields_for_model( - User, - fields=["first_name", "last_name", "email", "date_of_birth"], - widgets={"date_of_birth": SelectDate}, - ) - - def clean_member(self): - subscriber = self.cleaned_data.get("member") - if subscriber: - subscriber = User.objects.filter(id=subscriber.id).first() - return subscriber - - def clean(self): - cleaned_data = super().clean() - if ( - cleaned_data.get("member") is None - and "last_name" not in self.errors.as_data() - and "first_name" not in self.errors.as_data() - and "email" not in self.errors.as_data() - and "date_of_birth" not in self.errors.as_data() - ): - self.errors.pop("member", None) - if self.errors: - return cleaned_data - if User.objects.filter(email=cleaned_data.get("email")).first() is not None: - self.add_error( - "email", - ValidationError(_("A user with that email address already exists")), - ) - else: - u = User( - last_name=self.cleaned_data.get("last_name"), - first_name=self.cleaned_data.get("first_name"), - email=self.cleaned_data.get("email"), - date_of_birth=self.cleaned_data.get("date_of_birth"), - ) - u.generate_username() - u.set_password(secrets.token_urlsafe(nbytes=10)) - u.save() - cleaned_data["member"] = u - elif cleaned_data.get("member") is not None: - self.errors.pop("last_name", None) - self.errors.pop("first_name", None) - self.errors.pop("email", None) - self.errors.pop("date_of_birth", None) - if cleaned_data.get("member") is None: - # This should be handled here, - # but it is done in the Subscription model's clean method - # TODO investigate why! - raise ValidationError( - _( - "You must either choose an existing " - "user or create a new one properly" - ) - ) - return cleaned_data - - class NewSubscription(CreateView): template_name = "subscription/subscription.jinja" form_class = SubscriptionForm From 75406f7b58ce5664bfa5d20baa003924d6090ad0 Mon Sep 17 00:00:00 2001 From: imperosol Date: Tue, 26 Nov 2024 01:49:34 +0100 Subject: [PATCH 2/9] Tabs jinja component --- core/static/core/style.scss | 32 +++++++++++++++ core/templates/core/macros.jinja | 67 +++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 21481454..664c1b99 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -370,6 +370,38 @@ a:not(.button) { } } + .tabs { + .tab-headers { + display: flex; + flex-flow: row wrap; + .tab-header{ + margin: 0; + flex: 1 1; + border-radius: 5px 5px 0 0; + font-size: 100%; + + @media (max-width: 800px) { + flex-wrap: wrap; + } + &.active { + background-color: $white-color; + border: 1px solid $primary-neutral-dark-color; + border-bottom: none; + } + &:not(.active) { + background-color: $primary-neutral-dark-color; + color: darken($white-color, 5%); + &:hover { + background-color: lighten($primary-neutral-dark-color, 15%); + } + } + } + } + section { + padding: 20px; + } + } + .tool_bar { overflow: auto; padding: 4px; diff --git a/core/templates/core/macros.jinja b/core/templates/core/macros.jinja index 43a90d07..021f1918 100644 --- a/core/templates/core/macros.jinja +++ b/core/templates/core/macros.jinja @@ -170,12 +170,12 @@ {% endmacro %} {% macro paginate_htmx(current_page, paginator) %} - {# Add pagination buttons for pages without Alpine but supporting framgents. + {# Add pagination buttons for pages without Alpine but supporting fragments. This must be coupled with a view that handles pagination - with the Django Paginator object and supports framgents. + with the Django Paginator object and supports fragments. - The relpaced fragment will be #content so make sure you are calling this macro inside your content block. + The replaced fragment will be #content so make sure you are calling this macro inside your content block. Parameters: current_page (django.core.paginator.Page): the current page object @@ -247,9 +247,9 @@ {% macro select_all_checkbox(form_id) %} +{% endblock %} +{% block additional_css %} + + +{% endblock %} + +{% macro form_fragment(form_object, post_url) %} + {# Include the form fragment inside a with block, + in order to inject the right form in the right place #} + {% with form=form_object, post_url=post_url %} + {% include "subscription/fragments/creation_form.jinja" %} + {% endwith %} +{% endmacro %} + {% block content %}

{% trans %}New subscription{% endtrans %}

-
-
- {% csrf_token %} - {{ form.non_field_errors() }} -

{{ form.member.errors }} {{ form.member }}

-
-

{{ form.first_name.errors }} {{ form.first_name }}

-

{{ form.last_name.errors }} {{ form.last_name }}

-

{{ form.email.errors }} {{ form.email }}

-

{{ form.date_of_birth.errors }} {{ form.date_of_birth }}

-
-

{{ form.subscription_type.errors }} {{ form.subscription_type }}

-

{{ form.payment_method.errors }} {{ - form.payment_method }}

-

{% trans %}Eboutic is reserved to specific users. In doubt, don't use it.{% endtrans %}

-

{{ form.location.errors }} {{ form.location }}

-

-
-{% endblock %} - -{% block script %} - {{ super() }} - +
+ {{ tabs([ + (_("Existing member"), form_fragment(existing_user_form, existing_user_post_url)), + (_("New member"), form_fragment(new_user_form, new_user_post_url)), + ]) }} +
{% endblock %} diff --git a/subscription/urls.py b/subscription/urls.py index 6bd1fa65..47dbf21e 100644 --- a/subscription/urls.py +++ b/subscription/urls.py @@ -15,10 +15,31 @@ from django.urls import path -from subscription.views import NewSubscription, SubscriptionsStatsView +from subscription.views import ( + CreateSubscriptionExistingUserFragment, + CreateSubscriptionNewUserFragment, + NewSubscription, + SubscriptionCreatedFragment, + SubscriptionsStatsView, +) urlpatterns = [ # Subscription views path("", NewSubscription.as_view(), name="subscription"), + path( + "fragment/existing-user/", + CreateSubscriptionExistingUserFragment.as_view(), + name="fragment-existing-user", + ), + path( + "fragment/new-user/", + CreateSubscriptionNewUserFragment.as_view(), + name="fragment-new-user", + ), + path( + "fragment//creation-success", + SubscriptionCreatedFragment.as_view(), + name="creation-success", + ), path("stats/", SubscriptionsStatsView.as_view(), name="stats"), ] diff --git a/subscription/views.py b/subscription/views.py index a6ecc294..3e8cf0f1 100644 --- a/subscription/views.py +++ b/subscription/views.py @@ -13,85 +13,94 @@ # # -import secrets - -from django import forms from django.conf import settings +from django.contrib.auth.mixins import UserPassesTestMixin from django.core.exceptions import PermissionDenied -from django.urls import reverse_lazy -from django.views.generic.edit import CreateView, FormView +from django.urls import reverse, reverse_lazy +from django.utils.timezone import localdate +from django.views.generic import CreateView, DetailView, TemplateView +from django.views.generic.edit import FormView -from subscription.forms import SelectionDateForm, SubscriptionForm +from subscription.forms import ( + SelectionDateForm, + SubscriptionExistingUserForm, + SubscriptionNewUserForm, +) from subscription.models import Subscription -class NewSubscription(CreateView): +class CanCreateSubscriptionMixin(UserPassesTestMixin): + def test_func(self): + return self.request.user.can_create_subscription + + +class NewSubscription(CanCreateSubscriptionMixin, TemplateView): template_name = "subscription/subscription.jinja" - form_class = SubscriptionForm - def dispatch(self, request, *arg, **kwargs): - if request.user.can_create_subscription: - return super().dispatch(request, *arg, **kwargs) - raise PermissionDenied + def get_context_data(self, **kwargs): + return super().get_context_data(**kwargs) | { + "existing_user_form": SubscriptionExistingUserForm(), + "new_user_form": SubscriptionNewUserForm(), + "existing_user_post_url": reverse("subscription:fragment-existing-user"), + "new_user_post_url": reverse("subscription:fragment-new-user"), + } - def get_initial(self): - if "member" in self.request.GET: - return { - "member": self.request.GET["member"], - "subscription_type": "deux-semestres", - } - return {"subscription_type": "deux-semestres"} - def form_valid(self, form): - form.instance.subscription_start = Subscription.compute_start( - duration=settings.SITH_SUBSCRIPTIONS[form.instance.subscription_type][ - "duration" - ], - user=form.instance.member, +class CreateSubscriptionFragment(CanCreateSubscriptionMixin, CreateView): + template_name = "subscription/fragments/creation_form.jinja" + + def get_success_url(self): + return reverse( + "subscription:creation-success", kwargs={"subscription_id": self.object.id} ) - form.instance.subscription_end = Subscription.compute_end( - duration=settings.SITH_SUBSCRIPTIONS[form.instance.subscription_type][ - "duration" - ], - start=form.instance.subscription_start, - user=form.instance.member, - ) - return super().form_valid(form) + + +class CreateSubscriptionExistingUserFragment(CreateSubscriptionFragment): + """Create a subscription for a user who already exists.""" + + form_class = SubscriptionExistingUserForm + extra_context = {"post_url": reverse_lazy("subscription:fragment-existing-user")} + + +class CreateSubscriptionNewUserFragment(CreateSubscriptionFragment): + """Create a subscription for a user who already exists.""" + + form_class = SubscriptionNewUserForm + extra_context = {"post_url": reverse_lazy("subscription:fragment-new-user")} + + +class SubscriptionCreatedFragment(CanCreateSubscriptionMixin, DetailView): + template_name = "subscription/fragments/creation_success.jinja" + model = Subscription + pk_url_kwarg = "subscription_id" + context_object_name = "subscription" class SubscriptionsStatsView(FormView): template_name = "subscription/stats.jinja" form_class = SelectionDateForm + success_url = reverse_lazy("subscriptions:stats") def dispatch(self, request, *arg, **kwargs): - import datetime - - self.start_date = datetime.datetime.today() + self.start_date = localdate() self.end_date = self.start_date - res = super().dispatch(request, *arg, **kwargs) if request.user.is_root or request.user.is_board_member: - return res + return super().dispatch(request, *arg, **kwargs) 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"] - res = super().post(request, *args, **kwargs) - if request.user.is_root or request.user.is_board_member: - return res - raise PermissionDenied + return super().post(request, *args, **kwargs) def get_initial(self): - init = { + 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"), } - return init def get_context_data(self, **kwargs): - from subscription.models import Subscription - kwargs = super().get_context_data(**kwargs) kwargs["subscriptions_total"] = Subscription.objects.filter( subscription_end__gte=self.end_date, subscription_start__lte=self.start_date @@ -100,6 +109,3 @@ class SubscriptionsStatsView(FormView): kwargs["payment_types"] = settings.SITH_COUNTER_PAYMENT_METHOD kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS return kwargs - - def get_success_url(self, **kwargs): - return reverse_lazy("subscriptions:stats") diff --git a/vite.config.mts b/vite.config.mts index 564781f3..955590b7 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -85,6 +85,7 @@ export default defineConfig((config: UserConfig) => { inject({ // biome-ignore lint/style/useNamingConvention: that's how it's called Alpine: "alpinejs", + htmx: "htmx.org", }), viteStaticCopy({ targets: [ From 8dcfc604a027127b7cf6a6737b0d880aad7d356b Mon Sep 17 00:00:00 2001 From: imperosol Date: Tue, 26 Nov 2024 17:16:57 +0100 Subject: [PATCH 4/9] write tests --- subscription/tests/__init__.py | 0 .../{tests.py => tests/test_dates.py} | 2 + subscription/tests/test_new_susbcription.py | 151 ++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 subscription/tests/__init__.py rename subscription/{tests.py => tests/test_dates.py} (99%) create mode 100644 subscription/tests/test_new_susbcription.py diff --git a/subscription/tests/__init__.py b/subscription/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/subscription/tests.py b/subscription/tests/test_dates.py similarity index 99% rename from subscription/tests.py rename to subscription/tests/test_dates.py index d853c55a..181246b1 100644 --- a/subscription/tests.py +++ b/subscription/tests/test_dates.py @@ -12,6 +12,8 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # +"""Tests focused on the computing of subscription end, start and duration""" + from datetime import date import freezegun diff --git a/subscription/tests/test_new_susbcription.py b/subscription/tests/test_new_susbcription.py new file mode 100644 index 00000000..8ea51d68 --- /dev/null +++ b/subscription/tests/test_new_susbcription.py @@ -0,0 +1,151 @@ +"""Tests focused on testing subscription creation""" + +from datetime import timedelta +from typing import Callable + +import pytest +from dateutil.relativedelta import relativedelta +from django.test import Client +from django.urls import reverse +from django.utils.timezone import localdate +from model_bakery import baker +from pytest_django.asserts import assertRedirects +from pytest_django.fixtures import SettingsWrapper + +from core.baker_recipes import board_user, old_subscriber_user, subscriber_user +from core.models import User +from subscription.forms import SubscriptionExistingUserForm, SubscriptionNewUserForm + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "user_factory", + [old_subscriber_user.make, lambda: baker.make(User)], +) +def test_form_existing_user_valid( + user_factory: Callable[[], User], settings: SettingsWrapper +): + """Test `SubscriptionExistingUserForm`""" + user = user_factory() + data = { + "member": user, + "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_invalid(settings: SettingsWrapper): + """Test `SubscriptionExistingUserForm`, with users that shouldn't subscribe.""" + user = subscriber_user.make() + # make sure the current subscription will end in a long time + last_sub = user.subscriptions.order_by("subscription_end").last() + last_sub.subscription_end = localdate() + timedelta(weeks=50) + last_sub.save() + data = { + "member": user, + "subscription_type": "deux-semestres", + "location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0], + "payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0], + } + form = SubscriptionExistingUserForm(data) + + assert not form.is_valid() + with pytest.raises(ValueError): + form.save() + + +@pytest.mark.django_db +def test_form_new_user(settings: SettingsWrapper): + data = { + "first_name": "John", + "last_name": "Doe", + "email": "jdoe@utbm.fr", + "date_of_birth": localdate() - relativedelta(years=18), + "subscription_type": "deux-semestres", + "location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0], + "payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0], + } + form = SubscriptionNewUserForm(data) + assert form.is_valid() + form.save() + user = User.objects.get(email="jdoe@utbm.fr") + assert user.username == "jdoe" + assert user.is_subscribed + + # if trying to instantiate a new form with the same email, + # it should fail + form = SubscriptionNewUserForm(data) + assert not form.is_valid() + with pytest.raises(ValueError): + form.save() + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "user_factory", [lambda: baker.make(User, is_superuser=True), board_user.make] +) +def test_load_page(client: Client, user_factory: Callable[[], User]): + """Just check the page doesn't crash.""" + client.force_login(user_factory()) + res = client.get(reverse("subscription:subscription")) + assert res.status_code == 200 + + +@pytest.mark.django_db +def test_submit_form_existing_user(client: Client, settings: SettingsWrapper): + client.force_login(board_user.make()) + user = old_subscriber_user.make() + response = client.post( + reverse("subscription:fragment-existing-user"), + { + "member": user.id, + "subscription_type": "deux-semestres", + "location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0], + "payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0], + }, + ) + user.refresh_from_db() + assert user.is_subscribed + current_subscription = user.subscriptions.order_by("-subscription_start").first() + assertRedirects( + response, + reverse( + "subscription:creation-success", + kwargs={"subscription_id": current_subscription.id}, + ), + ) + + +@pytest.mark.django_db +def test_submit_form_new_user(client: Client, settings: SettingsWrapper): + client.force_login(board_user.make()) + response = client.post( + reverse("subscription:fragment-new-user"), + { + "first_name": "John", + "last_name": "Doe", + "email": "jdoe@utbm.fr", + "date_of_birth": localdate() - relativedelta(years=18), + "subscription_type": "deux-semestres", + "location": settings.SITH_SUBSCRIPTION_LOCATIONS[0][0], + "payment_method": settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0], + }, + ) + user = User.objects.get(email="jdoe@utbm.fr") + assert user.is_subscribed + current_subscription = user.subscriptions.order_by("-subscription_start").first() + assertRedirects( + response, + reverse( + "subscription:creation-success", + kwargs={"subscription_id": current_subscription.id}, + ), + ) From 83bb4b3b129ccd52ccc19a7fc5435572756c09bb Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 15:38:40 +0100 Subject: [PATCH 5/9] add translation --- locale/fr/LC_MESSAGES/django.po | 331 +++++++++++++++++--------------- 1 file changed, 174 insertions(+), 157 deletions(-) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 4b717fd4..aab13378 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-19 00:47+0100\n" +"POT-Creation-Date: 2024-11-27 15:24+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -40,7 +40,7 @@ msgstr "code postal" msgid "country" msgstr "pays" -#: accounting/models.py:67 core/models.py:390 +#: accounting/models.py:67 core/models.py:391 msgid "phone" msgstr "téléphone" @@ -126,8 +126,8 @@ msgstr "numéro" msgid "journal" msgstr "classeur" -#: accounting/models.py:256 core/models.py:945 core/models.py:1456 -#: core/models.py:1501 core/models.py:1530 core/models.py:1554 +#: accounting/models.py:256 core/models.py:956 core/models.py:1467 +#: core/models.py:1512 core/models.py:1541 core/models.py:1565 #: counter/models.py:689 counter/models.py:793 counter/models.py:997 #: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312 #: forum/models.py:413 @@ -165,7 +165,7 @@ msgid "accounting type" msgstr "type comptable" #: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460 -#: accounting/models.py:492 core/models.py:1529 core/models.py:1555 +#: accounting/models.py:492 core/models.py:1540 core/models.py:1566 #: counter/models.py:759 msgid "label" msgstr "étiquette" @@ -218,7 +218,7 @@ msgstr "Compte" msgid "Company" msgstr "Entreprise" -#: accounting/models.py:307 core/models.py:337 sith/settings.py:421 +#: accounting/models.py:307 core/models.py:338 sith/settings.py:421 msgid "Other" msgstr "Autre" @@ -372,7 +372,7 @@ msgstr "Compte en banque : " #: core/templates/core/user_preferences.jinja:48 #: counter/templates/counter/last_ops.jinja:35 #: counter/templates/counter/last_ops.jinja:65 -#: election/templates/election/election_detail.jinja:187 +#: election/templates/election/election_detail.jinja:191 #: forum/templates/forum/macros.jinja:21 #: launderette/templates/launderette/launderette_admin.jinja:16 #: launderette/views.py:210 pedagogy/templates/pedagogy/guide.jinja:99 @@ -424,7 +424,7 @@ msgstr "Nouveau compte club" #: counter/templates/counter/counter_list.jinja:17 #: counter/templates/counter/counter_list.jinja:33 #: counter/templates/counter/counter_list.jinja:49 -#: election/templates/election/election_detail.jinja:184 +#: election/templates/election/election_detail.jinja:188 #: forum/templates/forum/macros.jinja:20 forum/templates/forum/macros.jinja:62 #: launderette/templates/launderette/launderette_list.jinja:16 #: pedagogy/templates/pedagogy/guide.jinja:98 @@ -774,7 +774,7 @@ msgstr "Opération liée : " #: core/templates/core/user_preferences.jinja:65 #: counter/templates/counter/cash_register_summary.jinja:28 #: forum/templates/forum/reply.jinja:39 -#: subscription/templates/subscription/subscription.jinja:25 +#: subscription/templates/subscription/fragments/creation_form.jinja:9 #: trombi/templates/trombi/comment.jinja:26 #: trombi/templates/trombi/edit_profile.jinja:13 #: trombi/templates/trombi/user_tools.jinja:13 @@ -956,7 +956,7 @@ msgid "Begin date" msgstr "Date de début" #: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:206 -#: election/views.py:170 subscription/views.py:38 +#: election/views.py:170 subscription/forms.py:21 msgid "End date" msgstr "Date de fin" @@ -1025,11 +1025,11 @@ msgstr "actif" msgid "short description" msgstr "description courte" -#: club/models.py:81 core/models.py:392 +#: club/models.py:81 core/models.py:393 msgid "address" msgstr "Adresse" -#: club/models.py:98 core/models.py:303 +#: club/models.py:98 core/models.py:304 msgid "home" msgstr "home" @@ -1048,12 +1048,12 @@ msgstr "Un club avec ce nom UNIX existe déjà." msgid "user" msgstr "nom d'utilisateur" -#: club/models.py:354 core/models.py:356 election/models.py:178 +#: club/models.py:354 core/models.py:357 election/models.py:178 #: election/models.py:212 trombi/models.py:210 msgid "role" msgstr "rôle" -#: club/models.py:359 core/models.py:89 counter/models.py:298 +#: club/models.py:359 core/models.py:90 counter/models.py:298 #: counter/models.py:329 election/models.py:13 election/models.py:115 #: election/models.py:188 forum/models.py:61 forum/models.py:245 msgid "description" @@ -1068,7 +1068,7 @@ msgid "Enter a valid address. Only the root of the address is needed." msgstr "" "Entrez une adresse valide. Seule la racine de l'adresse est nécessaire." -#: club/models.py:427 com/models.py:82 com/models.py:309 core/models.py:946 +#: club/models.py:427 com/models.py:82 com/models.py:309 core/models.py:957 msgid "is moderated" msgstr "est modéré" @@ -1334,6 +1334,7 @@ msgid "No mailing list existing for this club" msgstr "Aucune mailing liste n'existe pour ce club" #: club/templates/club/mailing.jinja:72 +#: subscription/templates/subscription/subscription.jinja:34 msgid "New member" msgstr "Nouveau membre" @@ -1439,7 +1440,7 @@ msgstr "résumé" msgid "content" msgstr "contenu" -#: com/models.py:71 core/models.py:1499 launderette/models.py:88 +#: com/models.py:71 core/models.py:1510 launderette/models.py:88 #: launderette/models.py:124 launderette/models.py:167 msgid "type" msgstr "type" @@ -1489,7 +1490,7 @@ msgstr "weekmail" msgid "rank" msgstr "rang" -#: com/models.py:295 core/models.py:911 core/models.py:961 +#: com/models.py:295 core/models.py:922 core/models.py:972 msgid "file" msgstr "fichier" @@ -1917,7 +1918,7 @@ msgid "Format: 16:9 | Resolution: 1920x1080" msgstr "Format : 16:9 | Résolution : 1920x1080" #: com/views.py:78 com/views.py:199 election/views.py:167 -#: subscription/views.py:35 +#: subscription/forms.py:18 msgid "Start date" msgstr "Date de début" @@ -1972,30 +1973,30 @@ msgstr "" "Vous devez êtres un membre du bureau du club sélectionné pour poster dans le " "Weekmail." -#: core/models.py:84 +#: core/models.py:85 msgid "meta group status" msgstr "status du meta-groupe" -#: core/models.py:86 +#: core/models.py:87 msgid "Whether a group is a meta group or not" msgstr "Si un groupe est un meta-groupe ou pas" -#: core/models.py:172 +#: core/models.py:173 #, python-format msgid "%(value)s is not a valid promo (between 0 and %(end)s)" msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)" -#: core/models.py:256 +#: core/models.py:257 msgid "username" msgstr "nom d'utilisateur" -#: core/models.py:260 +#: core/models.py:261 msgid "Required. 254 characters or fewer. Letters, digits and ./+/-/_ only." msgstr "" "Requis. Pas plus de 254 caractères. Uniquement des lettres, numéros, et ./" "+/-/_" -#: core/models.py:266 +#: core/models.py:267 msgid "" "Enter a valid username. This value may contain only letters, numbers and ./" "+/-/_ characters." @@ -2003,43 +2004,43 @@ msgstr "" "Entrez un nom d'utilisateur correct. Uniquement des lettres, numéros, et ./" "+/-/_" -#: core/models.py:272 +#: core/models.py:273 msgid "A user with that username already exists." msgstr "Un utilisateur de ce nom existe déjà" -#: core/models.py:274 +#: core/models.py:275 msgid "first name" msgstr "Prénom" -#: core/models.py:275 +#: core/models.py:276 msgid "last name" msgstr "Nom" -#: core/models.py:276 +#: core/models.py:277 msgid "email address" msgstr "adresse email" -#: core/models.py:277 +#: core/models.py:278 msgid "date of birth" msgstr "date de naissance" -#: core/models.py:278 +#: core/models.py:279 msgid "nick name" msgstr "surnom" -#: core/models.py:280 +#: core/models.py:281 msgid "staff status" msgstr "status \"staff\"" -#: core/models.py:282 +#: core/models.py:283 msgid "Designates whether the user can log into this admin site." msgstr "Est-ce que l'utilisateur peut se logger à la partie admin du site." -#: core/models.py:285 +#: core/models.py:286 msgid "active" msgstr "actif" -#: core/models.py:288 +#: core/models.py:289 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -2047,163 +2048,163 @@ msgstr "" "Est-ce que l'utilisateur doit être traité comme actif. Désélectionnez au " "lieu de supprimer les comptes." -#: core/models.py:292 +#: core/models.py:293 msgid "date joined" msgstr "date d'inscription" -#: core/models.py:293 +#: core/models.py:294 msgid "last update" msgstr "dernière mise à jour" -#: core/models.py:295 +#: core/models.py:296 msgid "superuser" msgstr "super-utilisateur" -#: core/models.py:297 +#: core/models.py:298 msgid "Designates whether this user is a superuser. " msgstr "Est-ce que l'utilisateur est super-utilisateur." -#: core/models.py:311 +#: core/models.py:312 msgid "profile" msgstr "profil" -#: core/models.py:319 +#: core/models.py:320 msgid "avatar" msgstr "avatar" -#: core/models.py:327 +#: core/models.py:328 msgid "scrub" msgstr "blouse" -#: core/models.py:333 +#: core/models.py:334 msgid "sex" msgstr "Genre" -#: core/models.py:337 +#: core/models.py:338 msgid "Man" msgstr "Homme" -#: core/models.py:337 +#: core/models.py:338 msgid "Woman" msgstr "Femme" -#: core/models.py:339 +#: core/models.py:340 msgid "pronouns" msgstr "pronoms" -#: core/models.py:341 +#: core/models.py:342 msgid "tshirt size" msgstr "taille de t-shirt" -#: core/models.py:344 +#: core/models.py:345 msgid "-" msgstr "-" -#: core/models.py:345 +#: core/models.py:346 msgid "XS" msgstr "XS" -#: core/models.py:346 +#: core/models.py:347 msgid "S" msgstr "S" -#: core/models.py:347 +#: core/models.py:348 msgid "M" msgstr "M" -#: core/models.py:348 +#: core/models.py:349 msgid "L" msgstr "L" -#: core/models.py:349 +#: core/models.py:350 msgid "XL" msgstr "XL" -#: core/models.py:350 +#: core/models.py:351 msgid "XXL" msgstr "XXL" -#: core/models.py:351 +#: core/models.py:352 msgid "XXXL" msgstr "XXXL" -#: core/models.py:359 +#: core/models.py:360 msgid "Student" msgstr "Étudiant" -#: core/models.py:360 +#: core/models.py:361 msgid "Administrative agent" msgstr "Personnel administratif" -#: core/models.py:361 +#: core/models.py:362 msgid "Teacher" msgstr "Enseignant" -#: core/models.py:362 +#: core/models.py:363 msgid "Agent" msgstr "Personnel" -#: core/models.py:363 +#: core/models.py:364 msgid "Doctor" msgstr "Doctorant" -#: core/models.py:364 +#: core/models.py:365 msgid "Former student" msgstr "Ancien étudiant" -#: core/models.py:365 +#: core/models.py:366 msgid "Service" msgstr "Service" -#: core/models.py:371 +#: core/models.py:372 msgid "department" msgstr "département" -#: core/models.py:378 +#: core/models.py:379 msgid "dpt option" msgstr "Filière" -#: core/models.py:380 pedagogy/models.py:70 pedagogy/models.py:294 +#: core/models.py:381 pedagogy/models.py:70 pedagogy/models.py:294 msgid "semester" msgstr "semestre" -#: core/models.py:381 +#: core/models.py:382 msgid "quote" msgstr "citation" -#: core/models.py:382 +#: core/models.py:383 msgid "school" msgstr "école" -#: core/models.py:384 +#: core/models.py:385 msgid "promo" msgstr "promo" -#: core/models.py:387 +#: core/models.py:388 msgid "forum signature" msgstr "signature du forum" -#: core/models.py:389 +#: core/models.py:390 msgid "second email address" msgstr "adresse email secondaire" -#: core/models.py:391 +#: core/models.py:392 msgid "parent phone" msgstr "téléphone des parents" -#: core/models.py:394 +#: core/models.py:395 msgid "parent address" msgstr "adresse des parents" -#: core/models.py:397 +#: core/models.py:398 msgid "is subscriber viewable" msgstr "profil visible par les cotisants" -#: core/models.py:591 +#: core/models.py:594 msgid "A user with that username already exists" msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" -#: core/models.py:750 core/templates/core/macros.jinja:75 +#: core/models.py:761 core/templates/core/macros.jinja:75 #: core/templates/core/macros.jinja:77 core/templates/core/macros.jinja:78 #: core/templates/core/user_detail.jinja:100 #: core/templates/core/user_detail.jinja:101 @@ -2214,8 +2215,8 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" #: core/templates/core/user_detail.jinja:112 #: core/templates/core/user_detail.jinja:113 #: core/templates/core/user_edit.jinja:21 -#: election/templates/election/election_detail.jinja:132 -#: election/templates/election/election_detail.jinja:134 +#: election/templates/election/election_detail.jinja:136 +#: election/templates/election/election_detail.jinja:138 #: forum/templates/forum/macros.jinja:105 #: forum/templates/forum/macros.jinja:107 #: forum/templates/forum/macros.jinja:109 @@ -2223,101 +2224,101 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" msgid "Profile" msgstr "Profil" -#: core/models.py:861 +#: core/models.py:872 msgid "Visitor" msgstr "Visiteur" -#: core/models.py:868 +#: core/models.py:879 msgid "receive the Weekmail" msgstr "recevoir le Weekmail" -#: core/models.py:869 +#: core/models.py:880 msgid "show your stats to others" msgstr "montrez vos statistiques aux autres" -#: core/models.py:871 +#: core/models.py:882 msgid "get a notification for every click" msgstr "avoir une notification pour chaque click" -#: core/models.py:874 +#: core/models.py:885 msgid "get a notification for every refilling" msgstr "avoir une notification pour chaque rechargement" -#: core/models.py:900 sas/forms.py:81 +#: core/models.py:911 sas/forms.py:81 msgid "file name" msgstr "nom du fichier" -#: core/models.py:904 core/models.py:1257 +#: core/models.py:915 core/models.py:1268 msgid "parent" msgstr "parent" -#: core/models.py:918 +#: core/models.py:929 msgid "compressed file" msgstr "version allégée" -#: core/models.py:925 +#: core/models.py:936 msgid "thumbnail" msgstr "miniature" -#: core/models.py:933 core/models.py:950 +#: core/models.py:944 core/models.py:961 msgid "owner" msgstr "propriétaire" -#: core/models.py:937 core/models.py:1274 +#: core/models.py:948 core/models.py:1285 msgid "edit group" msgstr "groupe d'édition" -#: core/models.py:940 core/models.py:1277 +#: core/models.py:951 core/models.py:1288 msgid "view group" msgstr "groupe de vue" -#: core/models.py:942 +#: core/models.py:953 msgid "is folder" msgstr "est un dossier" -#: core/models.py:943 +#: core/models.py:954 msgid "mime type" msgstr "type mime" -#: core/models.py:944 +#: core/models.py:955 msgid "size" msgstr "taille" -#: core/models.py:955 +#: core/models.py:966 msgid "asked for removal" msgstr "retrait demandé" -#: core/models.py:957 +#: core/models.py:968 msgid "is in the SAS" msgstr "est dans le SAS" -#: core/models.py:1026 +#: core/models.py:1037 msgid "Character '/' not authorized in name" msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier" -#: core/models.py:1028 core/models.py:1032 +#: core/models.py:1039 core/models.py:1043 msgid "Loop in folder tree" msgstr "Boucle dans l'arborescence des dossiers" -#: core/models.py:1035 +#: core/models.py:1046 msgid "You can not make a file be a children of a non folder file" msgstr "" "Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas " "un dossier" -#: core/models.py:1046 +#: core/models.py:1057 msgid "Duplicate file" msgstr "Un fichier de ce nom existe déjà" -#: core/models.py:1063 +#: core/models.py:1074 msgid "You must provide a file" msgstr "Vous devez fournir un fichier" -#: core/models.py:1240 +#: core/models.py:1251 msgid "page unix name" msgstr "nom unix de la page" -#: core/models.py:1246 +#: core/models.py:1257 msgid "" "Enter a valid page name. This value may contain only unaccented letters, " "numbers and ./+/-/_ characters." @@ -2325,55 +2326,55 @@ msgstr "" "Entrez un nom de page correct. Uniquement des lettres non accentuées, " "numéros, et ./+/-/_" -#: core/models.py:1264 +#: core/models.py:1275 msgid "page name" msgstr "nom de la page" -#: core/models.py:1269 +#: core/models.py:1280 msgid "owner group" msgstr "groupe propriétaire" -#: core/models.py:1282 +#: core/models.py:1293 msgid "lock user" msgstr "utilisateur bloquant" -#: core/models.py:1289 +#: core/models.py:1300 msgid "lock_timeout" msgstr "décompte du déblocage" -#: core/models.py:1339 +#: core/models.py:1350 msgid "Duplicate page" msgstr "Une page de ce nom existe déjà" -#: core/models.py:1342 +#: core/models.py:1353 msgid "Loop in page tree" msgstr "Boucle dans l'arborescence des pages" -#: core/models.py:1453 +#: core/models.py:1464 msgid "revision" msgstr "révision" -#: core/models.py:1454 +#: core/models.py:1465 msgid "page title" msgstr "titre de la page" -#: core/models.py:1455 +#: core/models.py:1466 msgid "page content" msgstr "contenu de la page" -#: core/models.py:1496 +#: core/models.py:1507 msgid "url" msgstr "url" -#: core/models.py:1497 +#: core/models.py:1508 msgid "param" msgstr "param" -#: core/models.py:1502 +#: core/models.py:1513 msgid "viewed" msgstr "vue" -#: core/models.py:1560 +#: core/models.py:1571 msgid "operation type" msgstr "type d'opération" @@ -2393,27 +2394,27 @@ msgstr "500, Erreur Serveur" msgid "Welcome!" msgstr "Bienvenue !" -#: core/templates/core/base.jinja:104 core/templates/core/base/navbar.jinja:43 +#: core/templates/core/base.jinja:105 core/templates/core/base/navbar.jinja:43 msgid "Contacts" msgstr "Contacts" -#: core/templates/core/base.jinja:105 +#: core/templates/core/base.jinja:106 msgid "Legal notices" msgstr "Mentions légales" -#: core/templates/core/base.jinja:106 +#: core/templates/core/base.jinja:107 msgid "Intellectual property" msgstr "Propriété intellectuelle" -#: core/templates/core/base.jinja:107 +#: core/templates/core/base.jinja:108 msgid "Help & Documentation" msgstr "Aide & Documentation" -#: core/templates/core/base.jinja:108 +#: core/templates/core/base.jinja:109 msgid "R&D" msgstr "R&D" -#: core/templates/core/base.jinja:111 +#: core/templates/core/base.jinja:112 msgid "Site created by the IT Department of the AE" msgstr "Site réalisé par le Pôle Informatique de l'AE" @@ -3135,8 +3136,8 @@ msgid "Not subscribed" msgstr "Non cotisant" #: core/templates/core/user_detail.jinja:162 -#: subscription/templates/subscription/subscription.jinja:4 -#: subscription/templates/subscription/subscription.jinja:8 +#: subscription/templates/subscription/subscription.jinja:6 +#: subscription/templates/subscription/subscription.jinja:31 msgid "New subscription" msgstr "Nouvelle cotisation" @@ -4512,7 +4513,7 @@ msgstr "candidature" #: election/templates/election/candidate_form.jinja:4 #: election/templates/election/candidate_form.jinja:13 -#: election/templates/election/election_detail.jinja:175 +#: election/templates/election/election_detail.jinja:179 msgid "Candidate" msgstr "Candidater" @@ -4520,20 +4521,20 @@ msgstr "Candidater" msgid "Candidature are closed for this election" msgstr "Les candidatures sont fermées pour cette élection" -#: election/templates/election/election_detail.jinja:19 +#: election/templates/election/election_detail.jinja:23 msgid "Polls close " msgstr "Votes fermés" -#: election/templates/election/election_detail.jinja:21 +#: election/templates/election/election_detail.jinja:25 msgid "Polls closed " msgstr "Votes fermés" -#: election/templates/election/election_detail.jinja:23 +#: election/templates/election/election_detail.jinja:27 msgid "Polls will open " msgstr "Les votes ouvriront " -#: election/templates/election/election_detail.jinja:25 #: election/templates/election/election_detail.jinja:29 +#: election/templates/election/election_detail.jinja:33 #: election/templates/election/election_list.jinja:32 #: election/templates/election/election_list.jinja:35 #: election/templates/election/election_list.jinja:40 @@ -4542,58 +4543,58 @@ msgstr "Les votes ouvriront " msgid " at " msgstr " à " -#: election/templates/election/election_detail.jinja:26 +#: election/templates/election/election_detail.jinja:30 msgid "and will close " msgstr "et fermeront" -#: election/templates/election/election_detail.jinja:34 +#: election/templates/election/election_detail.jinja:38 msgid "You already have submitted your vote." msgstr "Vous avez déjà soumis votre vote." -#: election/templates/election/election_detail.jinja:36 +#: election/templates/election/election_detail.jinja:40 msgid "You have voted in this election." msgstr "Vous avez déjà voté pour cette élection." -#: election/templates/election/election_detail.jinja:49 election/views.py:98 +#: election/templates/election/election_detail.jinja:53 election/views.py:98 msgid "Blank vote" msgstr "Vote blanc" -#: election/templates/election/election_detail.jinja:71 +#: election/templates/election/election_detail.jinja:75 msgid "You may choose up to" msgstr "Vous pouvez choisir jusqu'à" -#: election/templates/election/election_detail.jinja:71 +#: election/templates/election/election_detail.jinja:75 msgid "people." msgstr "personne(s)" -#: election/templates/election/election_detail.jinja:108 +#: election/templates/election/election_detail.jinja:112 msgid "Choose blank vote" msgstr "Choisir de voter blanc" -#: election/templates/election/election_detail.jinja:116 -#: election/templates/election/election_detail.jinja:159 +#: election/templates/election/election_detail.jinja:120 +#: election/templates/election/election_detail.jinja:163 msgid "votes" msgstr "votes" -#: election/templates/election/election_detail.jinja:178 +#: election/templates/election/election_detail.jinja:182 msgid "Add a new list" msgstr "Ajouter une nouvelle liste" -#: election/templates/election/election_detail.jinja:182 +#: election/templates/election/election_detail.jinja:186 msgid "Add a new role" msgstr "Ajouter un nouveau rôle" -#: election/templates/election/election_detail.jinja:192 +#: election/templates/election/election_detail.jinja:196 msgid "Submit the vote !" msgstr "Envoyer le vote !" -#: election/templates/election/election_detail.jinja:201 -#: election/templates/election/election_detail.jinja:206 +#: election/templates/election/election_detail.jinja:205 +#: election/templates/election/election_detail.jinja:210 msgid "Show more" msgstr "Montrer plus" -#: election/templates/election/election_detail.jinja:202 -#: election/templates/election/election_detail.jinja:207 +#: election/templates/election/election_detail.jinja:206 +#: election/templates/election/election_detail.jinja:211 msgid "Show less" msgstr "Montrer moins" @@ -5790,6 +5791,10 @@ msgstr "Weekmail envoyé avec succès" msgid "AE tee-shirt" msgstr "Tee-shirt AE" +#: subscription/forms.py:83 +msgid "A user with that email address already exists" +msgstr "Un utilisateur avec cette adresse email existe déjà" + #: subscription/models.py:34 msgid "Bad subscription type" msgstr "Mauvais type de cotisation" @@ -5814,10 +5819,36 @@ msgstr "fin de la cotisation" msgid "location" msgstr "lieu" -#: subscription/models.py:106 +#: subscription/models.py:107 msgid "You can not subscribe many time for the same period" msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période" +#: subscription/templates/subscription/fragments/creation_success.jinja:4 +#, python-format +msgid "Subscription created for %(user)s" +msgstr "Cotisation créée pour %(user)s" + +#: subscription/templates/subscription/fragments/creation_success.jinja:8 +#, python-format +msgid "" +"%(user)s received its new %(type)s subscription. It will be active until " +"%(end)s included." +msgstr "" +"%(user)s a reçu sa nouvelle cotisaton %(type)s. Elle sert active jusqu'au " +"%(end)s inclu." + +#: subscription/templates/subscription/fragments/creation_success.jinja:16 +msgid "Go to user profile" +msgstr "Voir le profil de l'utilisateur" + +#: subscription/templates/subscription/fragments/creation_success.jinja:25 +msgid "Create another subscription" +msgstr "Créer une nouvelle cotisation" + +#: subscription/templates/subscription/subscription.jinja +msgid "Existing member" +msgstr "Membre existant" + #: subscription/templates/subscription/stats.jinja:27 msgid "Total subscriptions" msgstr "Cotisations totales" @@ -5826,20 +5857,6 @@ msgstr "Cotisations totales" msgid "Subscriptions by type" msgstr "Cotisations par type" -#: subscription/templates/subscription/subscription.jinja:23 -msgid "Eboutic is reserved to specific users. In doubt, don't use it." -msgstr "" -"Eboutic est réservé à des cas particuliers. Dans le doute, ne l'utilisez pas." - -#: subscription/views.py:78 -msgid "A user with that email address already exists" -msgstr "Un utilisateur avec cette adresse email existe déjà" - -#: subscription/views.py:102 -msgid "You must either choose an existing user or create a new one properly" -msgstr "" -"Vous devez soit choisir un utilisateur existant, soit en créer un proprement" - #: trombi/models.py:55 msgid "subscription deadline" msgstr "fin des inscriptions" From fc0e689d4ef28842c677f8418dc6c4024bd1d8e3 Mon Sep 17 00:00:00 2001 From: imperosol Date: Thu, 28 Nov 2024 11:53:35 +0100 Subject: [PATCH 6/9] add initial values to forms --- subscription/forms.py | 8 ++++++++ subscription/views.py | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/subscription/forms.py b/subscription/forms.py index 7710825b..1f4c193d 100644 --- a/subscription/forms.py +++ b/subscription/forms.py @@ -23,6 +23,14 @@ class SelectionDateForm(forms.Form): class SubscriptionForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + initial = kwargs.pop("initial", {}) + if "subscription_type" not in initial: + initial["subscription_type"] = "deux-semestres" + if "payment_method" not in initial: + initial["payment_method"] = "CARD" + super().__init__(*args, initial=initial, **kwargs) + def save(self, *args, **kwargs): if self.errors: # let django deal with the error messages diff --git a/subscription/views.py b/subscription/views.py index 3e8cf0f1..2948391b 100644 --- a/subscription/views.py +++ b/subscription/views.py @@ -39,7 +39,9 @@ class NewSubscription(CanCreateSubscriptionMixin, TemplateView): def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | { - "existing_user_form": SubscriptionExistingUserForm(), + "existing_user_form": SubscriptionExistingUserForm( + initial={"member": self.request.GET.get("member")} + ), "new_user_form": SubscriptionNewUserForm(), "existing_user_post_url": reverse("subscription:fragment-existing-user"), "new_user_post_url": reverse("subscription:fragment-new-user"), From 04b4b34bfeeb6aa148261895e686de991633a332 Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 29 Nov 2024 15:48:40 +0100 Subject: [PATCH 7/9] add back user profiles on subscription form --- core/static/core/forms.scss | 89 ++++++++++++++ core/static/core/style.scss | 110 +++--------------- core/templates/core/macros.jinja | 21 +++- locale/fr/LC_MESSAGES/django.po | 30 ++--- subscription/forms.py | 4 + .../creation-form-existing-user-index.ts | 25 ++++ .../static/subscription/css/subscription.scss | 28 +++++ .../forms/create_existing_user.html | 14 +++ .../subscription/forms/create_new_user.html | 1 + .../fragments/creation_form.jinja | 2 +- .../fragments/creation_success.jinja | 15 ++- .../templates/subscription/subscription.jinja | 16 ++- 12 files changed, 229 insertions(+), 126 deletions(-) create mode 100644 core/static/core/forms.scss create mode 100644 subscription/static/bundled/subscription/creation-form-existing-user-index.ts create mode 100644 subscription/static/subscription/css/subscription.scss create mode 100644 subscription/templates/subscription/forms/create_existing_user.html create mode 100644 subscription/templates/subscription/forms/create_new_user.html diff --git a/core/static/core/forms.scss b/core/static/core/forms.scss new file mode 100644 index 00000000..7dab0484 --- /dev/null +++ b/core/static/core/forms.scss @@ -0,0 +1,89 @@ +@import "colors"; + +/** + * Style related to forms + */ + +a.button, +button, +input[type="button"], +input[type="submit"], +input[type="reset"], +input[type="file"] { + border: none; + text-decoration: none; + background-color: $background-button-color; + padding: 0.4em; + margin: 0.1em; + font-size: 1.2em; + border-radius: 5px; + color: black; + + &:hover { + background: hsl(0, 0%, 83%); + } +} + +a.button, +input[type="button"], +input[type="submit"], +input[type="reset"], +input[type="file"] { + font-weight: bold; +} + +a.button:not(:disabled), +button:not(:disabled), +input[type="button"]:not(:disabled), +input[type="submit"]:not(:disabled), +input[type="reset"]:not(:disabled), +input[type="checkbox"]:not(:disabled), +input[type="file"]:not(:disabled) { + cursor: pointer; +} + +input, +textarea[type="text"], +[type="number"] { + border: none; + text-decoration: none; + background-color: $background-button-color; + padding: 0.4em; + margin: 0.1em; + font-size: 1.2em; + border-radius: 5px; + max-width: 95%; +} + +textarea { + border: none; + text-decoration: none; + background-color: $background-button-color; + padding: 7px; + font-size: 1.2em; + border-radius: 5px; + font-family: sans-serif; +} + +select { + border: none; + text-decoration: none; + font-size: 1.2em; + background-color: $background-button-color; + padding: 10px; + border-radius: 5px; + cursor: pointer; +} + +a:not(.button) { + text-decoration: none; + color: $primary-dark-color; + + &:hover { + color: $primary-light-color; + } + + &:active { + color: $primary-color; + } +} diff --git a/core/static/core/style.scss b/core/static/core/style.scss index dc9e85d7..b9296e2e 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -1,4 +1,5 @@ @import "colors"; +@import "forms"; /*--------------------------MEDIA QUERY HELPERS------------------------*/ $small-devices: 576px; @@ -13,91 +14,6 @@ body { font-family: sans-serif; } - -a.button, -button, -input[type="button"], -input[type="submit"], -input[type="reset"], -input[type="file"] { - border: none; - text-decoration: none; - background-color: $background-button-color; - padding: 0.4em; - margin: 0.1em; - font-size: 1.2em; - border-radius: 5px; - color: black; - - &:hover { - background: hsl(0, 0%, 83%); - } -} - -a.button, -input[type="button"], -input[type="submit"], -input[type="reset"], -input[type="file"] { - font-weight: bold; -} - -a.button:not(:disabled), -button:not(:disabled), -input[type="button"]:not(:disabled), -input[type="submit"]:not(:disabled), -input[type="reset"]:not(:disabled), -input[type="checkbox"]:not(:disabled), -input[type="file"]:not(:disabled) { - cursor: pointer; -} - -input, -textarea[type="text"], -[type="number"] { - border: none; - text-decoration: none; - background-color: $background-button-color; - padding: 0.4em; - margin: 0.1em; - font-size: 1.2em; - border-radius: 5px; - max-width: 95%; -} - -textarea { - border: none; - text-decoration: none; - background-color: $background-button-color; - padding: 7px; - font-size: 1.2em; - border-radius: 5px; - font-family: sans-serif; -} - -select { - border: none; - text-decoration: none; - font-size: 1.2em; - background-color: $background-button-color; - padding: 10px; - border-radius: 5px; - cursor: pointer; -} - -a:not(.button) { - text-decoration: none; - color: $primary-dark-color; - - &:hover { - color: $primary-light-color; - } - - &:active { - color: $primary-color; - } -} - [aria-busy] { --loading-size: 50px; --loading-stroke: 5px; @@ -1281,26 +1197,26 @@ u, /*-----------------------------USER PROFILE----------------------------*/ .user_mini_profile { - height: 100%; - width: 100%; + --gap-size: 1em; + max-height: 100%; + max-width: 100%; + display: flex; + flex-direction: column; + gap: var(--gap-size); img { - max-width: 100%; max-height: 100%; + max-width: 100%; } .user_mini_profile_infos { padding: 0.2em; - height: 20%; + max-height: 20%; display: flex; flex-wrap: nowrap; justify-content: space-around; font-size: 0.9em; - div { - max-height: 100%; - } - .user_mini_profile_infos_text { text-align: center; @@ -1311,10 +1227,10 @@ u, } .user_mini_profile_picture { - height: 80%; - display: flex; - justify-content: center; - align-items: center; + max-height: calc(80% - var(--gap-size)); + max-width: 100%; + display: block; + margin: auto; } } diff --git a/core/templates/core/macros.jinja b/core/templates/core/macros.jinja index 021f1918..8615b570 100644 --- a/core/templates/core/macros.jinja +++ b/core/templates/core/macros.jinja @@ -66,7 +66,12 @@ {% if user.promo and user.promo_has_logo() %}
- Promo {{ user.promo }} + Promo {{ user.promo }}
{% endif %} @@ -74,8 +79,11 @@ {% if user.profile_pict %} {% trans %}Profile{% endtrans %} {% else %} - {% trans %}Profile{% endtrans %} + {% trans %}Profile{% endtrans %} {% endif %} @@ -286,6 +294,13 @@

{{ tabs([("tab 1", "Hello"), ("tab 2", "World")], "x-model=current_tab") }} + + If you want to have translated tab titles, you can enclose the macro call + in a with block : + + {% with title=_("title"), content=_("Content") %} + {{ tabs([(tab1, content)]) }} + {% endwith %} #}
\n" @@ -362,8 +362,8 @@ msgstr "Compte en banque : " #: core/templates/core/file_detail.jinja:62 #: core/templates/core/file_moderation.jinja:48 #: core/templates/core/group_detail.jinja:26 -#: core/templates/core/group_list.jinja:25 core/templates/core/macros.jinja:96 -#: core/templates/core/macros.jinja:115 core/templates/core/page_prop.jinja:14 +#: core/templates/core/group_list.jinja:25 core/templates/core/macros.jinja:104 +#: core/templates/core/macros.jinja:123 core/templates/core/page_prop.jinja:14 #: core/templates/core/user_account_detail.jinja:41 #: core/templates/core/user_account_detail.jinja:77 #: core/templates/core/user_clubs.jinja:34 @@ -1334,7 +1334,7 @@ msgid "No mailing list existing for this club" msgstr "Aucune mailing liste n'existe pour ce club" #: club/templates/club/mailing.jinja:72 -#: subscription/templates/subscription/subscription.jinja:34 +#: subscription/templates/subscription/subscription.jinja:39 msgid "New member" msgstr "Nouveau membre" @@ -2204,8 +2204,8 @@ msgstr "profil visible par les cotisants" msgid "A user with that username already exists" msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" -#: core/models.py:761 core/templates/core/macros.jinja:75 -#: core/templates/core/macros.jinja:77 core/templates/core/macros.jinja:78 +#: core/models.py:761 core/templates/core/macros.jinja:80 +#: core/templates/core/macros.jinja:84 core/templates/core/macros.jinja:85 #: core/templates/core/user_detail.jinja:100 #: core/templates/core/user_detail.jinja:101 #: core/templates/core/user_detail.jinja:103 @@ -2753,29 +2753,29 @@ msgstr "Partager sur Facebook" msgid "Tweet" msgstr "Tweeter" -#: core/templates/core/macros.jinja:85 +#: core/templates/core/macros.jinja:93 #, python-format msgid "Subscribed until %(subscription_end)s" msgstr "Cotisant jusqu'au %(subscription_end)s" -#: core/templates/core/macros.jinja:86 +#: core/templates/core/macros.jinja:94 msgid "Account number: " msgstr "Numéro de compte : " -#: core/templates/core/macros.jinja:91 launderette/models.py:188 +#: core/templates/core/macros.jinja:99 launderette/models.py:188 msgid "Slot" msgstr "Créneau" -#: core/templates/core/macros.jinja:104 +#: core/templates/core/macros.jinja:112 #: launderette/templates/launderette/launderette_admin.jinja:20 msgid "Tokens" msgstr "Jetons" -#: core/templates/core/macros.jinja:258 +#: core/templates/core/macros.jinja:266 msgid "Select All" msgstr "Tout sélectionner" -#: core/templates/core/macros.jinja:259 +#: core/templates/core/macros.jinja:267 msgid "Unselect All" msgstr "Tout désélectionner" @@ -3137,7 +3137,7 @@ msgstr "Non cotisant" #: core/templates/core/user_detail.jinja:162 #: subscription/templates/subscription/subscription.jinja:6 -#: subscription/templates/subscription/subscription.jinja:31 +#: subscription/templates/subscription/subscription.jinja:37 msgid "New subscription" msgstr "Nouvelle cotisation" @@ -5791,7 +5791,7 @@ msgstr "Weekmail envoyé avec succès" msgid "AE tee-shirt" msgstr "Tee-shirt AE" -#: subscription/forms.py:83 +#: subscription/forms.py:93 msgid "A user with that email address already exists" msgstr "Un utilisateur avec cette adresse email existe déjà" @@ -5841,7 +5841,7 @@ msgstr "" msgid "Go to user profile" msgstr "Voir le profil de l'utilisateur" -#: subscription/templates/subscription/fragments/creation_success.jinja:25 +#: subscription/templates/subscription/fragments/creation_success.jinja:24 msgid "Create another subscription" msgstr "Créer une nouvelle cotisation" diff --git a/subscription/forms.py b/subscription/forms.py index 1f4c193d..ab74adcb 100644 --- a/subscription/forms.py +++ b/subscription/forms.py @@ -61,6 +61,8 @@ class SubscriptionNewUserForm(SubscriptionForm): assert user.is_subscribed """ + template_name = "subscription/forms/create_new_user.html" + __user_fields = forms.fields_for_model( User, ["first_name", "last_name", "email", "date_of_birth"], @@ -114,6 +116,8 @@ class SubscriptionNewUserForm(SubscriptionForm): class SubscriptionExistingUserForm(SubscriptionForm): """Form to add a subscription to an existing user.""" + template_name = "subscription/forms/create_existing_user.html" + class Meta: model = Subscription fields = ["member", "subscription_type", "payment_method", "location"] diff --git a/subscription/static/bundled/subscription/creation-form-existing-user-index.ts b/subscription/static/bundled/subscription/creation-form-existing-user-index.ts new file mode 100644 index 00000000..b997ad7b --- /dev/null +++ b/subscription/static/bundled/subscription/creation-form-existing-user-index.ts @@ -0,0 +1,25 @@ +document.addEventListener("alpine:init", () => { + Alpine.data("existing_user_subscription_form", () => ({ + loading: false, + profileFragment: "" as string, + + async init() { + const userSelect = document.getElementById("id_member") as HTMLSelectElement; + userSelect.addEventListener("change", async () => { + await this.loadProfile(Number.parseInt(userSelect.value)); + }); + await this.loadProfile(Number.parseInt(userSelect.value)); + }, + + async loadProfile(userId: number) { + if (!Number.isInteger(userId)) { + this.profileFragment = ""; + return; + } + this.loading = true; + const response = await fetch(`/user/${userId}/mini/`); + this.profileFragment = await response.text(); + this.loading = false; + }, + })); +}); diff --git a/subscription/static/subscription/css/subscription.scss b/subscription/static/subscription/css/subscription.scss new file mode 100644 index 00000000..fd388574 --- /dev/null +++ b/subscription/static/subscription/css/subscription.scss @@ -0,0 +1,28 @@ +#subscription-form form { + .form-content.existing-user { + max-height: 100%; + display: flex; + flex: 1 1 auto; + flex-direction: row; + + @media screen and (max-width: 700px) { + flex-direction: column-reverse; + } + + /* Make the form fields take exactly the space they need, + * then display the user profile right in the middle of the remaining space. */ + fieldset { + flex: 0 1 auto; + } + + #subscription-form-user-mini-profile { + display: flex; + flex: 1 1 auto; + justify-content: center; + } + + .user_mini_profile { + height: 300px; + } + } +} \ No newline at end of file diff --git a/subscription/templates/subscription/forms/create_existing_user.html b/subscription/templates/subscription/forms/create_existing_user.html new file mode 100644 index 00000000..2f1cbc99 --- /dev/null +++ b/subscription/templates/subscription/forms/create_existing_user.html @@ -0,0 +1,14 @@ +{% load static %} +{% load i18n %} + + +
+
+ {{ form.as_p }} +
+
+
diff --git a/subscription/templates/subscription/forms/create_new_user.html b/subscription/templates/subscription/forms/create_new_user.html new file mode 100644 index 00000000..c22df09b --- /dev/null +++ b/subscription/templates/subscription/forms/create_new_user.html @@ -0,0 +1 @@ +{{ form.as_p }} \ No newline at end of file diff --git a/subscription/templates/subscription/fragments/creation_form.jinja b/subscription/templates/subscription/fragments/creation_form.jinja index 92f4c1a3..697c04bc 100644 --- a/subscription/templates/subscription/fragments/creation_form.jinja +++ b/subscription/templates/subscription/fragments/creation_form.jinja @@ -5,6 +5,6 @@ hx-swap="outerHTML" > {% csrf_token %} - {{ form.as_p() }} + {{ form }} diff --git a/subscription/templates/subscription/fragments/creation_success.jinja b/subscription/templates/subscription/fragments/creation_success.jinja index 41b78ee9..6a50c2e3 100644 --- a/subscription/templates/subscription/fragments/creation_success.jinja +++ b/subscription/templates/subscription/fragments/creation_success.jinja @@ -4,18 +4,21 @@ {% trans user=subscription.member %}Subscription created for {{ user }}{% endtrans %}

- - {% trans trimmed user=subscription.member.get_short_name(), type=subscription.subscription_type, end=subscription.subscription_end %} - {{ user }} received its new {{ type }} subscription. - It will be active until {{ end }} included. + {% trans trimmed + user=subscription.member.get_short_name(), + type=subscription.subscription_type, + end=subscription.subscription_end + %} + {{ user }} received its new {{ type }} subscription. + It will be active until {{ end }} included. {% endtrans %}

- + {% trans %}Go to user profile{% endtrans %} - + {# We don't know if this fragment is displayed after creating a subscription for a previously existing user or for a newly created one. Thus, we don't know which form should be used to create another subscription diff --git a/subscription/templates/subscription/subscription.jinja b/subscription/templates/subscription/subscription.jinja index b1657808..1a88e4f3 100644 --- a/subscription/templates/subscription/subscription.jinja +++ b/subscription/templates/subscription/subscription.jinja @@ -13,10 +13,16 @@ If the aforementioned bug is resolved, you can remove this. #} {% block additional_js %} + {% endblock %} {% block additional_css %} + {% endblock %} {% macro form_fragment(form_object, post_url) %} @@ -30,9 +36,11 @@ {% block content %}

{% trans %}New subscription{% endtrans %}

- {{ tabs([ - (_("Existing member"), form_fragment(existing_user_form, existing_user_post_url)), - (_("New member"), form_fragment(new_user_form, new_user_post_url)), - ]) }} + {% with title1=_("Existing member"), title2=_("New member") %} + {{ tabs([ + (title1, form_fragment(existing_user_form, existing_user_post_url)), + (title2, form_fragment(new_user_form, new_user_post_url)), + ]) }} + {% endwith %}
{% endblock %} From 1c79c252624e73750ca01314f05833a8c10ab0ad Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 1 Dec 2024 18:14:09 +0100 Subject: [PATCH 8/9] better tab style --- core/static/core/style.scss | 44 ++++++++++++++++++++------------ core/templates/core/macros.jinja | 6 ++--- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/core/static/core/style.scss b/core/static/core/style.scss index b9296e2e..50892df3 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -290,29 +290,39 @@ body { } .tabs { + border-radius: 5px; + .tab-headers { display: flex; flex-flow: row wrap; - .tab-header{ - margin: 0; - flex: 1 1; - border-radius: 5px 5px 0 0; - font-size: 100%; + background-color: $primary-neutral-light-color; + padding: 3px 12px 12px; + column-gap: 20px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; - @media (max-width: 800px) { - flex-wrap: wrap; + .tab-header { + border: none; + padding-right: 0; + padding-left: 0; + font-size: 120%; + background-color: unset; + position: relative; + &:after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + border-bottom: 4px solid darken($primary-neutral-light-color, 10%); + border-radius: 2px; + transition: all 0.2s ease-in-out; } - &.active { - background-color: $white-color; - border: 1px solid $primary-neutral-dark-color; - border-bottom: none; + &:hover:after { + border-bottom-color: darken($primary-neutral-light-color, 20%); } - &:not(.active) { - background-color: $primary-neutral-dark-color; - color: darken($white-color, 5%); - &:hover { - background-color: lighten($primary-neutral-dark-color, 15%); - } + &.active:after { + border-bottom-color: $primary-dark-color; } } } diff --git a/core/templates/core/macros.jinja b/core/templates/core/macros.jinja index 8615b570..6ab52cad 100644 --- a/core/templates/core/macros.jinja +++ b/core/templates/core/macros.jinja @@ -303,7 +303,7 @@ {% endwith %} #}
{% endfor %}
-
+
{% for title, content in tab_list %} -
+
{{ content }}
{% endfor %} From 9667c791623cfe3ea5bc508c3a72f4c6722c79c1 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 1 Dec 2024 18:24:11 +0100 Subject: [PATCH 9/9] remove htmx-ext-response-targets --- core/static/bundled/htmx-index.js | 2 -- package-lock.json | 6 ------ package.json | 1 - subscription/templates/subscription/subscription.jinja | 5 ++--- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/core/static/bundled/htmx-index.js b/core/static/bundled/htmx-index.js index 72fa5120..56edea4a 100644 --- a/core/static/bundled/htmx-index.js +++ b/core/static/bundled/htmx-index.js @@ -1,5 +1,3 @@ import htmx from "htmx.org"; -import "htmx-ext-response-targets/response-targets"; - Object.assign(window, { htmx }); diff --git a/package-lock.json b/package-lock.json index cb6483bf..05418a69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "d3-force-3d": "^3.0.5", "easymde": "^2.18.0", "glob": "^11.0.0", - "htmx-ext-response-targets": "^2.0.1", "htmx.org": "^2.0.3", "jquery": "^3.7.1", "jquery-ui": "^1.14.0", @@ -4141,11 +4140,6 @@ "node": ">= 0.4" } }, - "node_modules/htmx-ext-response-targets": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/htmx-ext-response-targets/-/htmx-ext-response-targets-2.0.1.tgz", - "integrity": "sha512-uCMw098+0xcrs7UW/s8l8hqj5wfOaVnVV7286cS+TNMNguo8fQpi/PEaZuT4VUysIiRcjj4pcTkuaP6Q9iJ3XA==" - }, "node_modules/htmx.org": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz", diff --git a/package.json b/package.json index 7640a11f..2ca46967 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "easymde": "^2.18.0", "glob": "^11.0.0", "htmx.org": "^2.0.3", - "htmx-ext-response-targets": "^2.0.1", "jquery": "^3.7.1", "jquery-ui": "^1.14.0", "jquery.shorten": "^1.0.0", diff --git a/subscription/templates/subscription/subscription.jinja b/subscription/templates/subscription/subscription.jinja index 1a88e4f3..98916827 100644 --- a/subscription/templates/subscription/subscription.jinja +++ b/subscription/templates/subscription/subscription.jinja @@ -12,10 +12,9 @@ So we give them here. If the aforementioned bug is resolved, you can remove this. #} {% block additional_js %} - + {% endblock %} @@ -35,7 +34,7 @@ {% block content %}

{% trans %}New subscription{% endtrans %}

-
+
{% with title1=_("Existing member"), title2=_("New member") %} {{ tabs([ (title1, form_fragment(existing_user_form, existing_user_post_url)),