From 5ae3b27e83f4c2da290d9851fba21305d2780927 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 14 Feb 2023 09:24:28 +0100 Subject: [PATCH] Item validity: Compute month ranges one day shorter --- src/pretix/base/models/items.py | 12 +++++++++++- src/tests/base/test_item_validity.py | 25 ++++++++++++++++--------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index c58103a690..e6f2183cb8 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -850,8 +850,18 @@ class Item(LoggedModel): valid_until = requested_start.astimezone(tz) if self.validity_dynamic_duration_months: + valid_until -= timedelta(days=1) + + replace_day = valid_until.day + max_day_start_month = calendar.monthrange(valid_until.year, valid_until.month)[1] + if replace_day == max_day_start_month: + # This is a correction for month passes that start e.g. on March 1st – their previous day should + # be "last of previous month", not "28th of previous month". + replace_day = 31 + replace_year = valid_until.year replace_month = valid_until.month + self.validity_dynamic_duration_months + while replace_month > 12: replace_month -= 12 replace_year += 1 @@ -859,7 +869,7 @@ class Item(LoggedModel): replace_date = date( year=replace_year, month=replace_month, - day=min(valid_until.day, max_day), + day=min(replace_day, max_day), ) if self.validity_dynamic_duration_days: replace_date += timedelta(days=self.validity_dynamic_duration_days) diff --git a/src/tests/base/test_item_validity.py b/src/tests/base/test_item_validity.py index 85f8320f1d..ec42419ac9 100644 --- a/src/tests/base/test_item_validity.py +++ b/src/tests/base/test_item_validity.py @@ -42,12 +42,13 @@ def dt(*args, is_dst=None, **kwargs): (0, 0, 1, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 9, 23, 59, 59)), # "day pass" (0, 0, 3, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 11, 23, 59, 59)), # "3-day pass" (30, 6, 3, 0, dt(2023, 2, 9, 10, 30, 0), dt(2023, 2, 12, 6, 29, 59)), # "3-day pass with day end at 6:30" - (0, 0, 0, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 9, 23, 59, 59)), # "month pass" - (0, 0, 3, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 12, 23, 59, 59)), # "month pass + 3 days" - (30, 6, 0, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 10, 6, 29, 59)), # "month pass with day end at 6:30" - (30, 6, 1, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 11, 6, 29, 59)), # "month pass + 1 day with day end at 6:30" - (0, 0, 0, 12, dt(2023, 2, 9, 10, 30, 0), dt(2024, 2, 9, 23, 59, 59)), # "year pass" - (30, 6, 0, 12, dt(2023, 2, 9, 10, 30, 0), dt(2024, 2, 10, 6, 29, 59)), # "year pass with day end at 6:30" + (0, 0, 0, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 8, 23, 59, 59)), # "month pass" + (0, 0, 3, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 11, 23, 59, 59)), # "month pass + 3 days" + (30, 6, 0, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 9, 6, 29, 59)), # "month pass with day end at 6:30" + (30, 6, 1, 1, dt(2023, 2, 9, 10, 30, 0), dt(2023, 3, 10, 6, 29, 59)), # "month pass + 1 day with day end at 6:30" + (0, 0, 0, 12, dt(2023, 2, 9, 10, 30, 0), dt(2024, 2, 8, 23, 59, 59)), # "year pass" + (0, 0, 0, 12, dt(2023, 1, 1, 10, 30, 0), dt(2023, 12, 31, 23, 59, 59)), # "year pass" + (30, 6, 0, 12, dt(2023, 2, 9, 10, 30, 0), dt(2024, 2, 9, 6, 29, 59)), # "year pass with day end at 6:30" # Calendrical edge cases @@ -55,9 +56,9 @@ def dt(*args, is_dst=None, **kwargs): (0, 0, 2, 0, dt(2023, 3, 25, 10, 30, 0), dt(2023, 3, 26, 23, 59, 59)), # Month + day across a DST change - (0, 0, 1, 1, dt(2023, 2, 25, 10, 30, 0), dt(2023, 3, 26, 23, 59, 59)), + (0, 0, 1, 1, dt(2023, 2, 26, 10, 30, 0), dt(2023, 3, 26, 23, 59, 59)), - # Day + hour with possibly non-existant end time during DST change + # Day + hour with possibly non-existent end time during DST change (30, 2, 1, 0, dt(2023, 3, 25, 10, 30, 0), dt(2023, 3, 26, 3, 29, 59)), # Day + hour with ambiguous end time during DST change @@ -65,11 +66,17 @@ def dt(*args, is_dst=None, **kwargs): # Month with short month following (0, 0, 0, 1, dt(2023, 1, 31, 10, 30, 0), dt(2023, 2, 28, 23, 59, 59)), + (0, 0, 0, 1, dt(2023, 1, 30, 10, 30, 0), dt(2023, 2, 28, 23, 59, 59)), + (0, 0, 0, 1, dt(2023, 1, 29, 10, 30, 0), dt(2023, 2, 28, 23, 59, 59)), + (0, 0, 0, 1, dt(2023, 1, 28, 10, 30, 0), dt(2023, 2, 27, 23, 59, 59)), + (0, 0, 0, 1, dt(2023, 2, 1, 10, 30, 0), dt(2023, 2, 28, 23, 59, 59)), # Interaction on months and leap days (0, 0, 0, 1, dt(2024, 1, 31, 10, 30, 0), dt(2024, 2, 29, 23, 59, 59)), + (0, 0, 0, 12, dt(2023, 3, 1, 10, 30, 0), dt(2024, 2, 29, 23, 59, 59)), + (0, 0, 0, 12, dt(2024, 3, 1, 10, 30, 0), dt(2025, 2, 28, 23, 59, 59)), (0, 0, 0, 12, dt(2024, 2, 29, 10, 30, 0), dt(2025, 2, 28, 23, 59, 59)), - (0, 0, 0, 12, dt(2024, 1, 31, 10, 30, 0), dt(2025, 1, 31, 23, 59, 59)), + (0, 0, 0, 12, dt(2024, 1, 31, 10, 30, 0), dt(2025, 1, 30, 23, 59, 59)), ]) def test_dynamic_validity(minutes, hours, days, months, start, expected_end): i = Item(