API: Support for date ranges in exports

This commit is contained in:
Raphael Michel
2023-01-13 13:48:45 +01:00
parent dcf5e67196
commit 8114b47c8c
3 changed files with 96 additions and 0 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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(