mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
API: Support for date ranges in exports
This commit is contained in:
@@ -22,8 +22,10 @@
|
||||
from django import forms
|
||||
from django.http import QueryDict
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
|
||||
|
||||
|
||||
class FormFieldWrapperField(serializers.Field):
|
||||
@@ -142,6 +144,12 @@ class JobRunSerializer(serializers.Serializer):
|
||||
allow_null=not v.required,
|
||||
validators=v.validators,
|
||||
)
|
||||
elif isinstance(v, DateFrameField):
|
||||
self.fields[k] = SerializerDateFrameField(
|
||||
required=v.required,
|
||||
allow_null=not v.required,
|
||||
validators=v.validators,
|
||||
)
|
||||
else:
|
||||
self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required)
|
||||
|
||||
@@ -151,5 +159,40 @@ class JobRunSerializer(serializers.Serializer):
|
||||
for k, v in self.fields.items():
|
||||
if isinstance(v, serializers.ManyRelatedField) and k not in data:
|
||||
data[k] = []
|
||||
|
||||
for fk in self.fields.keys():
|
||||
# Backwards compatibility for exports that used to take e.g. (date_from, date_to) or (event_date_from, event_date_to)
|
||||
# and now only take date_range.
|
||||
if fk.endswith("_range") and isinstance(self.fields[fk], SerializerDateFrameField) and fk not in data:
|
||||
if fk.replace("_range", "_from") in data:
|
||||
d_from = data.pop(fk.replace("_range", "_from"))
|
||||
if d_from:
|
||||
d_from = serializers.DateField().to_internal_value(d_from)
|
||||
else:
|
||||
d_from = None
|
||||
if fk.replace("_range", "_to") in data:
|
||||
d_to = data.pop(fk.replace("_range", "_to"))
|
||||
if d_to:
|
||||
d_to = serializers.DateField().to_internal_value(d_to)
|
||||
else:
|
||||
d_to = None
|
||||
data[fk] = f'{d_from.isoformat() if d_from else ""}/{d_to.isoformat() if d_to else ""}'
|
||||
|
||||
data = super().to_internal_value(data)
|
||||
return data
|
||||
|
||||
def is_valid(self, raise_exception=False):
|
||||
super().is_valid(raise_exception=raise_exception)
|
||||
|
||||
fields_keys = set(self.fields.keys())
|
||||
input_keys = set(self.initial_data.keys())
|
||||
|
||||
additional_fields = input_keys - fields_keys
|
||||
|
||||
if bool(additional_fields):
|
||||
self._errors['fields'] = ['Additional fields not allowed: {}.'.format(list(additional_fields))]
|
||||
|
||||
if self._errors and raise_exception:
|
||||
raise ValidationError(self.errors)
|
||||
|
||||
return not bool(self._errors)
|
||||
|
||||
@@ -24,11 +24,13 @@ from datetime import date, datetime, time, timedelta
|
||||
from itertools import groupby
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import gettext_lazy, pgettext_lazy
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.helpers.daterange import daterange
|
||||
|
||||
@@ -382,6 +384,22 @@ class DateFrameField(forms.MultiValueField):
|
||||
return super().clean(value)
|
||||
|
||||
|
||||
class SerializerDateFrameField(serializers.CharField):
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data is None:
|
||||
return None
|
||||
try:
|
||||
resolve_timeframe_to_dates_inclusive(now(), data, pytz.UTC)
|
||||
except:
|
||||
raise ValidationError("Invalid date frame")
|
||||
|
||||
def to_representation(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
|
||||
"""
|
||||
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of dates
|
||||
|
||||
@@ -162,6 +162,7 @@ def test_org_validate_events(token_client, organizer, team, event):
|
||||
def test_run_success(token_client, organizer, team, event):
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/exporters/orderlist/run/'.format(organizer.slug, event.slug), data={
|
||||
'_format': 'xlsx',
|
||||
'date_range': 'year_this'
|
||||
}, format='json')
|
||||
assert resp.status_code == 202
|
||||
assert "download" in resp.data
|
||||
@@ -170,6 +171,40 @@ def test_run_success(token_client, organizer, team, event):
|
||||
assert resp["Content-Type"] == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_run_success_old_date_frame(token_client, organizer, team, event):
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/exporters/orderlist/run/'.format(organizer.slug, event.slug), data={
|
||||
'_format': 'xlsx',
|
||||
'date_from': '2020-01-01',
|
||||
'date_to': '2023-12-31'
|
||||
}, format='json')
|
||||
assert resp.status_code == 202
|
||||
assert "download" in resp.data
|
||||
resp = token_client.get("/" + resp.data["download"].split("/", 3)[3])
|
||||
assert resp.status_code == 200
|
||||
assert resp["Content-Type"] == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_run_date_frame_validation(token_client, organizer, team, event):
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/exporters/orderlist/run/'.format(organizer.slug, event.slug), data={
|
||||
'_format': 'xlsx',
|
||||
'date_range': 'invalid'
|
||||
}, format='json')
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"date_range": ["Invalid date frame"]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_run_additional_fields_forbidden(token_client, organizer, team, event):
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/exporters/orderlist/run/'.format(organizer.slug, event.slug), data={
|
||||
'_format': 'xlsx',
|
||||
'foobar': 'invalid'
|
||||
}, format='json')
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"fields": ["Additional fields not allowed: ['foobar']."]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_download_nonexisting(token_client, organizer, team, event):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/exporters/orderlist/download/{}/{}/'.format(
|
||||
|
||||
Reference in New Issue
Block a user