Projet

Général

Profil

0001-manager-import-export-events-update-event-if-slug-ex.patch

Lauréline Guérin, 01 octobre 2020 10:14

Télécharger (20,9 ko)

Voir les différences:

Subject: [PATCH] manager: import/export events - update event if slug exists
 (#42343)

 chrono/manager/forms.py                       |  69 +++++--
 .../chrono/manager_agenda_settings.html       |   5 +-
 chrono/manager/urls.py                        |   5 +
 chrono/manager/views.py                       |  68 ++++++-
 tests/test_manager.py                         | 174 ++++++++++++++++--
 5 files changed, 286 insertions(+), 35 deletions(-)
chrono/manager/forms.py
27 27
from django.utils.encoding import force_text
28 28
from django.utils.six import StringIO
29 29
from django.utils.timezone import make_aware
30
from django.utils.timezone import now
30 31
from django.utils.translation import ugettext_lazy as _
31 32

  
32 33
from chrono.agendas.models import (
......
44 45
    AgendaNotificationsSettings,
45 46
    AgendaReminderSettings,
46 47
    WEEKDAYS_LIST,
48
    generate_slug,
47 49
)
48 50

  
49 51
from . import widgets
......
365 367
            dialect = None
366 368

  
367 369
        events = []
368
        slugs = set()
370
        warnings = {}
371
        events_by_slug = {e.slug: e for e in Event.objects.filter(agenda=self.agenda_pk)}
372
        event_ids_with_bookings = set(
373
            Booking.objects.filter(
374
                event__agenda=self.agenda_pk, cancellation_datetime__isnull=True
375
            ).values_list('event_id', flat=True)
376
        )
377
        seen_slugs = set(events_by_slug.keys())
369 378
        for i, csvline in enumerate(csv.reader(StringIO(content), dialect=dialect)):
370 379
            if not csvline:
371 380
                continue
......
373 382
                raise ValidationError(_('Invalid file format. (line %d)') % (i + 1))
374 383
            if i == 0 and csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')):
375 384
                continue
376
            event = Event()
377
            event.agenda_id = self.agenda_pk
385

  
386
            # label needed to generate a slug
387
            label = None
388
            if len(csvline) >= 5:
389
                label = force_text(csvline[4])
390

  
391
            # get or create event
392
            event = None
393
            slug = None
394
            if len(csvline) >= 6:
395
                slug = force_text(csvline[5]) if csvline[5] else None
396
                # get existing event if relevant
397
                if slug and slug in seen_slugs:
398
                    event = events_by_slug[slug]
399
                    # update label
400
                    event.label = label
401
            if event is None:
402
                # new event
403
                event = Event(agenda_id=self.agenda_pk, label=label)
404
                # generate a slug if not provided
405
                event.slug = slug or generate_slug(event, seen_slugs=seen_slugs, agenda=self.agenda_pk)
406
                # maintain caches
407
                seen_slugs.add(event.slug)
408
                events_by_slug[event.slug] = event
409

  
378 410
            for datetime_fmt in (
379 411
                '%Y-%m-%d %H:%M',
380 412
                '%d/%m/%Y %H:%M',
......
383 415
                '%d/%m/%Y %H:%M:%S',
384 416
            ):
385 417
                try:
386
                    event_datetime = datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt)
418
                    event_datetime = make_aware(
419
                        datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt)
420
                    )
387 421
                except ValueError:
388 422
                    continue
389
                event.start_datetime = make_aware(event_datetime)
423
                if (
424
                    event.pk is not None
425
                    and event.start_datetime != event_datetime
426
                    and event.start_datetime > now()
427
                    and event.pk in event_ids_with_bookings
428
                    and event.pk not in warnings
429
                ):
430
                    # event start datetime has changed, event is not past and has not cancelled bookings
431
                    # => warn the user
432
                    warnings[event.pk] = event
433
                event.start_datetime = event_datetime
390 434
                break
391 435
            else:
392 436
                raise ValidationError(_('Invalid file format. (date/time format, line %d)') % (i + 1))
......
401 445
                    raise ValidationError(
402 446
                        _('Invalid file format. (number of places in waiting list, line %d)') % (i + 1)
403 447
                    )
404
            if len(csvline) >= 5:
405
                event.label = force_text(csvline[4])
406
            exclude = ['desk', 'meeting_type']
407
            if len(csvline) >= 6:
408
                event.slug = force_text(csvline[5]) if csvline[5] else None
409
                if event.slug and event.slug in slugs:
410
                    raise ValidationError(_('File contains duplicated event identifiers: %s') % event.slug)
411
                else:
412
                    slugs.add(event.slug)
413
            else:
414
                exclude += ['slug']
448

  
415 449
            column_index = 7
416 450
            for more_attr in ('description', 'pricing', 'url'):
417 451
                if len(csvline) >= column_index:
......
435 469
                    raise ValidationError(_('Invalid file format. (duration, line %d)') % (i + 1))
436 470

  
437 471
            try:
438
                event.full_clean(exclude=exclude)
472
                event.full_clean(exclude=['desk', 'meeting_type'])
439 473
            except ValidationError as e:
440 474
                errors = [_('Invalid file format:\n')]
441 475
                for label, field_errors in e.message_dict.items():
......
449 483
                raise ValidationError(errors)
450 484
            events.append(event)
451 485
        self.events = events
486
        self.warnings = warnings
452 487

  
453 488
    @staticmethod
454 489
    def get_verbose_name(field_name):
chrono/manager/templates/chrono/manager_agenda_settings.html
21 21
  <ul class="extra-actions-menu">
22 22
    <li><a rel="popup" href="{% url 'chrono-manager-agenda-edit' pk=object.id %}">{% trans 'Options' %}</a></li>
23 23
    <li><a rel="popup" class="action-duplicate" href="{% url 'chrono-manager-agenda-duplicate' pk=object.pk %}">{% trans 'Duplicate' %}</a></li>
24
    <li><a download href="{% url 'chrono-manager-agenda-export' pk=object.id %}">{% trans 'Export' %}</a></li>
24
    <li><a download href="{% url 'chrono-manager-agenda-export' pk=object.id %}">{% trans 'Export Configuration (JSON)' %}</a></li>
25
    {% if object.kind == 'events' %}
26
      <li><a download href="{% url 'chrono-manager-agenda-export-events' pk=object.pk %}">{% trans 'Export Events (CSV)' %}</a></li>
27
    {% endif %}
25 28
    {% if user.is_staff %}
26 29
      <li><a rel="popup" href="{% url 'chrono-manager-agenda-delete' pk=object.id %}">{% trans 'Delete' %}</a></li>
27 30
    {% endif %}
chrono/manager/urls.py
73 73
        views.agenda_import_events,
74 74
        name='chrono-manager-agenda-import-events',
75 75
    ),
76
    url(
77
        r'^agendas/(?P<pk>\d+)/export-events$',
78
        views.agenda_export_events,
79
        name='chrono-manager-agenda-export-events',
80
    ),
76 81
    url(
77 82
        r'^agendas/(?P<pk>\d+)/notifications$',
78 83
        views.agenda_notifications_settings,
chrono/manager/views.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import csv
17 18
import datetime
18 19
import itertools
19 20
import json
......
1330 1331
    template_name = 'chrono/manager_import_events.html'
1331 1332
    agenda = None
1332 1333

  
1334
    def set_agenda(self, **kwargs):
1335
        self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
1336

  
1333 1337
    def get_form_kwargs(self):
1334 1338
        kwargs = super(AgendaImportEventsView, self).get_form_kwargs()
1335 1339
        kwargs['agenda_pk'] = self.kwargs['pk']
......
1338 1342
    def form_valid(self, form):
1339 1343
        if form.events:
1340 1344
            # existing event slugs for this agenda
1341
            seen_slugs = set(self.agenda.event_set.values_list('slug', flat=True))
1342 1345
            for event in form.events:
1343
                event.agenda_id = self.kwargs['pk']
1344
                event.save(seen_slugs=seen_slugs)  # optimization: seen_slugs
1346
                event.agenda = self.agenda
1347
                event.save()
1345 1348
            messages.info(self.request, _('%d events have been imported.') % len(form.events))
1349
            for event in form.warnings.values():
1350
                messages.warning(
1351
                    self.request,
1352
                    _('Event "%s" start date has changed. Do not forget to notify the registrants.')
1353
                    % (event.label or event.slug),
1354
                )
1346 1355
        return super(AgendaImportEventsView, self).form_valid(form)
1347 1356

  
1348 1357

  
1349 1358
agenda_import_events = AgendaImportEventsView.as_view()
1350 1359

  
1351 1360

  
1361
class AgendaExportEventsView(ManagedAgendaMixin, View):
1362
    template_name = 'chrono/manager_export_events.html'
1363
    agenda = None
1364

  
1365
    def set_agenda(self, **kwargs):
1366
        self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
1367

  
1368
    def get(self, request, *args, **kwargs):
1369
        response = HttpResponse(content_type='text/csv')
1370
        today = datetime.date.today()
1371
        response['Content-Disposition'] = 'attachment; filename="export_agenda_events_{}_{}.csv"'.format(
1372
            self.agenda.slug, today.strftime('%Y%m%d')
1373
        )
1374
        writer = csv.writer(response)
1375
        # headers
1376
        writer.writerow(
1377
            [
1378
                _('date'),
1379
                _('time'),
1380
                _('number of places'),
1381
                _('number of places in waiting list'),
1382
                _('label'),
1383
                _('identifier'),
1384
                _('description'),
1385
                _('pricing'),
1386
                _('URL'),
1387
                _('publication date'),
1388
                _('duration'),
1389
            ]
1390
        )
1391
        for event in self.agenda.event_set.all():
1392
            start_datetime = localtime(event.start_datetime)
1393
            writer.writerow(
1394
                [
1395
                    start_datetime.strftime('%Y-%m-%d'),
1396
                    start_datetime.strftime('%H:%M'),
1397
                    event.places,
1398
                    event.waiting_list_places,
1399
                    event.label,
1400
                    event.slug,
1401
                    event.description,
1402
                    event.pricing,
1403
                    event.url,
1404
                    event.publication_date.strftime('%Y-%m-%d') if event.publication_date else '',
1405
                    event.duration,
1406
                ]
1407
            )
1408
        return response
1409

  
1410

  
1411
agenda_export_events = AgendaExportEventsView.as_view()
1412

  
1413

  
1352 1414
class AgendaNotificationsSettingsView(ManagedAgendaMixin, UpdateView):
1353 1415
    template_name = 'chrono/manager_agenda_notifications_form.html'
1354 1416
    model = AgendaNotificationsSettings
tests/test_manager.py
1404 1404
    assert Event.objects.count() == 0
1405 1405

  
1406 1406

  
1407
def test_export_events(app, admin_user):
1408
    agenda = Agenda.objects.create(label=u'Foo bar')
1409

  
1410
    app = login(app)
1411
    resp = app.get('/manage/agendas/%s/export-events' % agenda.id)
1412
    csv_export = resp.text
1413
    assert (
1414
        csv_export
1415
        == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date,duration\r\n'
1416
    )
1417

  
1418
    resp = app.get('/manage/agendas/%s/import-events' % agenda.id)
1419
    resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,00:30,10', 'text/csv')
1420
    resp.form.submit(status=302)
1421

  
1422
    resp = app.get('/manage/agendas/%s/export-events' % agenda.id)
1423
    csv_export = resp.text
1424
    assert (
1425
        csv_export
1426
        == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date,duration\r\n'
1427
        '2016-09-16,00:30,10,0,,foo-bar-event,,,,,\r\n'
1428
    )
1429

  
1430
    resp = app.get('/manage/agendas/%s/import-events' % agenda.id)
1431
    resp.form['events_csv_file'] = Upload(
1432
        't.csv',
1433
        b'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16,90',
1434
        'text/csv',
1435
    )
1436
    resp.form.submit(status=302)
1437
    resp = app.get('/manage/agendas/%s/export-events' % agenda.id)
1438
    csv_export = resp.text
1439
    assert (
1440
        csv_export
1441
        == 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date,duration\r\n'
1442
        '2016-09-16,00:30,10,0,,foo-bar-event,,,,,\r\n'
1443
        '2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16,90\r\n'
1444
    )
1445

  
1446

  
1447
def test_export_events_wrong_kind(app, admin_user):
1448
    agenda = Agenda.objects.create(label=u'Foo bar', kind='meetings')
1449

  
1450
    app = login(app)
1451
    app.get('/manage/agendas/%s/export-events' % agenda.id, status=404)
1452
    agenda.kind = 'virtual'
1453
    agenda.save()
1454
    app.get('/manage/agendas/%s/export-events' % agenda.id, status=404)
1455

  
1456

  
1407 1457
def test_import_events(app, admin_user):
1408 1458
    agenda = Agenda(label=u'Foo bar')
1409 1459
    agenda.save()
......
1549 1599
    assert event.slug == 'slug'
1550 1600
    resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
1551 1601
    resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,label,slug', 'text/csv')
1552
    resp = resp.form.submit(status=200)
1553
    assert 'Event with this Agenda and Identifier already exists.' in resp.text
1554
    assert '__all__' not in resp.text
1602
    resp = resp.form.submit(status=302)
1603
    assert Event.objects.count() == 1
1604
    event = Event.objects.latest('pk')
1605
    assert event.slug == 'slug'
1555 1606

  
1556 1607
    # additional optional attributes
1557 1608
    Event.objects.all().delete()
......
1602 1653
    resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
1603 1654
    resp.form['events_csv_file'] = Upload(
1604 1655
        't.csv',
1605
        b'2016-09-16,18:00,10,5,labela,,,pricing,\n'
1606
        b'2016-09-17,18:00,10,5,labela,,,pricing,\n'
1607
        b'2016-09-18,18:00,10,5,labela,,,pricing,\n'
1656
        b'2016-09-16,18:00,10,5,labela,labelb,,pricing,\n'
1657
        b'2016-09-17,18:00,10,5,labela,labelb-1,,pricing,\n'
1658
        b'2016-09-18,18:00,10,5,labela,labelb-2,,pricing,\n'
1608 1659
        b'2016-09-18,18:00,10,5,labelb,,,pricing,\n'
1609 1660
        b'2016-09-18,18:00,10,5,labelb,,,pricing,\n',
1610 1661
        'text/csv',
1611 1662
    )
1612 1663
    with CaptureQueriesContext(connection) as ctx:
1613 1664
        resp = resp.form.submit(status=302)
1614
        assert len(ctx.captured_queries) == 24
1665
        assert len(ctx.captured_queries) == 22
1615 1666
    assert Event.objects.count() == 5
1667
    assert set(Event.objects.values_list('slug', flat=True)) == set(
1668
        ['labelb', 'labelb-1', 'labelb-2', 'labelb-3', 'labelb-4']
1669
    )
1616 1670

  
1617 1671
    # forbidden numerical slug
1618 1672
    Event.objects.all().delete()
......
1622 1676
    assert 'value cannot be a number' in resp.text
1623 1677
    assert 'Identifier:' in resp.text  # verbose_name is shown, not field name ('slug:')
1624 1678

  
1625
    # handle duplicated slug
1626
    Event.objects.all().delete()
1679

  
1680
def test_import_events_existing_event(app, admin_user, freezer):
1681
    agenda = Agenda.objects.create(label=u'Foo bar')
1682

  
1683
    app = login(app)
1627 1684
    resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
1628 1685
    resp.form['events_csv_file'] = Upload(
1629
        't.csv', b'2016-09-16,18:00,10,5,label,slug\n' b'2016-09-16,18:00,10,5,label,slug\n', 'text/csv',
1686
        't.csv', b'2016-09-16,18:00,10,5,label,slug\n2016-09-16,18:00,10,5,label,slug\n', 'text/csv',
1630 1687
    )
1631
    resp = resp.form.submit(status=200)
1632
    assert 'duplicated event identifiers' in resp.text
1688
    resp.form.submit(status=302)
1689
    assert agenda.event_set.count() == 1
1690
    event = Event.objects.latest('pk')
1691

  
1692
    def check_import(date, time, with_alert):
1693
        resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
1694
        resp.form['events_csv_file'] = Upload(
1695
            't.csv', b'%s,%s,10,5,label,slug\n' % (date.encode(), time.encode()), 'text/csv',
1696
        )
1697
        resp = resp.form.submit(status=302).follow()
1698
        assert agenda.event_set.count() == 1
1699
        event.refresh_from_db()
1700
        if with_alert:
1701
            assert (
1702
                '<li class="warning">Event &quot;label&quot; start date has changed. Do not forget to notify the registrants.</li>'
1703
                in resp.text
1704
            )
1705
        else:
1706
            assert (
1707
                '<li class="warning">Event &quot;label&quot; start date has changed. Do not forget to notify the registrants.</li>'
1708
                not in resp.text
1709
            )
1710
        assert event.start_datetime == make_aware(
1711
            datetime.datetime(*[int(v) for v in date.split('-')], *[int(v) for v in time.split(':')])
1712
        )
1713

  
1714
    # change date or time
1715
    # event in the past, no alert, with or without booking
1716
    Booking.objects.create(
1717
        event=event, cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30))
1718
    )
1719
    check_import('2016-09-15', '18:00', False)  # change date
1720
    check_import('2016-09-15', '17:00', False)  # change time
1721
    # available booking
1722
    Booking.objects.create(event=event)
1723
    check_import('2016-09-14', '17:00', False)  # change date
1724
    check_import('2016-09-14', '16:00', False)  # change time
1725

  
1726
    # date in the future
1727
    freezer.move_to('2016-09-01')
1728
    # warn if available booking only
1729
    check_import('2016-09-13', '16:00', True)  # change date
1730
    check_import('2016-09-13', '15:00', True)  # change time
1731
    # no available booking
1732
    Booking.objects.all().delete()
1733
    Booking.objects.create(
1734
        event=event, cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30))
1735
    )
1736
    check_import('2016-09-12', '15:00', False)  # change date
1737
    check_import('2016-09-12', '14:00', False)  # change time
1738

  
1739
    # check there is a message per changed event
1740
    resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
1741
    resp.form['events_csv_file'] = Upload(
1742
        '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',
1743
    )
1744
    resp.form.submit(status=302)
1745
    assert agenda.event_set.count() == 2
1746
    event2 = Event.objects.latest('pk')
1747
    Booking.objects.create(event=event)
1748
    Booking.objects.create(event=event2)
1749

  
1750
    resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
1751
    resp.form['events_csv_file'] = Upload(
1752
        't.csv',
1753
        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',
1754
        'text/csv',
1755
    )
1756
    resp = resp.form.submit(status=302).follow()
1757
    assert agenda.event_set.count() == 2
1758
    assert (
1759
        resp.text.count(
1760
            'Event &quot;label&quot; start date has changed. Do not forget to notify the registrants.'
1761
        )
1762
        == 1
1763
    )
1764
    assert (
1765
        resp.text.count(
1766
            'Event &quot;other_slug&quot; start date has changed. Do not forget to notify the registrants.'
1767
        )
1768
        == 1
1769
    )
1770

  
1771

  
1772
def test_import_events_wrong_kind(app, admin_user):
1773
    agenda = Agenda.objects.create(label=u'Foo bar', kind='meetings')
1774

  
1775
    app = login(app)
1776
    app.get('/manage/agendas/%s/import-events' % agenda.id, status=404)
1777
    agenda.kind = 'virtual'
1778
    agenda.save()
1779
    app.get('/manage/agendas/%s/import-events' % agenda.id, status=404)
1633 1780

  
1634 1781

  
1635 1782
def test_add_meetings_agenda(app, admin_user):
......
3321 3468
    agenda.save()
3322 3469

  
3323 3470
    app = login(app)
3324
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
3325 3471
    with freezegun.freeze_time('2020-06-15'):
3326
        resp = resp.click('Export')
3472
        resp = app.get('/manage/agendas/%s/export' % agenda.id)
3327 3473
    assert resp.headers['content-type'] == 'application/json'
3328 3474
    assert resp.headers['content-disposition'] == 'attachment; filename="export_agenda_foo-bar_20200615.json"'
3329 3475
    agenda_export = resp.text
3330
-