From 0f4040f9a59f93906166f70bbfaf016c8d3dd791 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Wed, 6 Nov 2019 15:20:07 +0100
Subject: [PATCH 1/2] api: add a field 'slug' to Event (#15726)

---
 chrono/agendas/migrations/0028_event_slug.py  | 37 +++++++++++++++++++
 .../migrations/0029_auto_20191106_1320.py     | 20 ++++++++++
 chrono/agendas/models.py                      | 18 +++++++++
 chrono/manager/forms.py                       | 19 +++++++++-
 .../chrono/manager_agenda_settings.html       |  2 +-
 .../chrono/manager_sample_events.txt          |  4 +-
 chrono/manager/views.py                       |  4 +-
 tests/test_agendas.py                         | 16 ++++++++
 tests/test_manager.py                         | 16 ++++++++
 9 files changed, 129 insertions(+), 7 deletions(-)
 create mode 100644 chrono/agendas/migrations/0028_event_slug.py
 create mode 100644 chrono/agendas/migrations/0029_auto_20191106_1320.py

diff --git a/chrono/agendas/migrations/0028_event_slug.py b/chrono/agendas/migrations/0028_event_slug.py
new file mode 100644
index 0000000..e720f31
--- /dev/null
+++ b/chrono/agendas/migrations/0028_event_slug.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+from django.utils.text import slugify
+
+
+def set_slug_on_events(apps, schema_editor):
+    Event = apps.get_model('agendas', 'Event')
+    for event in Event.objects.filter(slug=''):
+        base_slug = slugify(event.label)
+        slug = base_slug
+        i = 1
+        while True:
+            if not Event.objects.filter(slug=slug).exists():
+                break
+            slug = '%s-%s' % (base_slug, i)
+            i += 1
+        event.slug = slug
+        event.save(update_fields=['slug'])
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0027_event_description'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='event',
+            name='slug',
+            field=models.SlugField(default='', max_length=160, verbose_name='Identifier'),
+            preserve_default=False,
+        ),
+        migrations.RunPython(set_slug_on_events, lambda x, y: None),
+    ]
diff --git a/chrono/agendas/migrations/0029_auto_20191106_1320.py b/chrono/agendas/migrations/0029_auto_20191106_1320.py
new file mode 100644
index 0000000..14dfe0c
--- /dev/null
+++ b/chrono/agendas/migrations/0029_auto_20191106_1320.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2019-11-06 12:20
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0028_event_slug'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='slug',
+            field=models.SlugField(max_length=160, unique=True, verbose_name='Identifier'),
+        ),
+    ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 8192608..5e54e45 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -289,6 +289,7 @@ class Event(models.Model):
             _('Places in waiting list'), default=0)
     label = models.CharField(_('Label'), max_length=150, null=True, blank=True,
             help_text=_('Optional label to identify this date.'))
+    slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
     description = models.TextField(_('Description'), null=True, blank=True,
             help_text=_('Optional event description.'))
     full = models.BooleanField(default=False)
@@ -305,6 +306,16 @@ class Event(models.Model):
 
     def save(self, *args, **kwargs):
         self.check_full()
+        if not self.slug:
+            base_slug = slugify(self.label)
+            slug = base_slug
+            i = 1
+            while True:
+                if not Event.objects.filter(slug=slug).exists():
+                    break
+                slug = '%s-%s' % (base_slug, i)
+                i += 1
+            self.slug = slug
         return super(Event, self).save(*args, **kwargs)
 
     def check_full(self):
@@ -345,6 +356,12 @@ class Event(models.Model):
     def import_json(cls, data):
         data['start_datetime'] = make_aware(datetime.datetime.strptime(
             data['start_datetime'], '%Y-%m-%d %H:%M:%S'))
+        if 'slug' in data:
+            event, created = cls.objects.get_or_create(slug=data['slug'], defaults=data)
+            if not created:
+                for k, v in data.items():
+                    setattr(event, k, v)
+            return event
         return cls(**data)
 
     def export_json(self):
@@ -353,6 +370,7 @@ class Event(models.Model):
             'places': self.places,
             'waiting_list_places': self.waiting_list_places,
             'label': self.label,
+            'slug': self.slug,
             'description': self.description,
         }
 
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 149351f..cbd66c0 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -63,6 +63,16 @@ class AgendaEditForm(AgendaAddForm):
         fields = ['label', 'slug', 'edit_role', 'view_role', 'minimal_booking_delay', 'maximal_booking_delay']
 
 
+class NewEventForm(forms.ModelForm):
+    class Meta:
+        model = Event
+        widgets = {
+                'agenda': forms.HiddenInput(),
+                'start_datetime': DateTimeWidget(),
+        }
+        exclude = ['full', 'meeting_type', 'desk', 'slug']
+
+
 class EventForm(forms.ModelForm):
     class Meta:
         model = Event
@@ -220,9 +230,14 @@ class ImportEventsForm(forms.Form):
                 except ValueError:
                     raise ValidationError(_('Invalid file format. (number of places in waiting list, line %d)') % (i+1))
             if len(csvline) >= 5:
-                event.label = ' '.join([force_text(x) for x in csvline[4:]])
+                event.label = force_text(csvline[4])
+            exclude = ['agenda', 'desk', 'meeting_type']
+            if len(csvline) >= 6:
+                event.slug = ' '.join([force_text(x) for x in csvline[5:]])
+            else:
+                exclude += ['slug']
             try:
-                event.full_clean(exclude=['agenda', 'desk', 'meeting_type'])
+                event.full_clean(exclude=exclude)
             except ValidationError as e:
                 errors = [
                     _('Invalid file format. (%(label)s: %(errors)s, line %(line)d)') % {
diff --git a/chrono/manager/templates/chrono/manager_agenda_settings.html b/chrono/manager/templates/chrono/manager_agenda_settings.html
index 561c843..c4435c1 100644
--- a/chrono/manager/templates/chrono/manager_agenda_settings.html
+++ b/chrono/manager/templates/chrono/manager_agenda_settings.html
@@ -46,7 +46,7 @@
           data-total="{{event.waiting_list_places}}" data-booked="{{event.waiting_list}}"
         {% endif %}
         ><a rel="popup" href="{% if user_can_manage %}{% url 'chrono-manager-event-edit' pk=event.id %}{% else %}#{% endif %}">
-        {% if event.label %}{{event.label}} / {% endif %}
+        {% if event.label %}{{event.label}} {% endif %}[{% trans "identifier:" %} {{ event.slug }}] /
         {{ event.start_datetime }}
         {% if event.full %}/ <span class="full">{% trans "full" %}</span>{% endif %}
         (
diff --git a/chrono/manager/templates/chrono/manager_sample_events.txt b/chrono/manager/templates/chrono/manager_sample_events.txt
index cbbb10c..1d283ff 100644
--- a/chrono/manager/templates/chrono/manager_sample_events.txt
+++ b/chrono/manager/templates/chrono/manager_sample_events.txt
@@ -1,2 +1,2 @@
-{% load i18n %}{% trans 'date' %},{% trans 'time' %},{% trans 'number of places' %},{% trans 'number of places in waiting list' %},{% trans 'label' %}
-{{ some_future_date|date:"Y-m-d" }},{{ some_future_date|date:"H:i" }},15,0,{% trans "example event" %}
+{% load i18n %}{% trans 'date' %},{% trans 'time' %},{% trans 'number of places' %},{% trans 'number of places in waiting list' %},{% trans 'label' %},{% trans 'identifier' %}
+{{ some_future_date|date:"Y-m-d" }},{{ some_future_date|date:"H:i" }},15,0,{% trans "example event" as label %}{{ label }},{{ label|slugify }}
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index eec79b9..f14bb8e 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -36,7 +36,7 @@ from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod,
                                    Booking, Desk, TimePeriodException,
                                    ICSError, AgendaImportError)
 
-from .forms import (AgendaAddForm, AgendaEditForm, EventForm, NewMeetingTypeForm, MeetingTypeForm,
+from .forms import (AgendaAddForm, AgendaEditForm, NewEventForm, EventForm, NewMeetingTypeForm, MeetingTypeForm,
                     TimePeriodForm, ImportEventsForm, NewDeskForm, DeskForm, TimePeriodExceptionForm,
                     ExceptionsImportForm, AgendasImportForm, TimePeriodAddForm)
 from .utils import import_site
@@ -567,7 +567,7 @@ agenda_export = AgendaExport.as_view()
 class AgendaAddEventView(ManagedAgendaMixin, CreateView):
     template_name = 'chrono/manager_event_form.html'
     model = Event
-    form_class = EventForm
+    form_class = NewEventForm
 
 agenda_add_event = AgendaAddEventView.as_view()
 
diff --git a/tests/test_agendas.py b/tests/test_agendas.py
index c332bda..60d7ffa 100644
--- a/tests/test_agendas.py
+++ b/tests/test_agendas.py
@@ -123,11 +123,19 @@ def test_slug():
     agenda.save()
     assert agenda.slug == 'foo-bar'
 
+    event = Event.objects.create(start_datetime=now(), places=42, agenda=agenda, label='Foo bar')
+    assert event.slug == 'foo-bar'
+
+
 def test_existing_slug():
     agenda = Agenda(label=u'Foo bar', slug='bar')
     agenda.save()
     assert agenda.slug == 'bar'
 
+    event = Event.objects.create(start_datetime=now(), places=42, agenda=agenda, label='Foo bar', slug='bar')
+    assert event.slug == 'bar'
+
+
 def test_duplicate_slugs():
     agenda = Agenda(label=u'Foo baz')
     agenda.save()
@@ -139,6 +147,14 @@ def test_duplicate_slugs():
     agenda.save()
     assert agenda.slug == 'foo-baz-2'
 
+    event = Event.objects.create(start_datetime=now(), places=42, agenda=agenda, label='Foo bar')
+    assert event.slug == 'foo-bar'
+    event = Event.objects.create(start_datetime=now(), places=42, agenda=agenda, label='Foo bar')
+    assert event.slug == 'foo-bar-1'
+    event = Event.objects.create(start_datetime=now(), places=42, agenda=agenda, label='Foo bar')
+    assert event.slug == 'foo-bar-2'
+
+
 def test_event_manager():
     agenda = Agenda(label=u'Foo baz')
     agenda.save()
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 99e224d..0d71b6f 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -636,6 +636,22 @@ def test_import_events(app, admin_user):
                                                    '"2016-09-19"\t"18:00"\t"10"'.encode('iso-8859-15'), 'text/csv')
     resp = resp.form.submit(status=302)
     assert Event.objects.count() == 2
+    Event.objects.all().delete()
+
+    resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
+    resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,label,slug', 'text/csv')
+    resp = resp.form.submit(status=302)
+    assert Event.objects.count() == 1
+    event = Event.objects.latest('pk')
+    assert event.start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0))
+    assert event.places == 10
+    assert event.waiting_list_places == 5
+    assert event.label == 'label'
+    assert event.slug == 'slug'
+    resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
+    resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,label,slug', 'text/csv')
+    resp = resp.form.submit(status=200)
+    assert 'Invalid file format. (slug: Event with this Identifier already exists.' in resp.text
 
 
 def test_add_meetings_agenda(app, admin_user):
-- 
2.20.1

