Memberships: Check valid_from/valid_until for parallel usage (#3975)

This commit is contained in:
Raphael Michel
2024-03-15 16:40:41 +01:00
committed by GitHub
parent b6221ab6d9
commit 9f794290dc
5 changed files with 272 additions and 16 deletions

View File

@@ -49,7 +49,8 @@ class MembershipType(LoggedModel):
allow_parallel_usage = models.BooleanField(
verbose_name=_('Parallel usage is allowed'),
help_text=_('If this is selected, the membership can be used to purchase tickets for events happening at the same time. Note '
'that this will only check for an identical start time of the events, not for any overlap between events.'),
'that this will only check for an identical start time of the events, not for any overlap between events. An overlap '
'check will be performed if there is a product-level validity of the ticket.'),
default=False
)
max_usages = models.PositiveIntegerField(

View File

@@ -29,8 +29,8 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from pretix.base.models import (
AbstractPosition, Customer, Event, Item, Membership, Order, OrderPosition,
SubEvent,
AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
OrderPosition, SubEvent,
)
from pretix.helpers import OF_SELF
@@ -82,7 +82,8 @@ def create_membership(customer: Customer, position: OrderPosition):
)
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False):
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False,
valid_from_not_chosen=False):
"""
Validate that a set of cart or order positions. This currently does not validate
@@ -132,7 +133,11 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
qs = qs.exclude(order_id=ignored_order.pk)
m._used_at_dates = [
(op.subevent or op.order.event).date_from
for op in qs
for op in qs if not op.valid_from or not op.valid_until
]
m._used_for_ranges = [
(op.valid_from, op.valid_until)
for op in qs if op.valid_from or op.valid_until
]
for p in applicable_positions:
@@ -192,13 +197,42 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
m.usages += 1
if not m.membership_type.allow_parallel_usage:
df = ev.date_from
if any(abs(df - d) < timedelta(minutes=1) for d in m._used_at_dates):
raise ValidationError(
_('You are trying to use a membership of type "{type}" for an event taking place at {date}, '
'however you already used the same membership for a different ticket at the same time.').format(
type=m.membership_type.name,
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
if isinstance(p, (OrderPosition, CartPosition)):
# override_ variants are for usage of fake cart in OrderChangeManager
valid_from = getattr(p, 'override_valid_from', p.valid_from)
valid_until = getattr(p, 'override_valid_until', p.valid_until)
else: # future safety, not technically defined on AbstractPosition
valid_from = None
valid_until = None
if (valid_from or valid_until) and not (p.item.validity_dynamic_start_choice and valid_from_not_chosen):
for used_range in m._used_for_ranges:
if valid_from and valid_from > used_range[1]:
continue
if valid_until and valid_until < used_range[0]:
continue
raise ValidationError(
_('You are trying to use a membership of type "{type}" for a ticket valid from {valid_from} '
'until {valid_until}, however you already used the same membership for a different ticket '
'that overlaps with this time frame ({conflict_from} {conflict_until}).').format(
type=m.membership_type.name,
valid_from=date_format(valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_from else _('start'),
valid_until=date_format(valid_until.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_until else _('open end'),
conflict_from=date_format(used_range[0].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[0] else _('start'),
conflict_until=date_format(used_range[1].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[1] else _('open end'),
)
)
)
m._used_at_dates.append(ev.date_from)
m._used_for_ranges.append((p.valid_from, p.valid_until))
if not valid_from or not valid_until:
df = ev.date_from
if any(abs(df - d) < timedelta(minutes=1) for d in m._used_at_dates):
raise ValidationError(
_('You are trying to use a membership of type "{type}" for an event taking place at {date}, '
'however you already used the same membership for a different ticket at the same time.').format(
type=m.membership_type.name,
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
)
)
m._used_at_dates.append(ev.date_from)

View File

@@ -2671,6 +2671,7 @@ class OrderChangeManager:
for p in self.order.positions.all():
cp = CartPosition(
event=self.event,
item=p.item,
variation=p.variation,
attendee_name_parts=p.attendee_name_parts,
@@ -2691,16 +2692,23 @@ class OrderChangeManager:
positions_to_fake_cart[op.position].seat = op.seat
elif isinstance(op, self.MembershipOperation):
positions_to_fake_cart[op.position].used_membership = op.membership
elif isinstance(op, self.ChangeValidFromOperation):
positions_to_fake_cart[op.position].override_valid_from = op.valid_from
elif isinstance(op, self.ChangeValidUntilOperation):
positions_to_fake_cart[op.position].override_valid_until = op.valid_until
elif isinstance(op, self.CancelOperation) and op.position in positions_to_fake_cart:
fake_cart.remove(positions_to_fake_cart[op.position])
elif isinstance(op, self.AddOperation):
cp = CartPosition(
event=self.event,
item=op.item,
variation=op.variation,
used_membership=op.membership,
subevent=op.subevent,
seat=op.seat,
)
cp.override_valid_from = op.valid_from
cp.override_valid_until = op.valid_until
fake_cart.append(cp)
try:
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order, testmode=self.order.testmode)