Repository: carpentries/amy

Issue:
Crash when importing existing event
1.  Copy AMY db to desktop.
2.  Run local server.
3.  Create new event.
4.  Import from `http://dgasmith.github.io/2015-08-24-stanford/`. (Note: this event is already in the database.)
5.  Crash.



Discussion:
@gvwilson I assume this happens with the code from #426?

If that's the case, the error happens because `Event.slug` doesn't have the unique constraint.

So we're able to add more events with the same slug. The error appears when getting one particular event by it's slug: we can't because there are more than 1.

Proposed solutions:
1. Make `Event.slug` unique, or
2. implement #259 

Yup, this is with #426.

> If that's the case, the error happens because |Event.slug| doesn't 
> have the unique constraint.
> 
> So we're able to add more events with the same slug.

Right, so before trying to add, we should 'get' to see if that slug 
already exists and reject the new Event if so, yes?
Thanks,
Greg

@gvwilson this can be done automatically if we use ModelForm and set `unique=True` to `Event.slug` or we can extend our existing `clean` method on that ModelForm to check for the unique constraints.

I'd opt for the 1. option.

I trust your judgment.
Thanks,
Greg


Resolving patch:
diff --git a/workshops/migrations/0027_auto_20150714_1143.py b/workshops/migrations/0027_auto_20150714_1143.py
new file mode 100644
--- /dev/null
+++ b/workshops/migrations/0027_auto_20150714_1143.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0026_add_missing_airports'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='slug',
+            field=models.CharField(unique=True, null=True, blank=True, max_length=100),
+        ),
+    ]
diff --git a/workshops/models.py b/workshops/models.py
--- a/workshops/models.py
+++ b/workshops/models.py
@@ -292,7 +292,7 @@ class Event(models.Model):
     start      = models.DateField(null=True, blank=True,
                                   help_text='Setting this and url "publishes" the event.')
     end        = models.DateField(null=True, blank=True)
-    slug       = models.CharField(max_length=STR_LONG, null=True, blank=True)
+    slug       = models.CharField(max_length=STR_LONG, null=True, blank=True, unique=True)
     url        = models.CharField(max_length=STR_LONG, unique=True, null=True, blank=True,
                                   help_text='Setting this and startdate "publishes" the event.')
     reg_key    = models.CharField(max_length=STR_REG_KEY, null=True, blank=True, verbose_name="Eventbrite key")
@@ -331,6 +331,7 @@ def get_by_ident(ident):
             return Event.objects.get(slug=ident)
 
     def save(self, *args, **kwargs):
+        self.slug = self.slug or None
         self.url = self.url or None
         super(Event, self).save(*args, **kwargs)
 


Test patch:
diff --git a/workshops/test/test_event.py b/workshops/test/test_event.py
--- a/workshops/test/test_event.py
+++ b/workshops/test/test_event.py
@@ -137,18 +137,19 @@ def setUp(self):
         self._setUpUsersAndLogin()
 
         # Create a test site
-        test_site = Site.objects.create(domain='example.com',
-                                        fullname='Test Site')
+        self.test_site = Site.objects.create(domain='example.com',
+                                             fullname='Test Site')
 
         # Create a test tag
-        test_tag = Tag.objects.create(name='Test Tag', details='For testing')
+        self.test_tag = Tag.objects.create(name='Test Tag',
+                                           details='For testing')
 
         # Create fifty new events
         for i in range(50):
             event_start = datetime.now()
             Event.objects.create(start=event_start,
                                  slug='test_event_{0}'.format(i),
-                                 site=test_site,
+                                 site=self.test_site,
                                  admin_fee=0)
 
     def test_events_view_paginated(self):
@@ -220,12 +221,11 @@ def test_page_no_too_large_returns_last_page(self):
 
     def test_add_minimal_event(self):
         site = Site.objects.get(fullname='Test Site')
-        tag = Tag.objects.get(name='Test Tag')
         response = self.client.post(
             reverse('event_add'),
             {
                 'site': site.id,
-                'tags': [tag.id],
+                'tags': [self.test_tag.id],
                 'organizer': site.id,
             })
         if response.status_code == 302:
@@ -235,19 +235,19 @@ def test_add_minimal_event(self):
             assert event.site == site, (
                 'New event has wrong site: {} != {}'.format(event.site, site))
             tags = list(event.tags.all())
-            assert tags == [tag], (
-                'New event has wrong tags: {} != {}'.format(tags, [tag]))
+            assert tags == [self.test_tag], (
+                'New event has wrong tags: {} != {}'.format(tags,
+                                                            [self.test_tag]))
         else:
             self._check_status_code_and_parse(response, 200)
             assert False, 'expected 302 redirect after post'
 
     def test_add_two_minimal_events(self):
         site = Site.objects.get(fullname='Test Site')
-        tag = Tag.objects.get(name='Test Tag')
         url = reverse('event_add')
         data = {
                 'site': site.id,
-                'tags': [tag.id],
+                'tags': [self.test_tag.id],
                 'organizer': site.id,
             }
         response = self.client.post(url, data)
@@ -261,6 +261,40 @@ def test_add_two_minimal_events(self):
                 'expected 302 redirect after second post, got {}'.format(
                     response.status_code))
 
+    def test_unique_slug(self):
+        """Ensure events with the same slugs are prohibited.
+
+        This is a regression test for
+        https://github.com/swcarpentry/amy/issues/427"""
+        data = {
+            'site': self.test_site.id,
+            'tags': [self.test_tag.id],
+            'slug': 'testing-unique-slug',
+        }
+        response = self.client.post(reverse('event_add'), data)
+        assert response.status_code == 302
+
+        response = self.client.post(reverse('event_add'), data)
+        with self.assertRaises(AssertionError):
+            self._check_status_code_and_parse(response, 200)
+
+    def test_unique_empty_slug(self):
+        """Ensure events with no slugs are saved to the DB.
+
+        This is a regression test introduces with one change from
+        https://github.com/swcarpentry/amy/issues/427
+        (saving empty slug strings to the DB should result in NULL values)."""
+        data = {
+            'site': self.test_site.id,
+            'tags': [self.test_tag.id],
+            'slug': '',
+        }
+        response = self.client.post(reverse('event_add'), data)
+        assert response.status_code == 302
+
+        response = self.client.post(reverse('event_add'), data)
+        assert response.status_code == 302
+
 
 class TestEventNotes(TestBase):
     """Make sure notes once written are saved forever!"""