330 lines
13 KiB
Python
330 lines
13 KiB
Python
import collections
|
|
|
|
from django.contrib.auth.models import Group
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from rest_framework import serializers
|
|
from rest_framework.exceptions import ValidationError
|
|
|
|
from chrono.agendas.models import AbsenceReason, Agenda, Booking, Category, Event, Subscription
|
|
|
|
|
|
def get_objects_from_slugs(slugs, qs):
|
|
slugs = set(slugs)
|
|
objects = qs.filter(slug__in=slugs)
|
|
if len(objects) != len(slugs):
|
|
unknown_slugs = sorted(slugs - {obj.slug for obj in objects})
|
|
unknown_slugs = ', '.join(unknown_slugs)
|
|
raise ValidationError(('invalid slugs: %s') % unknown_slugs)
|
|
return objects
|
|
|
|
|
|
class StringOrListField(serializers.ListField):
|
|
def to_internal_value(self, data):
|
|
if isinstance(data, str):
|
|
data = [s.strip() for s in data.split(',') if s.strip()]
|
|
return super().to_internal_value(data)
|
|
|
|
|
|
class CommaSeparatedStringField(serializers.ListField):
|
|
def get_value(self, dictionary):
|
|
return super(serializers.ListField, self).get_value(dictionary)
|
|
|
|
def to_internal_value(self, data):
|
|
data = [s.strip() for s in data.split(',') if s.strip()]
|
|
return super().to_internal_value(data)
|
|
|
|
|
|
class SlotSerializer(serializers.Serializer):
|
|
label = serializers.CharField(max_length=250, allow_blank=True)
|
|
user_external_id = serializers.CharField(max_length=250, allow_blank=True)
|
|
user_name = serializers.CharField(max_length=250, allow_blank=True) # compatibility
|
|
user_first_name = serializers.CharField(max_length=250, allow_blank=True)
|
|
user_last_name = serializers.CharField(max_length=250, allow_blank=True)
|
|
user_display_label = serializers.CharField(max_length=250, allow_blank=True)
|
|
user_email = serializers.CharField(max_length=250, allow_blank=True)
|
|
user_phone_number = serializers.CharField(max_length=16, allow_blank=True)
|
|
exclude_user = serializers.BooleanField(default=False)
|
|
events = serializers.CharField(max_length=16, allow_blank=True)
|
|
bypass_delays = serializers.BooleanField(default=False)
|
|
form_url = serializers.CharField(max_length=250, allow_blank=True)
|
|
backoffice_url = serializers.URLField(allow_blank=True)
|
|
cancel_callback_url = serializers.URLField(allow_blank=True)
|
|
count = serializers.IntegerField(min_value=1)
|
|
cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
|
|
force_waiting_list = serializers.BooleanField(default=False)
|
|
use_color_for = serializers.CharField(max_length=250, allow_blank=True)
|
|
|
|
|
|
class SlotsSerializer(SlotSerializer):
|
|
slots = StringOrListField(required=True, child=serializers.CharField(max_length=160, allow_blank=False))
|
|
|
|
def validate(self, attrs):
|
|
super().validate(attrs)
|
|
if not attrs.get('slots'):
|
|
raise serializers.ValidationError({'slots': _('This field is required.')})
|
|
return attrs
|
|
|
|
|
|
class EventsSlotsSerializer(SlotSerializer):
|
|
slots = StringOrListField(required=True, child=serializers.CharField(max_length=160))
|
|
|
|
def validate(self, attrs):
|
|
super().validate(attrs)
|
|
if 'slots' not in attrs:
|
|
raise serializers.ValidationError({'slots': _('This field is required.')})
|
|
if not attrs.get('user_external_id'):
|
|
raise serializers.ValidationError({'user_external_id': _('This field is required.')})
|
|
return attrs
|
|
|
|
|
|
class MultipleAgendasEventsSlotsSerializer(EventsSlotsSerializer):
|
|
def validate_slots(self, value):
|
|
allowed_agenda_slugs = self.context['allowed_agenda_slugs']
|
|
slots_agenda_slugs = set()
|
|
for slot in value:
|
|
try:
|
|
agenda_slug, event_slug = slot.split('@')
|
|
except ValueError:
|
|
raise ValidationError(_('Invalid format for slot %s') % slot)
|
|
if not agenda_slug:
|
|
raise ValidationError(_('Missing agenda slug in slot %s') % slot)
|
|
if not event_slug:
|
|
raise ValidationError(_('Missing event slug in slot %s') % slot)
|
|
slots_agenda_slugs.add(agenda_slug)
|
|
|
|
extra_agendas = slots_agenda_slugs - set(allowed_agenda_slugs)
|
|
if extra_agendas:
|
|
extra_agendas = ', '.join(sorted(extra_agendas))
|
|
raise ValidationError(
|
|
_('Some events belong to agendas that are not present in querystring: %s' % extra_agendas)
|
|
)
|
|
return value
|
|
|
|
|
|
class RecurringFillslotsSerializer(MultipleAgendasEventsSlotsSerializer):
|
|
def validate_slots(self, value):
|
|
super().validate_slots(value)
|
|
open_event_slugs = collections.defaultdict(set)
|
|
for agenda in self.context['agendas']:
|
|
for event in agenda.get_open_recurring_events():
|
|
open_event_slugs[agenda.slug].add(event.slug)
|
|
|
|
slots = collections.defaultdict(lambda: collections.defaultdict(list))
|
|
for slot in value:
|
|
try:
|
|
slugs, day = slot.split(':')
|
|
day = int(day)
|
|
except ValueError:
|
|
raise ValidationError(_('invalid slot: %s') % slot)
|
|
|
|
agenda_slug, event_slug = slugs.split('@')
|
|
if event_slug not in open_event_slugs[agenda_slug]:
|
|
raise ValidationError(
|
|
_('event %(event_slug)s of agenda %(agenda_slug)s is not bookable')
|
|
% {'event_slug': event_slug, 'agenda_slug': agenda_slug}
|
|
)
|
|
|
|
# convert ISO day number to db lookup day number
|
|
day = (day + 1) % 7 + 1
|
|
slots[agenda_slug][event_slug].append(day)
|
|
|
|
return slots
|
|
|
|
|
|
class BookingSerializer(serializers.ModelSerializer):
|
|
user_absence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
|
|
|
class Meta:
|
|
model = Booking
|
|
fields = ['id', 'in_waiting_list', 'user_was_present', 'user_absence_reason', 'extra_data']
|
|
read_only_fields = ['id', 'in_waiting_list']
|
|
|
|
def validate_user_absence_reason(self, value):
|
|
if not value:
|
|
return ''
|
|
|
|
if not self.instance.event.agenda.absence_reasons_group:
|
|
raise serializers.ValidationError(_('unknown absence reason'))
|
|
|
|
reasons_qs = self.instance.event.agenda.absence_reasons_group.absence_reasons
|
|
try:
|
|
reason = reasons_qs.get(slug=value)
|
|
value = reason.label
|
|
except AbsenceReason.DoesNotExist:
|
|
if not reasons_qs.filter(label=value).exists():
|
|
raise serializers.ValidationError(_('unknown absence reason'))
|
|
|
|
return value
|
|
|
|
|
|
class ResizeSerializer(serializers.Serializer):
|
|
count = serializers.IntegerField(min_value=1)
|
|
|
|
|
|
class StatisticsFiltersSerializer(serializers.Serializer):
|
|
time_interval = serializers.ChoiceField(choices=('day', _('Day')), default='day')
|
|
start = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
|
|
end = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
|
|
category = serializers.SlugField(required=False, allow_blank=False, max_length=256)
|
|
agenda = serializers.SlugField(required=False, allow_blank=False, max_length=256)
|
|
group_by = serializers.ListField(
|
|
required=False, child=serializers.SlugField(allow_blank=False, max_length=256)
|
|
)
|
|
|
|
|
|
class DateRangeSerializer(serializers.Serializer):
|
|
datetime_formats = ['%Y-%m-%d', '%Y-%m-%d %H:%M', 'iso-8601']
|
|
|
|
date_start = serializers.DateTimeField(required=False, input_formats=datetime_formats)
|
|
date_end = serializers.DateTimeField(required=False, input_formats=datetime_formats)
|
|
|
|
|
|
class DatetimesSerializer(DateRangeSerializer):
|
|
min_places = serializers.IntegerField(min_value=1, default=1)
|
|
user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=True)
|
|
exclude_user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=True)
|
|
events = serializers.CharField(required=False, max_length=32, allow_blank=True)
|
|
hide_disabled = serializers.BooleanField(default=False)
|
|
bypass_delays = serializers.BooleanField(default=False)
|
|
|
|
def validate(self, attrs):
|
|
super().validate(attrs)
|
|
if (
|
|
'user_external_id' in attrs
|
|
and 'exclude_user_external_id' in attrs
|
|
and attrs['user_external_id'] != attrs['exclude_user_external_id']
|
|
):
|
|
raise ValidationError(
|
|
{'user_external_id': _('user_external_id and exclude_user_external_id have different values')}
|
|
)
|
|
return attrs
|
|
|
|
|
|
class AgendaOrSubscribedSlugsMixin(metaclass=serializers.SerializerMetaclass):
|
|
agendas = CommaSeparatedStringField(
|
|
required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
|
|
)
|
|
subscribed = CommaSeparatedStringField(
|
|
required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
|
|
)
|
|
|
|
def validate(self, attrs):
|
|
super().validate(attrs)
|
|
if 'agendas' not in attrs and 'subscribed' not in attrs:
|
|
raise ValidationError(_('Either "agendas" or "subscribed" parameter is required.'))
|
|
if 'agendas' in attrs and 'subscribed' in attrs:
|
|
raise ValidationError(_('"agendas" and "subscribed" parameters are mutually exclusive.'))
|
|
user_external_id = attrs.get('user_external_id')
|
|
if 'subscribed' in attrs and not user_external_id:
|
|
raise ValidationError(
|
|
{'user_external_id': _('This field is required when using "subscribed" parameter.')}
|
|
)
|
|
|
|
if 'subscribed' in attrs:
|
|
agendas = Agenda.objects.filter(subscriptions__user_external_id=user_external_id).distinct()
|
|
if attrs['subscribed'] != ['all']:
|
|
agendas = agendas.filter(category__slug__in=attrs['subscribed'])
|
|
attrs['agendas'] = agendas
|
|
else:
|
|
attrs['agenda_slugs'] = self.agenda_slugs
|
|
return attrs
|
|
|
|
def validate_agendas(self, value):
|
|
self.agenda_slugs = value
|
|
return get_objects_from_slugs(value, qs=Agenda.objects.filter(kind='events'))
|
|
|
|
|
|
class MultipleAgendasDatetimesSerializer(AgendaOrSubscribedSlugsMixin, DatetimesSerializer):
|
|
show_past_events = serializers.BooleanField(default=False)
|
|
|
|
|
|
class AgendaSlugsSerializer(serializers.Serializer):
|
|
agendas = CommaSeparatedStringField(
|
|
required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
|
|
)
|
|
|
|
|
|
class EventSerializer(serializers.ModelSerializer):
|
|
recurrence_days = CommaSeparatedStringField(
|
|
required=False, child=serializers.IntegerField(min_value=0, max_value=6)
|
|
)
|
|
|
|
class Meta:
|
|
model = Event
|
|
fields = [
|
|
'start_datetime',
|
|
'recurrence_days',
|
|
'recurrence_week_interval',
|
|
'recurrence_end_date',
|
|
'duration',
|
|
'publication_datetime',
|
|
'places',
|
|
'waiting_list_places',
|
|
'label',
|
|
'description',
|
|
'pricing',
|
|
'url',
|
|
]
|
|
|
|
|
|
class AgendaSerializer(serializers.ModelSerializer):
|
|
edit_role = serializers.CharField(required=False, max_length=150)
|
|
view_role = serializers.CharField(required=False, max_length=150)
|
|
category = serializers.SlugField(required=False, max_length=160)
|
|
|
|
class Meta:
|
|
model = Agenda
|
|
fields = [
|
|
'slug',
|
|
'label',
|
|
'kind',
|
|
'minimal_booking_delay',
|
|
'minimal_booking_delay_in_working_days',
|
|
'maximal_booking_delay',
|
|
'anonymize_delay',
|
|
'edit_role',
|
|
'view_role',
|
|
'category',
|
|
]
|
|
|
|
def get_role(self, value):
|
|
try:
|
|
return Group.objects.get(name=value)
|
|
except Group.DoesNotExist:
|
|
raise serializers.ValidationError(_('unknown role: %s' % value))
|
|
|
|
def validate_edit_role(self, value):
|
|
return self.get_role(value)
|
|
|
|
def validate_view_role(self, value):
|
|
return self.get_role(value)
|
|
|
|
def validate_category(self, value):
|
|
try:
|
|
return Category.objects.get(slug=value)
|
|
except Category.DoesNotExist:
|
|
raise serializers.ValidationError(_('unknown category: %s' % value))
|
|
|
|
def validate(self, attrs):
|
|
super().validate(attrs)
|
|
if attrs.get('minimal_booking_delay_in_working_days') and attrs.get('kind', 'events') != 'events':
|
|
raise ValidationError(
|
|
{
|
|
'minimal_booking_delay_in_working_days': _('Option not available on %s agenda')
|
|
% attrs['kind']
|
|
}
|
|
)
|
|
return attrs
|
|
|
|
|
|
class SubscriptionSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Subscription
|
|
fields = ['user_external_id', 'date_start', 'date_end']
|
|
|
|
def validate(self, attrs):
|
|
super().validate(attrs)
|
|
if attrs['date_start'] > attrs['date_end']:
|
|
raise ValidationError(_('start_datetime must be before end_datetime'))
|
|
return attrs
|