Repository: carpentries/amy

Issue:
Repair slow tests
There's a number of tests that run really slow (a couple seconds). Let's try to fix them to run < 1s.

A tool for finding slow tests: https://github.com/realpython/django-slow-tests



Discussion:

Resolving patch:
diff --git a/workshops/forms.py b/workshops/forms.py
--- a/workshops/forms.py
+++ b/workshops/forms.py
@@ -2,7 +2,9 @@
 
 from django import forms
 from django.core.validators import RegexValidator
-from django.forms import HiddenInput, CheckboxSelectMultiple, TextInput
+from django.forms import (
+    HiddenInput, CheckboxSelectMultiple, TextInput, modelformset_factory,
+)
 
 from captcha.fields import ReCaptchaField
 from crispy_forms.helper import FormHelper
@@ -612,3 +614,8 @@ class Media:
         # thanks to this, {{ form.media }} in the template will generate
         # a <link href=""> (for CSS files) or <script src=""> (for JS files)
         js = ('calendar_popup.js', )
+
+
+# `extra`: number of forms populated via `initial` parameter; it's hardcoded in
+# `views.todos_add`
+TodoFormSet = modelformset_factory(TodoItem, form=SimpleTodoForm, extra=10)
diff --git a/workshops/views.py b/workshops/views.py
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -62,6 +62,7 @@
     ProfileUpdateRequestForm, PersonLookupForm, bootstrap_helper_wider_labels,
     SimpleTodoForm, bootstrap_helper_inline_formsets, BootstrapHelper,
     AdminLookupForm, ProfileUpdateRequestFormNoCaptcha, MembershipForm,
+    TodoFormSet,
 )
 from workshops.util import (
     upload_person_task_csv,  verify_upload_person_task,
@@ -2182,10 +2183,8 @@ def todos_add(request, event_ident):
 
     initial = []
     base = dt.now()
-    if event.start and event.end:
-        extra = 9
-    else:
-        extra = 10
+
+    if not event.start or not event.end:
         initial = [
             {
                 'title': 'Set date with host',
@@ -2194,9 +2193,6 @@ def todos_add(request, event_ident):
             },
         ]
 
-    TodoFormSet = modelformset_factory(TodoItem, form=SimpleTodoForm,
-                                       extra=extra)
-
     formset = TodoFormSet(queryset=TodoItem.objects.none(), initial=initial + [
         {
             'title': 'Set up a workshop website',


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
@@ -4,7 +4,9 @@
 
 from django.test import TestCase
 from django.core.urlresolvers import reverse
+from django.db.utils import IntegrityError
 from ..models import (Event, Host, Tag, Person, Role, Task, Award, Badge)
+from ..forms import EventForm
 from .base import TestBase
 
 
@@ -105,41 +107,6 @@ def test_unpublished_events(self):
         self.assertNotIn(event_considered_published,
                          Event.objects.unpublished_events())
 
-    def test_edit_event(self):
-        """ Test that an event can be edited, and that people can be
-            added from the event edit page.
-        """
-
-        event = Event.objects.all()[0]
-        tag = Tag.objects.get(name='Test Tag')
-        role = Role.objects.get(name='Test Role')
-        url, values = self._get_initial_form_index(0, 'event_edit', event.id)
-        assert len(values) > 0
-        values['event-end'] = ''
-        values['event-tags'] = tag.id
-        assert "event-reg_key" in values, \
-            'No reg key in initial form'
-        new_reg_key = 'test_reg_key'
-        assert event.reg_key != new_reg_key, \
-            'Would be unable to tell if reg_key had changed'
-        values['event-reg_key'] = new_reg_key
-        response = self.client.post(url, values, follow=True)
-        content = response.content.decode('utf-8')
-        assert new_reg_key in content
-
-        url, values = self._get_initial_form_index(1, 'event_edit', event.id)
-        assert "task-person_0" in values, \
-            'No person select in initial form'
-
-        person = Person.objects.all()[0]
-        values['task-person_1'] = person.id
-        values['task-role'] = role.id
-
-        response = self.client.post(url, values, follow=True)
-        content = response.content.decode('utf-8')
-        assert "/workshops/person/1" in content
-        assert "Test Role" in content
-
     def test_delete_event(self):
         """Make sure deleted event and its tasks are no longer accessible."""
         event = Event.objects.get(slug="starts_today_ongoing")
@@ -370,17 +337,10 @@ def test_unique_slug(self):
 
         This is a regression test for
         https://github.com/swcarpentry/amy/issues/427"""
-        data = {
-            'host': self.test_host.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)
+        Event.objects.create(host=self.test_host, slug='testing-unique-slug')
+        with self.assertRaises(IntegrityError):
+            Event.objects.create(host=self.test_host,
+                                 slug='testing-unique-slug')
 
     def test_unique_empty_slug(self):
         """Ensure events with no slugs are saved to the DB.
@@ -441,61 +401,36 @@ def test_negative_admin_fee_attendance(self):
 
         This is a regression test for
         https://github.com/swcarpentry/amy/issues/435."""
-        data = {
-            'host': self.test_host.id,
-            'tags': [self.test_tag.id],
-            'slug': 'test-event',
-            'admin_fee': -1200,
-        }
-        S = "Ensure this value is greater than or equal to 0"
-        response = self.client.post(reverse('event_add'), data)
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert content.count(S) == 1
+        error_str = "Ensure this value is greater than or equal to 0."
 
         data = {
             'host': self.test_host.id,
             'tags': [self.test_tag.id],
             'slug': 'test-event',
             'admin_fee': -1200,
-            'attendance': -36,
         }
-        response = self.client.post(reverse('event_add'), data)
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert content.count(S) == 2
 
-        data = {
-            'host': self.test_host.id,
-            'tags': [self.test_tag.id],
-            'slug': 'test-event',
-            'attendance': -36,
-        }
-        S = "Ensure this value is greater than or equal to 0"
-        response = self.client.post(reverse('event_add'), data)
-        assert response.status_code == 200
-        content = response.content.decode('utf-8')
-        assert content.count(S) == 1
+        f = EventForm(data)
+        self.assertIn('admin_fee', f.errors)
+        self.assertIn(error_str, f.errors['admin_fee'])
 
-        data = {
-            'host': self.test_host.id,
-            'tags': [self.test_tag.id],
-            'slug': 'test-event',
-            'admin_fee': 0,
-            'attendance': 0,
-        }
-        response = self.client.post(reverse('event_add'), data)
-        assert response.status_code == 302
+        del data['admin_fee']
+        data['attendance'] = -36
+        f = EventForm(data)
+        self.assertIn('attendance', f.errors)
+        self.assertIn(error_str, f.errors['attendance'])
 
-        data = {
-            'host': self.test_host.id,
-            'tags': [self.test_tag.id],
-            'slug': 'test-event2',
-            'admin_fee': 1200,
-            'attendance': 36,
-        }
-        response = self.client.post(reverse('event_add'), data)
-        assert response.status_code == 302
+        data['admin_fee'] = 0
+        data['attendance'] = 0
+        f = EventForm(data)
+        self.assertNotIn('admin_fee', f.errors)
+        self.assertNotIn('attendance', f.errors)
+
+        data['slug'] = 'test-event2'
+        data['admin_fee'] = 1200
+        data['attendance'] = 36
+        f = EventForm(data)
+        self.assertTrue(f.is_valid())
 
     def test_number_of_attendees_increasing(self):
         """Ensure event.attendance gets bigger after adding new learners."""
@@ -503,16 +438,12 @@ def test_number_of_attendees_increasing(self):
         event.attendance = 0  # testing for numeric case
         event.save()
 
-        url, values = self._get_initial_form_index(1, 'event_edit', event.pk)
-        values['task-role'] = self.learner.pk
-        values['task-event'] = event.pk
-        values['task-person_0'] = str(self.spiderman)
-        values['task-person_1'] = self.spiderman.pk
-
-        rv = self.client.post(reverse('event_edit', args=[event.pk]), values,
-                              follow=True)
-        self._check_status_code_and_parse(rv, 200)
-
+        data = {
+            'task-role': self.learner.pk,
+            'task-event': event.pk,
+            'task-person_1': self.spiderman.pk,
+        }
+        self.client.post(reverse('event_edit', args=[event.pk]), data)
         event.refresh_from_db()
         assert event.attendance == 1
 
@@ -528,13 +459,15 @@ def test_slug_against_illegal_characters(self):
         for slug in ['a/b', 'a b', 'a!b', 'a.b', 'a\\b', 'a?b']:
             with self.subTest(slug=slug):
                 data['slug'] = slug
-                rv = self.client.post(reverse('event_add'), data, follow=False)
-                self.assertEqual(rv.status_code, 200)
+                f = EventForm(data)
+                self.assertEqual(f.is_valid(), False)
+                self.assertIn('slug', f.errors)
 
         # allow dashes in the slugs
         data['slug'] = 'a-b'
-        rv = self.client.post(reverse('event_add'), data, follow=False)
-        self.assertEqual(rv.status_code, 200)
+        f = EventForm(data)
+        self.assertEqual(f.is_valid(), False)
+        self.assertNotIn('slug', f.errors)
 
     def test_display_of_event_without_start_date(self):
         """A bug prevented events without start date to throw a 404.
diff --git a/workshops/test/test_person.py b/workshops/test/test_person.py
--- a/workshops/test/test_person.py
+++ b/workshops/test/test_person.py
@@ -8,6 +8,7 @@
 
 from django.test import TransactionTestCase
 
+from ..forms import PersonForm
 from ..models import (
     Person, Task, Qualification, Award, Role, Event, KnowledgeDomain, Badge,
     Lesson, Host
@@ -40,19 +41,11 @@ def test_edit_person_email_when_airport_not_set(self):
         self._test_edit_person_email(self.spiderman)
 
     def test_edit_person_empty_family_name(self):
-        url, values = self._get_initial_form_index(0, 'person_edit',
-                                                   self.ironman.id)
-        assert 'person-family' in values, \
-            'No family name in initial form'
-
-        values['person-family'] = '' # family name cannot be empty
-        response = self.client.post(url, values)
-        assert response.status_code == 200, \
-            'Expected error page with status 200, got status {0}'.format(response.status_code)
-        doc = self._parse(response=response)
-        errors = self._collect_errors(doc)
-        assert errors, \
-            'Expected error messages in response page'
+        data = {
+            'person-family': '',  # family name cannot be empty
+        }
+        f = PersonForm(data)
+        self.assertIn('family', f.errors)
 
     def _test_edit_person_email(self, person):
         url, values = self._get_initial_form_index(0, 'person_edit', person.id)
diff --git a/workshops/test/test_todos.py b/workshops/test/test_todos.py
--- a/workshops/test/test_todos.py
+++ b/workshops/test/test_todos.py
@@ -3,6 +3,7 @@
 from django.core.urlresolvers import reverse
 
 from ..models import Event, TodoItem, TodoItemQuerySet
+from ..forms import TodoFormSet
 from .base import TestBase
 
 
@@ -16,31 +17,46 @@ def setUp(self):
 
     def test_adding_many_todos(self):
         """Test if todos_add view really adds multiple standard TODOs to the
-        event."""
+        event. To speed things up, the calls to `todos_add` were cut out (since
+        there was no way to efficiently get fields from the webpage itself)."""
         event = Event.objects.filter(slug__endswith="-upcoming") \
-                             .order_by("-pk")[0]
-        event.end = event.start + timedelta(days=2)
-        event.save()
+                             .order_by("-pk").first()
 
         # check if the event has 0 todos
         assert event.todoitem_set.all().count() == 0
 
-        # add standard todos
-        ident = event.get_ident()
-        url, form = self._get_initial_form('todos_add', ident)
-
-        # fix: turn Nones into empty strings
-        for key, value in form.items():
-            if value is None:
-                form[key] = ''
-
-        rv = self.client.post(reverse('todos_add', args=[ident]), form)
-
-        # let's check if the form passes
-        assert rv.status_code == 302
+        # add some todos
+        data = {
+            'form-INITIAL_FORMS': 0,
+            'form-TOTAL_FORMS': 3,
+            'form-MIN_NUM_FORMS': 0,
+            'form-MAX_NUM_FORMS': 1000,
+            'form-0-id': '',
+            'form-0-title': 'Set date with host',
+            'form-0-due': '2016-01-24',
+            'form-0-additional': '',
+            'form-0-event': event.pk,
+            'form-0-completed': False,
+            'form-1-id': '',
+            'form-1-title': 'Set up a workshop website',
+            'form-1-due': '2016-01-24',
+            'form-1-additional': '',
+            'form-1-event': event.pk,
+            'form-1-completed': False,
+            'form-2-id': '',
+            'form-2-title': 'Find first instructor',
+            'form-2-due': '2016-01-24',
+            'form-2-additional': '',
+            'form-2-event': event.pk,
+            'form-2-completed': False,
+        }
+        formset = TodoFormSet(data)
+
+        assert formset.is_valid()
+        formset.save()
 
         # finally let's check there are some new todos
-        assert event.todoitem_set.all().count() == 9
+        assert event.todoitem_set.all().count() == 3
 
     def test_mark_completed(self):
         """Test if TODO is properly being marked as completed."""