Projet

Général

Profil

0002-manager-create-event-recurrences-when-end-date-is-sp.patch

Valentin Deniaud, 17 février 2021 16:34

Télécharger (11,7 ko)

Voir les différences:

Subject: [PATCH 2/2] manager: create event recurrences when end date is
 specified (#51218)

 chrono/agendas/models.py    | 34 +++++++++++++++++++---------
 chrono/manager/forms.py     | 21 ++++++++++++++---
 tests/test_agendas.py       |  8 +++----
 tests/test_import_export.py | 15 +++++++++++++
 tests/test_manager.py       | 45 +++++++++++++++++++++++++++++++++++++
 5 files changed, 105 insertions(+), 18 deletions(-)
chrono/agendas/models.py
590 590
        else:
591 591
            recurring_events = self.event_set.filter(recurrence_rule__isnull=False)
592 592
        for event in recurring_events:
593
            events.extend(event.get_recurrences(min_start, max_start, excluded_datetimes))
593
            events.extend(event.get_recurrences(min_start, max_start, excluded_datetimes, slug_separator=':'))
594 594

  
595 595
        events.sort(key=lambda x: [getattr(x, field) for field in Event._meta.ordering])
596 596
        return events
......
1196 1196
        )
1197 1197
        data = clean_import_data(cls, data)
1198 1198
        if data.get('slug'):
1199
            cls.objects.update_or_create(slug=data['slug'], defaults=data)
1200
            return
1201
        event = cls(**data)
1202
        event.save()
1199
            event, _ = cls.objects.update_or_create(slug=data['slug'], defaults=data)
1200
        else:
1201
            event = cls(**data)
1202
            event.save()
1203
        if event.recurrence_rule and event.recurrence_end_date:
1204
            event.refresh_from_db()
1205
            event.create_all_recurrences()
1203 1206

  
1204 1207
    def export_json(self):
1205 1208
        recurrence_end_date = (
......
1253 1256
            raise ValueError('Multiple events found for specified datetime.')
1254 1257

  
1255 1258
        event = events[0]
1256
        event.slug = event.slug.replace(':', '--')
1257

  
1258 1259
        with transaction.atomic():
1259 1260
            try:
1260 1261
                return Event.objects.get(agenda=self.agenda, slug=event.slug)
......
1262 1263
                event.save()
1263 1264
                return event
1264 1265

  
1265
    def get_recurrences(self, min_datetime, max_datetime, excluded_datetimes=None):
1266
    def get_recurrences(self, min_datetime, max_datetime, excluded_datetimes=None, slug_separator='--'):
1266 1267
        recurrences = []
1267 1268
        rrule_set = rruleset()
1268 1269
        # do not generate recurrences for existing events
......
1298 1299
            event = copy.copy(event_base)
1299 1300
            # add timezone back
1300 1301
            aware_start_datetime = make_aware(start_datetime)
1301
            event.slug = '%s:%s' % (event.slug, aware_start_datetime.strftime('%Y-%m-%d-%H%M'))
1302
            event.slug = '%s%s%s' % (
1303
                event.slug,
1304
                slug_separator,
1305
                aware_start_datetime.strftime('%Y-%m-%d-%H%M'),
1306
            )
1302 1307
            event.start_datetime = aware_start_datetime.astimezone(utc)
1303 1308
            recurrences.append(event)
1304 1309

  
......
1335 1340
            return None
1336 1341
        return rrule
1337 1342

  
1338
    def has_recurrences_booked(self):
1343
    def has_recurrences_booked(self, after=None):
1339 1344
        return Booking.objects.filter(
1340
            event__primary_event=self, event__start_datetime__gt=now(), cancellation_datetime__isnull=True
1345
            event__primary_event=self,
1346
            event__start_datetime__gt=after or now(),
1347
            cancellation_datetime__isnull=True,
1341 1348
        ).exists()
1342 1349

  
1350
    def create_all_recurrences(self, excluded_datetimes=None):
1351
        max_datetime = datetime.datetime.combine(self.recurrence_end_date, datetime.time(0, 0))
1352
        recurrences = self.get_recurrences(localtime(now()), make_aware(max_datetime), excluded_datetimes)
1353
        Event.objects.bulk_create(recurrences)
1354

  
1343 1355

  
1344 1356
class BookingColor(models.Model):
1345 1357
    COLOR_COUNT = 8
chrono/manager/forms.py
27 27
from django.forms import ValidationError
28 28
from django.utils.encoding import force_text
29 29
from django.utils.six import StringIO
30
from django.utils.timezone import make_aware
31
from django.utils.timezone import now
30
from django.utils.timezone import now, localtime, make_aware, make_naive
32 31
from django.utils.translation import ugettext_lazy as _
33 32

  
34 33
from chrono.agendas.models import (
......
213 212
                    'This field cannot be modified because some recurrences have bookings attached to them.'
214 213
                )
215 214

  
215
    def clean(self):
216
        super().clean()
217
        if 'recurrence_end_date' in self.changed_data and self.instance.has_recurrences_booked(
218
            after=self.cleaned_data['recurrence_end_date']
219
        ):
220
            raise ValidationError(_('Bookings exist after this date.'))
221

  
216 222
    def save(self, *args, **kwargs):
217 223
        with transaction.atomic():
218 224
            if any(field for field in self.changed_data if field in self.protected_fields):
......
224 230
                    if field not in self.protected_fields
225 231
                }
226 232
                self.instance.recurrences.update(**update_fields)
227
            return super().save(*args, **kwargs)
233

  
234
            event = super().save(*args, **kwargs)
235
            if event.recurrence_end_date:
236
                self.instance.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete()
237
                excluded_datetimes = [
238
                    make_naive(dt)
239
                    for dt in self.instance.recurrences.values_list('start_datetime', flat=True)
240
                ]
241
                event.create_all_recurrences(excluded_datetimes)
242
        return event
228 243

  
229 244

  
230 245
class AgendaResourceForm(forms.Form):
tests/test_agendas.py
1867 1867
    assert len(recurrences) == 3
1868 1868

  
1869 1869
    first_event = recurrences[0]
1870
    assert first_event.slug == event.slug + ':2021-01-06-1300'
1870
    assert first_event.slug == event.slug + '--2021-01-06-1300'
1871 1871

  
1872 1872
    event_json = event.export_json()
1873 1873
    first_event_json = first_event.export_json()
......
1877 1877
    second_event = recurrences[1]
1878 1878
    assert second_event.start_datetime == first_event.start_datetime + datetime.timedelta(days=7)
1879 1879
    assert second_event.start_datetime.weekday() == first_event.start_datetime.weekday()
1880
    assert second_event.slug == 'event:2021-01-13-1300'
1880
    assert second_event.slug == 'event--2021-01-13-1300'
1881 1881

  
1882 1882
    different_fields = ['slug', 'start_datetime']
1883 1883
    second_event_json = second_event.export_json()
......
1901 1901
    recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8))
1902 1902
    event_before_dst, event_after_dst = recurrences
1903 1903
    assert event_before_dst.start_datetime.hour + 1 == event_after_dst.start_datetime.hour
1904
    assert event_before_dst.slug == 'agenda-event:2020-10-24-1400'
1905
    assert event_after_dst.slug == 'agenda-event:2020-10-31-1400'
1904
    assert event_before_dst.slug == 'agenda-event--2020-10-24-1400'
1905
    assert event_after_dst.slug == 'agenda-event--2020-10-31-1400'
1906 1906

  
1907 1907
    freezer.move_to('2020-11-24 12:00')
1908 1908
    new_recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8))
tests/test_import_export.py
200 200
        start_datetime=now(),
201 201
        repeat='daily',
202 202
        places=10,
203
        slug='test',
203 204
    )
204 205
    event.refresh_from_db()
205 206
    event.get_or_create_event_recurrence(event.start_datetime + datetime.timedelta(days=3))
......
221 222
    assert event.repeat == 'daily'
222 223
    assert event.recurrence_rule == {'freq': DAILY}
223 224

  
225
    # importing event with end recurrence date creates recurrences
226
    event.recurrence_end_date = now() + datetime.timedelta(days=7)
227
    event.save()
228
    output = get_output_of_command('export_site')
229
    import_site(data={}, clean=True)
230

  
231
    with tempfile.NamedTemporaryFile() as f:
232
        f.write(force_bytes(output))
233
        f.flush()
234
        call_command('import_site', f.name)
235

  
236
    event = Event.objects.get(slug='test')
237
    assert Event.objects.filter(primary_event=event).count() == 7
238

  
224 239

  
225 240
def test_import_export_permissions(app):
226 241
    meetings_agenda = Agenda.objects.create(label='Foo Bar', kind='meetings')
tests/test_manager.py
1522 1522
    assert 'Delete' not in resp.text
1523 1523

  
1524 1524

  
1525
def test_edit_recurring_event_with_end_date(settings, app, admin_user, freezer):
1526
    freezer.move_to('2021-01-12 12:10')
1527
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1528
    event = Event.objects.create(start_datetime=now(), places=10, repeat='daily', agenda=agenda)
1529

  
1530
    app = login(app)
1531
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
1532
    resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=5)).strftime('%Y-%m-%d')
1533
    resp = resp.form.submit()
1534

  
1535
    # recurrences are created automatically
1536
    event = Event.objects.get(recurrence_rule__isnull=False)
1537
    assert Event.objects.filter(primary_event=event).count() == 5
1538
    assert Event.objects.filter(primary_event=event, start_datetime=now()).exists()
1539

  
1540
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
1541
    resp.form['start_datetime_1'] = (localtime() + datetime.timedelta(hours=1)).strftime('%H:%M')
1542
    resp = resp.form.submit()
1543
    assert Event.objects.filter(primary_event=event).count() == 5
1544
    assert Event.objects.filter(
1545
        primary_event=event, start_datetime=now() + datetime.timedelta(hours=1)
1546
    ).exists()
1547
    # old recurrences were deleted
1548
    assert not Event.objects.filter(primary_event=event, start_datetime=now()).exists()
1549

  
1550
    # editing recurrence_end_date is permitted as long as bookings are not impacted
1551
    event_recurrence = Event.objects.get(primary_event=event, start_datetime__day=15)
1552
    Booking.objects.create(event=event_recurrence)
1553
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
1554
    resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=6)).strftime('%Y-%m-%d')
1555
    resp = resp.form.submit()
1556
    assert Event.objects.filter(primary_event=event).count() == 6
1557

  
1558
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
1559
    resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=4)).strftime('%Y-%m-%d')
1560
    resp = resp.form.submit()
1561
    assert Event.objects.filter(primary_event=event).count() == 4
1562

  
1563
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
1564
    resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=2)).strftime('%Y-%m-%d')
1565
    resp = resp.form.submit()
1566
    assert Event.objects.filter(primary_event=event).count() == 4
1567
    assert 'Bookings exist after this date' in resp.text
1568

  
1569

  
1525 1570
def test_booked_places(app, admin_user):
1526 1571
    agenda = Agenda(label=u'Foo bar')
1527 1572
    agenda.save()
1528
-