From 4d1c82ca4dfb3ec1b75d040676fd354df7150515 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 7 Nov 2019 12:27:05 +0100
Subject: [PATCH] agendas: slug unicity for Agenda, Desk and MeetingType
 (#34044)

---
 .../migrations/0030_auto_20191107_1200.py     | 58 ++++++++++++++
 .../migrations/0031_auto_20191107_1225.py     | 28 +++++++
 chrono/agendas/models.py                      | 78 ++++++++-----------
 3 files changed, 119 insertions(+), 45 deletions(-)
 create mode 100644 chrono/agendas/migrations/0030_auto_20191107_1200.py
 create mode 100644 chrono/agendas/migrations/0031_auto_20191107_1225.py

diff --git a/chrono/agendas/migrations/0030_auto_20191107_1200.py b/chrono/agendas/migrations/0030_auto_20191107_1200.py
new file mode 100644
index 0000000..8e551bd
--- /dev/null
+++ b/chrono/agendas/migrations/0030_auto_20191107_1200.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations
+from django.utils.text import slugify
+
+
+def generate_slug(instance, **query_filters):
+    base_slug = slugify(instance.label)
+    slug = base_slug
+    i = 1
+    while True:
+        queryset = instance._meta.model.objects.filter(slug=slug, **query_filters).exclude(pk=instance.pk)
+        if not queryset.exists():
+            break
+        slug = '%s-%s' % (base_slug, i)
+        i += 1
+    return slug
+
+
+def set_slug_on_agendas(apps, schema_editor):
+    Agenda = apps.get_model('agendas', 'Agenda')
+    for agenda in Agenda.objects.all().order_by('-pk'):
+        if not Agenda.objects.filter(slug=agenda.slug).exclude(pk=agenda.pk).exists():
+            continue
+        agenda.slug = generate_slug(agenda)
+        agenda.save(update_fields=['slug'])
+
+
+def set_slug_on_desks(apps, schema_editor):
+    Desk = apps.get_model('agendas', 'Desk')
+    for desk in Desk.objects.all().order_by('-pk'):
+        if not Desk.objects.filter(slug=desk.slug, agenda=desk.agenda).exclude(pk=desk.pk).exists():
+            continue
+        desk.slug = generate_slug(desk, agenda=desk.agenda)
+        desk.save(update_fields=['slug'])
+
+
+def set_slug_on_meetingtypes(apps, schema_editor):
+    MeetingType = apps.get_model('agendas', 'MeetingType')
+    for meetingtype in MeetingType.objects.all().order_by('-pk'):
+        if not MeetingType.objects.filter(slug=meetingtype.slug, agenda=meetingtype.agenda).exclude(pk=meetingtype.pk).exists():
+            continue
+        meetingtype.slug = generate_slug(meetingtype, agenda=meetingtype.agenda)
+        meetingtype.save(update_fields=['slug'])
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0029_auto_20191106_1320'),
+    ]
+
+    operations = [
+        migrations.RunPython(set_slug_on_agendas, lambda x, y: None),
+        migrations.RunPython(set_slug_on_desks, lambda x, y: None),
+        migrations.RunPython(set_slug_on_meetingtypes, lambda x, y: None),
+    ]
diff --git a/chrono/agendas/migrations/0031_auto_20191107_1225.py b/chrono/agendas/migrations/0031_auto_20191107_1225.py
new file mode 100644
index 0000000..e0c900c
--- /dev/null
+++ b/chrono/agendas/migrations/0031_auto_20191107_1225.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2019-11-07 11:25
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0030_auto_20191107_1200'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='agenda',
+            name='slug',
+            field=models.SlugField(max_length=160, unique=True, verbose_name='Identifier'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='desk',
+            unique_together=set([('agenda', 'slug')]),
+        ),
+        migrations.AlterUniqueTogether(
+            name='meetingtype',
+            unique_together=set([('agenda', 'slug')]),
+        ),
+    ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 5e54e45..6a56c9f 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -49,6 +49,18 @@ def is_midnight(dtime):
     return dtime.hour == 0 and dtime.minute == 0
 
 
+def generate_slug(instance, **query_filters):
+    base_slug = slugify(instance.label)
+    slug = base_slug
+    i = 1
+    while True:
+        if not instance._meta.model.objects.filter(slug=slug, **query_filters).exists():
+            break
+        slug = '%s-%s' % (base_slug, i)
+        i += 1
+    return slug
+
+
 class ICSError(Exception):
     pass
 
@@ -59,7 +71,7 @@ class AgendaImportError(Exception):
 
 class Agenda(models.Model):
     label = models.CharField(_('Label'), max_length=150)
-    slug = models.SlugField(_('Identifier'), max_length=160)
+    slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
     kind = models.CharField(_('Kind'), max_length=20, choices=AGENDA_KINDS, default='events')
     minimal_booking_delay = models.PositiveIntegerField(
             _('Minimal booking delay (in days)'), default=1)
@@ -77,17 +89,7 @@ class Agenda(models.Model):
 
     def save(self, *args, **kwargs):
         if not self.slug:
-            base_slug = slugify(self.label)
-            slug = base_slug
-            i = 1
-            while True:
-                try:
-                    Agenda.objects.get(slug=slug)
-                except self.DoesNotExist:
-                    break
-                slug = '%s-%s' % (base_slug, i)
-                i += 1
-            self.slug = slug
+            self.slug = generate_slug(self)
         super(Agenda, self).save(*args, **kwargs)
 
     def get_absolute_url(self):
@@ -149,6 +151,9 @@ class Agenda(models.Model):
                 except Group.DoesNotExist:
                     raise AgendaImportError(_('Missing "%s" role') % permissions[permission])
         agenda, created = cls.objects.get_or_create(slug=data['slug'], defaults=data)
+        if not created:
+            for k, v in data.items():
+                setattr(agenda, k, v)
         if data['kind'] == 'events':
             if overwrite:
                 Event.objects.filter(agenda=agenda).delete()
@@ -252,25 +257,21 @@ class MeetingType(models.Model):
 
     class Meta:
         ordering = ['duration', 'label']
+        unique_together = ['agenda', 'slug']
 
     def save(self, *args, **kwargs):
         if not self.slug:
-            base_slug = slugify(self.label)
-            slug = base_slug
-            i = 1
-            while True:
-                try:
-                    MeetingType.objects.get(slug=slug, agenda=self.agenda)
-                except self.DoesNotExist:
-                    break
-                slug = '%s-%s' % (base_slug, i)
-                i += 1
-            self.slug = slug
+            self.slug = generate_slug(self, agenda=self.agenda)
         super(MeetingType, self).save(*args, **kwargs)
 
     @classmethod
     def import_json(cls, data):
-        return cls(**data)
+        meeting_type, created = cls.objects.get_or_create(
+            slug=data['slug'], agenda=data['agenda'], defaults=data)
+        if not created:
+            for k, v in data.items():
+                setattr(meeting_type, k, v)
+        return meeting_type
 
     def export_json(self):
         return {
@@ -307,15 +308,7 @@ 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
+            self.slug = generate_slug(self)
         return super(Event, self).save(*args, **kwargs)
 
     def check_full(self):
@@ -447,27 +440,22 @@ class Desk(models.Model):
 
     class Meta:
         ordering = ['label']
+        unique_together = ['agenda', 'slug']
 
     def save(self, *args, **kwargs):
         if not self.slug:
-            base_slug = slugify(self.label)
-            slug = base_slug
-            i = 1
-            while True:
-                try:
-                    Desk.objects.get(slug=slug, agenda=self.agenda)
-                except self.DoesNotExist:
-                    break
-                slug = '%s-%s' % (base_slug, i)
-                i += 1
-            self.slug = slug
+            self.slug = generate_slug(self, agenda=self.agenda)
         super(Desk, self).save(*args, **kwargs)
 
     @classmethod
     def import_json(cls, data):
         timeperiods = data.pop('timeperiods')
         exceptions = data.pop('exceptions')
-        instance, created = cls.objects.get_or_create(**data)
+        instance, created = cls.objects.get_or_create(
+            slug=data['slug'], agenda=data['agenda'], defaults=data)
+        if not created:
+            for k, v in data.items():
+                setattr(instance, k, v)
         for timeperiod in timeperiods:
             timeperiod['desk'] = instance
             TimePeriod.import_json(timeperiod).save()
-- 
2.20.1

