mirror of
https://github.com/pretix/pretix.git
synced 2026-05-15 16:54:00 +00:00
initial implementation
This commit is contained in:
@@ -34,7 +34,7 @@ from django.utils.timezone import now
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
BASE_CHOICES = (
|
EVENT_CHOICES = (
|
||||||
('date_from', _('Event start')),
|
('date_from', _('Event start')),
|
||||||
('date_to', _('Event end')),
|
('date_to', _('Event end')),
|
||||||
('date_admission', _('Event admission')),
|
('date_admission', _('Event admission')),
|
||||||
@@ -42,6 +42,11 @@ BASE_CHOICES = (
|
|||||||
('presale_end', _('Presale end')),
|
('presale_end', _('Presale end')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ORDER_CHOICES = (
|
||||||
|
('datetime', _('Moment of order')),
|
||||||
|
)
|
||||||
|
ORDER_CHOICES_KEYS = [choice[0] for choice in ORDER_CHOICES]
|
||||||
|
|
||||||
RelativeDate = namedtuple('RelativeDate', ['days', 'minutes', 'time', 'is_after', 'base_date_name'], defaults=(0, None, None, False, 'date_from'))
|
RelativeDate = namedtuple('RelativeDate', ['days', 'minutes', 'time', 'is_after', 'base_date_name'], defaults=(0, None, None, False, 'date_from'))
|
||||||
|
|
||||||
|
|
||||||
@@ -52,15 +57,15 @@ class RelativeDateWrapper:
|
|||||||
time to calculate the date based on a base point.
|
time to calculate the date based on a base point.
|
||||||
|
|
||||||
The base point can be the date_from, date_to, date_admission, presale_start or presale_end
|
The base point can be the date_from, date_to, date_admission, presale_start or presale_end
|
||||||
attribute of an event or subevent. If the respective attribute is not set, ``date_from``
|
attribute of an event or subevent, as well as a time of order.
|
||||||
will be used.
|
If the respective attribute is not set, ``date_from`` will be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data: Union[datetime.datetime, RelativeDate]):
|
def __init__(self, data: Union[datetime.datetime, RelativeDate]):
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
def date(self, event) -> datetime.date:
|
def date(self, reference) -> datetime.date:
|
||||||
from .models import SubEvent
|
from .models import SubEvent, Event, Order
|
||||||
|
|
||||||
if isinstance(self.data, datetime.datetime):
|
if isinstance(self.data, datetime.datetime):
|
||||||
return self.data.date()
|
return self.data.date()
|
||||||
@@ -70,15 +75,27 @@ class RelativeDateWrapper:
|
|||||||
if self.data.minutes is not None:
|
if self.data.minutes is not None:
|
||||||
raise ValueError('A minute-based relative datetime can not be used as a date')
|
raise ValueError('A minute-based relative datetime can not be used as a date')
|
||||||
|
|
||||||
tz = ZoneInfo(event.settings.timezone)
|
if self.data.base_date_name in ORDER_CHOICES_KEYS:
|
||||||
if isinstance(event, SubEvent):
|
if not isinstance(reference, Order):
|
||||||
|
raise ValueError('A order-based relative datetime choice must be used with an order object')
|
||||||
|
order = reference
|
||||||
|
event = order.event
|
||||||
|
base_date = getattr(order, self.data.base_date_name)
|
||||||
|
elif isinstance(reference, SubEvent):
|
||||||
|
subevent = reference
|
||||||
|
event = subevent
|
||||||
base_date = (
|
base_date = (
|
||||||
getattr(event, self.data.base_date_name)
|
getattr(subevent, self.data.base_date_name)
|
||||||
or getattr(event.event, self.data.base_date_name)
|
or getattr(subevent.event, self.data.base_date_name)
|
||||||
or event.date_from
|
or subevent.date_from
|
||||||
)
|
)
|
||||||
else:
|
elif isinstance(reference, Event):
|
||||||
|
event = reference
|
||||||
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
||||||
|
else:
|
||||||
|
raise ValueError("Only event, subevent or order objects are supported")
|
||||||
|
|
||||||
|
tz = ZoneInfo(event.settings.timezone)
|
||||||
|
|
||||||
if self.data.is_after:
|
if self.data.is_after:
|
||||||
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)
|
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)
|
||||||
@@ -86,21 +103,33 @@ class RelativeDateWrapper:
|
|||||||
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)
|
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)
|
||||||
return new_date.date()
|
return new_date.date()
|
||||||
|
|
||||||
def datetime(self, event) -> datetime.datetime:
|
def datetime(self, reference) -> datetime.datetime:
|
||||||
from .models import SubEvent
|
from .models import SubEvent, Event, Order
|
||||||
|
|
||||||
if isinstance(self.data, (datetime.datetime, datetime.date)):
|
if isinstance(self.data, (datetime.datetime, datetime.date)):
|
||||||
return self.data
|
return self.data
|
||||||
else:
|
else:
|
||||||
tz = ZoneInfo(event.settings.timezone)
|
if self.data.base_date_name in ORDER_CHOICES_KEYS:
|
||||||
if isinstance(event, SubEvent):
|
if not isinstance(reference, Order):
|
||||||
|
raise ValueError('A order-based relative datetime choice must be used with an order object')
|
||||||
|
order = reference
|
||||||
|
event = order.event
|
||||||
|
base_date = getattr(order, self.data.base_date_name)
|
||||||
|
elif isinstance(reference, SubEvent):
|
||||||
|
subevent = reference
|
||||||
|
event = subevent
|
||||||
base_date = (
|
base_date = (
|
||||||
getattr(event, self.data.base_date_name)
|
getattr(subevent, self.data.base_date_name)
|
||||||
or getattr(event.event, self.data.base_date_name)
|
or getattr(subevent.event, self.data.base_date_name)
|
||||||
or event.date_from
|
or subevent.date_from
|
||||||
)
|
)
|
||||||
else:
|
elif isinstance(reference, Event):
|
||||||
|
event = reference
|
||||||
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
||||||
|
else:
|
||||||
|
raise ValueError("Only event, subevent or order objects are supported")
|
||||||
|
|
||||||
|
tz = ZoneInfo(event.settings.timezone)
|
||||||
|
|
||||||
if self.data.minutes is not None:
|
if self.data.minutes is not None:
|
||||||
if self.data.is_after:
|
if self.data.is_after:
|
||||||
@@ -172,7 +201,7 @@ class RelativeDateWrapper:
|
|||||||
minutes=None,
|
minutes=None,
|
||||||
is_after=len(parts) > 4 and parts[4] == "after",
|
is_after=len(parts) > 4 and parts[4] == "after",
|
||||||
)
|
)
|
||||||
if data.base_date_name not in [k[0] for k in BASE_CHOICES]:
|
if data.base_date_name not in [k[0] for k in EVENT_CHOICES + ORDER_CHOICES]:
|
||||||
raise ValueError('{} is not a valid base date'.format(data.base_date_name))
|
raise ValueError('{} is not a valid base date'.format(data.base_date_name))
|
||||||
else:
|
else:
|
||||||
data = parser.parse(input)
|
data = parser.parse(input)
|
||||||
@@ -311,11 +340,17 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
]
|
]
|
||||||
if kwargs.get('limit_choices'):
|
if kwargs.get('limit_choices'):
|
||||||
limit = kwargs.pop('limit_choices')
|
limit = kwargs.pop('limit_choices')
|
||||||
choices = [(k, v) for k, v in BASE_CHOICES if k in limit]
|
choices = [(k, v) for k, v in EVENT_CHOICES if k in limit]
|
||||||
else:
|
else:
|
||||||
choices = BASE_CHOICES
|
choices = EVENT_CHOICES
|
||||||
|
|
||||||
|
self.relative_to_order = kwargs.pop('relative_to_order', False)
|
||||||
|
if self.relative_to_order:
|
||||||
|
choices += ORDER_CHOICES
|
||||||
|
|
||||||
if not kwargs.get('required', True):
|
if not kwargs.get('required', True):
|
||||||
status_choices.insert(0, ('unset', _('Not set')))
|
status_choices.insert(0, ('unset', _('Not set')))
|
||||||
|
|
||||||
fields = reldatetimeparts(
|
fields = reldatetimeparts(
|
||||||
status=forms.ChoiceField(
|
status=forms.ChoiceField(
|
||||||
choices=status_choices,
|
choices=status_choices,
|
||||||
@@ -359,12 +394,13 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def set_event(self, event):
|
def set_event(self, event):
|
||||||
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = [
|
choices = [
|
||||||
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
(k, v) for k, v in EVENT_CHOICES if getattr(event, k, None)
|
||||||
]
|
|
||||||
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = [
|
|
||||||
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
|
||||||
]
|
]
|
||||||
|
if self.relative_to_order:
|
||||||
|
choices += ORDER_CHOICES
|
||||||
|
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = choices
|
||||||
|
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = choices
|
||||||
|
|
||||||
def compress(self, data_list):
|
def compress(self, data_list):
|
||||||
if not data_list:
|
if not data_list:
|
||||||
@@ -404,6 +440,10 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto):
|
elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto):
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
|
elif data.status == 'relative' and data.rel_days_relationto in ORDER_CHOICES_KEYS and data.rel_days_relation == 'before':
|
||||||
|
raise ValidationError(_('A relative date in relation to an order can only be after the order has been placed'))
|
||||||
|
elif data.status == 'relative' and data.rel_mins_relationto in ORDER_CHOICES_KEYS and data.rel_mins_relation == 'before':
|
||||||
|
raise ValidationError(_('A relative date in relation to an order can only be after the order has been placed'))
|
||||||
|
|
||||||
return super().clean(value)
|
return super().clean(value)
|
||||||
|
|
||||||
@@ -424,13 +464,14 @@ class RelativeDateWidget(RelativeDateTimeWidget):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.status_choices = kwargs.pop('status_choices')
|
self.status_choices = kwargs.pop('status_choices')
|
||||||
|
base_choices = kwargs.pop('base_choices')
|
||||||
widgets = reldateparts(
|
widgets = reldateparts(
|
||||||
status=forms.RadioSelect(choices=self.status_choices),
|
status=forms.RadioSelect(choices=self.status_choices),
|
||||||
absolute=forms.DateInput(
|
absolute=forms.DateInput(
|
||||||
attrs={'class': 'datepickerfield'}
|
attrs={'class': 'datepickerfield'}
|
||||||
),
|
),
|
||||||
rel_days_number=forms.NumberInput(),
|
rel_days_number=forms.NumberInput(),
|
||||||
rel_days_relationto=forms.Select(choices=kwargs.pop('base_choices')),
|
rel_days_relationto=forms.Select(choices=base_choices),
|
||||||
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||||
)
|
)
|
||||||
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
|
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
|
||||||
@@ -474,6 +515,12 @@ class RelativeDateField(RelativeDateTimeField):
|
|||||||
]
|
]
|
||||||
if not kwargs.get('required', True):
|
if not kwargs.get('required', True):
|
||||||
status_choices.insert(0, ('unset', _('Not set')))
|
status_choices.insert(0, ('unset', _('Not set')))
|
||||||
|
|
||||||
|
choices = EVENT_CHOICES
|
||||||
|
self.relative_to_order = kwargs.pop('relative_to_order', False)
|
||||||
|
if self.relative_to_order:
|
||||||
|
choices += ORDER_CHOICES
|
||||||
|
|
||||||
fields = reldateparts(
|
fields = reldateparts(
|
||||||
status=forms.ChoiceField(
|
status=forms.ChoiceField(
|
||||||
choices=status_choices,
|
choices=status_choices,
|
||||||
@@ -486,7 +533,7 @@ class RelativeDateField(RelativeDateTimeField):
|
|||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_relationto=forms.ChoiceField(
|
rel_days_relationto=forms.ChoiceField(
|
||||||
choices=BASE_CHOICES,
|
choices=choices,
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_relation=forms.ChoiceField(
|
rel_days_relation=forms.ChoiceField(
|
||||||
@@ -495,15 +542,18 @@ class RelativeDateField(RelativeDateTimeField):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if 'widget' not in kwargs:
|
if 'widget' not in kwargs:
|
||||||
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
|
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=choices)
|
||||||
forms.MultiValueField.__init__(
|
forms.MultiValueField.__init__(
|
||||||
self, fields=fields, require_all_fields=False, *args, **kwargs
|
self, fields=fields, require_all_fields=False, *args, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_event(self, event):
|
def set_event(self, event):
|
||||||
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = [
|
choices = [
|
||||||
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
(k, v) for k, v in EVENT_CHOICES if getattr(event, k, None)
|
||||||
]
|
]
|
||||||
|
if self.relative_to_order:
|
||||||
|
choices += ORDER_CHOICES
|
||||||
|
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = choices
|
||||||
|
|
||||||
def compress(self, data_list):
|
def compress(self, data_list):
|
||||||
if not data_list:
|
if not data_list:
|
||||||
@@ -527,6 +577,9 @@ class RelativeDateField(RelativeDateTimeField):
|
|||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto):
|
elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto):
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
|
elif data.status == 'relative' and data.rel_days_relationto in ORDER_CHOICES_KEYS and data.rel_days_relation == 'before':
|
||||||
|
raise ValidationError(_('A relative date in relation to an order can only be after the order has been placed'))
|
||||||
|
|
||||||
|
|
||||||
return forms.MultiValueField.clean(self, value)
|
return forms.MultiValueField.clean(self, value)
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from datetime import datetime, time
|
from datetime import datetime, time, timedelta
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django_scopes import scope
|
from django_scopes import scope
|
||||||
|
|
||||||
from pretix.base.models import Event, Organizer
|
from pretix.base.models import Event, Organizer, Order
|
||||||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||||||
|
|
||||||
TOKYO = ZoneInfo('Asia/Tokyo')
|
TOKYO = ZoneInfo('Asia/Tokyo')
|
||||||
@@ -46,6 +46,7 @@ def event():
|
|||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_absolute_date(event):
|
def test_absolute_date(event):
|
||||||
d = datetime(2017, 12, 25, 5, 0, 0, tzinfo=TOKYO)
|
d = datetime(2017, 12, 25, 5, 0, 0, tzinfo=TOKYO)
|
||||||
@@ -147,3 +148,25 @@ def test_unserialize():
|
|||||||
|
|
||||||
rdw = RelativeDateWrapper.from_string('RELDATE/minutes/60/date_from/')
|
rdw = RelativeDateWrapper.from_string('RELDATE/minutes/60/date_from/')
|
||||||
assert rdw.data == RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60)
|
assert rdw.data == RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_relative_to_order(event):
|
||||||
|
with scope(organizer=event.organizer):
|
||||||
|
order_moment = datetime(2020, 3, 29, 18, 0, 0, tzinfo=TOKYO)
|
||||||
|
|
||||||
|
order = Order.objects.create(
|
||||||
|
code='FOO', event=event, email='dummy@dummy.test',
|
||||||
|
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
|
||||||
|
datetime=order_moment,
|
||||||
|
expires=order_moment + timedelta(days=10),
|
||||||
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||||
|
total=23, locale='en'
|
||||||
|
)
|
||||||
|
|
||||||
|
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='datetime', minutes=None))
|
||||||
|
assert rdw.datetime(order).astimezone(TOKYO) == datetime(2020, 3, 28, 18, 0, 0, tzinfo=TOKYO)
|
||||||
|
assert rdw.to_string() == 'RELDATE/1/-/datetime/'
|
||||||
|
|
||||||
|
rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='datetime', minutes=None, is_after=True))
|
||||||
|
assert rdw.datetime(order).astimezone(TOKYO) == datetime(2020, 3, 30, 18, 0, 0, tzinfo=TOKYO)
|
||||||
|
assert rdw.to_string() == 'RELDATE/1/-/datetime/after'
|
||||||
|
|||||||
Reference in New Issue
Block a user