api: include custom fields in events details (#63288)

This commit is contained in:
Lauréline Guérin 2022-04-07 17:01:57 +02:00
parent 51f554aeaf
commit 0c9b65cead
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
8 changed files with 148 additions and 24 deletions

View File

@ -1827,6 +1827,14 @@ class Event(models.Model):
datetime_part = self.slug.rsplit('--')[-1]
return datetime.datetime.strptime(datetime_part, '%Y-%m-%d-%H%M')
def get_custom_fields(self):
if not self.agenda.events_type:
return {}
custom_fields = {}
for custom_field in self.agenda.events_type.get_custom_fields():
custom_fields[custom_field['varname']] = self.custom_fields.get(custom_field['varname'])
return custom_fields
class EventsType(models.Model):
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)

View File

@ -220,6 +220,9 @@ class AgendaOrSubscribedSlugsMixin(DateRangeMixin):
user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
guardian_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
def get_agenda_qs(self):
return Agenda.objects.filter(kind='events').select_related('events_type')
def validate(self, attrs):
super().validate(attrs)
if 'agendas' not in attrs and 'subscribed' not in attrs:
@ -244,7 +247,7 @@ class AgendaOrSubscribedSlugsMixin(DateRangeMixin):
if 'date_end' in attrs:
# subscription must start before requested date end
lookups['subscriptions__date_start__lte'] = attrs['date_end']
agendas = Agenda.objects.filter(**lookups).distinct().select_related('category')
agendas = self.get_agenda_qs().filter(**lookups).distinct().select_related('category')
if attrs['subscribed'] != ['all']:
agendas = agendas.filter(category__slug__in=attrs['subscribed'])
@ -257,7 +260,7 @@ class AgendaOrSubscribedSlugsMixin(DateRangeMixin):
def validate_agendas(self, value):
self.agenda_slugs = value
return get_objects_from_slugs(value, qs=Agenda.objects.filter(kind='events'))
return get_objects_from_slugs(value, qs=self.get_agenda_qs())
class MultipleAgendasDatetimesSerializer(AgendaOrSubscribedSlugsMixin, DatetimesSerializer):

View File

@ -503,6 +503,8 @@ def get_event_detail(
'url': event.url,
'duration': event.duration,
}
for key, value in event.get_custom_fields().items():
details['custom_field_%s' % key] = value
if booking:
details['booking'] = {
'id': booking.pk,
@ -803,12 +805,13 @@ class Datetimes(APIView):
serializer_class = serializers.DatetimesSerializer
def get(self, request, agenda_identifier=None, format=None):
agenda_qs = Agenda.objects.select_related('events_type')
try:
agenda = Agenda.objects.get(slug=agenda_identifier)
agenda = agenda_qs.get(slug=agenda_identifier)
except Agenda.DoesNotExist:
try:
# legacy access by agenda id
agenda = Agenda.objects.get(id=int(agenda_identifier))
agenda = agenda_qs.get(id=int(agenda_identifier))
except (ValueError, Agenda.DoesNotExist):
raise Http404()
if agenda.kind != 'events':
@ -1653,6 +1656,7 @@ class RecurringFillslots(APIView):
payload = serializer.validated_data
user_external_id = payload['user_external_id']
agendas = Agenda.prefetch_events(data['agendas'], user_external_id=user_external_id)
agendas_by_id = {x.id: x for x in agendas}
if data['action'] == 'update':
events_to_book = self.get_event_recurrences(
@ -1702,6 +1706,9 @@ class RecurringFillslots(APIView):
# exclude full events
full_events = list(events_to_book.filter(full=True))
# don't reload agendas and events types
for event in full_events:
event.agenda = agendas_by_id[event.agenda_id]
events_to_book = events_to_book.filter(full=False)
events_to_book = events_to_book.annotate(
@ -1739,8 +1746,13 @@ class RecurringFillslots(APIView):
'full_events': [get_event_detail(request, x, multiple_agendas=True) for x in full_events],
}
if payload.get('include_booked_events_detail'):
# don't reload agendas and events types
for event in events_to_book:
event.agenda = agendas_by_id[event.agenda_id]
events_to_book_by_id = {x.id: x for x in events_to_book}
response['booked_events'] = [
get_event_detail(request, x.event, booking=x, multiple_agendas=True) for x in created_bookings
get_event_detail(request, events_to_book_by_id[x.event_id], booking=x, multiple_agendas=True)
for x in created_bookings
]
return Response(response)
@ -1805,7 +1817,9 @@ class EventsFillslots(APIView):
multiple_agendas = False
def post(self, request, agenda_identifier):
self.agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events')
self.agenda = get_object_or_404(
Agenda.objects.select_related('events_type'), slug=agenda_identifier, kind='events'
)
return self.fillslots(request)
def fillslots(self, request):
@ -1892,18 +1906,26 @@ class EventsFillslots(APIView):
# create missing bookings
created_bookings = Booking.objects.bulk_create(bookings)
# don't reload agendas and events types
for event in events:
event.agenda = agendas_by_ids[event.agenda_id]
events_by_id = {x.id: x for x in events}
response = {
'err': 0,
'booking_count': len(bookings),
'booked_events': [
get_event_detail(request, x.event, booking=x, multiple_agendas=self.multiple_agendas)
get_event_detail(
request, events_by_id[x.event_id], booking=x, multiple_agendas=self.multiple_agendas
)
for x in created_bookings
if x.event.pk not in waiting_list_event_ids
if x.event_id not in waiting_list_event_ids
],
'waiting_list_events': [
get_event_detail(request, x.event, booking=x, multiple_agendas=self.multiple_agendas)
get_event_detail(
request, events_by_id[x.event_id], booking=x, multiple_agendas=self.multiple_agendas
)
for x in created_bookings
if x.event.pk in waiting_list_event_ids
if x.event_id in waiting_list_event_ids
],
'cancelled_booking_count': cancelled_count,
}

View File

@ -7,7 +7,7 @@ from django.test import override_settings
from django.test.utils import CaptureQueriesContext
from django.utils.timezone import localtime, make_aware, make_naive, now
from chrono.agendas.models import Agenda, Booking, Desk, Event, TimePeriodException
from chrono.agendas.models import Agenda, Booking, Desk, Event, EventsType, TimePeriodException
from tests.utils import login
pytestmark = pytest.mark.django_db
@ -177,7 +177,7 @@ def test_datetime_api_backoffice_url(app, admin_user):
assert event.label in app.get(url).text
def test_datetime_api_min_places(app):
def test_datetimes_api_min_places(app):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(start_datetime=now() + datetime.timedelta(days=7), places=5, agenda=agenda)
@ -198,6 +198,35 @@ def test_datetime_api_min_places(app):
assert resp.json['err'] == 1
def test_datetimes_api_(app):
events_type = EventsType.objects.create(
label='Foo',
custom_fields=[
{'varname': 'text', 'label': 'Text', 'field_type': 'text'},
{'varname': 'textarea', 'label': 'TextArea', 'field_type': 'textarea'},
{'varname': 'bool', 'label': 'Bool', 'field_type': 'bool'},
],
)
agenda = Agenda.objects.create(label='Foo bar', kind='events', events_type=events_type)
Event.objects.create(
slug='event-slug',
start_datetime=localtime(now() + datetime.timedelta(days=5)).replace(hour=17, minute=0),
places=5,
agenda=agenda,
custom_fields={
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
},
)
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
assert len(ctx.captured_queries) == 2
assert resp.json['data'][0]['custom_field_text'] == 'foo'
assert resp.json['data'][0]['custom_field_textarea'] == 'foo bar'
assert resp.json['data'][0]['custom_field_bool'] is True
@pytest.mark.freeze_time('2021-02-23')
def test_datetimes_api_exclude_slots(app):
agenda = Agenda.objects.create(

View File

@ -15,6 +15,11 @@ def test_status(app, user):
start_datetime=(now() + datetime.timedelta(days=5)).replace(hour=10, minute=0),
places=10,
agenda=agenda,
custom_fields={
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
},
)
agenda2 = Agenda.objects.create(label='Foo bar2', kind='events', minimal_booking_delay=0)
# other event with the same slug but in another agenda
@ -31,9 +36,6 @@ def test_status(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.get('/api/agenda/%s/status/%s/' % (agenda.slug, event.slug))
assert resp.json['err'] == 0
assert resp.json['places']['total'] == 10
assert resp.json['places']['available'] == 9
assert resp.json['places']['reserved'] == 1
assert resp.json == {
'err': 0,
'id': 'event-slug',
@ -59,6 +61,44 @@ def test_status(app, user):
}
assert 'waiting_list_total' not in resp.json['places']
events_type = EventsType.objects.create(
label='Foo',
custom_fields=[
{'varname': 'text', 'label': 'Text', 'field_type': 'text'},
{'varname': 'textarea', 'label': 'TextArea', 'field_type': 'textarea'},
{'varname': 'bool', 'label': 'Bool', 'field_type': 'bool'},
],
)
agenda.events_type = events_type
agenda.save()
resp = app.get('/api/agenda/%s/status/%s/' % (agenda.slug, event.slug))
assert resp.json == {
'err': 0,
'id': 'event-slug',
'slug': 'event-slug',
'text': str(event),
'label': '',
'agenda_label': 'Foo bar',
'date': localtime(event.start_datetime).strftime('%Y-%m-%d'),
'datetime': localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S'),
'description': None,
'pricing': None,
'url': None,
'disabled': False,
'duration': None,
'custom_field_text': 'foo',
'custom_field_textarea': 'foo bar',
'custom_field_bool': True,
'api': {
'bookings_url': 'http://testserver/api/agenda/foo-bar/bookings/event-slug/',
'fillslot_url': 'http://testserver/api/agenda/foo-bar/fillslot/event-slug/',
'status_url': 'http://testserver/api/agenda/foo-bar/status/event-slug/',
'check_url': 'http://testserver/api/agenda/foo-bar/check/event-slug/',
'backoffice_url': 'http://testserver/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk),
},
'places': {'available': 9, 'reserved': 1, 'total': 10, 'full': False, 'has_waiting_list': False},
}
Booking(event=event, in_waiting_list=True).save()
event.waiting_list_places = 5
event.save()

View File

@ -14,6 +14,7 @@ from chrono.agendas.models import (
Category,
Desk,
Event,
EventsType,
MeetingType,
Person,
Resource,
@ -2381,11 +2382,13 @@ def test_fillslot_past_events_recurring_event(app, user):
@pytest.mark.parametrize('action', ['book', 'update'])
def test_recurring_events_api_fillslots(app, user, freezer, action):
freezer.move_to('2021-09-06 12:00')
events_type = EventsType.objects.create(label='Foo')
agenda = Agenda.objects.create(
label='Foo bar',
kind='events',
minimal_booking_delay=7,
maximal_booking_delay=35, # only 4 bookable weeks
events_type=events_type,
)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
@ -2459,7 +2462,9 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
assert events.filter(booked_waiting_list_places=1).count() == 1
params['user_external_id'] = 'user_id_3'
resp = app.post_json(fillslots_url, params=params)
with CaptureQueriesContext(connection) as ctx:
resp = app.post_json(fillslots_url, params=params)
assert len(ctx.captured_queries) in [12, 13]
# everything goes in waiting list
assert events.filter(booked_waiting_list_places=1).count() == 6
# but an event was full
@ -3619,8 +3624,9 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user):
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
events_type = EventsType.objects.create(label='Foo')
for i in range(20):
agenda = Agenda.objects.create(slug=f'{i}', kind='events')
agenda = Agenda.objects.create(slug=f'{i}', kind='events', events_type=events_type)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
start, end = now(), now() + datetime.timedelta(days=30)
event = Event.objects.create(
@ -3636,7 +3642,11 @@ def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
with CaptureQueriesContext(connection) as ctx:
resp = app.post_json(
'/api/agendas/recurring-events/fillslots/?action=update&agendas=%s' % agenda_slugs,
params={'slots': events_to_book, 'user_external_id': 'user'},
params={
'slots': events_to_book,
'user_external_id': 'user',
'include_booked_events_detail': True,
},
)
assert resp.json['booking_count'] == 180
assert len(ctx.captured_queries) == 13
@ -3662,7 +3672,8 @@ def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
events_type = EventsType.objects.create(label='Foo')
agenda = Agenda.objects.create(label='Foo bar', kind='events', events_type=events_type)
event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=1),
@ -3680,7 +3691,9 @@ def test_api_events_fillslots(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
params = {'user_external_id': 'user_id', 'slots': 'event,event-2'}
resp = app.post_json(fillslots_url, params=params)
with CaptureQueriesContext(connection) as ctx:
resp = app.post_json(fillslots_url, params=params)
assert len(ctx.captured_queries) == 11
assert resp.json['booking_count'] == 2
assert len(resp.json['booked_events']) == 2
assert resp.json['booked_events'][0]['id'] == 'event'
@ -4082,7 +4095,8 @@ def test_api_events_fillslots_preserve_out_of_delays_bookings(app, user, freezer
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_multiple_agendas(app, user):
first_agenda = Agenda.objects.create(label='First agenda', kind='events')
events_type = EventsType.objects.create(label='Foo')
first_agenda = Agenda.objects.create(label='First agenda', kind='events', events_type=events_type)
Desk.objects.create(agenda=first_agenda, slug='_exceptions_holder')
first_event = Event.objects.create(
label='Event',
@ -4106,7 +4120,9 @@ def test_api_events_fillslots_multiple_agendas(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
params = {'user_external_id': 'user_id', 'slots': event_slugs}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
with CaptureQueriesContext(connection) as ctx:
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
assert len(ctx.captured_queries) == 16
assert resp.json['booking_count'] == 2
assert len(resp.json['booked_events']) == 2
assert resp.json['booked_events'][0]['id'] == 'first-agenda@event'

View File

@ -11,6 +11,7 @@ from chrono.agendas.models import (
Category,
Desk,
Event,
EventsType,
Person,
SharedCustodyAgenda,
SharedCustodyHolidayRule,
@ -359,9 +360,12 @@ def test_datetimes_multiple_agendas_sort(app):
@pytest.mark.freeze_time('2021-05-06 14:00')
def test_datetimes_multiple_agendas_queries(app):
events_type = EventsType.objects.create(label='Foo')
category = Category.objects.create(label='Category A')
for i in range(10):
agenda = Agenda.objects.create(label=str(i), kind='events', category=category)
agenda = Agenda.objects.create(
label=str(i), kind='events', category=category, events_type=events_type
)
Subscription.objects.create(
agenda=agenda,
user_external_id='xxx',

View File

@ -10,6 +10,7 @@ from chrono.agendas.models import (
Category,
Desk,
Event,
EventsType,
Person,
SharedCustodyAgenda,
SharedCustodyHolidayRule,
@ -300,9 +301,10 @@ def test_recurring_events_api_list_multiple_agendas(app):
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_recurring_events_api_list_multiple_agendas_queries(app):
events_type = EventsType.objects.create(label='Foo')
category = Category.objects.create(label='Category A')
for i in range(20):
agenda = Agenda.objects.create(slug=f'{i}', kind='events', category=category)
agenda = Agenda.objects.create(slug=f'{i}', kind='events', category=category, events_type=events_type)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
start, end = now(), now() + datetime.timedelta(days=30)
event = Event.objects.create(