api: set presence reason on bookings (#63810)

This commit is contained in:
Lauréline Guérin 2022-04-14 17:50:45 +02:00
parent 30afa66e3f
commit 3a1655adff
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
4 changed files with 226 additions and 13 deletions

View File

@ -139,39 +139,70 @@ class RecurringFillslotsSerializer(MultipleAgendasEventsSlotsSerializer):
class BookingSerializer(serializers.ModelSerializer):
user_absence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
user_presence_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']
fields = [
'id',
'in_waiting_list',
'user_was_present',
'user_absence_reason',
'user_presence_reason',
'extra_data',
]
read_only_fields = ['id', 'in_waiting_list', 'extra_data']
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['user_absence_reason'] = self.instance.user_check_type
ret['user_absence_reason'] = (
self.instance.user_check_type if self.instance.user_was_present is False else ''
)
ret['user_presence_reason'] = (
self.instance.user_check_type if self.instance.user_was_present is True else ''
)
return ret
def validate_user_absence_reason(self, value):
def _validate_check_type(self, kind, value):
if not value:
return ''
if not self.instance.event.agenda.check_type_group:
raise serializers.ValidationError(_('unknown absence reason'))
error_messages = {
'absence': _('unknown absence reason'),
'presence': _('unknown presence reason'),
}
check_types_qs = self.instance.event.agenda.check_type_group.check_types.absences()
if not self.instance.event.agenda.check_type_group:
raise serializers.ValidationError(error_messages[kind])
check_types_qs = self.instance.event.agenda.check_type_group.check_types.filter(kind=kind)
try:
check_type = check_types_qs.get(slug=value)
value = check_type.label
except CheckType.DoesNotExist:
if not check_types_qs.filter(label=value).exists():
raise serializers.ValidationError(_('unknown absence'))
raise serializers.ValidationError(error_messages[kind])
return value
def validate_user_absence_reason(self, value):
return self._validate_check_type('absence', value)
def validate_user_presence_reason(self, value):
return self._validate_check_type('presence', value)
def validate(self, attrs):
super().validate(attrs)
if 'user_absence_reason' in attrs and 'user_presence_reason' in attrs:
raise ValidationError(
{'user_absence_reason': _('can not set user_absence_reason and user_presence_reason')}
)
if 'user_absence_reason' in attrs:
attrs['user_check_type'] = attrs['user_absence_reason']
del attrs['user_absence_reason']
elif 'user_presence_reason' in attrs:
attrs['user_check_type'] = attrs['user_presence_reason']
del attrs['user_presence_reason']
return attrs

View File

@ -385,6 +385,11 @@ def get_agenda_detail(request, agenda, check_events=False):
for r in agenda.check_type_group.check_types.all()
if r.kind == 'absence'
]
agenda_detail['presence_reasons'] = [
{'id': r.slug, 'slug': r.slug, 'text': r.label, 'label': r.label}
for r in agenda.check_type_group.check_types.all()
if r.kind == 'presence'
]
elif agenda.accept_meetings():
agenda_detail['api'] = {
'meetings_url': request.build_absolute_uri(
@ -2227,13 +2232,17 @@ class BookingFilter(filters.FilterSet):
date_start = filters.DateFilter(field_name='event__start_datetime', lookup_expr='gte')
date_end = filters.DateFilter(field_name='event__start_datetime', lookup_expr='lt')
user_absence_reason = filters.CharFilter(method='filter_user_absence_reason')
user_presence_reason = filters.CharFilter(method='filter_user_presence_reason')
def filter_event(self, queryset, name, value):
# we want to include bookings of event recurrences
return queryset.filter(Q(event__slug=value) | Q(event__primary_event__slug=value))
def filter_user_absence_reason(self, queryset, name, value):
return queryset.filter(user_check_type=value)
return queryset.filter(user_check_type=value, user_was_present=False)
def filter_user_presence_reason(self, queryset, name, value):
return queryset.filter(user_check_type=value, user_was_present=True)
class Meta:
model = Booking
@ -2245,6 +2254,7 @@ class BookingFilter(filters.FilterSet):
'date_end',
'user_was_present',
'user_absence_reason',
'user_presence_reason',
'in_waiting_list',
]
@ -2325,13 +2335,19 @@ class BookingAPI(APIView):
if (
self.booking.event.checked
and self.booking.event.agenda.disable_check_update
and ('user_was_present' in request.data or 'user_absence_reason' in request.data)
and (
'user_was_present' in request.data
or 'user_absence_reason' in request.data
or 'user_presence_reason' in request.data
)
):
raise APIErrorBadRequest(N_('event is marked as checked'), err=5)
user_was_present = serializer.validated_data.get('user_was_present', self.booking.user_was_present)
if user_was_present is True and 'user_absence_reason' in request.data:
raise APIErrorBadRequest(N_('user is marked as present, can not set absence reason'), err=6)
if user_was_present is False and 'user_presence_reason' in request.data:
raise APIErrorBadRequest(N_('user is marked as absent, can not set presence reason'), err=6)
serializer.save()
extra_data = {k: v for k, v in request.data.items() if k not in serializer.validated_data}
@ -2343,7 +2359,7 @@ class BookingAPI(APIView):
if 'user_was_present' in request.data:
self.booking.secondary_booking_set.update(user_was_present=self.booking.user_was_present)
self.booking.event.set_is_checked()
if 'user_absence_reason' in request.data:
if 'user_absence_reason' in request.data or 'user_presence_reason' in request.data:
self.booking.secondary_booking_set.update(user_check_type=self.booking.user_check_type)
if extra_data:
self.booking.secondary_booking_set.update(extra_data=self.booking.extra_data)

View File

@ -30,6 +30,7 @@ def test_agendas_api(app):
group = CheckTypeGroup.objects.create(label='Foo')
reason = CheckType.objects.create(group=group, label='Foo bar')
reason2 = CheckType.objects.create(group=group, label='Foo bar baz')
reason3 = CheckType.objects.create(group=group, label='Foo bar baz presence', kind='presence')
events_type = EventsType.objects.create(label='Type A')
events_type2 = EventsType.objects.create(label='Type B')
event_agenda = Agenda.objects.create(
@ -80,6 +81,9 @@ def test_agendas_api(app):
{'id': reason.slug, 'slug': reason.slug, 'text': reason.label, 'label': reason.label},
{'id': reason2.slug, 'slug': reason2.slug, 'text': reason2.label, 'label': reason2.label},
],
'presence_reasons': [
{'id': reason3.slug, 'slug': reason3.slug, 'text': reason3.label, 'label': reason3.label},
],
'api': {
'datetimes_url': 'http://testserver/api/agenda/foo-bar/datetimes/',
'fillslots_url': 'http://testserver/api/agenda/foo-bar/fillslots/',
@ -120,6 +124,9 @@ def test_agendas_api(app):
{'id': reason.slug, 'slug': reason.slug, 'text': reason.label, 'label': reason.label},
{'id': reason2.slug, 'slug': reason2.slug, 'text': reason2.label, 'label': reason2.label},
],
'presence_reasons': [
{'id': reason3.slug, 'slug': reason3.slug, 'text': reason3.label, 'label': reason3.label},
],
'api': {
'datetimes_url': 'http://testserver/api/agenda/foo-bar-3/datetimes/',
'fillslots_url': 'http://testserver/api/agenda/foo-bar-3/fillslots/',

View File

@ -147,6 +147,7 @@ def test_bookings_api(app, user):
'in_waiting_list': False,
'user_was_present': None,
'user_absence_reason': '',
'user_presence_reason': '',
'extra_data': None,
},
{
@ -154,6 +155,7 @@ def test_bookings_api(app, user):
'in_waiting_list': False,
'user_was_present': None,
'user_absence_reason': '',
'user_presence_reason': '',
'extra_data': None,
'event': resp.json['data'][1]['event'],
},
@ -162,6 +164,7 @@ def test_bookings_api(app, user):
'in_waiting_list': False,
'user_was_present': None,
'user_absence_reason': '',
'user_presence_reason': '',
'extra_data': None,
'event': resp.json['data'][1]['event'],
},
@ -303,8 +306,10 @@ def test_bookings_api_filter_user_absence_reason(app, user):
event = Event.objects.create(
agenda=agenda, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 0, 0)), places=10
)
Booking.objects.create(event=event, user_external_id='42')
booking2 = Booking.objects.create(event=event, user_external_id='42', user_check_type='foo-bar')
Booking.objects.create(event=event, user_external_id='42', user_was_present=False)
booking2 = Booking.objects.create(
event=event, user_external_id='42', user_was_present=False, user_check_type='foo-bar'
)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_absence_reason': 'foo'})
@ -313,6 +318,33 @@ def test_bookings_api_filter_user_absence_reason(app, user):
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_absence_reason': 'foo-bar'})
assert resp.json['err'] == 0
assert [b['id'] for b in resp.json['data']] == [booking2.pk]
Booking.objects.update(user_was_present=True)
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_absence_reason': 'foo-bar'})
assert resp.json['err'] == 0
assert [b['id'] for b in resp.json['data']] == []
def test_bookings_api_filter_user_presence_reason(app, user):
agenda = Agenda.objects.create(label='Foo bar')
event = Event.objects.create(
agenda=agenda, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 0, 0)), places=10
)
Booking.objects.create(event=event, user_external_id='42', user_was_present=True)
booking2 = Booking.objects.create(
event=event, user_external_id='42', user_was_present=True, user_check_type='foo-bar'
)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_presence_reason': 'foo'})
assert resp.json['err'] == 0
assert [b['id'] for b in resp.json['data']] == []
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_presence_reason': 'foo-bar'})
assert resp.json['err'] == 0
assert [b['id'] for b in resp.json['data']] == [booking2.pk]
Booking.objects.update(user_was_present=False)
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_presence_reason': 'foo-bar'})
assert resp.json['err'] == 0
assert [b['id'] for b in resp.json['data']] == []
def test_bookings_api_filter_in_waiting_list(app, user):
@ -391,7 +423,8 @@ def test_booking_api_present(app, user, flag):
resp = app.get('/api/booking/%s/' % booking.pk)
assert resp.json['booking_id'] == booking.pk
assert resp.json['user_was_present'] == flag
assert resp.json['user_absence_reason'] == 'foobar'
assert resp.json['user_absence_reason'] == ('foobar' if flag is False else '')
assert resp.json['user_presence_reason'] == ('foobar' if flag is True else '')
@pytest.mark.parametrize('flag', [True, False])
@ -627,6 +660,132 @@ def test_booking_patch_api_absence_reason(app, user):
assert resp.json['err'] == 0
def test_booking_patch_api_presence_reason(app, user):
agenda = Agenda.objects.create(kind='events')
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
booking = Booking.objects.create(event=event, user_was_present=True)
app.authorization = ('Basic', ('john.doe', 'password'))
# reasons not defined on agenda
resp = app.patch_json(
'/api/booking/%s/' % booking.pk, params={'user_presence_reason': 'foobar'}, status=400
)
assert resp.json['err'] == 4
assert resp.json['err_desc'] == 'invalid payload'
group = CheckTypeGroup.objects.create(label='Foo')
check_type_presence = CheckType.objects.create(group=group, label='Foo bar', kind='presence')
check_type_absence = CheckType.objects.create(group=group, label='Foo baz', kind='absence')
resp = app.patch_json(
'/api/booking/%s/' % booking.pk, params={'user_presence_reason': 'foobar'}, status=400
)
assert resp.json['err'] == 4
assert resp.json['err_desc'] == 'invalid payload'
# wrong kind
resp = app.patch_json(
'/api/booking/%s/' % booking.pk, params={'user_presence_reason': 'Foo baz'}, status=400
)
assert resp.json['err'] == 4
assert resp.json['err_desc'] == 'invalid payload'
resp = app.patch_json(
'/api/booking/%s/' % booking.pk, params={'user_presence_reason': check_type_absence.slug}, status=400
)
assert resp.json['err'] == 4
assert resp.json['err_desc'] == 'invalid payload'
# set check_type
agenda.check_type_group = group
agenda.save()
# it works with label
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_presence_reason': 'Foo bar'})
booking.refresh_from_db()
assert booking.user_check_type == 'Foo bar'
# reset
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_presence_reason': ''})
booking.refresh_from_db()
assert booking.user_check_type == ''
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_presence_reason': None})
booking.refresh_from_db()
assert booking.user_check_type == ''
# make secondary bookings
Booking.objects.create(event=event, primary_booking=booking, user_was_present=True)
Booking.objects.create(event=event, primary_booking=booking, user_was_present=True)
# and other booking
other_booking = Booking.objects.create(event=event)
# it works also with slug
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_presence_reason': check_type_presence.slug})
booking.refresh_from_db()
assert booking.user_check_type == 'Foo bar'
# all secondary bookings are updated
assert list(booking.secondary_booking_set.values_list('user_check_type', flat=True)) == [
'Foo bar',
'Foo bar',
]
other_booking.refresh_from_db()
assert other_booking.user_check_type == '' # not changed
# user_was_present is False, can not set user_presence_reason
Booking.objects.update(user_was_present=False)
resp = app.patch_json(
'/api/booking/%s/' % booking.pk, params={'user_presence_reason': check_type_presence.slug}, status=400
)
assert resp.json['err'] == 6
assert resp.json['err_desc'] == 'user is marked as absent, can not set presence reason'
# but it's ok if user_was_present is set to True
resp = app.patch_json(
'/api/booking/%s/' % booking.pk,
params={'user_presence_reason': check_type_presence.slug, 'user_was_present': True},
)
assert resp.json['err'] == 0
booking.refresh_from_db()
assert booking.user_was_present is True
assert booking.user_check_type == 'Foo bar'
# mark the event as checked
event.checked = True
event.save()
resp = app.patch_json(
'/api/booking/%s/' % booking.pk, params={'user_presence_reason': check_type_presence.slug}
)
assert resp.json['err'] == 0
# now disable check update
agenda.disable_check_update = True
agenda.save()
resp = app.patch_json(
'/api/booking/%s/' % booking.pk, params={'user_presence_reason': check_type_presence.slug}, status=400
)
assert resp.json['err'] == 5
assert resp.json['err_desc'] == 'event is marked as checked'
resp = app.patch_json('/api/booking/%s/' % booking.pk)
assert resp.json['err'] == 0
def test_booking_patch_api_both_reasons(app, user):
group = CheckTypeGroup.objects.create(label='Foo')
CheckType.objects.create(group=group, label='Foo bar', kind='presence')
CheckType.objects.create(group=group, label='Foo baz', kind='absence')
agenda = Agenda.objects.create(kind='events', check_type_group=group)
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
booking = Booking.objects.create(event=event)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.patch_json(
'/api/booking/%s/' % booking.pk,
params={'user_presence_reason': 'foo-bar', 'user_absence_reason': 'foo-baz'},
status=400,
)
assert resp.json['err'] == 4
assert resp.json['err_desc'] == 'invalid payload'
def test_booking_patch_api_extra_data(app, user):
agenda = Agenda.objects.create(kind='events')
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)