From b8c8114108f49f4af25d16be1466450b573d407a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 17 Jun 2022 14:37:56 +0200
Subject: [PATCH 1/2] misc: split some manager tests

---
 tests/manager/test_event.py           | 887 -------------------------
 tests/manager/test_event_timesheet.py | 898 ++++++++++++++++++++++++++
 2 files changed, 898 insertions(+), 887 deletions(-)
 create mode 100644 tests/manager/test_event_timesheet.py

diff --git a/tests/manager/test_event.py b/tests/manager/test_event.py
index ee69aae7..c4f59a17 100644
--- a/tests/manager/test_event.py
+++ b/tests/manager/test_event.py
@@ -2445,890 +2445,3 @@ def test_event_check_primary_booking(app, admin_user):
     user_bookings = resp.pyquery.find('td.booking-username.waiting')
     assert len(user_bookings) == 1
     assert user_bookings[0].text == 'Jane Doe (2 places)'
-
-
-def test_events_timesheet_wrong_kind(app, admin_user):
-    agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
-
-    app = login(app)
-    app.get('/manage/agendas/%s/events/timesheet' % agenda.id, status=404)
-    agenda.kind = 'virtual'
-    agenda.save()
-    app.get('/manage/agendas/%s/events/timesheet' % agenda.id, status=404)
-
-
-def test_events_timesheet_form(app, admin_user):
-    agenda = Agenda.objects.create(label='Events', kind='events')
-
-    login(app)
-    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
-    resp.form['date_start'] = '2022-01-01'
-    resp.form['date_end'] = '2021-12-31'
-    resp = resp.form.submit()
-    assert resp.context['form'].errors['date_end'] == ['End date must be greater than start date.']
-
-    resp.form['date_end'] = '2022-04-02'
-    resp = resp.form.submit()
-    assert resp.context['form'].errors['date_end'] == ['Please select an interval of no more than 3 months.']
-
-    resp.form['date_end'] = '2022-04-01'
-    resp = resp.form.submit()
-    assert resp.context['form'].errors == {}
-
-
-@pytest.mark.freeze_time('2022-02-15')
-def test_events_timesheet_slots(app, admin_user):
-    start, end = (
-        now() - datetime.timedelta(days=15),
-        now() + datetime.timedelta(days=14),
-    )  # 2022-02-31, 2022-03-01
-    agenda = Agenda.objects.create(label='Events', kind='events')
-    Event.objects.create(label='event 1', start_datetime=start, places=10, agenda=agenda)
-    event2 = Event.objects.create(
-        label='event 2', start_datetime=start + datetime.timedelta(days=1), places=10, agenda=agenda
-    )
-    event3 = Event.objects.create(label='event 3', start_datetime=now(), places=10, agenda=agenda)
-    Event.objects.create(
-        label='event cancelled',
-        start_datetime=now() + datetime.timedelta(days=4),
-        places=10,
-        agenda=agenda,
-        cancelled=True,
-    )
-    event4 = Event.objects.create(
-        label='event 4', start_datetime=end - datetime.timedelta(days=1), places=10, agenda=agenda
-    )
-    Event.objects.create(label='event 5', start_datetime=end, places=10, agenda=agenda)
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:1',
-        user_first_name='Subscription',
-        user_last_name='42',
-        date_start=start,
-        date_end=end + datetime.timedelta(days=1),
-    )
-    recurring_event1 = Event.objects.create(
-        label='recurring 1',
-        start_datetime=start,
-        places=10,
-        agenda=agenda,
-        recurrence_days=[0, 1],
-        recurrence_end_date=end,
-    )
-    recurring_event1.create_all_recurrences()
-    recurring_event2 = Event.objects.create(
-        label='recurring 2',
-        start_datetime=start,
-        places=10,
-        agenda=agenda,
-        recurrence_days=[1, 2],
-        recurrence_end_date=end,
-    )
-    recurring_event2.create_all_recurrences()
-
-    login(app)
-    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
-    resp.form['date_start'] = '2022-02-01'
-    resp.form['date_end'] = '2022-02-28'
-    with CaptureQueriesContext(connection) as ctx:
-        resp = resp.form.submit()
-        assert len(ctx.captured_queries) == 7
-
-    slots = resp.context['form'].get_slots()
-    assert slots['dates'] == [
-        [
-            datetime.date(2022, 2, 1),
-            datetime.date(2022, 2, 2),
-            datetime.date(2022, 2, 7),
-            datetime.date(2022, 2, 8),
-            datetime.date(2022, 2, 9),
-            datetime.date(2022, 2, 14),
-            datetime.date(2022, 2, 15),
-            datetime.date(2022, 2, 16),
-            datetime.date(2022, 2, 21),
-            datetime.date(2022, 2, 22),
-            datetime.date(2022, 2, 23),
-            datetime.date(2022, 2, 28),
-        ]
-    ]
-    assert slots['events'] == [
-        event2,
-        recurring_event1,
-        recurring_event2,
-        event3,
-        event4,
-    ]
-    assert slots['users'][0]['users'] == [
-        {
-            'user_id': 'user:1',
-            'user_first_name': 'Subscription',
-            'user_last_name': '42',
-            'extra_data': {},
-            'events': [
-                {
-                    'event': event2,
-                    'dates': {date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 1)},
-                },
-                {
-                    'event': recurring_event1,
-                    'dates': {date: False for date in slots['dates'][0] if date.weekday() in [0, 1]},
-                },
-                {
-                    'event': recurring_event2,
-                    'dates': {date: False for date in slots['dates'][0] if date.weekday() in [1, 2]},
-                },
-                {
-                    'event': event3,
-                    'dates': {
-                        date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 15)
-                    },
-                },
-                {
-                    'event': event4,
-                    'dates': {
-                        date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 28)
-                    },
-                },
-            ],
-        },
-    ]
-    assert slots['extra_data'] == []
-
-
-@pytest.mark.freeze_time('2022-02-15')
-def test_events_timesheet_subscription_limits(app, admin_user):
-    agenda = Agenda.objects.create(label='Events', kind='events')
-    event1 = Event.objects.create(
-        start_datetime=make_aware(datetime.datetime(2022, 2, 1, 17, 0)), places=10, agenda=agenda
-    )
-    event2 = Event.objects.create(
-        start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
-    )
-    event3 = Event.objects.create(
-        start_datetime=make_aware(datetime.datetime(2022, 2, 28, 17, 0)), places=10, agenda=agenda
-    )
-
-    dates = [
-        ('2022-01-31', '2022-02-01'),
-        ('2022-02-01', '2022-02-02'),
-        ('2022-02-01', '2022-02-15'),
-        ('2022-02-01', '2022-02-16'),
-        ('2022-02-15', '2022-02-28'),
-        ('2022-02-15', '2022-03-01'),
-        ('2022-02-16', '2022-03-01'),
-        ('2022-02-01', '2022-03-01'),
-        ('2022-02-28', '2022-03-01'),
-        ('2022-03-01', '2022-03-02'),
-    ]
-
-    for start, end in dates:
-        Subscription.objects.create(
-            agenda=agenda,
-            user_external_id='user:%s-%s' % (start, end),
-            user_first_name='Subscription',
-            user_last_name='%s - %s' % (start, end),
-            date_start=datetime.datetime.strptime(start, '%Y-%m-%d'),
-            date_end=datetime.datetime.strptime(end, '%Y-%m-%d'),
-        )
-
-    login(app)
-    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
-    resp.form['date_start'] = '2022-02-01'
-    resp.form['date_end'] = '2022-02-28'
-    resp = resp.form.submit()
-
-    slots = resp.context['form'].get_slots()
-    assert slots['dates'] == [
-        [
-            datetime.date(2022, 2, 1),
-            datetime.date(2022, 2, 15),
-            datetime.date(2022, 2, 28),
-        ]
-    ]
-
-    assert slots['events'] == [
-        event1,
-        event2,
-        event3,
-    ]
-    users = slots['users'][0]['users']
-    assert len(users) == 8
-    assert users[0]['user_id'] == 'user:2022-02-01-2022-02-02'
-    assert users[1]['user_id'] == 'user:2022-02-01-2022-02-15'
-    assert users[2]['user_id'] == 'user:2022-02-01-2022-02-16'
-    assert users[3]['user_id'] == 'user:2022-02-01-2022-03-01'
-    assert users[4]['user_id'] == 'user:2022-02-15-2022-02-28'
-    assert users[5]['user_id'] == 'user:2022-02-15-2022-03-01'
-    assert users[6]['user_id'] == 'user:2022-02-16-2022-03-01'
-    assert users[7]['user_id'] == 'user:2022-02-28-2022-03-01'
-
-
-def test_events_timesheet_users(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
-    )
-
-    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', 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()
-    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()
-    Booking.objects.create(
-        event=event,
-        user_external_id='user:1',
-        user_first_name='User',
-        user_last_name='Secondary',
-        primary_booking=booking1,
-    )
-
-    login(app)
-    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
-    resp.form['date_start'] = '2022-02-01'
-    resp.form['date_end'] = '2022-02-28'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert [u['user_id'] for u in slots['users'][0]['users']] == [
-        'user:2',
-        'user:5',
-        'user:3',
-        'user:4',
-        'user:1',
-    ]
-
-    start = datetime.date(2022, 2, 1)
-    end = datetime.date(2022, 3, 1)
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:1',
-        user_first_name='Subscription',
-        user_last_name='42',
-        date_start=start,
-        date_end=end,
-    )
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:9',
-        user_first_name='Subscription',
-        user_last_name='43',
-        date_start=start,
-        date_end=end,
-    )
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:10',
-        user_first_name='Subscription',
-        user_last_name='14',
-        date_start=start,
-        date_end=end,
-    )
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:7',
-        user_first_name='Subscription',
-        user_last_name='Waiting',
-        date_start=start,
-        date_end=end,
-    )
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:42',
-        user_first_name='Subscription',
-        user_last_name='Too soon',
-        date_start=start - datetime.timedelta(days=1),
-        date_end=start,
-    )
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:43',
-        user_first_name='Subscription',
-        user_last_name='Too late',
-        date_start=end + datetime.timedelta(days=1),
-        date_end=end + datetime.timedelta(days=2),
-    )
-
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert [u['user_id'] for u in slots['users'][0]['users']] == [
-        'user:2',
-        'user:5',
-        'user:6',
-        'user:10',
-        'user:3',
-        'user:4',
-        'user:1',
-        'user:9',
-        'user:7',
-    ]
-
-
-def test_events_timesheet_user_ids(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 = Booking.objects.create(event=event, user_first_name='User', user_last_name='42')
-
-    login(app)
-    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
-    resp.form['date_start'] = '2022-02-01'
-    resp.form['date_end'] = '2022-02-28'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    # no user_id found
-    assert [u['user_id'] for u in slots['users'][0]['users']] == []
-    assert [u['user_first_name'] for u in slots['users'][0]['users']] == []
-    assert [u['user_last_name'] for u in slots['users'][0]['users']] == []
-
-    booking.user_external_id = 'user:1'
-    booking.save()
-
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert [u['user_id'] for u in slots['users'][0]['users']] == [
-        'user:1',
-    ]
-    assert [u['user_first_name'] for u in slots['users'][0]['users']] == ['User']
-    assert [u['user_last_name'] for u in slots['users'][0]['users']] == ['42']
-
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:1',
-        user_first_name='Subscription',
-        user_last_name='41',
-        date_start=datetime.date(2022, 2, 1),
-        date_end=datetime.date(2022, 3, 1),
-    )
-
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert [u['user_id'] for u in slots['users'][0]['users']] == [
-        'user:1',
-    ]
-    assert [u['user_first_name'] for u in slots['users'][0]['users']] == [
-        'Subscription',
-    ]
-    assert [u['user_last_name'] for u in slots['users'][0]['users']] == [
-        '41',
-    ]
-
-
-@pytest.mark.freeze_time('2022-02-01')
-def test_events_timesheet_booked(app, admin_user):
-    agenda = Agenda.objects.create(label='Events', kind='events')
-    event_date = make_aware(datetime.datetime(2022, 2, 15, 17, 0))
-    event1 = Event.objects.create(label='event 1', start_datetime=event_date, places=10, agenda=agenda)
-    event2 = Event.objects.create(label='event 2', start_datetime=event_date, places=10, agenda=agenda)
-    event3 = Event.objects.create(label='event 3', start_datetime=event_date, places=10, agenda=agenda)
-    recurring_event1 = Event.objects.create(
-        label='recurring 1',
-        start_datetime=event_date,
-        places=10,
-        agenda=agenda,
-        recurrence_days=[1],
-        recurrence_end_date=event_date + datetime.timedelta(days=1),
-    )
-    recurring_event1.create_all_recurrences()
-    recurring_event1_occurence = recurring_event1.recurrences.first()
-    recurring_event2 = Event.objects.create(
-        label='recurring 2',
-        start_datetime=event_date,
-        places=10,
-        agenda=agenda,
-        recurrence_days=[1],
-        recurrence_end_date=event_date + datetime.timedelta(days=1),
-    )
-    recurring_event2.create_all_recurrences()
-    recurring_event2_occurence = recurring_event2.recurrences.first()
-
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:1',
-        user_first_name='Subscription',
-        user_last_name='42',
-        date_start=datetime.date(2022, 2, 1),
-        date_end=datetime.date(2022, 3, 1),
-    )
-    Booking.objects.create(
-        event=event1,
-        user_external_id='user:1',
-        user_first_name='User',
-        user_last_name='42',
-    )
-    Booking.objects.create(
-        event=event2,
-        user_external_id='user:1',
-        user_first_name='User',
-        user_last_name='42',
-        cancellation_datetime=now(),
-    )
-    Booking.objects.create(
-        event=recurring_event1_occurence,
-        user_external_id='user:1',
-        user_first_name='User',
-        user_last_name='42',
-    )
-    Booking.objects.create(
-        event=recurring_event2_occurence,
-        user_external_id='user:1',
-        user_first_name='User',
-        user_last_name='42',
-        cancellation_datetime=now(),
-    )
-
-    login(app)
-    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
-    resp.form['date_start'] = '2022-02-01'
-    resp.form['date_end'] = '2022-02-28'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert slots['events'] == [
-        event1,
-        event2,
-        event3,
-        recurring_event1,
-        recurring_event2,
-    ]
-    assert len(slots['users'][0]['users']) == 1
-    assert slots['users'][0]['users'][0]['events'] == [
-        {
-            'event': event1,
-            'dates': {datetime.date(2022, 2, 15): True},
-        },
-        {
-            'event': event2,
-            'dates': {datetime.date(2022, 2, 15): False},
-        },
-        {
-            'event': event3,
-            'dates': {datetime.date(2022, 2, 15): False},
-        },
-        {
-            'event': recurring_event1,
-            'dates': {datetime.date(2022, 2, 15): True},
-        },
-        {
-            'event': recurring_event2,
-            'dates': {datetime.date(2022, 2, 15): False},
-        },
-    ]
-
-
-def test_events_timesheet_extra_data(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='User',
-        user_last_name='42',
-        user_external_id='user:1',
-        extra_data={'foo': 'bar', 'baz': 'blah'},
-    )
-
-    login(app)
-    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
-    resp.form['date_start'] = '2022-02-01'
-    resp.form['date_end'] = '2022-02-28'
-    resp.form['extra_data'] = ' foo '
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert resp.text.count('<div class="page_break">') == 0
-
-    assert len(slots['users']) == 1
-    assert slots['users'][0]['grouper'] == ''
-    assert len(slots['users'][0]['users']) == 1
-    assert slots['extra_data'] == ['foo']
-    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'bar'
-
-    resp.form['extra_data'] = ' foo ,baz,,'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert len(slots['users'][0]['users']) == 1
-    assert slots['extra_data'] == ['foo', 'baz']
-    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'bar'
-    assert slots['users'][0]['users'][0]['extra_data']['baz'] == 'blah'
-
-    resp.form['extra_data'] = 'unknown'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert len(slots['users'][0]['users']) == 1
-    assert slots['extra_data'] == ['unknown']
-    assert slots['users'][0]['users'][0]['extra_data']['unknown'] == ''
-
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:1',
-        user_first_name='Subscription',
-        user_last_name='41',
-        date_start=datetime.date(2022, 2, 1),
-        date_end=datetime.date(2022, 3, 1),
-        extra_data={'foo': 'baz'},
-    )
-
-    resp.form['extra_data'] = ' foo '
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert len(slots['users'][0]['users']) == 1
-    assert slots['extra_data'] == ['foo']
-    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'baz'
-
-    resp.form['extra_data'] = ' foo ,baz,,'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert len(slots['users'][0]['users']) == 1
-    assert slots['extra_data'] == ['foo', 'baz']
-    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'baz'
-    assert slots['users'][0]['users'][0]['extra_data']['baz'] == ''
-
-    Booking.objects.create(
-        event=event,
-        user_first_name='User',
-        user_last_name='43',
-        user_external_id='user:2',
-        extra_data={'foo': 'bar', 'baz': 'aa'},
-    )
-    Booking.objects.create(
-        event=event,
-        user_first_name='User',
-        user_last_name='44',
-        user_external_id='user:3',
-        extra_data={'foo': 'bar2', 'baz': 'aa'},
-    )
-
-    resp.form['extra_data'] = ''
-    resp.form['group_by'] = 'foo'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert resp.text.count('<div class="page_break">') == 0
-
-    assert len(slots['users']) == 3
-    assert slots['users'][0]['grouper'] == 'bar'
-    assert len(slots['users'][0]['users']) == 1
-    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
-    assert slots['users'][1]['grouper'] == 'bar2'
-    assert len(slots['users'][1]['users']) == 1
-    assert slots['users'][1]['users'][0]['user_id'] == 'user:3'
-    assert slots['users'][2]['grouper'] == 'baz'
-    assert len(slots['users'][2]['users']) == 1
-    assert slots['users'][2]['users'][0]['user_id'] == 'user:1'
-
-    resp.form['with_page_break'] = True
-    resp = resp.form.submit()
-    assert resp.text.count('<div class="page_break">') == 2
-
-    resp.form['group_by'] = 'baz'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert len(slots['users']) == 2
-    assert slots['users'][0]['grouper'] == 'aa'
-    assert len(slots['users'][0]['users']) == 2
-    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
-    assert slots['users'][0]['users'][1]['user_id'] == 'user:3'
-    assert slots['users'][1]['grouper'] == ''
-    assert len(slots['users'][1]['users']) == 1
-    assert slots['users'][1]['users'][0]['user_id'] == 'user:1'
-
-    Subscription.objects.update(extra_data={'foo': 'baz', 'baz': 'blah'})
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert len(slots['users']) == 2
-    assert slots['users'][0]['grouper'] == 'aa'
-    assert len(slots['users'][0]['users']) == 2
-    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
-    assert slots['users'][0]['users'][1]['user_id'] == 'user:3'
-    assert slots['users'][1]['grouper'] == 'blah'
-    assert len(slots['users'][1]['users']) == 1
-    assert slots['users'][1]['users'][0]['user_id'] == 'user:1'
-
-    resp.form['group_by'] = 'unknown'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert len(slots['users']) == 1
-    assert slots['users'][0]['grouper'] == ''
-    assert len(slots['users'][0]['users']) == 3
-
-
-def test_events_timesheet_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/timesheet' % agenda.pk)
-    resp.form['date_start'] = '2022-02-01'
-    resp.form['date_end'] = '2022-02-28'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert slots['users'][0]['users'][0]['user_id'] == 'user:3'
-    assert slots['users'][0]['users'][1]['user_id'] == 'user:1'
-    assert slots['users'][0]['users'][2]['user_id'] == 'user:2'
-
-    resp.form['sort'] = 'lastname,firstname'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert slots['users'][0]['users'][0]['user_id'] == 'user:3'
-    assert slots['users'][0]['users'][1]['user_id'] == 'user:1'
-    assert slots['users'][0]['users'][2]['user_id'] == 'user:2'
-
-    resp.form['sort'] = 'firstname,lastname'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
-    assert slots['users'][0]['users'][1]['user_id'] == 'user:1'
-    assert slots['users'][0]['users'][2]['user_id'] == 'user:3'
-
-
-@pytest.mark.freeze_time('2022-04-01')
-def test_events_timesheet_date_display(app, admin_user):
-    agenda = Agenda.objects.create(label='Events', kind='events')
-    recurring_event = Event.objects.create(
-        label='recurring 1',
-        start_datetime=make_aware(datetime.datetime(2022, 1, 1, 12, 0)),
-        places=10,
-        agenda=agenda,
-        recurrence_days=[0],
-        recurrence_end_date=datetime.date(2022, 4, 1),
-    )
-    recurring_event.create_all_recurrences()
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:1',
-        user_first_name='Subscription',
-        user_last_name='42',
-        date_start=datetime.date(2022, 1, 1),
-        date_end=datetime.date(2022, 4, 1),
-        extra_data={'foo': 'bar'},
-    )
-
-    login(app)
-    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
-    resp.form['date_start'] = '2022-01-01'
-    resp.form['date_end'] = '2022-03-31'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert slots['dates'] == [
-        [
-            datetime.date(2022, 1, 3),
-            datetime.date(2022, 1, 10),
-            datetime.date(2022, 1, 17),
-            datetime.date(2022, 1, 24),
-            datetime.date(2022, 1, 31),
-            datetime.date(2022, 2, 7),
-            datetime.date(2022, 2, 14),
-            datetime.date(2022, 2, 21),
-            datetime.date(2022, 2, 28),
-            datetime.date(2022, 3, 7),
-            datetime.date(2022, 3, 14),
-            datetime.date(2022, 3, 21),
-            datetime.date(2022, 3, 28),
-        ]
-    ]
-
-    resp.form['date_display'] = 'month'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert slots['dates'] == [
-        [
-            datetime.date(2022, 1, 3),
-            datetime.date(2022, 1, 10),
-            datetime.date(2022, 1, 17),
-            datetime.date(2022, 1, 24),
-            datetime.date(2022, 1, 31),
-        ],
-        [
-            datetime.date(2022, 2, 7),
-            datetime.date(2022, 2, 14),
-            datetime.date(2022, 2, 21),
-            datetime.date(2022, 2, 28),
-        ],
-        [
-            datetime.date(2022, 3, 7),
-            datetime.date(2022, 3, 14),
-            datetime.date(2022, 3, 21),
-            datetime.date(2022, 3, 28),
-        ],
-    ]
-
-    resp.form['date_display'] = 'week'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert resp.text.count('<div class="page_break">') == 12
-
-    assert slots['dates'] == [
-        [datetime.date(2022, 1, 3)],
-        [datetime.date(2022, 1, 10)],
-        [datetime.date(2022, 1, 17)],
-        [datetime.date(2022, 1, 24)],
-        [datetime.date(2022, 1, 31)],
-        [datetime.date(2022, 2, 7)],
-        [datetime.date(2022, 2, 14)],
-        [datetime.date(2022, 2, 21)],
-        [datetime.date(2022, 2, 28)],
-        [datetime.date(2022, 3, 7)],
-        [datetime.date(2022, 3, 14)],
-        [datetime.date(2022, 3, 21)],
-        [datetime.date(2022, 3, 28)],
-    ]
-
-    resp.form['date_display'] = 'custom'
-    resp = resp.form.submit()
-    assert resp.context['form'].errors['custom_nb_dates_per_page'] == ['This field is required.']
-
-    resp.form['custom_nb_dates_per_page'] = 10
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert slots['dates'] == [
-        [
-            datetime.date(2022, 1, 3),
-            datetime.date(2022, 1, 10),
-            datetime.date(2022, 1, 17),
-            datetime.date(2022, 1, 24),
-            datetime.date(2022, 1, 31),
-            datetime.date(2022, 2, 7),
-            datetime.date(2022, 2, 14),
-            datetime.date(2022, 2, 21),
-            datetime.date(2022, 2, 28),
-            datetime.date(2022, 3, 7),
-        ],
-        [
-            datetime.date(2022, 3, 14),
-            datetime.date(2022, 3, 21),
-            datetime.date(2022, 3, 28),
-        ],
-    ]
-
-    resp.form['custom_nb_dates_per_page'] = 3
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-
-    assert slots['dates'] == [
-        [
-            datetime.date(2022, 1, 3),
-            datetime.date(2022, 1, 10),
-            datetime.date(2022, 1, 17),
-        ],
-        [
-            datetime.date(2022, 1, 24),
-            datetime.date(2022, 1, 31),
-            datetime.date(2022, 2, 7),
-        ],
-        [
-            datetime.date(2022, 2, 14),
-            datetime.date(2022, 2, 21),
-            datetime.date(2022, 2, 28),
-        ],
-        [
-            datetime.date(2022, 3, 7),
-            datetime.date(2022, 3, 14),
-            datetime.date(2022, 3, 21),
-        ],
-        [
-            datetime.date(2022, 3, 28),
-        ],
-    ]
-
-    Subscription.objects.create(
-        agenda=agenda,
-        user_external_id='user:2',
-        user_first_name='Subscription',
-        user_last_name='43',
-        date_start=datetime.date(2022, 1, 1),
-        date_end=datetime.date(2022, 4, 1),
-        extra_data={'foo': 'baz'},
-    )
-
-    resp.form['date_display'] = 'week'
-    resp.form['group_by'] = 'foo'
-    resp = resp.form.submit()
-    slots = resp.context['form'].get_slots()
-    assert resp.text.count('<div class="page_break">') == 12
-
-    resp.form['with_page_break'] = True
-    resp = resp.form.submit()
-    assert resp.text.count('<div class="page_break">') == 25
-
-
-def test_events_timesheet_pdf(app, admin_user):
-    agenda = Agenda.objects.create(label='Events', kind='events')
-
-    login(app)
-    resp = app.get(
-        '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&sort=lastname,firstname&date_display=all&orientation=portrait'
-        % agenda.pk
-    )
-    assert resp.headers['Content-Type'] == 'application/pdf'
-    assert (
-        resp.headers['Content-Disposition']
-        == 'attachment; filename="timesheet_events_2022-02-01_2022-02-28.pdf"'
-    )
-
-    # form invalid
-    resp = app.get(
-        '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&sort=lastname,firstname&date_display=all'
-        % agenda.pk
-    )
-    assert resp.context['form'].errors['orientation'] == ['This field is required.']
diff --git a/tests/manager/test_event_timesheet.py b/tests/manager/test_event_timesheet.py
new file mode 100644
index 00000000..40f1ed22
--- /dev/null
+++ b/tests/manager/test_event_timesheet.py
@@ -0,0 +1,898 @@
+import datetime
+
+import pytest
+from django.db import connection
+from django.test.utils import CaptureQueriesContext
+from django.utils.timezone import make_aware, now
+
+from chrono.agendas.models import Agenda, Booking, Event, Subscription
+from tests.utils import login
+
+pytestmark = pytest.mark.django_db
+
+
+def test_events_timesheet_wrong_kind(app, admin_user):
+    agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
+
+    app = login(app)
+    app.get('/manage/agendas/%s/events/timesheet' % agenda.id, status=404)
+    agenda.kind = 'virtual'
+    agenda.save()
+    app.get('/manage/agendas/%s/events/timesheet' % agenda.id, status=404)
+
+
+def test_events_timesheet_form(app, admin_user):
+    agenda = Agenda.objects.create(label='Events', kind='events')
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-01-01'
+    resp.form['date_end'] = '2021-12-31'
+    resp = resp.form.submit()
+    assert resp.context['form'].errors['date_end'] == ['End date must be greater than start date.']
+
+    resp.form['date_end'] = '2022-04-02'
+    resp = resp.form.submit()
+    assert resp.context['form'].errors['date_end'] == ['Please select an interval of no more than 3 months.']
+
+    resp.form['date_end'] = '2022-04-01'
+    resp = resp.form.submit()
+    assert resp.context['form'].errors == {}
+
+
+@pytest.mark.freeze_time('2022-02-15')
+def test_events_timesheet_slots(app, admin_user):
+    start, end = (
+        now() - datetime.timedelta(days=15),
+        now() + datetime.timedelta(days=14),
+    )  # 2022-02-31, 2022-03-01
+    agenda = Agenda.objects.create(label='Events', kind='events')
+    Event.objects.create(label='event 1', start_datetime=start, places=10, agenda=agenda)
+    event2 = Event.objects.create(
+        label='event 2', start_datetime=start + datetime.timedelta(days=1), places=10, agenda=agenda
+    )
+    event3 = Event.objects.create(label='event 3', start_datetime=now(), places=10, agenda=agenda)
+    Event.objects.create(
+        label='event cancelled',
+        start_datetime=now() + datetime.timedelta(days=4),
+        places=10,
+        agenda=agenda,
+        cancelled=True,
+    )
+    event4 = Event.objects.create(
+        label='event 4', start_datetime=end - datetime.timedelta(days=1), places=10, agenda=agenda
+    )
+    Event.objects.create(label='event 5', start_datetime=end, places=10, agenda=agenda)
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:1',
+        user_first_name='Subscription',
+        user_last_name='42',
+        date_start=start,
+        date_end=end + datetime.timedelta(days=1),
+    )
+    recurring_event1 = Event.objects.create(
+        label='recurring 1',
+        start_datetime=start,
+        places=10,
+        agenda=agenda,
+        recurrence_days=[0, 1],
+        recurrence_end_date=end,
+    )
+    recurring_event1.create_all_recurrences()
+    recurring_event2 = Event.objects.create(
+        label='recurring 2',
+        start_datetime=start,
+        places=10,
+        agenda=agenda,
+        recurrence_days=[1, 2],
+        recurrence_end_date=end,
+    )
+    recurring_event2.create_all_recurrences()
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-02-01'
+    resp.form['date_end'] = '2022-02-28'
+    with CaptureQueriesContext(connection) as ctx:
+        resp = resp.form.submit()
+        assert len(ctx.captured_queries) == 7
+
+    slots = resp.context['form'].get_slots()
+    assert slots['dates'] == [
+        [
+            datetime.date(2022, 2, 1),
+            datetime.date(2022, 2, 2),
+            datetime.date(2022, 2, 7),
+            datetime.date(2022, 2, 8),
+            datetime.date(2022, 2, 9),
+            datetime.date(2022, 2, 14),
+            datetime.date(2022, 2, 15),
+            datetime.date(2022, 2, 16),
+            datetime.date(2022, 2, 21),
+            datetime.date(2022, 2, 22),
+            datetime.date(2022, 2, 23),
+            datetime.date(2022, 2, 28),
+        ]
+    ]
+    assert slots['events'] == [
+        event2,
+        recurring_event1,
+        recurring_event2,
+        event3,
+        event4,
+    ]
+    assert slots['users'][0]['users'] == [
+        {
+            'user_id': 'user:1',
+            'user_first_name': 'Subscription',
+            'user_last_name': '42',
+            'extra_data': {},
+            'events': [
+                {
+                    'event': event2,
+                    'dates': {date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 1)},
+                },
+                {
+                    'event': recurring_event1,
+                    'dates': {date: False for date in slots['dates'][0] if date.weekday() in [0, 1]},
+                },
+                {
+                    'event': recurring_event2,
+                    'dates': {date: False for date in slots['dates'][0] if date.weekday() in [1, 2]},
+                },
+                {
+                    'event': event3,
+                    'dates': {
+                        date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 15)
+                    },
+                },
+                {
+                    'event': event4,
+                    'dates': {
+                        date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 28)
+                    },
+                },
+            ],
+        },
+    ]
+    assert slots['extra_data'] == []
+
+
+@pytest.mark.freeze_time('2022-02-15')
+def test_events_timesheet_subscription_limits(app, admin_user):
+    agenda = Agenda.objects.create(label='Events', kind='events')
+    event1 = Event.objects.create(
+        start_datetime=make_aware(datetime.datetime(2022, 2, 1, 17, 0)), places=10, agenda=agenda
+    )
+    event2 = Event.objects.create(
+        start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
+    )
+    event3 = Event.objects.create(
+        start_datetime=make_aware(datetime.datetime(2022, 2, 28, 17, 0)), places=10, agenda=agenda
+    )
+
+    dates = [
+        ('2022-01-31', '2022-02-01'),
+        ('2022-02-01', '2022-02-02'),
+        ('2022-02-01', '2022-02-15'),
+        ('2022-02-01', '2022-02-16'),
+        ('2022-02-15', '2022-02-28'),
+        ('2022-02-15', '2022-03-01'),
+        ('2022-02-16', '2022-03-01'),
+        ('2022-02-01', '2022-03-01'),
+        ('2022-02-28', '2022-03-01'),
+        ('2022-03-01', '2022-03-02'),
+    ]
+
+    for start, end in dates:
+        Subscription.objects.create(
+            agenda=agenda,
+            user_external_id='user:%s-%s' % (start, end),
+            user_first_name='Subscription',
+            user_last_name='%s - %s' % (start, end),
+            date_start=datetime.datetime.strptime(start, '%Y-%m-%d'),
+            date_end=datetime.datetime.strptime(end, '%Y-%m-%d'),
+        )
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-02-01'
+    resp.form['date_end'] = '2022-02-28'
+    resp = resp.form.submit()
+
+    slots = resp.context['form'].get_slots()
+    assert slots['dates'] == [
+        [
+            datetime.date(2022, 2, 1),
+            datetime.date(2022, 2, 15),
+            datetime.date(2022, 2, 28),
+        ]
+    ]
+
+    assert slots['events'] == [
+        event1,
+        event2,
+        event3,
+    ]
+    users = slots['users'][0]['users']
+    assert len(users) == 8
+    assert users[0]['user_id'] == 'user:2022-02-01-2022-02-02'
+    assert users[1]['user_id'] == 'user:2022-02-01-2022-02-15'
+    assert users[2]['user_id'] == 'user:2022-02-01-2022-02-16'
+    assert users[3]['user_id'] == 'user:2022-02-01-2022-03-01'
+    assert users[4]['user_id'] == 'user:2022-02-15-2022-02-28'
+    assert users[5]['user_id'] == 'user:2022-02-15-2022-03-01'
+    assert users[6]['user_id'] == 'user:2022-02-16-2022-03-01'
+    assert users[7]['user_id'] == 'user:2022-02-28-2022-03-01'
+
+
+def test_events_timesheet_users(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
+    )
+
+    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', 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()
+    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()
+    Booking.objects.create(
+        event=event,
+        user_external_id='user:1',
+        user_first_name='User',
+        user_last_name='Secondary',
+        primary_booking=booking1,
+    )
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-02-01'
+    resp.form['date_end'] = '2022-02-28'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert [u['user_id'] for u in slots['users'][0]['users']] == [
+        'user:2',
+        'user:5',
+        'user:3',
+        'user:4',
+        'user:1',
+    ]
+
+    start = datetime.date(2022, 2, 1)
+    end = datetime.date(2022, 3, 1)
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:1',
+        user_first_name='Subscription',
+        user_last_name='42',
+        date_start=start,
+        date_end=end,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:9',
+        user_first_name='Subscription',
+        user_last_name='43',
+        date_start=start,
+        date_end=end,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:10',
+        user_first_name='Subscription',
+        user_last_name='14',
+        date_start=start,
+        date_end=end,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:7',
+        user_first_name='Subscription',
+        user_last_name='Waiting',
+        date_start=start,
+        date_end=end,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:42',
+        user_first_name='Subscription',
+        user_last_name='Too soon',
+        date_start=start - datetime.timedelta(days=1),
+        date_end=start,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:43',
+        user_first_name='Subscription',
+        user_last_name='Too late',
+        date_start=end + datetime.timedelta(days=1),
+        date_end=end + datetime.timedelta(days=2),
+    )
+
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert [u['user_id'] for u in slots['users'][0]['users']] == [
+        'user:2',
+        'user:5',
+        'user:6',
+        'user:10',
+        'user:3',
+        'user:4',
+        'user:1',
+        'user:9',
+        'user:7',
+    ]
+
+
+def test_events_timesheet_user_ids(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 = Booking.objects.create(event=event, user_first_name='User', user_last_name='42')
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-02-01'
+    resp.form['date_end'] = '2022-02-28'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    # no user_id found
+    assert [u['user_id'] for u in slots['users'][0]['users']] == []
+    assert [u['user_first_name'] for u in slots['users'][0]['users']] == []
+    assert [u['user_last_name'] for u in slots['users'][0]['users']] == []
+
+    booking.user_external_id = 'user:1'
+    booking.save()
+
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert [u['user_id'] for u in slots['users'][0]['users']] == [
+        'user:1',
+    ]
+    assert [u['user_first_name'] for u in slots['users'][0]['users']] == ['User']
+    assert [u['user_last_name'] for u in slots['users'][0]['users']] == ['42']
+
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:1',
+        user_first_name='Subscription',
+        user_last_name='41',
+        date_start=datetime.date(2022, 2, 1),
+        date_end=datetime.date(2022, 3, 1),
+    )
+
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert [u['user_id'] for u in slots['users'][0]['users']] == [
+        'user:1',
+    ]
+    assert [u['user_first_name'] for u in slots['users'][0]['users']] == [
+        'Subscription',
+    ]
+    assert [u['user_last_name'] for u in slots['users'][0]['users']] == [
+        '41',
+    ]
+
+
+@pytest.mark.freeze_time('2022-02-01')
+def test_events_timesheet_booked(app, admin_user):
+    agenda = Agenda.objects.create(label='Events', kind='events')
+    event_date = make_aware(datetime.datetime(2022, 2, 15, 17, 0))
+    event1 = Event.objects.create(label='event 1', start_datetime=event_date, places=10, agenda=agenda)
+    event2 = Event.objects.create(label='event 2', start_datetime=event_date, places=10, agenda=agenda)
+    event3 = Event.objects.create(label='event 3', start_datetime=event_date, places=10, agenda=agenda)
+    recurring_event1 = Event.objects.create(
+        label='recurring 1',
+        start_datetime=event_date,
+        places=10,
+        agenda=agenda,
+        recurrence_days=[1],
+        recurrence_end_date=event_date + datetime.timedelta(days=1),
+    )
+    recurring_event1.create_all_recurrences()
+    recurring_event1_occurence = recurring_event1.recurrences.first()
+    recurring_event2 = Event.objects.create(
+        label='recurring 2',
+        start_datetime=event_date,
+        places=10,
+        agenda=agenda,
+        recurrence_days=[1],
+        recurrence_end_date=event_date + datetime.timedelta(days=1),
+    )
+    recurring_event2.create_all_recurrences()
+    recurring_event2_occurence = recurring_event2.recurrences.first()
+
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:1',
+        user_first_name='Subscription',
+        user_last_name='42',
+        date_start=datetime.date(2022, 2, 1),
+        date_end=datetime.date(2022, 3, 1),
+    )
+    Booking.objects.create(
+        event=event1,
+        user_external_id='user:1',
+        user_first_name='User',
+        user_last_name='42',
+    )
+    Booking.objects.create(
+        event=event2,
+        user_external_id='user:1',
+        user_first_name='User',
+        user_last_name='42',
+        cancellation_datetime=now(),
+    )
+    Booking.objects.create(
+        event=recurring_event1_occurence,
+        user_external_id='user:1',
+        user_first_name='User',
+        user_last_name='42',
+    )
+    Booking.objects.create(
+        event=recurring_event2_occurence,
+        user_external_id='user:1',
+        user_first_name='User',
+        user_last_name='42',
+        cancellation_datetime=now(),
+    )
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-02-01'
+    resp.form['date_end'] = '2022-02-28'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert slots['events'] == [
+        event1,
+        event2,
+        event3,
+        recurring_event1,
+        recurring_event2,
+    ]
+    assert len(slots['users'][0]['users']) == 1
+    assert slots['users'][0]['users'][0]['events'] == [
+        {
+            'event': event1,
+            'dates': {datetime.date(2022, 2, 15): True},
+        },
+        {
+            'event': event2,
+            'dates': {datetime.date(2022, 2, 15): False},
+        },
+        {
+            'event': event3,
+            'dates': {datetime.date(2022, 2, 15): False},
+        },
+        {
+            'event': recurring_event1,
+            'dates': {datetime.date(2022, 2, 15): True},
+        },
+        {
+            'event': recurring_event2,
+            'dates': {datetime.date(2022, 2, 15): False},
+        },
+    ]
+
+
+def test_events_timesheet_extra_data(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='User',
+        user_last_name='42',
+        user_external_id='user:1',
+        extra_data={'foo': 'bar', 'baz': 'blah'},
+    )
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-02-01'
+    resp.form['date_end'] = '2022-02-28'
+    resp.form['extra_data'] = ' foo '
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert resp.text.count('<div class="page_break">') == 0
+
+    assert len(slots['users']) == 1
+    assert slots['users'][0]['grouper'] == ''
+    assert len(slots['users'][0]['users']) == 1
+    assert slots['extra_data'] == ['foo']
+    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'bar'
+
+    resp.form['extra_data'] = ' foo ,baz,,'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users'][0]['users']) == 1
+    assert slots['extra_data'] == ['foo', 'baz']
+    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'bar'
+    assert slots['users'][0]['users'][0]['extra_data']['baz'] == 'blah'
+
+    resp.form['extra_data'] = 'unknown'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users'][0]['users']) == 1
+    assert slots['extra_data'] == ['unknown']
+    assert slots['users'][0]['users'][0]['extra_data']['unknown'] == ''
+
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:1',
+        user_first_name='Subscription',
+        user_last_name='41',
+        date_start=datetime.date(2022, 2, 1),
+        date_end=datetime.date(2022, 3, 1),
+        extra_data={'foo': 'baz'},
+    )
+
+    resp.form['extra_data'] = ' foo '
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users'][0]['users']) == 1
+    assert slots['extra_data'] == ['foo']
+    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'baz'
+
+    resp.form['extra_data'] = ' foo ,baz,,'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users'][0]['users']) == 1
+    assert slots['extra_data'] == ['foo', 'baz']
+    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'baz'
+    assert slots['users'][0]['users'][0]['extra_data']['baz'] == ''
+
+    Booking.objects.create(
+        event=event,
+        user_first_name='User',
+        user_last_name='43',
+        user_external_id='user:2',
+        extra_data={'foo': 'bar', 'baz': 'aa'},
+    )
+    Booking.objects.create(
+        event=event,
+        user_first_name='User',
+        user_last_name='44',
+        user_external_id='user:3',
+        extra_data={'foo': 'bar2', 'baz': 'aa'},
+    )
+
+    resp.form['extra_data'] = ''
+    resp.form['group_by'] = 'foo'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert resp.text.count('<div class="page_break">') == 0
+
+    assert len(slots['users']) == 3
+    assert slots['users'][0]['grouper'] == 'bar'
+    assert len(slots['users'][0]['users']) == 1
+    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
+    assert slots['users'][1]['grouper'] == 'bar2'
+    assert len(slots['users'][1]['users']) == 1
+    assert slots['users'][1]['users'][0]['user_id'] == 'user:3'
+    assert slots['users'][2]['grouper'] == 'baz'
+    assert len(slots['users'][2]['users']) == 1
+    assert slots['users'][2]['users'][0]['user_id'] == 'user:1'
+
+    resp.form['with_page_break'] = True
+    resp = resp.form.submit()
+    assert resp.text.count('<div class="page_break">') == 2
+
+    resp.form['group_by'] = 'baz'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users']) == 2
+    assert slots['users'][0]['grouper'] == 'aa'
+    assert len(slots['users'][0]['users']) == 2
+    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
+    assert slots['users'][0]['users'][1]['user_id'] == 'user:3'
+    assert slots['users'][1]['grouper'] == ''
+    assert len(slots['users'][1]['users']) == 1
+    assert slots['users'][1]['users'][0]['user_id'] == 'user:1'
+
+    Subscription.objects.update(extra_data={'foo': 'baz', 'baz': 'blah'})
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users']) == 2
+    assert slots['users'][0]['grouper'] == 'aa'
+    assert len(slots['users'][0]['users']) == 2
+    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
+    assert slots['users'][0]['users'][1]['user_id'] == 'user:3'
+    assert slots['users'][1]['grouper'] == 'blah'
+    assert len(slots['users'][1]['users']) == 1
+    assert slots['users'][1]['users'][0]['user_id'] == 'user:1'
+
+    resp.form['group_by'] = 'unknown'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users']) == 1
+    assert slots['users'][0]['grouper'] == ''
+    assert len(slots['users'][0]['users']) == 3
+
+
+def test_events_timesheet_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/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-02-01'
+    resp.form['date_end'] = '2022-02-28'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert slots['users'][0]['users'][0]['user_id'] == 'user:3'
+    assert slots['users'][0]['users'][1]['user_id'] == 'user:1'
+    assert slots['users'][0]['users'][2]['user_id'] == 'user:2'
+
+    resp.form['sort'] = 'lastname,firstname'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert slots['users'][0]['users'][0]['user_id'] == 'user:3'
+    assert slots['users'][0]['users'][1]['user_id'] == 'user:1'
+    assert slots['users'][0]['users'][2]['user_id'] == 'user:2'
+
+    resp.form['sort'] = 'firstname,lastname'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
+    assert slots['users'][0]['users'][1]['user_id'] == 'user:1'
+    assert slots['users'][0]['users'][2]['user_id'] == 'user:3'
+
+
+@pytest.mark.freeze_time('2022-04-01')
+def test_events_timesheet_date_display(app, admin_user):
+    agenda = Agenda.objects.create(label='Events', kind='events')
+    recurring_event = Event.objects.create(
+        label='recurring 1',
+        start_datetime=make_aware(datetime.datetime(2022, 1, 1, 12, 0)),
+        places=10,
+        agenda=agenda,
+        recurrence_days=[0],
+        recurrence_end_date=datetime.date(2022, 4, 1),
+    )
+    recurring_event.create_all_recurrences()
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:1',
+        user_first_name='Subscription',
+        user_last_name='42',
+        date_start=datetime.date(2022, 1, 1),
+        date_end=datetime.date(2022, 4, 1),
+        extra_data={'foo': 'bar'},
+    )
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-01-01'
+    resp.form['date_end'] = '2022-03-31'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert slots['dates'] == [
+        [
+            datetime.date(2022, 1, 3),
+            datetime.date(2022, 1, 10),
+            datetime.date(2022, 1, 17),
+            datetime.date(2022, 1, 24),
+            datetime.date(2022, 1, 31),
+            datetime.date(2022, 2, 7),
+            datetime.date(2022, 2, 14),
+            datetime.date(2022, 2, 21),
+            datetime.date(2022, 2, 28),
+            datetime.date(2022, 3, 7),
+            datetime.date(2022, 3, 14),
+            datetime.date(2022, 3, 21),
+            datetime.date(2022, 3, 28),
+        ]
+    ]
+
+    resp.form['date_display'] = 'month'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert slots['dates'] == [
+        [
+            datetime.date(2022, 1, 3),
+            datetime.date(2022, 1, 10),
+            datetime.date(2022, 1, 17),
+            datetime.date(2022, 1, 24),
+            datetime.date(2022, 1, 31),
+        ],
+        [
+            datetime.date(2022, 2, 7),
+            datetime.date(2022, 2, 14),
+            datetime.date(2022, 2, 21),
+            datetime.date(2022, 2, 28),
+        ],
+        [
+            datetime.date(2022, 3, 7),
+            datetime.date(2022, 3, 14),
+            datetime.date(2022, 3, 21),
+            datetime.date(2022, 3, 28),
+        ],
+    ]
+
+    resp.form['date_display'] = 'week'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert resp.text.count('<div class="page_break">') == 12
+
+    assert slots['dates'] == [
+        [datetime.date(2022, 1, 3)],
+        [datetime.date(2022, 1, 10)],
+        [datetime.date(2022, 1, 17)],
+        [datetime.date(2022, 1, 24)],
+        [datetime.date(2022, 1, 31)],
+        [datetime.date(2022, 2, 7)],
+        [datetime.date(2022, 2, 14)],
+        [datetime.date(2022, 2, 21)],
+        [datetime.date(2022, 2, 28)],
+        [datetime.date(2022, 3, 7)],
+        [datetime.date(2022, 3, 14)],
+        [datetime.date(2022, 3, 21)],
+        [datetime.date(2022, 3, 28)],
+    ]
+
+    resp.form['date_display'] = 'custom'
+    resp = resp.form.submit()
+    assert resp.context['form'].errors['custom_nb_dates_per_page'] == ['This field is required.']
+
+    resp.form['custom_nb_dates_per_page'] = 10
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert slots['dates'] == [
+        [
+            datetime.date(2022, 1, 3),
+            datetime.date(2022, 1, 10),
+            datetime.date(2022, 1, 17),
+            datetime.date(2022, 1, 24),
+            datetime.date(2022, 1, 31),
+            datetime.date(2022, 2, 7),
+            datetime.date(2022, 2, 14),
+            datetime.date(2022, 2, 21),
+            datetime.date(2022, 2, 28),
+            datetime.date(2022, 3, 7),
+        ],
+        [
+            datetime.date(2022, 3, 14),
+            datetime.date(2022, 3, 21),
+            datetime.date(2022, 3, 28),
+        ],
+    ]
+
+    resp.form['custom_nb_dates_per_page'] = 3
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert slots['dates'] == [
+        [
+            datetime.date(2022, 1, 3),
+            datetime.date(2022, 1, 10),
+            datetime.date(2022, 1, 17),
+        ],
+        [
+            datetime.date(2022, 1, 24),
+            datetime.date(2022, 1, 31),
+            datetime.date(2022, 2, 7),
+        ],
+        [
+            datetime.date(2022, 2, 14),
+            datetime.date(2022, 2, 21),
+            datetime.date(2022, 2, 28),
+        ],
+        [
+            datetime.date(2022, 3, 7),
+            datetime.date(2022, 3, 14),
+            datetime.date(2022, 3, 21),
+        ],
+        [
+            datetime.date(2022, 3, 28),
+        ],
+    ]
+
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:2',
+        user_first_name='Subscription',
+        user_last_name='43',
+        date_start=datetime.date(2022, 1, 1),
+        date_end=datetime.date(2022, 4, 1),
+        extra_data={'foo': 'baz'},
+    )
+
+    resp.form['date_display'] = 'week'
+    resp.form['group_by'] = 'foo'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+    assert resp.text.count('<div class="page_break">') == 12
+
+    resp.form['with_page_break'] = True
+    resp = resp.form.submit()
+    assert resp.text.count('<div class="page_break">') == 25
+
+
+def test_events_timesheet_pdf(app, admin_user):
+    agenda = Agenda.objects.create(label='Events', kind='events')
+
+    login(app)
+    resp = app.get(
+        '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&sort=lastname,firstname&date_display=all&orientation=portrait'
+        % agenda.pk
+    )
+    assert resp.headers['Content-Type'] == 'application/pdf'
+    assert (
+        resp.headers['Content-Disposition']
+        == 'attachment; filename="timesheet_events_2022-02-01_2022-02-28.pdf"'
+    )
+
+    # form invalid
+    resp = app.get(
+        '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&sort=lastname,firstname&date_display=all'
+        % agenda.pk
+    )
+    assert resp.context['form'].errors['orientation'] == ['This field is required.']
-- 
2.35.1

