Projet

Général

Profil

0002-agendas-allow-exceptions-to-recurring-events-50561.patch

Valentin Deniaud, 28 janvier 2021 17:21

Télécharger (14,3 ko)

Voir les différences:

Subject: [PATCH 2/2] agendas: allow exceptions to recurring events (#50561)

 .../migrations/0076_auto_20210127_1746.py     | 26 +++++++
 chrono/agendas/models.py                      | 31 +++++++-
 .../manager_events_agenda_settings.html       | 23 ++++++
 chrono/manager/views.py                       | 13 ++++
 tests/test_agendas.py                         | 77 +++++++++++++++++++
 tests/test_manager.py                         | 61 +++++++++++++++
 6 files changed, 229 insertions(+), 2 deletions(-)
 create mode 100644 chrono/agendas/migrations/0076_auto_20210127_1746.py
chrono/agendas/migrations/0076_auto_20210127_1746.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2021-01-27 16:46
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations
6

  
7

  
8
def create_exceptions_desk(apps, schema_editor):
9
    Agenda = apps.get_model('agendas', 'Agenda')
10
    Desk = apps.get_model('agendas', 'Desk')
11

  
12
    desks = []
13
    for agenda in Agenda.objects.filter(kind='events'):
14
        desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
15
        desk.import_timeperiod_exceptions_from_settings()
16

  
17

  
18
class Migration(migrations.Migration):
19

  
20
    dependencies = [
21
        ('agendas', '0075_event_recurrence_end_date'),
22
    ]
23

  
24
    operations = [
25
        migrations.RunPython(create_exceptions_desk, migrations.RunPython.noop),
26
    ]
chrono/agendas/models.py
581 581
            recurring_events = self.prefetched_recurring_events
582 582
        else:
583 583
            recurring_events = self.event_set.filter(recurrence_rule__isnull=False)
584

  
585
        exceptions = self.get_recurrence_exceptions(min_start, max_start)
584 586
        for event in recurring_events:
585
            events.extend(event.get_recurrences(min_start, max_start, excluded_datetimes))
587
            events.extend(event.get_recurrences(min_start, max_start, excluded_datetimes, exceptions))
586 588

  
587 589
        events.sort(key=lambda x: [getattr(x, field) for field in Event._meta.ordering])
588 590
        return events
......
596 598
        except (VariableDoesNotExist, TemplateSyntaxError):
597 599
            return
598 600

  
601
    def get_recurrence_exceptions(self, min_start, max_start):
602
        desk, _ = Desk.objects.get_or_create(agenda=self, slug='_exceptions_holder')
603
        return TimePeriodException.objects.filter(
604
            Q(desk=desk) | Q(unavailability_calendar__desks=desk),
605
            start_datetime__lt=max_start,
606
            end_datetime__gt=min_start,
607
        )
608

  
599 609
    def prefetch_desks_and_exceptions(self):
600 610
        if self.kind == 'meetings':
601 611
            desks = self.desk_set.all()
......
1155 1165
                event.save()
1156 1166
                return event
1157 1167

  
1158
    def get_recurrences(self, min_datetime, max_datetime, excluded_datetimes=None):
1168
    def get_recurrences(self, min_datetime, max_datetime, excluded_datetimes=None, exceptions=None):
1159 1169
        recurrences = []
1160 1170
        rrule_set = rruleset()
1161 1171
        # do not generate recurrences for existing events
1162 1172
        rrule_set._exdate = excluded_datetimes or []
1163 1173

  
1174
        exceptions = exceptions or self.agenda.get_recurrence_exceptions(min_datetime, max_datetime)
1175
        for exception in exceptions:
1176
            dtstart = localtime(exception.start_datetime)
1177
            start_datetime = localtime(self.start_datetime)
1178
            if start_datetime.time() < dtstart.time():
1179
                dtstart += datetime.timedelta(days=1)
1180
            dtstart = dtstart.replace(
1181
                hour=start_datetime.hour, minute=start_datetime.minute, second=0, microsecond=0
1182
            )
1183
            rrule_set.exrule(
1184
                rrule(
1185
                    freq=DAILY,
1186
                    dtstart=make_naive(dtstart),
1187
                    until=make_naive(exception.end_datetime),
1188
                )
1189
            )
1190

  
1164 1191
        event_base = Event(
1165 1192
            agenda=self.agenda,
1166 1193
            primary_event=self,
chrono/manager/templates/chrono/manager_events_agenda_settings.html
49 49
</div>
50 50
</div>
51 51

  
52
{% if has_recurring_events %}
53
<div class="section">
54
<h3>{% trans "Recurrence exceptions" %}
55
<a rel="popup" class="button" href="{% url 'chrono-manager-desk-add-import-time-period-exceptions' pk=desk.pk %}">{% trans 'Configure' %}</a>
56
</h3>
57
<div>
58
<ul class="objects-list single-links">
59
{% for exception in exceptions|slice:":5" %}
60
   <li><a rel="popup" {% if not exception.read_only %}href="{% url 'chrono-manager-time-period-exception-edit' pk=exception.pk %}"{% endif %}>
61
  {{ exception }}
62
  {% if not exception.read_only %}
63
  <a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-exception-delete' pk=exception.id %}">{% trans "remove" %}</a>
64
  {% endif %}
65
{% endfor %}
66
{% if exceptions|length > 5 %}
67
<li><a class="timeperiod-exception-all desk-{{ desk.pk }}" rel="popup" data-selector="div.timeperiod" href="{% url 'chrono-manager-time-period-exception-extract-list' pk=desk.id %}">({% trans 'see all exceptions' %})</a></li>
68
{% endif %}
69
<li><a class="add" rel="popup" href="{% url 'chrono-manager-agenda-add-time-period-exception' agenda_pk=object.pk pk=desk.pk %}">{% trans 'Add a time period exception' %}</a></li>
70
</ul>
71
</div>
72
</div>
73
{% endif %}
74

  
52 75
{% endblock %}
chrono/manager/views.py
623 623
            default_desk = Desk(agenda=self.object, label=_('Desk 1'))
624 624
            default_desk.save()
625 625
            default_desk.import_timeperiod_exceptions_from_settings(enable=True)
626
        elif self.object.kind == 'events':
627
            desk = Desk.objects.create(agenda=self.object, slug='_exceptions_holder')
628
            desk.import_timeperiod_exceptions_from_settings()
626 629
        return model_form
627 630

  
628 631
    def get_success_url(self):
......
1395 1398
        if self.agenda.kind == 'meetings':
1396 1399
            context['has_resources'] = Resource.objects.exists()
1397 1400
            context['has_unavailability_calendars'] = UnavailabilityCalendar.objects.exists()
1401
        if self.agenda.kind == 'events':
1402
            context['has_recurring_events'] = self.agenda.event_set.filter(
1403
                recurrence_rule__isnull=False
1404
            ).exists()
1405
            desk, _ = Desk.objects.get_or_create(agenda=self.agenda, slug='_exceptions_holder')
1406
            context['exceptions'] = TimePeriodException.objects.filter(
1407
                Q(desk=desk) | Q(unavailability_calendar__desks=desk),
1408
                end_datetime__gt=now(),
1409
            ).select_related('source')
1410
            context['desk'] = desk
1398 1411
        return context
1399 1412

  
1400 1413
    def get_events(self):
tests/test_agendas.py
1793 1793
    assert len(recurrences) == 5
1794 1794
    assert recurrences[0].start_datetime == start_datetime
1795 1795
    assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=4)
1796

  
1797

  
1798
@override_settings(
1799
    EXCEPTIONS_SOURCES={
1800
        'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
1801
    }
1802
)
1803
def test_recurring_events_exceptions(freezer):
1804
    freezer.move_to('2021-05-01 12:00')
1805
    agenda = Agenda.objects.create(label='Agenda', kind='events')
1806
    desk = Desk.objects.create(slug='_exceptions_holder', agenda=agenda)
1807
    event = Event.objects.create(
1808
        agenda=agenda,
1809
        start_datetime=now(),
1810
        repeat='daily',
1811
        places=5,
1812
    )
1813
    event.refresh_from_db()
1814
    start_datetime = localtime(event.start_datetime)
1815

  
1816
    recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
1817
    assert recurrences[0].start_datetime.strftime('%m-%d') == '05-01'
1818
    first_of_may = recurrences[0]
1819

  
1820
    recurrence = event.get_or_create_event_recurrence(first_of_may.start_datetime)
1821
    recurrence.delete()
1822

  
1823
    desk.import_timeperiod_exceptions_from_settings(enable=True)
1824
    recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
1825
    # 05-01 is a holiday
1826
    assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
1827
    with pytest.raises(ValueError):
1828
        recurrence = event.get_or_create_event_recurrence(first_of_may.start_datetime)
1829
    first_event = recurrences[0]
1830

  
1831
    # exception before first_event start_datetime
1832
    time_period_exception = TimePeriodException.objects.create(
1833
        desk=desk,
1834
        start_datetime=first_event.start_datetime - datetime.timedelta(hours=1),
1835
        end_datetime=first_event.start_datetime - datetime.timedelta(minutes=30),
1836
    )
1837
    recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
1838
    assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
1839

  
1840
    # exception wraps around first_event start_datetime
1841
    time_period_exception.end_datetime = first_event.start_datetime + datetime.timedelta(minutes=30)
1842
    time_period_exception.save()
1843
    recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
1844
    assert recurrences[0].start_datetime.strftime('%m-%d') == '05-03'
1845

  
1846
    # exception starts after first_event start_datetime
1847
    time_period_exception.start_datetime = first_event.start_datetime + datetime.timedelta(minutes=15)
1848
    time_period_exception.save()
1849
    recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
1850
    assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
1851
    assert recurrences[1].start_datetime.strftime('%m-%d') == '05-03'
1852

  
1853
    # exception spans multiple days
1854
    time_period_exception.end_datetime = first_event.start_datetime + datetime.timedelta(days=3)
1855
    time_period_exception.save()
1856
    recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
1857
    assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
1858
    assert recurrences[1].start_datetime.strftime('%m-%d') == '05-06'
1859

  
1860
    # move exception to unavailability calendar
1861
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
1862
    time_period_exception.desk = None
1863
    time_period_exception.unavailability_calendar = unavailability_calendar
1864
    time_period_exception.save()
1865
    recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
1866
    assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
1867
    assert recurrences[1].start_datetime.strftime('%m-%d') == '05-03'
1868

  
1869
    unavailability_calendar.desks.add(desk)
1870
    recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
1871
    assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
1872
    assert recurrences[1].start_datetime.strftime('%m-%d') == '05-06'
tests/test_manager.py
5606 5606
    assert resp.text.count('Swimming') == 2  # 1 booking + legend
5607 5607
    assert 'Booking colors:' in resp.text
5608 5608
    assert len(resp.pyquery.find('div.booking-colors span.booking-color-label')) == 2
5609

  
5610

  
5611
@override_settings(
5612
    EXCEPTIONS_SOURCES={
5613
        'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
5614
    }
5615
)
5616
def test_recurring_events_manage_exceptions(settings, app, admin_user, freezer):
5617
    freezer.move_to('2021-07-01 12:10')
5618

  
5619
    app = login(app)
5620
    resp = app.get('/manage/')
5621
    resp = resp.click('New')
5622
    resp.form['label'] = 'Foo bar'
5623
    resp.form['kind'] = 'events'
5624
    resp = resp.form.submit().follow()
5625

  
5626
    agenda = Agenda.objects.get(label='Foo bar')
5627
    assert agenda.desk_set.count() == 1
5628
    desk = agenda.desk_set.get(slug='_exceptions_holder')
5629

  
5630
    event = Event.objects.create(start_datetime=now(), places=10, agenda=agenda)
5631
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
5632
    assert not 'Recurrence exceptions' in resp.text
5633

  
5634
    event.repeat = 'daily'
5635
    event.save()
5636

  
5637
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2021, 7))
5638
    assert len(resp.pyquery.find('.event-info')) == 31
5639

  
5640
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
5641
    assert 'Recurrence exceptions' in resp.text
5642

  
5643
    resp = resp.click('Add a time period exception')
5644
    resp.form['start_datetime_0'] = now().strftime('%Y-%m-%d')
5645
    resp.form['start_datetime_1'] = now().strftime('%H:%M')
5646
    resp.form['end_datetime_0'] = (now() + datetime.timedelta(days=7)).strftime('%Y-%m-%d')
5647
    resp.form['end_datetime_1'] = (now() + datetime.timedelta(days=7)).strftime('%H:%M')
5648
    resp = resp.form.submit().follow()
5649
    assert desk.timeperiodexception_set.count() == 1
5650

  
5651
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2021, 7))
5652
    assert len(resp.pyquery.find('.event-info')) == 24
5653

  
5654
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
5655
    resp = resp.click('Configure', href='exceptions')
5656
    resp = resp.click('enable').follow()
5657
    assert TimePeriodException.objects.count() > 1
5658
    assert 'Bastille Day' in resp.text
5659

  
5660
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2021, 7))
5661
    assert len(resp.pyquery.find('.event-info')) == 23
5662

  
5663
    # add recurrence end date, which lead to recurrences creation
5664
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
5665
    resp.form['recurrence_end_date'] = (now() + datetime.timedelta(days=31)).strftime('%Y-%m-%d')
5666
    resp = resp.form.submit()
5667

  
5668
    # recurrences corresponding to exceptions have not been created
5669
    assert Event.objects.count() == 24
5609
-