From 7652f48dc1518c509c408e7937439da013dde8ef Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 18 Mar 2025 14:40:38 +0100 Subject: [PATCH] New consistent representation of date ranges with times (Z#23184657) --- src/pretix/base/models/event.py | 63 +++++++++++-------- .../pretixbase/email/order_details.html | 10 +-- .../pretixcontrol/checkin/index.html | 2 +- .../pretixcontrol/checkin/lists.html | 3 +- .../templates/pretixcontrol/items/quotas.html | 2 +- .../templates/pretixcontrol/order/index.html | 6 +- .../pretixcontrol/vouchers/index.html | 5 +- .../pretixpresale/event/checkout_addons.html | 6 +- .../event/checkout_membership.html | 8 +-- .../event/checkout_questions.html | 8 +-- .../pretixpresale/event/fragment_cart.html | 7 +-- .../event/fragment_change_form.html | 6 +- src/pretix/static/pretixpresale/js/ui/main.js | 5 +- .../static/pretixpresale/scss/_cart.scss | 2 +- src/tests/base/test_models.py | 52 +++++++++++++++ 15 files changed, 106 insertions(+), 79 deletions(-) diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 3de4f68bc5..db687f2483 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -60,6 +60,7 @@ from django.urls import reverse from django.utils.crypto import get_random_string from django.utils.formats import date_format from django.utils.functional import cached_property +from django.utils.html import format_html from django.utils.timezone import make_aware, now from django.utils.translation import gettext, gettext_lazy as _ from django_scopes import ScopedManager, scopes_disabled @@ -171,7 +172,7 @@ class EventMixin: self.date_to.astimezone(tz), ("D" if short else "l") ) - def get_date_range_display(self, tz=None, force_show_end=False, as_html=False) -> str: + def get_date_range_display(self, tz=None, force_show_end=False, as_html=False, try_to_show_times=False) -> str: """ Returns a formatted string containing the start date and the end date of the event with respect to the current locale and to the ``show_date_to`` @@ -180,36 +181,48 @@ class EventMixin: tz = tz or self.timezone if (not self.settings.show_date_to and not force_show_end) or not self.date_to: df, dt = self.date_from, self.date_from + show_times = try_to_show_times else: df, dt = self.date_from, self.date_to - return daterange(df.astimezone(tz), dt.astimezone(tz), as_html) + show_times = try_to_show_times and self.settings.show_times and ( + # Show times if start and end are on the same day ("08:00-10:00") + dt.astimezone(tz).date() == df.astimezone(tz).date() or + # Show times if start and end are on consecutive days and less than 24h ("23:00-03:00") + (dt.astimezone(tz).date() == df.astimezone(tz).date() + timedelta(days=1) and + dt.astimezone(tz).time() < df.astimezone(tz).time()) + ) + d = daterange(df.astimezone(tz), dt.astimezone(tz), as_html) + + if show_times: + if (not self.settings.show_date_to and not force_show_end) or not self.date_to: + time_str = _date(self.date_from.astimezone(tz), "TIME_FORMAT") + else: + time_str = '{}–{}'.format( + _date(self.date_from.astimezone(tz), "TIME_FORMAT"), + _date(self.date_to.astimezone(tz), "TIME_FORMAT"), + ) + + if as_html: + d = format_html( + d + ' ', + self.date_from.isoformat(), + str(self.timezone), + time_str, + ) + else: + d = d + ' ' + time_str + + return d + + def get_date_range_display_with_times(self) -> str: # Helper for usage from templates + return self.get_date_range_display(try_to_show_times=True) + + def get_date_range_display_with_times_as_html(self) -> str: # Helper for usage from templates + return self.get_date_range_display(try_to_show_times=True, as_html=True) def get_date_range_display_as_html(self, tz=None, force_show_end=False) -> str: return self.get_date_range_display(tz, force_show_end, as_html=True) - def get_time_range_display(self, tz=None, force_show_end=False) -> str: - """ - Returns a formatted string containing the start time and sometimes the end time - of the event with respect to the current locale and to the ``show_date_to`` - setting. Dates are not shown. This is usually used in combination with get_date_range_display - """ - tz = tz or self.timezone - - show_date_to = self.date_to and (self.settings.show_date_to or force_show_end) and ( - # Show date to if start and end are on the same day ("08:00-10:00") - self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() or - # Show date to if start and end are on consecutive days and less than 24h ("23:00-03:00") - (self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() + timedelta(days=1) and - self.date_to.astimezone(tz).time() < self.date_from.astimezone(tz).time()) - # Do not show end time if this is a 5-day event because there's no way to make it understandable - ) - if show_date_to: - return '{} – {}'.format( - _date(self.date_from.astimezone(tz), "TIME_FORMAT"), - _date(self.date_to.astimezone(tz), "TIME_FORMAT"), - ) - return _date(self.date_from.astimezone(tz), "TIME_FORMAT") - @property def timezone(self): return pytz_deprecation_shim.timezone(self.settings.timezone) diff --git a/src/pretix/base/templates/pretixbase/email/order_details.html b/src/pretix/base/templates/pretixbase/email/order_details.html index 2ec01146ee..77f5f556c5 100644 --- a/src/pretix/base/templates/pretixbase/email/order_details.html +++ b/src/pretix/base/templates/pretixbase/email/order_details.html @@ -15,10 +15,7 @@ {{ event.name }}
{% if event.has_subevents and ev.name|upper != event.name|upper %}{{ ev.name }}
{% endif %} - {{ ev.get_date_range_display }} - {% if event.settings.show_times %} - {{ ev.date_from|date:"TIME_FORMAT" }} - {% endif %} + {{ ev.get_date_range_display_with_times }} @@ -107,10 +104,7 @@ {% if groupkey.2.name|upper != event.name|upper %} {{ groupkey.2.name }} · {% endif %} - {{ groupkey.2.get_date_range_display }} - {% if event.settings.show_times %} - {{ groupkey.2.date_from|date:"TIME_FORMAT" }} - {% endif %} + {{ groupkey.2.get_date_range_display_with_times }} {% if groupkey.2.location %}
{{ groupkey.2.location|oneline }} diff --git a/src/pretix/control/templates/pretixcontrol/checkin/index.html b/src/pretix/control/templates/pretixcontrol/checkin/index.html index d22e2339e6..652a8819b0 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/index.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/index.html @@ -148,7 +148,7 @@ {{ e.item }}{% if e.variation %} – {{ e.variation }}{% endif %} {% if request.event.has_subevents and not checkinlist.subevent %} - {{ e.subevent.name }} – {{ e.subevent.get_date_range_display }} {{ e.subevent.date_from|date:"TIME_FORMAT" }} + {{ e.subevent.name }} – {{ e.subevent.get_date_range_display_with_times }} {% endif %} {% if seats %} diff --git a/src/pretix/control/templates/pretixcontrol/checkin/lists.html b/src/pretix/control/templates/pretixcontrol/checkin/lists.html index e131f7fdb0..4a96b051ff 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/lists.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/lists.html @@ -127,8 +127,7 @@ {% if request.event.has_subevents %} {% if cl.subevent %} - {{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }} - {{ cl.subevent.date_from|date:"TIME_FORMAT" }} + {{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display_with_times }} {% else %} diff --git a/src/pretix/control/templates/pretixcontrol/items/quotas.html b/src/pretix/control/templates/pretixcontrol/items/quotas.html index f77996662b..c6509296fa 100644 --- a/src/pretix/control/templates/pretixcontrol/items/quotas.html +++ b/src/pretix/control/templates/pretixcontrol/items/quotas.html @@ -85,7 +85,7 @@ {% if request.event.has_subevents %} - {{ q.subevent.name }} – {{ q.subevent.get_date_range_display }} {{ q.subevent.date_from|date:"TIME_FORMAT" }} + {{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }} {% endif %} {% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %} diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index defd7b96f9..ad5ae66c00 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -429,11 +429,7 @@ {% endif %} {% if line.subevent %}
- {{ line.subevent.name }} · {{ line.subevent.get_date_range_display }} - {% if event.settings.show_times %} - - {{ line.subevent.date_from|date:"TIME_FORMAT" }} - {% endif %} + {{ line.subevent.name }} · {{ line.subevent.get_date_range_display_with_times }} {% endif %} {% if line.used_membership %}
diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/index.html b/src/pretix/control/templates/pretixcontrol/vouchers/index.html index d8f7619ee2..befc9f0575 100644 --- a/src/pretix/control/templates/pretixcontrol/vouchers/index.html +++ b/src/pretix/control/templates/pretixcontrol/vouchers/index.html @@ -188,10 +188,7 @@ {% if request.event.has_subevents %} - {{ v.subevent.name }} – {{ v.subevent.get_date_range_display }} - {% if request.event.settings.show_times %} - {{ v.subevent.date_from|date:"TIME_FORMAT" }} - {% endif %} + {{ v.subevent.name }} – {{ v.subevent.get_date_range_display_with_times }} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_addons.html b/src/pretix/presale/templates/pretixpresale/event/checkout_addons.html index 30f1a8b462..6913f48d6e 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_addons.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_addons.html @@ -42,11 +42,7 @@ {% if form.pos.subevent %}

- {{ form.pos.subevent.name }} · {{ form.pos.subevent.get_date_range_display_as_html }} - {% if form.pos.event.settings.show_times %} - - {{ form.pos.subevent.date_from|date:"TIME_FORMAT" }} - {% endif %} + {{ form.pos.subevent.name }} · {{ form.pos.subevent.get_date_range_display_with_times_as_html }}

{% endif %} {% include "pretixpresale/event/fragment_addon_choice.html" with form=form %} diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_membership.html b/src/pretix/presale/templates/pretixpresale/event/checkout_membership.html index 51b54438e9..14f54e2020 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_membership.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_membership.html @@ -52,13 +52,7 @@
diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html index 2bf004bd7a..bbacb58ed4 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html @@ -130,13 +130,7 @@
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html index dc24d42d3a..cb83f25f0c 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html @@ -62,12 +62,7 @@ {{ line.subevent.name }}
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_change_form.html b/src/pretix/presale/templates/pretixpresale/event/fragment_change_form.html index 7ef183ae62..a3d13988e1 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_change_form.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_change_form.html @@ -21,11 +21,7 @@
    - {{ position.subevent.name }} · {{ position.subevent.get_date_range_display_as_html }} - {% if position.event.settings.show_times %} - - {{ position.subevent.date_from|date:"TIME_FORMAT" }} - {% endif %} + {{ position.subevent.name }} · {{ position.subevent.get_date_range_display_with_times_as_html }}
diff --git a/src/pretix/static/pretixpresale/js/ui/main.js b/src/pretix/static/pretixpresale/js/ui/main.js index 34b84b807c..ee6754a4a7 100644 --- a/src/pretix/static/pretixpresale/js/ui/main.js +++ b/src/pretix/static/pretixpresale/js/ui/main.js @@ -533,8 +533,8 @@ $(function () { form_handlers($("body")); var local_tz = moment.tz.guess() - $("span[data-timezone], small[data-timezone]").each(function() { - var t = moment.tz($(this).attr("data-time"), $(this).attr("data-timezone")) + $("span[data-timezone], small[data-timezone], time[data-timezone]").each(function() { + var t = moment.tz($(this).attr("datetime") || $(this).attr("data-time"), $(this).attr("data-timezone")) var tz = moment.tz.zone($(this).attr("data-timezone")) var tpl = ''; @@ -544,6 +544,7 @@ $(function () { }); if (t.tz(tz.name).format() !== t.tz(local_tz).format()) { var $add = $("") + $add.append(" ") $add.append($("").addClass("fa fa-globe")) if ($(this).is("[data-time-short]")) { $add.append($("").text(" " + t.tz(local_tz).format($("body").attr("data-timeformat")))) diff --git a/src/pretix/static/pretixpresale/scss/_cart.scss b/src/pretix/static/pretixpresale/scss/_cart.scss index 85f28f216a..2c91bf4b5b 100644 --- a/src/pretix/static/pretixpresale/scss/_cart.scss +++ b/src/pretix/static/pretixpresale/scss/_cart.scss @@ -83,7 +83,7 @@ position: relative; padding-left: 1.4em; text-indent: 0; - .fa, svg { + & > .fa, & > svg, & > dd > .fa, & > dd > svg { position: absolute; left: 0; top: .22em; diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index f61f13904d..b68d5b977f 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -35,6 +35,7 @@ import datetime import sys import time +import zoneinfo from datetime import date, timedelta from decimal import Decimal @@ -2458,6 +2459,57 @@ class EventTest(TestCase): item.save() assert Event.annotated(Event.objects, 'web').first().active_quotas == [] + @classscope(attr='organizer') + def test_date_range_display(self): + tz = zoneinfo.ZoneInfo("Europe/Berlin") + sets = ( + ( + datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz), + datetime.datetime(2025, 3, 9, 22, 0, 0, tzinfo=tz), + 'Sun, March 9th, 2025', + '', + 'Sun, March 9th, 2025 20:00–21:00', + ' ' + '' + ), + ( + datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz), + datetime.datetime(2025, 3, 10, 3, 0, 0, tzinfo=tz), + 'March 9th – 10th, 2025', + '', + 'March 9th – 10th, 2025 20:00–02:00', + ' ' + '' + ), + ( + datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz), + datetime.datetime(2025, 3, 12, 14, 0, 0, tzinfo=tz), + 'March 9th – 12th, 2025', + '', + 'March 9th – 12th, 2025', + '', + ), + ( + datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz), + None, + 'Sun, March 9th, 2025', + '', + 'Sun, March 9th, 2025 20:00', + ' ' + '' + ), + ) + + for i, (df, dt, expected, expected_html, expected_with_times, expected_with_times_html) in enumerate(sets): + event = Event.objects.create( + organizer=self.organizer, name='Dummy', slug=f'dummy{i}', + date_from=df, date_to=dt, + ) + assert event.get_date_range_display() == expected + assert event.get_date_range_display(as_html=True) == expected_html + assert event.get_date_range_display(try_to_show_times=True) == expected_with_times + assert event.get_date_range_display(try_to_show_times=True, as_html=True) == expected_with_times_html + class SubEventTest(TestCase): def setUp(self):