import codecs import datetime from unittest import mock import pytest import requests from django.core.management import call_command from django.db import connection from django.test import override_settings from django.test.utils import CaptureQueriesContext from webtest import Upload from chrono.agendas.models import Agenda, Booking, Desk, Event, EventsType, Subscription from chrono.utils.lingo import CheckType from chrono.utils.timezone import localtime, make_aware, now from tests.utils import login pytestmark = pytest.mark.django_db def test_add_event(app, admin_user): events_type = EventsType.objects.create( label='Foo', custom_fields=[{'varname': 'foo', 'label': 'Foo', 'field_type': 'text'}] ) agenda = Agenda.objects.create(label='Foo bar', maximal_booking_delay=0, events_type=events_type) Desk.objects.create(agenda=agenda, slug='_exceptions_holder') app = login(app) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) assert "This agenda doesn't have any event yet." in resp.text year = now().year + 1 resp = resp.click('New Event') assert 'custom_field_foo' not in resp.context['form'].fields resp.form['start_datetime_0'] = '%s-02-15' % year resp.form['start_datetime_1'] = '17:00' resp.form['places'] = 10 resp = resp.form.submit() resp = resp.follow() event = Event.objects.get(places=10) assert event.publication_datetime is None assert "This agenda doesn't have any event yet." not in resp.text assert '/manage/agendas/%s/events/%s/' % (agenda.id, event.id) in resp.text assert ('Feb. 15, %s, 5 p.m.' % year) in resp.text resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id) assert resp_datetimes.json['data'][0]['text'] == 'Feb. 15, %s, 5 p.m.' % year assert resp_datetimes.json['data'][0]['datetime'] == '%s-02-15 17:00:00' % year # add with errors in datetime parts for parts in ( ('', ''), ('invalid', ''), ('', 'invalid'), ('2019-02-24', 'invalid'), ('invalid', '17:00'), ): resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click('New Event') resp.form['start_datetime_0'] = parts[0] resp.form['start_datetime_1'] = parts[1] resp.form['places'] = 10 resp = resp.form.submit() assert ( resp.text.count('Enter a valid date') or resp.text.count('Enter a valid time') == 1 or resp.text.count('This field is required.') >= 1 ) @pytest.mark.freeze_time('2021-05-06 14:00') def test_add_recurring_event(app, admin_user): agenda = Agenda.objects.create(label='Foo bar', kind='events') Desk.objects.create(agenda=agenda, slug='_exceptions_holder') app = login(app) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click('New Event') resp.form['start_datetime_0'] = '2021-06-01' resp.form['start_datetime_1'] = '17:00' resp.form['places'] = 10 resp.form['frequency'] = 'unique' # not a recurring event resp.form['recurrence_days'] = [1] resp.form.submit().follow() event = Event.objects.get() assert event.recurrence_days is None event.delete() # add recurring event resp.form['frequency'] = 'recurring' resp.form.submit().follow() event = Event.objects.get(primary_event__isnull=True) assert event.recurrence_days == [1] assert Event.objects.filter(primary_event=event).count() == 49 event.delete() # add recurring event with end date resp.form['recurrence_end_date'] = '2021-07-01' resp.form.submit().follow() event = Event.objects.get(primary_event__isnull=True) assert event.recurrence_days == [1] assert Event.objects.filter(primary_event=event).count() == 5 # add recurring event with end date in a very long time resp.form['recurrence_end_date'] = '2030-01-01' resp = resp.form.submit() assert 'Recurrence end date cannot be more than 3 years from now' in resp.text def test_add_event_on_missing_agenda(app, admin_user): app = login(app) app.get('/manage/agendas/%s/add-event' % '0', status=404) def test_add_event_as_manager(app, manager_user): agenda = Agenda(label='Foo bar') agenda.view_role = manager_user.groups.all()[0] agenda.save() Desk.objects.create(agenda=agenda, slug='_exceptions_holder') app = login(app, username='manager', password='manager') resp = app.get('/manage/agendas/%s/' % agenda.id, status=302) app.get('/manage/agendas/%s/add-event' % agenda.id, status=403) agenda.edit_role = manager_user.groups.all()[0] agenda.save() resp = app.get('/manage/agendas/%s/settings' % agenda.pk) assert '

Settings' in resp.text resp = resp.click('New Event') resp.form['start_datetime_0'] = '2016-02-15' resp.form['start_datetime_1'] = '17:00' resp.form['places'] = 10 resp = resp.form.submit() resp = resp.follow() event = Event.objects.get(places=10) assert "This agenda doesn't have any event yet." not in resp.text assert '/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id) in resp.text assert 'Feb. 15, 2016, 5 p.m.' in resp.text assert event.duration is None assert event.end_datetime is None resp = resp.click('New Event') resp.form['start_datetime_0'] = '2016-02-15' resp.form['start_datetime_1'] = '17:00' resp.form['duration'] = 45 resp.form['places'] = 12 resp = resp.form.submit() resp = resp.follow() event = Event.objects.get(places=12) assert event.duration == 45 assert event.end_datetime == event.start_datetime + datetime.timedelta(minutes=45) def test_add_event_third_millennium(app, admin_user): agenda = Agenda.objects.create(label='Foo bar') Desk.objects.create(agenda=agenda, slug='_exceptions_holder') assert Event.objects.filter(agenda=agenda).count() == 0 app = login(app) resp = app.get('/manage/agendas/%s/settings' % agenda.pk) assert '

Settings' in resp.text resp = resp.click('New Event') resp.form['start_datetime_0'] = '0022-02-15' resp.form['start_datetime_1'] = '17:00' resp.form['places'] = 10 resp.form['duration'] = 45 resp = resp.form.submit() assert resp.context['form'].errors['start_datetime'] == ['Year must be after 2000.'] assert Event.objects.filter(agenda=agenda).count() == 0 def test_edit_event(settings, app, admin_user): settings.LANGUAGE_CODE = 'fr-fr' # check date initial value format agenda = Agenda.objects.create(label='Foo bar') event = Event.objects.create( label='Foo', start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=agenda, ) event2 = Event.objects.create( label='Other', start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=agenda, ) assert event.duration is None assert event.end_datetime is None other_agenda = Agenda.objects.create(label='Foo bar') other_event = Event.objects.create( label='Foo', start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=other_agenda, ) assert event.slug == other_event.slug app = login(app) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk)) assert resp.form['start_datetime_0'].value == '2016-02-15' assert resp.form['start_datetime_1'].value == '17:00' assert resp.form['publication_datetime_0'].value == '' assert resp.form['publication_datetime_1'].value == '' assert resp.form['duration'].value == '' assert resp.form['description'].value == '' resp.form['start_datetime_0'] = '2016-02-16' resp.form['start_datetime_1'] = '17:00' resp.form['publication_datetime_0'] = '2020-05-11' resp.form['publication_datetime_1'] = '12:00' resp.form['duration'].value = 45 resp.form['places'] = 20 resp.form['description'] = 'A description' resp = resp.form.submit() settings.LANGUAGE_CODE = 'en' resp = resp.follow() assert '/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id) in resp.text assert 'Feb. 16, 2016, 5 p.m.' in resp.text event.refresh_from_db() assert event.places == 20 assert str(event.publication_datetime) == '2020-05-11 10:00:00+00:00' assert str(event.publication_datetime.tzinfo) == 'UTC' assert event.duration == 45 assert event.end_datetime == event.start_datetime + datetime.timedelta(minutes=45) assert event.description == 'A description' assert event.slug == other_event.slug # check slug edition resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk)) resp.form['slug'] = event2.slug resp = resp.form.submit() assert resp.context['form'].errors['slug'] == ['Another event exists with the same identifier.'] def test_event_digit_slug(app, admin_user): agenda = Agenda(label='Foo bar') agenda.maximal_booking_delay = 0 agenda.save() event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10) app = login(app) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['slug'] = 42 resp = resp.form.submit() assert 'value cannot be a number' in resp.text def test_edit_missing_event(app, admin_user): app = login(app) app.get('/manage/agendas/999/', status=404) def test_edit_event_as_manager(app, manager_user): agenda = Agenda(label='Foo bar') agenda.view_role = manager_user.groups.all()[0] agenda.save() Desk.objects.create(agenda=agenda, slug='_exceptions_holder') event = Event.objects.create( start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=agenda, publication_datetime=make_aware(datetime.datetime(2020, 5, 11)), ) app = login(app, username='manager', password='manager') resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id), status=403) agenda.edit_role = manager_user.groups.all()[0] agenda.save() resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click('Feb. 15, 2016, 5 p.m.') assert resp.form['start_datetime_0'].value == '2016-02-15' assert resp.form['start_datetime_1'].value == '17:00' assert resp.form['publication_datetime_0'].value == '2020-05-11' assert resp.form['publication_datetime_1'].value == '00:00' resp.form['start_datetime_0'] = '2016-02-16' resp.form['start_datetime_1'] = '17:00' resp.form['publication_datetime_0'] = '' resp.form['publication_datetime_1'] = '' resp.form['places'] = 20 resp = resp.form.submit() resp = resp.follow() assert '/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id) in resp.text assert 'Feb. 16, 2016, 5 p.m.' in resp.text event.refresh_from_db() assert event.publication_datetime is None def test_edit_event_with_custom_fields(app, admin_user): 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', kind='events') event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10) app = login(app) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk)) assert 'custom_field_text' not in resp.context['form'].fields assert 'custom_field_textarea' not in resp.context['form'].fields assert 'custom_field_bool' not in resp.context['form'].fields agenda.events_type = events_type agenda.save() resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk)) assert 'custom_field_text' in resp.context['form'].fields assert 'custom_field_textarea' in resp.context['form'].fields assert 'custom_field_bool' in resp.context['form'].fields resp.form.submit().follow() event.refresh_from_db() assert event.custom_fields == { 'text': '', 'textarea': '', 'bool': None, } resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk)) resp.form['custom_field_text'] = 'foo' resp.form['custom_field_textarea'] = 'foo bar' resp.form['custom_field_bool'] = 'true' resp.form.submit().follow() event.refresh_from_db() assert event.custom_fields == { 'text': 'foo', 'textarea': 'foo bar', 'bool': True, } resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk)) assert resp.form['custom_field_text'].value == 'foo' assert resp.form['custom_field_textarea'].value == 'foo bar' assert resp.form['custom_field_bool'].value == 'true' resp.form['custom_field_text'] = '' resp.form['custom_field_textarea'] = '' resp.form['custom_field_bool'] = 'false' resp.form.submit().follow() event.refresh_from_db() assert event.custom_fields == { 'text': '', 'textarea': '', 'bool': False, } resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk)) resp.form['custom_field_bool'] = 'unknown' resp.form.submit().follow() event.refresh_from_db() assert event.custom_fields == { 'text': '', 'textarea': '', 'bool': None, } def test_edit_recurring_event(settings, app, admin_user, freezer): freezer.move_to('2021-01-12 12:10') events_type = EventsType.objects.create( label='Foo', custom_fields=[{'varname': 'foo', 'label': 'Foo', 'field_type': 'text'}] ) agenda = Agenda.objects.create( label='Foo bar', kind='events', minimal_booking_delay=15, maximal_booking_delay=30, events_type=events_type, ) Desk.objects.create(agenda=agenda, slug='_exceptions_holder') event = Event.objects.create(start_datetime=now(), places=10, agenda=agenda) app = login(app) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['frequency'] = 'recurring' resp.form['recurrence_days'] = [localtime().weekday()] resp = resp.form.submit() # no end date, events are created for the year to come assert Event.objects.count() == 54 assert Event.objects.last().start_datetime.strftime('%Y-%m-%d') == '2022-01-11' # specifying end date removes events resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['recurrence_end_date'] = '2021-06-01' resp = resp.form.submit() assert Event.objects.count() == 21 assert Event.objects.last().start_datetime.strftime('%Y-%m-%d') == '2021-05-25' # detail page doesn't exist resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.id, event.id), status=404) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) assert 'On Tuesdays at 1:10 p.m.' in resp.text # event is bookable regardless of minimal_booking_delay, since it has bookable recurrences assert len(resp.pyquery.find('.bookable')) == 1 # maximal_booking_delay is accounted for, because no recurrences are bookable freezer.move_to('2020-11-12') resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) assert len(resp.pyquery.find('.not-bookable')) == 1 # editing recurring event updates event recurrences event.refresh_from_db() event_recurrence = Event.objects.get(primary_event=event, start_datetime=event.start_datetime) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['places'] = 20 resp.form['custom_field_foo'] = 'bar' resp = resp.form.submit().follow() event_recurrence.refresh_from_db() assert event_recurrence.places == 20 assert event_recurrence.custom_fields == {'foo': 'bar'} event.refresh_from_db() assert event.custom_fields == {'foo': 'bar'} # but some fields should not be updated assert event_recurrence.slug != event.slug assert not event_recurrence.recurrence_days # changing recurrence attribute removes event recurrences resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['frequency'] = 'unique' resp = resp.form.submit().follow() assert not Event.objects.filter(primary_event=event).exists() # same goes with changing slug event.recurrence_days = [1] event.save() event.create_all_recurrences() event_recurrence = Event.objects.get(primary_event=event, start_datetime=event.start_datetime) assert Event.objects.filter(primary_event=event, slug__startswith='foo-bar-event').exists() resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['slug'] = 'hop' resp = resp.form.submit().follow() assert not Event.objects.filter(primary_event=event, slug__startswith='foo-bar-event').exists() # changing recurring attribute or slug is forbidden if there are bookings for future recurrences event_recurrence = Event.objects.get( primary_event=event, start_datetime=event.start_datetime + datetime.timedelta(days=7) ) Booking.objects.create(event=event_recurrence) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) assert 'disabled' in resp.form['frequency'].attrs assert all('disabled' in resp.form.get('recurrence_days', index=i).attrs for i in range(7)) assert 'disabled' in resp.form['recurrence_week_interval'].attrs assert 'disabled' in resp.form['slug'].attrs assert 'disabled' in resp.form['start_datetime_0'].attrs assert 'disabled' in resp.form['start_datetime_1'].attrs # changing it anyway doesn't work resp.form['slug'] = 'changed' resp = resp.form.submit() assert not Event.objects.filter(slug='changed').exists() # deletion of event recurrence is not allowed resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.id, event_recurrence.id)) assert 'Delete' not in resp.text resp = resp.click('Options') assert 'Delete' not in resp.text assert { 'slug', 'frequency', 'recurrence_days', 'recurence_weekly_interval', 'recurrence_end_date', 'publication_datetime_0', 'publication_datetime_1', 'custom_field_foo', }.isdisjoint(resp.form.fields) resp.form.submit().follow() # custom fields not changed event_recurrence.refresh_from_db() assert event_recurrence.custom_fields == {'foo': 'bar'} resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['recurrence_end_date'] = 'bad-date' resp = resp.form.submit() # no error def test_edit_recurring_event_with_end_date(settings, app, admin_user, freezer): freezer.move_to('2021-01-12 12:10') agenda = Agenda.objects.create(label='Foo bar', kind='events') event = Event.objects.create( start_datetime=now(), places=10, recurrence_days=list(range(7)), agenda=agenda ) app = login(app) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=5)).strftime('%Y-%m-%d') resp = resp.form.submit() # recurrences are created automatically event = Event.objects.get(recurrence_days__isnull=False) assert Event.objects.filter(primary_event=event).count() == 5 assert Event.objects.filter(primary_event=event, start_datetime=now()).exists() resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['start_datetime_1'] = (localtime() + datetime.timedelta(hours=1)).strftime('%H:%M') resp = resp.form.submit() assert Event.objects.filter(primary_event=event).count() == 5 assert Event.objects.filter( primary_event=event, start_datetime=now() + datetime.timedelta(hours=1) ).exists() # old recurrences were deleted assert not Event.objects.filter(primary_event=event, start_datetime=now()).exists() # if start datetime of a recurrence is edited, it stays that way recurrence = event.recurrences.first() recurrence.start_datetime += datetime.timedelta(hours=1) recurrence.save() resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.form.submit() assert Event.objects.filter(primary_event=event).count() == 5 assert Event.objects.filter(primary_event=event, start_datetime=recurrence.start_datetime).count() == 1 # ensure recurrence_end_date has not been propagated assert not Event.objects.filter(primary_event=event, recurrence_end_date__isnull=False).exists() # editing recurrence_end_date is permitted as long as bookings are not impacted event_recurrence = Event.objects.get(primary_event=event, start_datetime__day=15) Booking.objects.create(event=event_recurrence) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=6)).strftime('%Y-%m-%d') resp = resp.form.submit() assert Event.objects.filter(primary_event=event).count() == 6 resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=4)).strftime('%Y-%m-%d') resp = resp.form.submit() assert Event.objects.filter(primary_event=event).count() == 4 resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=2)).strftime('%Y-%m-%d') resp = resp.form.submit() assert Event.objects.filter(primary_event=event).count() == 4 assert 'Bookings exist after this date' in resp.text def test_edit_booked_event_disable_frequency_choice(settings, app, admin_user, freezer): freezer.move_to('2021-01-12 12:10') agenda = Agenda.objects.create(label='Foo bar', kind='events') event = Event.objects.create(start_datetime=now(), places=10, agenda=agenda) app = login(app) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) assert 'disabled' not in resp.form['frequency'].attrs assert 'This field will not be editable once event has bookings.' in resp.text Booking.objects.create(event=event) resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) assert 'disabled' in resp.form['frequency'].attrs assert 'cannot be modified' in resp.form['frequency'].attrs['title'] assert 'This field will not be editable once event has bookings.' not in resp.text def test_booked_places(app, admin_user): agenda = Agenda(label='Foo bar') agenda.save() event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda) event.save() Booking(event=event).save() Booking(event=event).save() app = login(app) day = event.start_datetime resp = app.get( '/manage/agendas/%s/month/%d/%d/%d/' % (agenda.id, day.year, day.month, day.day), status=200 ) assert '8 remaining places' in resp.text assert '(2/10 bookings)' in resp.text def test_event_classes(app, admin_user): agenda = Agenda(label='Foo bar') agenda.save() Desk.objects.create(agenda=agenda, slug='_exceptions_holder') event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda) event.save() for _ in range(2): Booking(event=event).save() app = login(app) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) assert 'full' not in resp.text assert 'overbooking' not in resp.text resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk)) assert 'Full' not in resp resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Full' not in resp for _ in range(8): Booking(event=event).save() resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) assert 'full' in resp.text assert 'overbooking' not in resp.text resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk)) assert 'Full' in resp resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Full' in resp Booking(event=event).save() resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) assert 'full' in resp.text assert 'overbooking' in resp.text def test_event_detail_backoffice_url_translation(app, admin_user): agenda = Agenda(label='Foo bar') agenda.save() Desk.objects.create(agenda=agenda, slug='_exceptions_holder') event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda) event.save() Booking.objects.create(event=event, backoffice_url='publik://default/foo/') app = login(app) resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk)) assert 'http://example.org/foo/' in resp.text def test_delete_event(app, admin_user): agenda = Agenda(label='Foo bar') agenda.save() Desk.objects.create(agenda=agenda, slug='_exceptions_holder') event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda) event.save() app = login(app) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.click('Delete') resp = resp.form.submit() assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id) assert Event.objects.count() == 0 def test_delete_busy_event(app, admin_user): agenda = Agenda(label='Foo bar') agenda.save() Desk.objects.create(agenda=agenda, slug='_exceptions_holder') event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda) event.save() app = login(app) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.click('Delete') assert 'Are you sure you want to delete this event?' in resp.text booking = Booking(event=event) booking.save() resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.click('Delete') assert 'This cannot be removed' in resp.text booking.cancellation_datetime = now() booking.save() resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.click('Delete') assert 'Are you sure you want to delete this event?' in resp.text # suddenly the booking is no longer cancelled, but the admin clicks on the # delete button. booking.cancellation_datetime = None booking.save() resp = resp.form.submit(status=403) def test_delete_recurring_event(app, admin_user, freezer): agenda = Agenda.objects.create(label='Foo bar', kind='events') Desk.objects.create(agenda=agenda, slug='_exceptions_holder') start_datetime = now() + datetime.timedelta(days=10) event = Event.objects.create( start_datetime=start_datetime, places=10, agenda=agenda, recurrence_days=[start_datetime.weekday()], recurrence_end_date=start_datetime + datetime.timedelta(days=15), ) event.create_all_recurrences() app = login(app) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.click('Delete') assert 'Are you sure you want to delete this event?' in resp.text event_recurrence = Event.objects.get(primary_event=event, start_datetime=event.start_datetime) booking = Booking.objects.create(event=event_recurrence) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.click('Delete') assert 'This cannot be removed' in resp.text booking.cancellation_datetime = now() booking.save() resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.click('Delete') assert 'Are you sure you want to delete this event?' in resp.text booking.cancellation_datetime = None booking.save() freezer.move_to(now() + datetime.timedelta(days=11)) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.click('Delete') assert 'Are you sure you want to delete this event?' in resp.text def test_delete_event_as_manager(app, manager_user): agenda = Agenda(label='Foo bar') agenda.edit_role = manager_user.groups.all()[0] agenda.save() Desk.objects.create(agenda=agenda, slug='_exceptions_holder') event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda) event.save() app = login(app, username='manager', password='manager') resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) resp = resp.click('Delete') resp = resp.form.submit() assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id) assert Event.objects.count() == 0 def test_export_events(app, admin_user): agenda = Agenda.objects.create(label='Foo bar') app = login(app) resp = app.get('/manage/agendas/%s/export-events' % agenda.id) csv_export = resp.text assert ( csv_export == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date/time,duration\r\n' ) resp = app.get('/manage/agendas/%s/import-events' % agenda.id) resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,00:30,10', 'text/csv') resp.form.submit(status=302) resp = app.get('/manage/agendas/%s/export-events' % agenda.id) csv_export = resp.text assert ( csv_export == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date/time,duration\r\n' '2016-09-16,00:30,10,0,,foo-bar-event,,,,,\r\n' ) resp = app.get('/manage/agendas/%s/import-events' % agenda.id) resp.form['events_csv_file'] = Upload( 't.csv', b'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 00:00,90', 'text/csv', ) resp.form.submit(status=302) resp = app.get('/manage/agendas/%s/export-events' % agenda.id) csv_export = resp.text assert ( csv_export == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date/time,duration\r\n' '2016-09-16,00:30,10,0,,foo-bar-event,,,,,\r\n' '2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 00:00,90\r\n' ) def test_export_events_wrong_kind(app, admin_user): agenda = Agenda.objects.create(label='Foo bar', kind='meetings') app = login(app) app.get('/manage/agendas/%s/export-events' % agenda.id, status=404) agenda.kind = 'virtual' agenda.save() app.get('/manage/agendas/%s/export-events' % agenda.id, status=404) def test_import_events(app, admin_user): agenda = Agenda(label='Foo bar') agenda.save() Desk.objects.create(agenda=agenda, slug='_exceptions_holder') app = login(app) resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) resp = resp.click('Import Events') sample_csv_resp = resp.click('Download sample file') assert sample_csv_resp.content_type == 'text/csv' assert sample_csv_resp.text.startswith('date,time') resp.form['events_csv_file'] = Upload('t.csv', sample_csv_resp.content, 'text/csv') resp = resp.form.submit(status=302) assert Event.objects.count() == 1 Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload('t.csv', b'xx', 'text/csv') resp = resp.form.submit(status=200) assert 'Invalid file format.' in resp.text resp.form['events_csv_file'] = Upload('t.csv', b'xxxx\0\0xxxx', 'text/csv') resp = resp.form.submit(status=200) assert 'Invalid file format.' in resp.text resp.form['events_csv_file'] = Upload('t.csv', b'2016-14-16,18:00', 'text/csv') resp = resp.form.submit(status=200) assert 'Invalid file format.' in resp.text resp.form['events_csv_file'] = Upload('t.csv', b'2016-14-16,18:00,10', 'text/csv') resp = resp.form.submit(status=200) assert 'Invalid file format. (date/time format, 1st event)' in resp.text with override_settings(LANGUAGE_CODE='fr-fr'): resp.form['events_csv_file'] = Upload('t.csv', b'2016-14-16,18:00,10', 'text/csv') resp = resp.form.submit(status=200) # ensure tag is not escaped assert '1er' in resp.text resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,blah', 'text/csv') resp = resp.form.submit(status=200) assert 'Invalid file format. (number of places,' in resp.text resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,blah', 'text/csv') resp = resp.form.submit(status=200) assert 'Invalid file format. (number of places in waiting list,' in resp.text resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,' + b'x' * 151, 'text/csv') resp = resp.form.submit(status=200) assert 'Ensure this value has at most 150' in resp.text resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10', 'text/csv') resp = resp.form.submit(status=302) assert Event.objects.count() == 1 assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) assert Event.objects.all()[0].places == 10 Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5', 'text/csv') resp = resp.form.submit(status=302) assert Event.objects.count() == 1 assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) assert Event.objects.all()[0].places == 10 assert Event.objects.all()[0].waiting_list_places == 5 Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload('t.csv', '2016-09-16,18:00,10,5,éléphant'.encode(), 'text/csv') resp = resp.form.submit(status=302) assert Event.objects.count() == 1 assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) assert Event.objects.all()[0].places == 10 assert Event.objects.all()[0].waiting_list_places == 5 assert Event.objects.all()[0].label == 'éléphant' Event.objects.all().delete() # BOM resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', codecs.BOM_UTF8 + '2016-09-16,18:00,10,5,éléphant'.encode(), 'text/csv' ) resp = resp.form.submit(status=302) assert Event.objects.count() == 1 assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) assert Event.objects.all()[0].places == 10 assert Event.objects.all()[0].waiting_list_places == 5 assert Event.objects.all()[0].label == 'éléphant' Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', '2016-09-16,18:00,10,5,éléphant'.encode('iso-8859-15'), 'text/csv' ) resp = resp.form.submit(status=302) assert Event.objects.count() == 1 assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) assert Event.objects.all()[0].places == 10 assert Event.objects.all()[0].waiting_list_places == 5 assert Event.objects.all()[0].label == 'éléphant' Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', '2016-09-16,18:00,10,5,éléphant'.encode('eucjp'), 'text/csv' ) resp = resp.form.submit(status=302) assert Event.objects.count() == 1 assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) assert Event.objects.all()[0].places == 10 assert Event.objects.all()[0].waiting_list_places == 5 assert Event.objects.all()[0].label == '\x8f«±l\x8f«±phant' # eucjp interpreted as iso-8859-15 Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'date,time,etc.\n' b'2016-09-16,18:00,10,5,bla bla bla\n' b'\n' b'2016-09-19,18:00,10', 'text/csv', ) resp = resp.form.submit(status=302) assert Event.objects.count() == 2 Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', '"date"\t"time"\t"etc."\n' '"2016-09-16"\t"18:00"\t"10"\t"5"\t"éléphant"\n' '"2016-09-19"\t"18:00"\t"10"'.encode('iso-8859-15'), 'text/csv', ) resp = resp.form.submit(status=302) assert Event.objects.count() == 2 Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,label,slug', 'text/csv') resp = resp.form.submit(status=302) assert Event.objects.count() == 1 event = Event.objects.latest('pk') assert event.start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) assert event.places == 10 assert event.waiting_list_places == 5 assert event.label == 'label' assert event.slug == 'slug' resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,label,slug', 'text/csv') resp = resp.form.submit(status=302) assert Event.objects.count() == 1 event = Event.objects.latest('pk') assert event.slug == 'slug' # additional optional attributes Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,label,slug,,,,,', 'text/csv') resp = resp.form.submit(status=302) assert Event.objects.count() == 1 event = Event.objects.get() assert event.description == '' assert event.pricing == '' assert event.url == '' assert event.publication_datetime is None assert event.duration is None Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16,90', 'text/csv', ) resp = resp.form.submit(status=302) assert Event.objects.count() == 1 event = Event.objects.get() assert event.description == 'description\nfoobar' assert event.pricing == 'pricing' assert event.url == 'url' assert str(event.publication_datetime) == '2016-10-15 22:00:00+00:00' assert str(event.publication_datetime.tzinfo) == 'UTC' assert event.duration == 90 Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 10:00,90', 'text/csv', ) resp = resp.form.submit(status=302) assert Event.objects.count() == 1 event = Event.objects.get() assert event.description == 'description\nfoobar' assert event.pricing == 'pricing' assert event.url == 'url' assert str(event.publication_datetime) == '2016-10-16 08:00:00+00:00' assert str(event.publication_datetime.tzinfo) == 'UTC' assert event.duration == 90 # publication date/time bad format resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,foobar', 'text/csv' ) resp = resp.form.submit(status=200) assert 'Invalid file format. (date/time format' in resp.text # duration bad format resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16,foobar', 'text/csv' ) resp = resp.form.submit(status=200) assert 'Invalid file format. (duration' in resp.text # import events with empty slugs Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'2016-09-16,18:00,10,5,labela,labelb,,pricing,\n' b'2016-09-17,18:00,10,5,labela,labelb-1,,pricing,\n' b'2016-09-18,18:00,10,5,labela,labelb-2,,pricing,\n' b'2016-09-18,18:00,10,5,labelb,,,pricing,\n' b'2016-09-18,18:00,10,5,labelb,,,pricing,\n', 'text/csv', ) with CaptureQueriesContext(connection) as ctx: resp = resp.form.submit(status=302) assert len(ctx.captured_queries) == 22 assert Event.objects.count() == 5 assert set(Event.objects.values_list('slug', flat=True)) == { 'labelb', 'labelb-1', 'labelb-2', 'labelb-3', 'labelb-4', } # forbidden numerical slug Event.objects.all().delete() resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,label,1234', 'text/csv') resp = resp.form.submit(status=200) assert 'value cannot be a number' in resp.text assert 'Identifier:' in resp.text # verbose_name is shown, not field name ('slug:') def test_import_event_nested_quotes(app, admin_user): agenda = Agenda(label='Foo bar') agenda.save() app = login(app) resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', ','.join( [ "2016-09-16", "18:00", "10", "5", "éléphant", "elephant", # the multiline description and final dot # and new line after ""éléphants"" are needed to trigger the bug. '''"Animation: De nombreux ""éléphants"". "''', ] ).encode(), 'text/csv', ) resp = resp.form.submit(status=302) assert Event.objects.count() == 1 assert 'De nombreux "éléphants"' in Event.objects.all()[0].description def test_import_events_existing_event(app, admin_user, freezer): agenda = Agenda.objects.create(label='Foo bar') Desk.objects.create(agenda=agenda, slug='_exceptions_holder') app = login(app) resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'2016-09-16,18:00,10,5,label,slug\n2016-09-16,18:00,10,5,label,slug\n', 'text/csv', ) resp.form.submit(status=302) assert agenda.event_set.count() == 1 event = Event.objects.latest('pk') def check_import(date, time, with_alert): resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'%s,%s,10,5,label,slug\n' % (date.encode(), time.encode()), 'text/csv', ) resp = resp.form.submit(status=302).follow() assert agenda.event_set.count() == 1 event.refresh_from_db() if with_alert: assert ( '
  • Event "label" start date has changed. Do not forget to notify the registrants.
  • ' in resp.text ) else: assert ( '
  • Event "label" start date has changed. Do not forget to notify the registrants.
  • ' not in resp.text ) assert event.start_datetime == make_aware( datetime.datetime(*(int(v) for v in date.split('-')), *(int(v) for v in time.split(':'))) ) # change date or time # event in the past, no alert, with or without booking Booking.objects.create( event=event, cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30)) ) check_import('2016-09-15', '18:00', False) # change date check_import('2016-09-15', '17:00', False) # change time # available booking Booking.objects.create(event=event) check_import('2016-09-14', '17:00', False) # change date check_import('2016-09-14', '16:00', False) # change time # date in the future freezer.move_to('2016-09-01') # warn if available booking only check_import('2016-09-13', '16:00', True) # change date check_import('2016-09-13', '15:00', True) # change time # no available booking Booking.objects.all().delete() Booking.objects.create( event=event, cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30)) ) check_import('2016-09-12', '15:00', False) # change date check_import('2016-09-12', '14:00', False) # change time # check there is a message per changed event resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'2016-09-16,18:00,10,5,label,slug\n2016-09-16,19:00,10,5,label,other_slug\n', 'text/csv', ) resp.form.submit(status=302) assert agenda.event_set.count() == 2 event2 = Event.objects.latest('pk') Booking.objects.create(event=event) Booking.objects.create(event=event2) resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp.form['events_csv_file'] = Upload( 't.csv', b'2016-09-17,18:00,10,5,label,slug\n2016-09-17,19:00,10,5,,other_slug\n2016-09-17,20:00,10,5,,other_slug\n', 'text/csv', ) resp = resp.form.submit(status=302).follow() assert agenda.event_set.count() == 2 assert ( resp.text.count( 'Event "label" start date has changed. Do not forget to notify the registrants.' ) == 1 ) assert ( resp.text.count( 'Event "other_slug" start date has changed. Do not forget to notify the registrants.' ) == 1 ) def test_import_events_wrong_kind(app, admin_user): agenda = Agenda.objects.create(label='Foo bar', kind='meetings') app = login(app) app.get('/manage/agendas/%s/import-events' % agenda.id, status=404) agenda.kind = 'virtual' agenda.save() app.get('/manage/agendas/%s/import-events' % agenda.id, status=404) @pytest.mark.freeze_time('2022-05-24') def test_event_detail(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, waiting_list_places=2, agenda=agenda, ) Booking.objects.create(event=event, user_last_name="User's 1") Booking.objects.create(event=event, user_last_name='User 2', in_waiting_list=True) login(app) resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk)) assert 'Bookings (1/10)' in resp.text assert 'User's 1, May 24, 2022, 2 a.m.' in resp.text assert 'Waiting List (1/2): 1 remaining place' in resp.text assert 'User 2, May 24, 2022, 2 a.m.' in resp.text agenda.booking_user_block_template = '{{ booking.user_name }} Foo Bar' agenda.save() resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk)) assert 'Bookings (1/10)' in resp.text assert '<b>User's 1</b> Foo Bar, May 24, 2022, 2 a.m.' in resp.text assert 'Waiting List (1/2): 1 remaining place' in resp.text assert '<b>User 2</b> Foo Bar, May 24, 2022, 2 a.m.' in resp.text def test_event_detail_redirect(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, waiting_list_places=2, agenda=agenda, ) login(app) resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.slug, event.slug), status=302) assert resp.location.endswith('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk)) def test_event_cancellation(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda ) login(app) resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk)) assert 'Bookings (0/10)' in resp.text resp = resp.click('Cancel', href='/cancel') assert 'related bookings' not in resp.text Booking.objects.create(event=event, user_last_name='User 1') Booking.objects.create(event=event, user_last_name='User 2') resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk)) assert 'Bookings (2/10)' in resp.text resp = resp.click('Cancel', href='manage/agendas/%d/events/%d/cancel' % (agenda.pk, event.pk)) assert '2 related bookings will also be cancelled.' in resp.text resp = resp.form.submit().follow() assert 'Cancelled' in resp.text assert 'Bookings (0/10)' in resp.text assert Booking.objects.filter(event=event, cancellation_datetime__isnull=False).count() == 2 resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk)) assert '/manage/agendas/%d/events/%d/cancel' % (agenda.pk, event.pk) not in resp.text def test_event_cancellation_error_report(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda ) day = event.start_datetime def mocked_requests_connection_error(*args, **kwargs): raise requests.exceptions.ConnectionError('unreachable') for _ in range(5): Booking.objects.create(event=event, cancel_callback_url='http://example.org/jump/trigger/') login(app) resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.id, day.year, day.month, day.day)) resp = resp.click('Cancellation error reports') assert 'No error report' in resp.text resp = app.get('/manage/agendas/%d/events/%d/cancel' % (agenda.pk, event.pk)) resp = resp.form.submit().follow() assert 'Cancellation in progress' in resp.text with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: mock_response = mock.Mock(status_code=200) mock_send.return_value = mock_response mock_send.side_effect = mocked_requests_connection_error call_command('cancel_events') event.refresh_from_db() assert not event.cancelled and not event.cancellation_scheduled assert not Booking.objects.filter(cancellation_datetime__isnull=False).exists() resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.id, day.year, day.month, day.day)) assert 'Errors occured during cancellation of event "xyz".' in resp.text # warning doesn't go away resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.id, day.year, day.month, day.day)) assert 'Errors occured during cancellation of event "xyz".' in resp.text resp = resp.click('Details') assert resp.text.count('unreachable') == 5 resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.id, day.year, day.month, day.day)) assert 'Errors occured during cancellation of event "xyz".' not in resp.text resp = resp.click('Cancellation error reports') assert '(5 failures)' in resp.text resp = resp.click(str(event)) resp = resp.click('Force cancellation') resp = resp.form.submit().follow() event.refresh_from_db() assert event.cancelled and not event.cancellation_scheduled assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 5 def test_event_cancellation_error_report_backofice_url_translation(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda ) day = event.start_datetime def mocked_requests_connection_error(*args, **kwargs): raise requests.exceptions.ConnectionError('unreachable') for _ in range(5): Booking.objects.create( event=event, cancel_callback_url='http://example.org/jump/trigger/', backoffice_url='publik://default/', ) login(app) resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.id, day.year, day.month, day.day)) resp = resp.click('Cancellation error reports') assert 'No error report' in resp.text resp = app.get('/manage/agendas/%d/events/%d/cancel' % (agenda.pk, event.pk)) resp = resp.form.submit().follow() assert 'Cancellation in progress' in resp.text with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: mock_response = mock.Mock(status_code=200) mock_send.return_value = mock_response mock_send.side_effect = mocked_requests_connection_error call_command('cancel_events') event.refresh_from_db() assert not event.cancelled and not event.cancellation_scheduled assert not Booking.objects.filter(cancellation_datetime__isnull=False).exists() resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.id, day.year, day.month, day.day)) assert 'Errors occured during cancellation of event "xyz".' in resp.text resp = resp.click('Cancellation error reports') assert '(5 failures)' in resp.text resp = resp.click(str(event)) assert 'http://example.org/' in resp.text def test_event_cancellation_forbidden(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda ) Booking.objects.create(event=event) booking = Booking.objects.create(event=event, backoffice_url='http://example.org/backoffice/xx/') login(app) resp = app.get('/manage/agendas/%d/events/%d/cancel' % (agenda.pk, event.pk)) assert 'event has bookings with no callback url configured' in resp.text assert 'Proceed with cancellation' not in resp.text booking.cancel() resp = app.get('/manage/agendas/%d/events/%d/cancel' % (agenda.pk, event.pk)) assert 'event has bookings with no callback url configured' not in resp.text assert 'Proceed with cancellation' in resp.text def test_event_booking_form_url(settings, app, admin_user): settings.TEMPLATE_VARS = {'eservices_url': 'http://demarches/'} agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda ) day = event.start_datetime login(app) assert event.get_booking_form_url() is None resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.id, day.year, day.month, day.day)) assert 'Booking form' not in resp.text resp = app.get('/manage/agendas/%d/events/open/' % agenda.pk) assert 'Booking form' not in resp.text resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk)) assert 'Booking form' not in resp.text agenda.booking_form_url = '{{ eservices_url }}backoffice/submission/inscription-aux-activites/' agenda.save() assert ( event.get_booking_form_url() == 'http://demarches/backoffice/submission/inscription-aux-activites/?agenda=%s&event=%s' % (agenda.slug, event.slug) ) resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.id, day.year, day.month, day.day)) assert ( 'Booking form' % ( agenda.slug, event.slug, 'http://testserver/manage/agendas/%d/month/%d/%d/%d/' % (agenda.pk, day.year, day.month, day.day), ) in resp.text ) resp = app.get('/manage/agendas/%d/events/open/' % agenda.pk) assert ( 'Booking form' % (agenda.slug, event.slug, 'http://testserver/manage/agendas/%d/events/open/' % agenda.pk) in resp.text ) resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk)) assert ( 'Booking form' % (agenda.slug, event.slug, 'http://testserver/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk)) in resp.text ) def test_booking_cancellation_events_agenda(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') event = Event(label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda) event.save() booking = Booking.objects.create(event=event) login(app) resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.id, event.id)) assert 'Bookings (1/10)' in resp.text resp = resp.click('Cancel', href='bookings/') resp = resp.form.submit() assert resp.location.endswith('/manage/agendas/%s/events/%s/' % (agenda.id, event.id)) booking.refresh_from_db() assert booking.cancellation_datetime resp = resp.follow() assert 'Bookings (0/10)' in resp.text # again app.get('/manage/agendas/%s/bookings/%s/cancel' % (agenda.pk, booking.pk), status=404) # test secondary booking primary = Booking.objects.create(event=event) secondary = Booking.objects.create(event=event, primary_booking=primary) resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk)) assert 'Bookings (2/10)' in resp.text assert '/manage/agendas/%s/bookings/%s/cancel' % (agenda.pk, primary.pk) in resp.text assert '/manage/agendas/%s/bookings/%s/cancel' % (agenda.pk, secondary.pk) not in resp.text app.get('/manage/agendas/%s/bookings/%s/cancel' % (agenda.pk, secondary.pk), status=404) app.get('/manage/agendas/%s/bookings/%s/cancel' % (agenda.pk, primary.pk)).form.submit() primary.refresh_from_db() secondary.refresh_from_db() assert primary.cancellation_datetime assert secondary.cancellation_datetime def test_event_check(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') Desk.objects.create(agenda=agenda, slug='_exceptions_holder') agenda2 = Agenda.objects.create(label='Events', kind='events') Desk.objects.create(agenda=agenda2, slug='_exceptions_holder') event = Event.objects.create( label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, waiting_list_places=5, agenda=agenda, ) booking1 = Booking.objects.create( event=event, user_external_id='user:1', user_first_name='User', user_last_name='42' ) Booking.objects.create( event=event, user_external_id='user:2', user_first_name="User's", user_last_name='01' ) Booking.objects.create( event=event, user_external_id='user:3', user_first_name='User', user_last_name='17' ) Booking.objects.create( event=event, user_external_id='user:4', user_first_name='User', user_last_name='35' ) Booking.objects.create( event=event, user_external_id='user:5', user_first_name='User', user_last_name='05' ) booking6 = Booking.objects.create( event=event, user_external_id='user:6', user_first_name='User', user_last_name='12 Cancelled' ) booking6.cancel() booking7 = Booking.objects.create( event=event, user_external_id='user:7', user_first_name='User', user_last_name='Waiting', in_waiting_list=True, ) booking8 = Booking.objects.create( event=event, user_external_id='user:8', user_first_name='User', user_last_name='Waiting and Cancelled', in_waiting_list=True, ) booking8.cancel() booking9 = Booking.objects.create( event=event, user_external_id='user:1', user_first_name='User', user_last_name='Secondary', primary_booking=booking1, ) login(app) # event not in past assert agenda.enable_check_for_future_events is False resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) not in resp app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=404) # not in past, but check for future events is enabled agenda.enable_check_for_future_events = True agenda.save() resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) in resp app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=200) # event is today agenda.enable_check_for_future_events = False agenda.save() event.start_datetime = localtime(now()).replace(hour=22, minute=0) # it's ok all the day event.save() resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) in resp app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=200) # unknown agenda app.get('/manage/agendas/%s/events/%s/check' % (0, event.pk), status=404) # wrong agenda app.get('/manage/agendas/%s/events/%s/check' % (agenda2.pk, event.pk), status=404) resp = resp.click('Check') assert ( resp.text.index('Bookings (6/10)') < resp.text.index("User's 01") < resp.text.index('User 05') < resp.text.index('User 17') < resp.text.index('User 35') < resp.text.index('User 42') < resp.text.index('Waiting List (1/5)') < resp.text.index('User Waiting') ) assert 'User 12 Cancelled' not in resp assert 'User Waiting and Cancelled' not in resp Subscription.objects.create( agenda=agenda, user_external_id='user:1', user_first_name='Subscription', user_last_name='42', date_start=now(), date_end=now() + datetime.timedelta(days=1), ) Subscription.objects.create( agenda=agenda, user_external_id='user:9', user_first_name='Subscription', user_last_name='43', date_start=now(), date_end=now() + datetime.timedelta(days=1), ) Subscription.objects.create( agenda=agenda, user_external_id='user:10', user_first_name='Subscription', user_last_name='14', date_start=now(), date_end=now() + datetime.timedelta(days=1), ) Subscription.objects.create( agenda=agenda, user_external_id='user:7', user_first_name='Subscription', user_last_name='Waiting', date_start=now(), date_end=now() + datetime.timedelta(days=1), ) Subscription.objects.create( agenda=agenda, user_external_id='user:42', user_first_name='Subscription', user_last_name='Too soon', date_start=now() - datetime.timedelta(days=1), date_end=now(), ) Subscription.objects.create( agenda=agenda, user_external_id='user:42', user_first_name='Subscription', user_last_name='Too late', date_start=now() + datetime.timedelta(days=1), date_end=now() + datetime.timedelta(days=2), ) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert ( resp.text.index('Bookings (6/10)') < resp.text.index("User's 01") < resp.text.index('User 05') < resp.text.index('User 12 Cancelled') < resp.text.index('Subscription 14') < resp.text.index('(Not booked)') < resp.text.index('User 17') < resp.text.index('User 35') < resp.text.index('User 42') < resp.text.index('Subscription 43') < resp.text.index('Waiting List (1/5)') < resp.text.index('User Waiting') < resp.text.index('User Waiting and Cancelled') ) assert 'Subscription Waiting' not in resp assert 'Subscription 42' not in resp assert 'Subscription too soon' not in resp assert 'Subscription too late' not in resp agenda.booking_user_block_template = '{{ booking.user_name }} Foo Bar' agenda.save() resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert '<b>User's 01</b> Foo Bar' in resp assert '<b>Subscription 14</b> Foo Bar' in resp assert '<b>User Waiting</b> Foo Bar' in resp # booking in waiting list token = resp.context['csrf_token'] app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking7.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking7.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # secondary booking app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking9.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking9.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # unknown agenda app.post( '/manage/agendas/%s/bookings/%s/presence' % (0, booking1.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/bookings/%s/absence' % (0, booking1.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # wrong agenda app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda2.pk, booking1.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda2.pk, booking1.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # cancelled event event.cancel() resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) not in resp app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=404) def test_event_checked(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events', booking_check_filters='foo,bar') Desk.objects.create(agenda=agenda, slug='_exceptions_holder') event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, agenda=agenda, ) login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Mark the event as checked' not in resp for i in range(8): user_was_present = None if i < 3: user_was_present = True elif i < 7: user_was_present = False Booking.objects.create( event=event, user_was_present=user_was_present, ) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Mark the event as checked' in resp assert event.checked is False resp = app.get('/manage/agendas/%s/settings' % agenda.id) assert 'checked tag' not in resp urls = [ '/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk), '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), '/manage/agendas/%s/month/%d/%d/%d/' % (agenda.pk, event.start_datetime.year, event.start_datetime.month, event.start_datetime.day), '/manage/agendas/%s/week/%d/%d/%d/' % (agenda.pk, event.start_datetime.year, event.start_datetime.month, event.start_datetime.day), '/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, event.start_datetime.year, event.start_datetime.month, event.start_datetime.day), ] for url in urls: resp = app.get(url) assert 'Checked' not in resp assert 'Presents 3' in resp assert 'Absents 4' in resp assert 'Not checked 1' in resp token = resp.context['csrf_token'] resp = app.post( '/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}, ) assert resp.location.endswith('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) event.refresh_from_db() assert event.checked is True resp = resp.follow() assert 'Mark the event as checked' not in resp resp = app.get('/manage/agendas/%s/settings' % agenda.id) assert 'checked tag' in resp Booking.objects.filter(user_was_present__isnull=True).update(user_was_present=True) for url in urls: resp = app.get(url) assert 'Checked' in resp assert 'Presents 4' in resp assert 'Absents 4' in resp assert 'meta meta-disabled' not in resp # event not in past agenda.disable_check_update = False agenda.save() assert agenda.enable_check_for_future_events is False event.start_datetime = now() + datetime.timedelta(days=1) event.save() app.post( '/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # not in past, but check for future events is enabled agenda.enable_check_for_future_events = True agenda.save() app.post( '/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=302, ) @mock.patch('chrono.manager.forms.get_agenda_check_types') def test_event_check_filters(check_types, app, admin_user): check_types.return_value = [ CheckType(slug='foo-reason', label='Foo reason', kind='absence'), CheckType(slug='bar-reason', label='Bar reason', kind='presence'), ] agenda = Agenda.objects.create( label='Events', kind='events', booking_check_filters='foo,bar', ) event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, agenda=agenda, ) Booking.objects.create( event=event, user_external_id='user:none', user_first_name='User', user_last_name='none' ) Booking.objects.create( event=event, user_external_id='user:empty', user_first_name='User', user_last_name='empty', extra_data={}, ) Booking.objects.create( event=event, user_external_id='user:1', user_first_name='User', user_last_name='foo-val1 bar-none presence', extra_data={'foo': 'val1', 'bar': ['val1']}, # bar is ignored, wrong value user_was_present=True, ) Booking.objects.create( event=event, user_external_id='user:2', user_first_name='User', user_last_name='foo-val2 bar-val1 absence', extra_data={'foo': 'val2', 'bar': 'val1'}, user_was_present=False, ) Booking.objects.create( event=event, user_external_id='user:3', user_first_name='User', user_last_name='foo-val1 bar-val2 not-checked', extra_data={'foo': 'val1', 'bar': 'val2'}, ) Booking.objects.create( event=event, user_external_id='user:4', user_first_name='User', user_last_name='foo-none bar-val2 reason-foo', extra_data={'bar': 'val2'}, user_was_present=False, user_check_type_slug='foo-reason', ) Booking.objects.create( event=event, user_external_id='user:5', user_first_name='User', user_last_name='foo-none bar-val2 reason-bar', extra_data={'bar': 'val2'}, user_was_present=True, user_check_type_slug='bar-reason', ) Booking.objects.create( event=event, user_external_id='user:6', user_first_name='User', user_last_name='foo-none bar-val2 cancelled-absence', extra_data={'bar': 'val2'}, user_was_present=False, user_check_type_slug='foo-reason', cancellation_datetime=now(), ) Booking.objects.create( event=event, user_external_id='user:7', user_first_name='User', user_last_name='foo-none bar-val2 cancelled-presence', extra_data={'bar': 'val2'}, user_was_present=True, user_check_type_slug='bar-reason', cancellation_datetime=now(), ) Subscription.objects.create( agenda=agenda, user_external_id='subscription:none', user_first_name='Subscription', user_last_name='none', date_start=event.start_datetime, date_end=event.start_datetime + datetime.timedelta(days=1), ) Subscription.objects.create( agenda=agenda, user_external_id='subscription:empty', user_first_name='Subscription', user_last_name='empty', extra_data={}, date_start=event.start_datetime, date_end=event.start_datetime + datetime.timedelta(days=1), ) Subscription.objects.create( agenda=agenda, user_external_id='subscription:1', user_first_name='Subscription', user_last_name='foo-val1 bar-none', extra_data={'foo': 'val1', 'bar': ['val1']}, # bar is ignored, wrong value date_start=event.start_datetime, date_end=event.start_datetime + datetime.timedelta(days=1), ) Subscription.objects.create( agenda=agenda, user_external_id='subscription:2', user_first_name='Subscription', user_last_name='foo-val2 bar-val1', extra_data={'foo': 'val2', 'bar': 'val1'}, date_start=event.start_datetime, date_end=event.start_datetime + datetime.timedelta(days=1), ) Subscription.objects.create( agenda=agenda, user_external_id='subscription:3', user_first_name='Subscription', user_last_name='foo-val1 bar-val2', extra_data={'foo': 'val1', 'bar': 'val2'}, date_start=event.start_datetime, date_end=event.start_datetime + datetime.timedelta(days=1), ) Subscription.objects.create( agenda=agenda, user_external_id='subscription:4', user_first_name='Subscription', user_last_name='foo-none bar-val2', extra_data={'bar': 'val2'}, date_start=event.start_datetime, date_end=event.start_datetime + datetime.timedelta(days=1), ) login(app) for params in [ {}, {'unknown': 'unknown'}, {'extra-data-unknown': 'unknown'}, {'extra-data-foo': 'unknown'}, ]: resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params=params) assert 'User none' in resp assert 'User empty' in resp assert 'User foo-val1 bar-none presence' in resp assert 'User foo-val2 bar-val1 absence' in resp assert 'User foo-val1 bar-val2 not-checked' in resp assert 'User foo-none bar-val2 reason-foo' in resp assert 'User foo-none bar-val2 reason-bar' in resp assert 'User foo-none bar-val2 cancelled-absence' in resp assert 'User foo-none bar-val2 cancelled-presence' in resp assert 'Subscription none' in resp assert 'Subscription empty' in resp assert 'Subscription foo-val1 bar-none' in resp assert 'Subscription foo-val2 bar-val1' in resp assert 'Subscription foo-val1 bar-val2' in resp assert 'Subscription foo-none bar-val2' in resp with CaptureQueriesContext(connection) as ctx: resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'extra-data-foo': 'val1'} ) assert len(ctx.captured_queries) == 10 assert 'User none' not in resp assert 'User empty' not in resp assert 'User foo-val1 bar-none presence' in resp assert 'User foo-val2 bar-val1 absence' not in resp assert 'User foo-val1 bar-val2 not-checked' in resp assert 'User foo-none bar-val2 reason-foo' not in resp assert 'User foo-none bar-val2 reason-bar' not in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' in resp assert 'Subscription foo-none bar-val2' not in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'extra-data-foo': 'val1', 'extra-data-bar': 'val2'}, ) assert 'User none' not in resp assert 'User empty' not in resp assert 'User foo-val1 bar-none presence' not in resp assert 'User foo-val2 bar-val1 absence' not in resp assert 'User foo-val1 bar-val2 not-checked' in resp assert 'User foo-none bar-val2 reason-foo' not in resp assert 'User foo-none bar-val2 reason-bar' not in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' not in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' in resp assert 'Subscription foo-none bar-val2' not in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'extra-data-foo': 'val2', 'extra-data-bar': 'val2'}, ) assert 'User none' not in resp assert 'User empty' not in resp assert 'User foo-val1 bar-none presence' not in resp assert 'User foo-val2 bar-val1 absence' not in resp assert 'User foo-val1 bar-val2 not-checked' not in resp assert 'User foo-none bar-val2 reason-foo' not in resp assert 'User foo-none bar-val2 reason-bar' not in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' not in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' not in resp assert 'Subscription foo-none bar-val2' not in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'booking-status': 'booked'} ) assert 'User none' in resp assert 'User empty' in resp assert 'User foo-val1 bar-none presence' in resp assert 'User foo-val2 bar-val1 absence' in resp assert 'User foo-val1 bar-val2 not-checked' in resp assert 'User foo-none bar-val2 reason-foo' in resp assert 'User foo-none bar-val2 reason-bar' in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' not in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' not in resp assert 'Subscription foo-none bar-val2' not in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'booking-status': 'not-booked'} ) assert 'User none' not in resp assert 'User empty' not in resp assert 'User foo-val1 bar-none presence' not in resp assert 'User foo-val2 bar-val1 absence' not in resp assert 'User foo-val1 bar-val2 not-checked' not in resp assert 'User foo-none bar-val2 reason-foo' not in resp assert 'User foo-none bar-val2 reason-bar' not in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' in resp assert 'Subscription empty' in resp assert 'Subscription foo-val1 bar-none' in resp assert 'Subscription foo-val2 bar-val1' in resp assert 'Subscription foo-val1 bar-val2' in resp assert 'Subscription foo-none bar-val2' in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'booking-status': 'cancelled'} ) assert 'User none' not in resp assert 'User empty' not in resp assert 'User foo-val1 bar-none presence' not in resp assert 'User foo-val2 bar-val1 absence' not in resp assert 'User foo-val1 bar-val2 not-checked' not in resp assert 'User foo-none bar-val2 reason-foo' not in resp assert 'User foo-none bar-val2 reason-bar' not in resp assert 'User foo-none bar-val2 cancelled-absence' in resp assert 'User foo-none bar-val2 cancelled-presence' in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' not in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' not in resp assert 'Subscription foo-none bar-val2' not in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'booking-status': 'not-checked'} ) assert 'User none' in resp assert 'User empty' in resp assert 'User foo-val1 bar-none presence' not in resp assert 'User foo-val2 bar-val1 absence' not in resp assert 'User foo-val1 bar-val2 not-checked' in resp assert 'User foo-none bar-val2 reason-foo' not in resp assert 'User foo-none bar-val2 reason-bar' not in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' not in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' not in resp assert 'Subscription foo-none bar-val2' not in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'booking-status': 'presence'} ) assert 'User none' not in resp assert 'User empty' not in resp assert 'User foo-val1 bar-none presence' in resp assert 'User foo-val2 bar-val1 absence' not in resp assert 'User foo-val1 bar-val2 not-checked' not in resp assert 'User foo-none bar-val2 reason-foo' not in resp assert 'User foo-none bar-val2 reason-bar' in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' not in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' not in resp assert 'Subscription foo-none bar-val2' not in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'booking-status': 'absence'} ) assert 'User none' not in resp assert 'User empty' not in resp assert 'User foo-val1 bar-none presence' not in resp assert 'User foo-val2 bar-val1 absence' in resp assert 'User foo-val1 bar-val2 not-checked' not in resp assert 'User foo-none bar-val2 reason-foo' in resp assert 'User foo-none bar-val2 reason-bar' not in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' not in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' not in resp assert 'Subscription foo-none bar-val2' not in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'booking-status': 'absence::foo-reason'}, ) assert 'User none' not in resp assert 'User empty' not in resp assert 'User foo-val1 bar-none presence' not in resp assert 'User foo-val2 bar-val1 absence' not in resp assert 'User foo-val1 bar-val2 not-checked' not in resp assert 'User foo-none bar-val2 reason-foo' in resp assert 'User foo-none bar-val2 reason-bar' not in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' not in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' not in resp assert 'Subscription foo-none bar-val2' not in resp resp = app.get( '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'booking-status': 'presence::bar-reason'}, ) assert 'User none' not in resp assert 'User empty' not in resp assert 'User foo-val1 bar-none presence' not in resp assert 'User foo-val2 bar-val1 absence' not in resp assert 'User foo-val1 bar-val2 not-checked' not in resp assert 'User foo-none bar-val2 reason-foo' not in resp assert 'User foo-none bar-val2 reason-bar' in resp assert 'User foo-none bar-val2 cancelled-absence' not in resp assert 'User foo-none bar-val2 cancelled-presence' not in resp assert 'Subscription none' not in resp assert 'Subscription empty' not in resp assert 'Subscription foo-val1 bar-none' not in resp assert 'Subscription foo-val2 bar-val1' not in resp assert 'Subscription foo-val1 bar-val2' not in resp assert 'Subscription foo-none bar-val2' not in resp def test_event_check_ordering(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda ) Booking.objects.create( event=event, user_first_name='BB', user_last_name='XX', user_external_id='user:1', ) Booking.objects.create( event=event, user_first_name='AA', user_last_name='YY', user_external_id='user:2', cancellation_datetime=now(), ) Subscription.objects.create( agenda=agenda, user_first_name='CC', user_last_name='WW', user_external_id='user:3', date_start=datetime.date(2022, 2, 1), date_end=datetime.date(2022, 3, 1), ) login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert resp.text.index('CC WW') < resp.text.index('BB XX') < resp.text.index('AA YY') resp = app.get('/manage/agendas/%s/events/%s/check?sort=lastname,firstname' % (agenda.pk, event.pk)) assert resp.text.index('CC WW') < resp.text.index('BB XX') < resp.text.index('AA YY') resp = app.get('/manage/agendas/%s/events/%s/check?sort=firstname,lastname' % (agenda.pk, event.pk)) assert resp.text.index('AA YY') < resp.text.index('BB XX') < resp.text.index('CC WW') @mock.patch('chrono.manager.forms.get_agenda_check_types') def test_event_check_booking(check_types, app, admin_user): check_types.return_value = [] agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, waiting_list_places=5, agenda=agenda, ) booking = Booking.objects.create(event=event, user_first_name='User', user_last_name='42') secondary_booking = Booking.objects.create( event=event, user_first_name='User', user_last_name='42', primary_booking=booking ) assert agenda.mark_event_checked_auto is False def _test_reset(): resp = app.post( '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, ).follow() assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp booking.refresh_from_db() assert booking.user_was_present is None assert booking.user_check_type_slug is None assert booking.user_check_type_label is None secondary_booking.refresh_from_db() assert secondary_booking.user_was_present is None assert secondary_booking.user_check_type_slug is None assert secondary_booking.user_check_type_label is None event.refresh_from_db() assert event.checked is False login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert resp.pyquery.find('td.booking-status')[0].text.strip() == '-' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 0 assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp # set as present token = resp.context['csrf_token'] resp = app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, ).follow() assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1 assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Presence' assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) in resp booking.refresh_from_db() assert booking.user_was_present is True assert booking.user_check_type_slug is None assert booking.user_check_type_label is None secondary_booking.refresh_from_db() assert secondary_booking.user_was_present is True assert secondary_booking.user_check_type_slug is None assert secondary_booking.user_check_type_label is None event.refresh_from_db() assert event.checked is False # reset _test_reset() agenda.mark_event_checked_auto = True agenda.save() # set as absent without check_type resp = app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, ).follow() assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Absent' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1 assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Absence' booking.refresh_from_db() assert booking.user_was_present is False assert booking.user_check_type_slug is None assert booking.user_check_type_label is None secondary_booking.refresh_from_db() assert secondary_booking.user_was_present is False assert secondary_booking.user_check_type_slug is None assert secondary_booking.user_check_type_label is None event.refresh_from_db() assert event.checked is True check_types.return_value = [ CheckType(slug='foo-reason', label='Foo reason', kind='absence'), ] resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert len(resp.pyquery.find('td.booking-actions form.absence select')) == 1 assert len(resp.pyquery.find('td.booking-actions form.presence select')) == 0 # reset _test_reset() # set as absent with check_type resp = app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'foo-reason'}, ).follow() assert 'Foo reason' in resp booking.refresh_from_db() assert booking.user_was_present is False assert booking.user_check_type_slug == 'foo-reason' assert booking.user_check_type_label == 'Foo reason' secondary_booking.refresh_from_db() assert secondary_booking.user_was_present is False assert secondary_booking.user_check_type_slug == 'foo-reason' assert secondary_booking.user_check_type_label == 'Foo reason' event.refresh_from_db() assert event.checked is True # reset _test_reset() # set as present without check_type resp = app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, ).follow() assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1 assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Presence' booking.refresh_from_db() assert booking.user_was_present is True assert booking.user_check_type_slug is None assert booking.user_check_type_label is None secondary_booking.refresh_from_db() assert secondary_booking.user_was_present is True assert secondary_booking.user_check_type_slug is None assert secondary_booking.user_check_type_label is None event.refresh_from_db() assert event.checked is True resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert len(resp.pyquery.find('td.booking-actions form.absence select')) == 1 assert len(resp.pyquery.find('td.booking-actions form.presence select')) == 0 check_types.return_value = [ CheckType(slug='foo-reason', label='Foo reason', kind='absence'), CheckType(slug='bar-reason', label='Bar reason', kind='presence'), ] resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert len(resp.pyquery.find('td.booking-actions form.absence select')) == 1 assert len(resp.pyquery.find('td.booking-actions form.presence select')) == 1 # reset _test_reset() # set as present with check_type resp = app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'}, ).follow() assert 'Bar reason' in resp booking.refresh_from_db() assert booking.user_was_present is True assert booking.user_check_type_slug == 'bar-reason' assert booking.user_check_type_label == 'Bar reason' secondary_booking.refresh_from_db() assert secondary_booking.user_was_present is True assert secondary_booking.user_check_type_slug == 'bar-reason' assert secondary_booking.user_check_type_label == 'Bar reason' event.refresh_from_db() assert event.checked is True # event is checked resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) in resp app.post( '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=302, ) app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=302, ) app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=302, ) # event not in past assert agenda.enable_check_for_future_events is False event.start_datetime = now() + datetime.timedelta(days=1) event.save() app.post( '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # not in past, but check for future events is enabled agenda.enable_check_for_future_events = True agenda.save() app.post( '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=302, ) app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=302, ) app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=302, ) # now disable check update agenda.disable_check_update = True agenda.save() resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) not in resp assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) not in resp assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=404, ) @mock.patch('chrono.manager.forms.get_agenda_check_types') def test_event_check_cancelled_booking(check_types, app, admin_user): check_types.return_value = [] agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, waiting_list_places=5, agenda=agenda, ) booking = Booking.objects.create(event=event, user_first_name='User', user_last_name='42') secondary_booking = Booking.objects.create( event=event, user_first_name='User', user_last_name='42', primary_booking=booking ) booking.cancel() Booking.objects.create(event=event, user_first_name='User', user_last_name='43') # no suscription: cancelled bookings are not displayed login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert len(resp.pyquery.find('td.booking-status')) == 1 assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) not in resp assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) not in resp assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp token = resp.context['csrf_token'] resp = app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, status=404, ) Subscription.objects.create( agenda=agenda, user_external_id='user:1', user_first_name='Subscription', user_last_name='42', date_start=now(), date_end=now() + datetime.timedelta(days=1), ) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert len(resp.pyquery.find('td.booking-status')) == 2 assert resp.pyquery.find('td.booking-status')[0].text.strip() == '(Cancelled)' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 0 assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp resp = app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, ).follow() assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1 assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Presence' booking.refresh_from_db() assert booking.cancellation_datetime is None assert booking.user_was_present is True secondary_booking.refresh_from_db() assert secondary_booking.cancellation_datetime is None assert secondary_booking.user_was_present is True booking.cancel() resp = app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, ).follow() assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Absent' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1 assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Absence' booking.refresh_from_db() assert booking.cancellation_datetime is None assert booking.user_was_present is False secondary_booking.refresh_from_db() assert secondary_booking.cancellation_datetime is None assert secondary_booking.user_was_present is False @mock.patch('chrono.manager.forms.get_agenda_check_types') def test_event_check_booking_ajax(check_types, app, admin_user): check_types.return_value = [ CheckType(slug='foo-reason', label='Foo reason', kind='absence'), CheckType(slug='bar-reason', label='Bar reason', kind='presence'), ] agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, waiting_list_places=5, agenda=agenda, ) booking = Booking.objects.create(event=event, user_first_name='User', user_last_name='42') login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) token = resp.context['csrf_token'] # set as present resp = app.post( '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'}, extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}, ) assert '' not in resp # because this is a fragment assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present\n \n (Bar reason)' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1 assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Presence' assert '' in resp # set as absent resp = app.post( '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'foo-reason'}, extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}, ) assert '' not in resp # because this is a fragment assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Absent\n \n (Foo reason)' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1 assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text.startswith('Absence') assert '' in resp # reset resp = app.post( '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk), params={'csrfmiddlewaretoken': token}, extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}, ) assert '' not in resp # because this is a fragment assert resp.pyquery.find('td.booking-status')[0].text.strip() == '-' @mock.patch('chrono.manager.forms.get_agenda_check_types') def test_event_check_subscription(check_types, app, admin_user): check_types.return_value = [ CheckType(slug='foo-reason', label='Foo reason', kind='absence'), CheckType(slug='bar-reason', label='Bar reason', kind='presence'), ] agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, waiting_list_places=5, agenda=agenda, ) subscription = Subscription.objects.create( agenda=agenda, user_external_id='user:1', user_first_name='Subscription', user_last_name='42', user_email='foo@bar.com', user_phone_number='06', extra_data={'foo': 'bar'}, date_start=now() - datetime.timedelta(days=1), date_end=now(), ) # existing booking: no check for subscription booking = Booking.objects.create( event=event, user_first_name='User', user_last_name='42', user_external_id='user:1' ) booking2 = Booking.objects.create( event=event, user_first_name='User', user_last_name='42', user_external_id='user:1' ) login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert ( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk) not in resp ) assert ( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) not in resp ) token = resp.context['csrf_token'] app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) booking2.delete() booking.cancel() resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert ( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk) not in resp ) assert ( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) not in resp ) app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) booking.delete() # set as present resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk) in resp assert '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) in resp app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'}, ) booking = Booking.objects.latest('pk') assert booking.user_was_present is True assert booking.user_check_type_slug == 'bar-reason' assert booking.user_check_type_label == 'Bar reason' assert booking.event == event assert booking.user_external_id == subscription.user_external_id assert booking.user_first_name == subscription.user_first_name assert booking.user_last_name == subscription.user_last_name assert booking.user_email == subscription.user_email assert booking.user_phone_number == subscription.user_phone_number assert booking.extra_data == subscription.extra_data booking.delete() # set as absent resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) in resp assert '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) in resp app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'foo-reason'}, ) booking = Booking.objects.latest('pk') assert booking.user_was_present is False assert booking.user_check_type_slug == 'foo-reason' assert booking.user_check_type_label == 'Foo reason' assert booking.event == event assert booking.user_external_id == subscription.user_external_id assert booking.user_first_name == subscription.user_first_name assert booking.user_last_name == subscription.user_last_name assert booking.user_email == subscription.user_email assert booking.user_phone_number == subscription.user_phone_number assert booking.extra_data == subscription.extra_data booking.delete() # mark the event as checked assert agenda.disable_check_update is False event.checked = True event.save() app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=302, ) Booking.objects.all().delete() app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=302, ) Booking.objects.all().delete() agenda.disable_check_update = True agenda.save() app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # check subscription dates agenda.disable_check_update = False agenda.save() subscription.date_start = now() - datetime.timedelta(days=2) subscription.date_end = now() - datetime.timedelta(days=1) subscription.save() app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) subscription.date_start = now() subscription.date_end = now() + datetime.timedelta(days=1) subscription.save() app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # event not in past subscription.date_end = now() + datetime.timedelta(days=2) subscription.save() assert agenda.enable_check_for_future_events is False event.start_datetime = now() + datetime.timedelta(days=1) event.save() app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # not in past, but check for future events is enabled agenda.enable_check_for_future_events = True agenda.save() app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=302, ) Booking.objects.all().delete() app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=302, ) @mock.patch('chrono.manager.forms.get_agenda_check_types') def test_event_check_extra_user_block(check_types, app, admin_user): check_types.return_value = [] agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, waiting_list_places=5, agenda=agenda, ) subscription = Subscription.objects.create( agenda=agenda, user_external_id='user:1', user_first_name='Subscription', user_last_name='41', date_start=now() - datetime.timedelta(days=1), date_end=now(), ) booking = Booking.objects.create( event=event, user_first_name='User', user_last_name='42', user_external_id='user:2' ) cancelled_booking = Booking.objects.create( event=event, user_first_name='User', user_last_name='43', user_external_id='user:3' ) cancelled_booking.cancel() waiting_booking = Booking.objects.create( event=event, user_first_name='User', user_last_name='44', user_external_id='user:4', in_waiting_list=True, ) login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/subscriptions/%s/extra-user-block' % (agenda.pk, subscription.pk) not in resp assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, booking.pk) not in resp assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, cancelled_booking.pk) not in resp assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, waiting_booking.pk) not in resp agenda.booking_extra_user_block_template = '{{ booking.user_name }} Foo Bar' agenda.save() resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert '/manage/agendas/%s/subscriptions/%s/extra-user-block' % (agenda.pk, subscription.pk) in resp assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, cancelled_booking.pk) in resp assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, waiting_booking.pk) in resp Subscription.objects.create( agenda=agenda, user_external_id='user:2', user_first_name='Subscription', user_last_name='42', date_start=now() - datetime.timedelta(days=1), date_end=now(), ) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) # booking url, not subscription url assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, booking.pk) in resp resp = app.get('/manage/agendas/%s/subscriptions/%s/extra-user-block' % (agenda.pk, subscription.pk)) assert resp.text == 'Subscription 41 Foo Bar' resp = app.get('/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, booking.pk)) assert resp.text == 'User 42 Foo Bar' resp = app.get('/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, cancelled_booking.pk)) assert resp.text == 'User 43 Foo Bar' resp = app.get('/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, waiting_booking.pk)) assert resp.text == 'User 44 Foo Bar' @mock.patch('chrono.manager.forms.get_agenda_check_types') def test_event_check_subscription_ajax(check_types, app, admin_user): check_types.return_value = [ CheckType(slug='foo-reason', label='Foo reason', kind='absence'), CheckType(slug='bar-reason', label='Bar reason', kind='presence'), ] agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, waiting_list_places=5, agenda=agenda, ) subscription = Subscription.objects.create( agenda=agenda, user_external_id='user:1', user_first_name='Subscription', user_last_name='42', date_start=now() - datetime.timedelta(days=1), date_end=now(), ) login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) token = resp.context['csrf_token'] # set as present resp = app.post( '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'}, extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}, ) assert '' not in resp # because this is a fragment assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present\n \n (Bar reason)' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1 assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Presence' assert '' in resp Booking.objects.all().delete() # set as absent resp = app.post( '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'foo-reason'}, extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}, ) assert '' not in resp # because this is a fragment assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Absent\n \n (Foo reason)' assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1 assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text.startswith('Absence') assert '' in resp @mock.patch('chrono.manager.forms.get_agenda_check_types') def test_event_check_all_bookings(check_types, app, admin_user): check_types.return_value = [ CheckType(slug='foo-reason', label='Foo reason', kind='absence'), CheckType(slug='bar-reason', label='Bar reason', kind='presence'), ] agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, waiting_list_places=5, agenda=agenda, ) booking1 = Booking.objects.create(event=event, user_first_name='User', user_last_name='42') assert agenda.mark_event_checked_auto is False login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) token = resp.context['csrf_token'] assert 'Mark all bookings without status' in resp assert '/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk) in resp assert '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk) in resp resp = app.post( '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token} ) booking1.refresh_from_db() assert booking1.user_was_present is False assert booking1.user_check_type_slug is None assert booking1.user_check_type_label is None event.refresh_from_db() assert event.checked is False agenda.mark_event_checked_auto = True agenda.save() resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Mark all bookings without status' not in resp assert '/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk) not in resp assert '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk) not in resp booking2 = Booking.objects.create(event=event, user_first_name='User', user_last_name='35') secondary_booking = Booking.objects.create( event=event, user_first_name='User', user_last_name='35', primary_booking=booking2 ) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Mark all bookings without status' in resp assert '/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk) in resp assert '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk) in resp app.post( '/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token} ) booking1.refresh_from_db() assert booking1.user_was_present is False assert booking1.user_check_type_slug is None assert booking1.user_check_type_label is None booking2.refresh_from_db() assert booking2.user_was_present is True assert booking2.user_check_type_slug is None assert booking2.user_check_type_label is None secondary_booking.refresh_from_db() assert secondary_booking.user_was_present is True assert secondary_booking.user_check_type_slug is None assert secondary_booking.user_check_type_label is None event.refresh_from_db() assert event.checked is True # event is checked booking3 = Booking.objects.create(event=event, user_first_name='User', user_last_name='51') resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Mark all bookings without status' in resp app.post( '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'foo-reason'}, ) booking1.refresh_from_db() assert booking1.user_was_present is False assert booking1.user_check_type_slug is None assert booking1.user_check_type_label is None booking2.refresh_from_db() assert booking2.user_was_present is True assert booking2.user_check_type_slug is None assert booking2.user_check_type_label is None booking3.refresh_from_db() assert booking3.user_was_present is False assert booking3.user_check_type_slug == 'foo-reason' assert booking3.user_check_type_label == 'Foo reason' booking4 = Booking.objects.create(event=event, user_first_name='User', user_last_name='52') resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Mark all bookings without status' in resp app.post( '/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'}, ) booking1.refresh_from_db() assert booking1.user_was_present is False assert booking1.user_check_type_slug is None assert booking1.user_check_type_label is None booking2.refresh_from_db() assert booking2.user_was_present is True assert booking2.user_check_type_slug is None assert booking2.user_check_type_label is None booking3.refresh_from_db() assert booking3.user_was_present is False assert booking3.user_check_type_slug == 'foo-reason' assert booking3.user_check_type_label == 'Foo reason' booking4.refresh_from_db() assert booking4.user_was_present is True assert booking4.user_check_type_slug == 'bar-reason' assert booking4.user_check_type_label == 'Bar reason' # now disable check update agenda.disable_check_update = True agenda.save() Booking.objects.create(event=event, user_first_name='User', user_last_name='52') resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Mark all bookings without status' not in resp app.post( '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'foo-reason'}, status=404, ) resp = app.post( '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'}, status=404, ) resp = app.post( '/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # event not in past agenda.disable_check_update = False agenda.save() assert agenda.enable_check_for_future_events is False event.start_datetime = now() + datetime.timedelta(days=1) event.save() app.post( '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) app.post( '/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=404, ) # not in past, but check for future events is enabled agenda.enable_check_for_future_events = True agenda.save() app.post( '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=302, ) app.post( '/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}, status=302, ) def test_event_check_primary_booking(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create( label='xyz', start_datetime=now() - datetime.timedelta(days=1), places=10, waiting_list_places=5, agenda=agenda, ) booking = Booking.objects.create(event=event, user_first_name='User', user_last_name='42') Booking.objects.create(event=event, user_first_name='John', user_last_name='Doe') booking_3 = Booking.objects.create( event=event, user_first_name='Jane', user_last_name='Doe', in_waiting_list=True ) # create secondary bookings Booking.objects.create(event=event, user_first_name='User', user_last_name='42', primary_booking=booking) Booking.objects.create(event=event, user_first_name='User', user_last_name='42', primary_booking=booking) Booking.objects.create( event=event, user_first_name='Jane', user_last_name='Doe', primary_booking=booking_3, in_waiting_list=True, ) login(app) resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) assert 'Bookings (4/10)' in resp.text user_bookings = resp.pyquery.find('td.booking-username.main-list') assert len(user_bookings) == 2 assert user_bookings[0].text.replace('\n', '').strip() == 'User 42 (3 places)' assert user_bookings[1].text.replace('\n', '').strip() == 'John Doe' assert 'Waiting List (2/5)' in resp.text user_bookings = resp.pyquery.find('td.booking-username.waiting') assert len(user_bookings) == 1 assert user_bookings[0].text.replace('\n', '').strip() == 'Jane Doe (2 places)' def test_duplicate_event(app, admin_user): agenda = Agenda.objects.create(label='Foo Bar', slug='foo-bar', kind='events') event = Event.objects.create( agenda=agenda, start_datetime=now(), places=10, waiting_list_places=50, label='Foo', duration=45, pricing='200€', url="http://example.com", description='foo', ) assert Event.objects.count() == 1 new_datetime = localtime(event.start_datetime.replace(year=event.start_datetime.year + 1)).replace( hour=17, minute=0 ) app = login(app) resp = app.get(f'/manage/agendas/{agenda.pk}/settings') resp = resp.click('Duplicate', href='events') resp.form['label'] = "Bar" resp.form['start_datetime_0'] = str(new_datetime.date()) resp.form['start_datetime_1'] = '17:00' resp = resp.form.submit().follow() assert Event.objects.count() == 2 duplicate = Event.objects.latest('pk') assert duplicate.start_datetime == new_datetime assert duplicate.agenda == event.agenda updated_fields = {"label", "start_datetime"} identical_fields = {f.name for f in Event._meta.fields} - updated_fields - {'id', 'slug'} for field in identical_fields: assert getattr(duplicate, field) == getattr(event, field) assert 'Event successfully duplicated.' in resp.text def test_duplicate_event_creates_recurrences(app, admin_user): agenda = Agenda.objects.create(label='Foo Bar', slug='foo-bar', kind='events') event_start = now().replace(hour=1, minute=0) recurring_event = Event.objects.create( agenda=agenda, start_datetime=event_start, recurrence_days=[0, 1, 2, 3, 4, 5, 6], recurrence_end_date=event_start + datetime.timedelta(days=6), places=10, label='Foo', ) assert Event.objects.count() == 1 app = login(app) resp = app.get(f'/manage/agendas/{agenda.pk}/settings') resp = resp.click('Duplicate', href='events') resp.form['label'] = "Bar" resp.form['start_datetime_0'] = str(event_start.date()) resp.form['start_datetime_1'] = '17:00' resp = resp.form.submit().follow() duplicate = Event.objects.filter(primary_event__isnull=True).latest('pk') assert duplicate != recurring_event assert duplicate.recurrences.count() == 6 event_recurrence = duplicate.recurrences.first() app.get(f'/manage/agendas/{agenda.pk}/events/{event_recurrence.pk}/duplicate', status=404)