From 962d1aa97ed8b39b9a25a1e74f5720d3d81bbef4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 31 Aug 2020 15:23:05 +0200
Subject: [PATCH 1/2] manager: slug generation optimization on event import
 (#46100)

---
 chrono/agendas/models.py | 23 +++++++++++++++++++----
 chrono/manager/views.py  |  4 +++-
 tests/test_manager.py    | 14 +++++++++-----
 3 files changed, 31 insertions(+), 10 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 9f95842..3830845 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -68,13 +68,28 @@ def is_midnight(dtime):
     return dtime.hour == 0 and dtime.minute == 0
 
 
-def generate_slug(instance, **query_filters):
+def generate_slug(instance, seen_slugs=None, **query_filters):
     base_slug = instance.base_slug
     slug = base_slug
     i = 1
-    while instance._meta.model.objects.filter(slug=slug, **query_filters).exists():
+
+    if seen_slugs is None:
+        # no optimization: check slug in DB each time
+        while instance._meta.model.objects.filter(slug=slug, **query_filters).exists():
+            slug = '%s-%s' % (base_slug, i)
+            i += 1
+        return slug
+
+    # seen_slugs is filled
+    while True:
+        if slug not in seen_slugs:
+            # check in DB to be sure, but only if not seen
+            queryset = instance._meta.model.objects.filter(slug=slug, **query_filters)
+            if not queryset.exists():
+                break
         slug = '%s-%s' % (base_slug, i)
         i += 1
+    seen_slugs.add(slug)
     return slug
 
 
@@ -810,12 +825,12 @@ class Event(models.Model):
         if self.cancellation_scheduled:
             return _('Cancellation in progress')
 
-    def save(self, *args, **kwargs):
+    def save(self, seen_slugs=None, *args, **kwargs):
         assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda"
         assert not (self.slug and self.slug.isdigit()), 'slug cannot be a number'
         self.check_full()
         if not self.slug:
-            self.slug = generate_slug(self, agenda=self.agenda)
+            self.slug = generate_slug(self, seen_slugs=seen_slugs, agenda=self.agenda)
         return super(Event, self).save(*args, **kwargs)
 
     @property
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 720619d..cb42e34 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -1337,9 +1337,11 @@ class AgendaImportEventsView(ManagedAgendaMixin, FormView):
 
     def form_valid(self, form):
         if form.events:
+            # existing event slugs for this agenda
+            seen_slugs = set(self.agenda.event_set.values_list('slug', flat=True))
             for event in form.events:
                 event.agenda_id = self.kwargs['pk']
-                event.save()
+                event.save(seen_slugs=seen_slugs)  # optimization: seen_slugs
             messages.info(self.request, _('%d events have been imported.') % len(form.events))
         return super(AgendaImportEventsView, self).form_valid(form)
 
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 3aefc01..6b31a40 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -1581,13 +1581,17 @@ def test_import_events(app, admin_user):
     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,,,pricing,\n'
-        b'2016-09-17,18:00,10,5,label,,,pricing,\n'
-        b'2016-09-18,18:00,10,5,label,,,pricing,\n',
+        b'2016-09-16,18:00,10,5,labela,,,pricing,\n'
+        b'2016-09-17,18:00,10,5,labela,,,pricing,\n'
+        b'2016-09-18,18:00,10,5,labela,,,pricing,\n'
+        b'2016-09-18,18:00,10,5,labelb,,,pricing,\n'
+        b'2016-09-18,18:00,10,5,labelb,,,pricing,\n',
         'text/csv',
     )
-    resp = resp.form.submit(status=302)
-    assert Event.objects.count() == 3
+    with CaptureQueriesContext(connection) as ctx:
+        resp = resp.form.submit(status=302)
+        assert len(ctx.captured_queries) == 24
+    assert Event.objects.count() == 5
 
     # forbidden numerical slug
     Event.objects.all().delete()
-- 
2.28.0

