From 11ab5c5eeb37822f32b715189bf5c074c81c97e4 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 5 Dec 2024 17:11:57 +0100 Subject: [PATCH 1/6] Event dashboard: Use intcomma in numbers (Z#23175343) (#4687) --- src/pretix/base/templatetags/money.py | 8 ++++---- src/pretix/control/views/dashboards.py | 19 +++++++++++-------- src/tests/base/test_templatetag.py | 8 ++++---- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/pretix/base/templatetags/money.py b/src/pretix/base/templatetags/money.py index fe9ff7d4b..ad4eff1cc 100644 --- a/src/pretix/base/templatetags/money.py +++ b/src/pretix/base/templatetags/money.py @@ -52,12 +52,12 @@ def money_filter(value: Decimal, arg='', hide_currency=False): # would make the numbers incorrect. If this branch executes, it's likely a bug in # pretix, but we won't show wrong numbers! if hide_currency: - return floatformat(value, 2) + return floatformat(value, "2g") else: - return '{} {}'.format(arg, floatformat(value, 2)) + return '{} {}'.format(arg, floatformat(value, "2g")) if hide_currency: - return floatformat(value, places) + return floatformat(value, f"{places}g") locale_parts = translation.get_language().split("-", 1) locale = locale_parts[0] @@ -70,7 +70,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False): try: return format_currency(value, arg, locale=locale) except: - return '{} {}'.format(arg, floatformat(value, places)) + return '{} {}'.format(arg, floatformat(value, f"{places}g")) @register.filter("money_numberfield") diff --git a/src/pretix/control/views/dashboards.py b/src/pretix/control/views/dashboards.py index 1b5f628e6..8b5b47e2a 100644 --- a/src/pretix/control/views/dashboards.py +++ b/src/pretix/control/views/dashboards.py @@ -38,6 +38,7 @@ from zoneinfo import ZoneInfo from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.contrib.humanize.templatetags.humanize import intcomma from django.db.models import ( Count, IntegerField, Max, Min, OuterRef, Prefetch, Q, Subquery, Sum, ) @@ -47,7 +48,6 @@ from django.http import JsonResponse from django.shortcuts import render from django.template.loader import get_template from django.urls import reverse -from django.utils import formats from django.utils.formats import date_format from django.utils.html import escape from django.utils.timezone import now @@ -67,6 +67,7 @@ from pretix.control.signals import ( from pretix.helpers.daterange import daterange from ...base.models.orders import CancellationRequest +from ...base.templatetags.money import money_filter from ..logdisplay import OVERVIEW_BANLIST NUM_WIDGET = '
{num}{text}
' @@ -111,7 +112,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs): return [ { - 'content': None if lazy else NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')), + 'content': None if lazy else NUM_WIDGET.format(num=intcomma(tickc), text=_('Attendees (ordered)')), 'lazy': 'attendees-ordered', 'display_size': 'small', 'priority': 100, @@ -121,7 +122,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs): }) + ('?subevent={}'.format(subevent.pk) if subevent else '') }, { - 'content': None if lazy else NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')), + 'content': None if lazy else NUM_WIDGET.format(num=intcomma(paidc), text=_('Attendees (paid)')), 'lazy': 'attendees-paid', 'display_size': 'small', 'priority': 100, @@ -132,7 +133,9 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs): }, { 'content': None if lazy else NUM_WIDGET.format( - num=formats.localize(round_decimal(rev, sender.currency)), text=_('Total revenue ({currency})').format(currency=sender.currency)), + num=money_filter(round_decimal(rev, sender.currency), sender.currency, hide_currency=True), + text=_('Total revenue ({currency})').format(currency=sender.currency) + ), 'lazy': 'total-revenue', 'display_size': 'small', 'priority': 100, @@ -207,7 +210,7 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs): widgets.append({ 'content': None if lazy else NUM_WIDGET.format( - num=str(happy), text=_('available to give to people on waiting list') + num=intcomma(happy), text=_('available to give to people on waiting list') ), 'lazy': 'waitinglist-avail', 'priority': 50, @@ -217,7 +220,7 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs): }) }) widgets.append({ - 'content': None if lazy else NUM_WIDGET.format(num=str(wles.count()), text=_('total waiting list length')), + 'content': None if lazy else NUM_WIDGET.format(num=intcomma(wles.count()), text=_('total waiting list length')), 'lazy': 'waitinglist-length', 'display_size': 'small', 'priority': 50, @@ -245,7 +248,7 @@ def quota_widgets(sender, subevent=None, lazy=False, **kwargs): status, left = qa.results[q] if q in qa.results else q.availability(allow_cache=True) widgets.append({ 'content': None if lazy else NUM_WIDGET.format( - num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e', + num='{}/{}'.format(intcomma(left), intcomma(q.size)) if q.size is not None else '\u221e', text=_('{quota} left').format(quota=escape(q.name)) ), 'lazy': 'quota-{}'.format(q.pk), @@ -297,7 +300,7 @@ def checkin_widget(sender, subevent=None, lazy=False, **kwargs): for cl in qs: widgets.append({ 'content': None if lazy else NUM_WIDGET.format( - num='{}/{}'.format(cl.inside_count, cl.position_count), + num='{}/{}'.format(intcomma(cl.inside_count), intcomma(cl.position_count)), text=_('Present – {list}').format(list=escape(cl.name)) ), 'lazy': 'checkin-{}'.format(cl.pk), diff --git a/src/tests/base/test_templatetag.py b/src/tests/base/test_templatetag.py index 13969cc0e..a9c9518de 100644 --- a/src/tests/base/test_templatetag.py +++ b/src/tests/base/test_templatetag.py @@ -77,7 +77,7 @@ def test_urlreplace_replace_parameter(): # rounding errors ("de", Decimal("1.234"), "EUR", "1,23" + NBSP + "€"), - ("de", Decimal("1023.1"), "JPY", "JPY 1023,10"), + ("de", Decimal("1023.1"), "JPY", "JPY 1.023,10"), ] ) def test_money_filter(locale, amount, currency, expected): @@ -99,9 +99,9 @@ def test_money_filter(locale, amount, currency, expected): @pytest.mark.parametrize( "locale,amount,currency,expected", [ - ("de", Decimal("1000.00"), "EUR", "1000,00"), - ("en", Decimal("1000.00"), "EUR", "1000.00"), - ("de", Decimal("1023.1"), "JPY", "1023,10"), + ("de", Decimal("1000.00"), "EUR", "1.000,00"), + ("en", Decimal("1000.00"), "EUR", "1,000.00"), + ("de", Decimal("1023.1"), "JPY", "1.023,10"), ] ) def test_money_filter_hidecurrency(locale, amount, currency, expected): From 7df7d2851874ff71a2c563984045c8ea28691b68 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 5 Dec 2024 17:38:31 +0100 Subject: [PATCH 2/6] Bump django-i18nfield to 1.9.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f794846ac..8f7185ace 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "django-formtools==2.5.1", "django-hierarkey==1.2.*", "django-hijack==3.7.*", - "django-i18nfield==1.9.*,>=1.9.4", + "django-i18nfield==1.9.*,>=1.9.5", "django-libsass==0.9", "django-localflavor==4.0", "django-markup", From 6132e4a2c417fd1ee396c74940c91a5643006f91 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 5 Dec 2024 17:56:17 +0100 Subject: [PATCH 3/6] Remove re-implementations if i18nfield API integration --- src/pretix/api/serializers/i18n.py | 58 +++++------------------------- src/pretix/base/settings.py | 3 +- 2 files changed, 10 insertions(+), 51 deletions(-) diff --git a/src/pretix/api/serializers/i18n.py b/src/pretix/api/serializers/i18n.py index 80a94c016..3fc91e041 100644 --- a/src/pretix/api/serializers/i18n.py +++ b/src/pretix/api/serializers/i18n.py @@ -19,57 +19,8 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # -from django.conf import settings from django.core.validators import URLValidator -from i18nfield.fields import I18nCharField, I18nTextField -from i18nfield.strings import LazyI18nString -from rest_framework.exceptions import ValidationError -from rest_framework.fields import Field -from rest_framework.serializers import ModelSerializer - - -class I18nField(Field): - def __init__(self, **kwargs): - self.allow_blank = kwargs.pop('allow_blank', False) - self.trim_whitespace = kwargs.pop('trim_whitespace', True) - self.max_length = kwargs.pop('max_length', None) - self.min_length = kwargs.pop('min_length', None) - super().__init__(**kwargs) - - def to_representation(self, value): - if hasattr(value, 'data'): - if isinstance(value.data, dict): - return value.data - elif value.data is None: - return None - else: - return { - settings.LANGUAGE_CODE: str(value.data) - } - elif value is None: - return None - else: - return { - settings.LANGUAGE_CODE: str(value) - } - - def to_internal_value(self, data): - if isinstance(data, str): - return LazyI18nString(data) - elif isinstance(data, dict): - if any([k not in dict(settings.LANGUAGES) for k in data.keys()]): - raise ValidationError('Invalid languages included.') - return LazyI18nString(data) - else: - raise ValidationError('Invalid data type.') - - -class I18nAwareModelSerializer(ModelSerializer): - pass - - -I18nAwareModelSerializer.serializer_field_mapping[I18nCharField] = I18nField -I18nAwareModelSerializer.serializer_field_mapping[I18nTextField] = I18nField +from i18nfield.rest_framework import I18nAwareModelSerializer, I18nField class I18nURLField(I18nField): @@ -84,3 +35,10 @@ class I18nURLField(I18nField): else: URLValidator()(value.data) return value + + +__all__ = [ + "I18nAwareModelSerializer", # for backwards compatibility + "I18nField", # for backwards compatibility + "I18nURLField", +] diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 26d6824cb..7a4d29eb0 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -56,6 +56,7 @@ from django.utils.translation import ( from django_countries.fields import Country from hierarkey.models import GlobalSettingsBase, Hierarkey from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput +from i18nfield.rest_framework import I18nField from i18nfield.strings import LazyI18nString from phonenumbers import PhoneNumber, parse from rest_framework import serializers @@ -63,7 +64,7 @@ from rest_framework import serializers from pretix.api.serializers.fields import ( ListMultipleChoiceField, UploadedFileField, ) -from pretix.api.serializers.i18n import I18nField, I18nURLField +from pretix.api.serializers.i18n import I18nURLField from pretix.base.forms import I18nMarkdownTextarea, I18nURLFormField from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule from pretix.base.reldate import ( From e2e0eca872473ca0ba60f94bbf8738b56e3d2221 Mon Sep 17 00:00:00 2001 From: Richard Schreiber Date: Fri, 6 Dec 2024 10:13:32 +0100 Subject: [PATCH 4/6] Fix accordion-radio fieldset css --- src/pretix/static/pretixpresale/scss/_forms.scss | 4 ++++ src/pretix/static/pretixpresale/scss/main.scss | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/pretix/static/pretixpresale/scss/_forms.scss b/src/pretix/static/pretixpresale/scss/_forms.scss index 67ebc5cae..5a01b95a5 100644 --- a/src/pretix/static/pretixpresale/scss/_forms.scss +++ b/src/pretix/static/pretixpresale/scss/_forms.scss @@ -135,9 +135,13 @@ a.btn, button.btn { margin: 0; } .panel-default>.accordion-radio>.panel-heading, fieldset.accordion-panel>legend>.panel-heading { + display: block; color: #333; background-color: #f5f5f5; padding: 8px 15px; + margin: 0; + line-height: 1.428571429; + font-size: 16px; input[type=radio] { margin-top: 0; diff --git a/src/pretix/static/pretixpresale/scss/main.scss b/src/pretix/static/pretixpresale/scss/main.scss index 19c6931e6..0ff27fb23 100644 --- a/src/pretix/static/pretixpresale/scss/main.scss +++ b/src/pretix/static/pretixpresale/scss/main.scss @@ -335,6 +335,11 @@ body.loading .container { } } +.font-normal { + font-style: normal; + font-weight: normal; +} + .blank-after { margin-bottom: 1em; } From 74edf10b04fc2fa7c081faf54fb93be2bfe4f5fb Mon Sep 17 00:00:00 2001 From: Richard Schreiber Date: Fri, 6 Dec 2024 11:07:11 +0100 Subject: [PATCH 5/6] Move cursor fix for disabled fieldsets from bootstrap to presale --- src/pretix/static/bootstrap/scss/bootstrap/_forms.scss | 2 +- src/pretix/static/pretixpresale/scss/_forms.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pretix/static/bootstrap/scss/bootstrap/_forms.scss b/src/pretix/static/bootstrap/scss/bootstrap/_forms.scss index e9645a30d..407a49a4e 100644 --- a/src/pretix/static/bootstrap/scss/bootstrap/_forms.scss +++ b/src/pretix/static/bootstrap/scss/bootstrap/_forms.scss @@ -70,7 +70,7 @@ input[type="checkbox"] { // Note: Neither radios nor checkboxes can be readonly. &[disabled], &.disabled, - fieldset[disabled] &:not(fieldset[disabled] > legend &) { + fieldset[disabled] & { cursor: $cursor-disabled; } } diff --git a/src/pretix/static/pretixpresale/scss/_forms.scss b/src/pretix/static/pretixpresale/scss/_forms.scss index 5a01b95a5..9713469a9 100644 --- a/src/pretix/static/pretixpresale/scss/_forms.scss +++ b/src/pretix/static/pretixpresale/scss/_forms.scss @@ -157,6 +157,10 @@ fieldset.accordion-panel > legend { fieldset.accordion-panel[disabled] > .panel-body { display: none; } +fieldset[disabled] legend input[type="radio"], +fieldset[disabled] legend input[type="checkbox"] { + cursor: default; +} .nav-tabs { border-bottom: 0px solid #ddd; From ae29240e58fac494980c20843419160bbb7a2912 Mon Sep 17 00:00:00 2001 From: Richard Schreiber Date: Fri, 6 Dec 2024 11:11:36 +0100 Subject: [PATCH 6/6] [A11y] Improve number inputs for screen-readers --- .../event/fragment_addon_choice.html | 32 +++++++++++-------- .../event/fragment_product_list.html | 15 +++++---- .../pretixpresale/event/voucher.html | 15 +++++---- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html b/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html index b0b5bab56..5498b63fc 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html @@ -6,13 +6,14 @@ {% load eventsignal %} {% load rich_text %} {% for c in form.categories %} + {% with category_idx=forloop.counter %}
{{ c.category.name }} {% if c.category.description %} {{ c.category.description|rich_text }} {% endif %} {% if c.min_count == c.max_count %} -

+

{% blocktrans trimmed count min_count=c.min_count %} You need to choose exactly one option from this category. {% plural %} @@ -21,7 +22,7 @@

{% elif c.min_count == 0 and c.max_count >= c.items|length and not c.multi_allowed %} {% elif c.min_count == 0 %} -

+

{% blocktrans trimmed count max_count=c.max_count %} You can choose {{ max_count }} option from this category. {% plural %} @@ -29,7 +30,7 @@ {% endblocktrans %}

{% else %} -

+

{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %} You can choose between {{ min_count }} and {{ max_count }} options from this category. @@ -58,7 +59,7 @@ {% endif %} {% if item.min_per_order and item.min_per_order > 1 %} -

+

{% blocktrans trimmed with num=item.min_per_order %} minimum amount to order: {{ num }} @@ -196,12 +197,14 @@ {% endif %} id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" - aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}"> + aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}" + aria-describedby="c-{{ form.pos.pk }}-{{ category_idx }}-addon-count-desc"> {% trans "Select" context "checkbox" %} {% else %} -

+
+ {% blocktrans with item=item.name %}Add {{ item }}, {{ var }} to cart{% endblocktrans %} + aria-label="{% trans "Quantity" %}"> -
+
{% endif %} {% else %} @@ -250,7 +253,7 @@ {% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %} {% endif %} {% if item.min_per_order and item.min_per_order > 1 %} -

+

{% blocktrans trimmed with num=item.min_per_order %} minimum amount to order: {{ num }} @@ -341,12 +344,13 @@ name="cp_{{ form.pos.pk }}_item_{{ item.id }}" id="cp_{{ form.pos.pk }}_item_{{ item.id }}" aria-label="{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}" - {% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.id }}-description"{% endif %}> + aria-describedby="c-{{ form.pos.pk }}-{{ category_idx }}-addon-count-desc"> {% trans "Select" context "checkbox" %} {% else %} -

+
+ {% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %} + aria-label="{% trans "Quantity" %}"> -
+ {% endif %} {% else %} @@ -370,6 +373,7 @@ {% endif %} {% endfor %} + {% endwith %} {% empty %} {% trans "There are no add-ons available for this product." %} diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html b/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html index 7f7d487b1..a3834b9d4 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_product_list.html @@ -219,7 +219,8 @@ {% trans "Select" context "checkbox" %} {% else %} -
+
+ {% blocktrans with item=item.name %}Add {{ item }}, {{ var }} to cart{% endblocktrans %} + aria-label="{% trans "Quantity" %}"> -
+ {% endif %} {% else %} @@ -370,7 +371,8 @@ {% trans "Select" context "checkbox" %} {% else %} -
+
+ {% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %} + aria-label="{% trans "Quantity" %}"> -
+ {% endif %} {% else %} diff --git a/src/pretix/presale/templates/pretixpresale/event/voucher.html b/src/pretix/presale/templates/pretixpresale/event/voucher.html index 706300f30..556222f22 100644 --- a/src/pretix/presale/templates/pretixpresale/event/voucher.html +++ b/src/pretix/presale/templates/pretixpresale/event/voucher.html @@ -231,16 +231,17 @@ {% trans "Select" context "checkbox" %} {% else %} -
+
+ {% blocktrans with item=item.name %}Add {{ item }}, {{ var }} to cart{% endblocktrans %} + aria-label="{% trans "Quantity" %}"> -
+ {% endif %} {% else %} {% else %} -
+
+ {% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %} + aria-label="{% trans "Quantity" %}"> -
+ {% endif %} {% else %}