Repository: carpentries/amy

Issue:
Denote cancelled workshops in AMY
Add tag for cancelled workshops.
This should also result in the workshop being unpublished.  
(So published == has date and has location and has workshop page and  tag != "cancelled")



Discussion:
Would it be useful to have reasons (for stats and further analysis):
- cancelled due to lack of funds
- cancelled due to problems in finding instructors
- cancelled due to venue problems [not sure if that is interesting for us, I think the first two are]
- other reasons

@pbanaszkiewicz 
Since we also have to record the reason behind the cancellation, we can add a `CharField` with a choice that lists the possible reason to the `Event` model.
Is this schema change good enough?

@apawlik I think the potential reasons for canceling a workshop are too complex to enumerate.  

Also we are thinking of cancelled meaning that a workshop was completely planned - i.e., had instructors, a venue, registered learners, etc. and had to cancel.

If we start talking to a host about doing a workshop and they can't do it because of lack of funds, don't have a venue, etc.  then it is considered "stalled."  

@maneesha Yes, I agree that to enumerate all reasons is not possible. I was just wondering (based on the past experience) whether capturing how often it was due to lack of instructor vs lack of funds was interesting for us to note any patterns. Though if we define _cancelled_ as all fully planned, then this is not applicable.

I think this may be a question to the Steering Committees whether we want to capture any stats on how many workshops become stalled due to lack of funds/troubles in finding instructors. The latter used to be a headache in the UK (more than the former) and knowing that was informative for SSI (but whether this is for the SCs now, it's a different question).


Resolving patch:
diff --git a/workshops/migrations/0113_add_tag_cancelled.py b/workshops/migrations/0113_add_tag_cancelled.py
new file mode 100644
--- /dev/null
+++ b/workshops/migrations/0113_add_tag_cancelled.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.7 on 2016-07-30 06:37
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+def add_tag_cancelled(apps, schema_editor):
+    Tag = apps.get_model('workshops', 'Tag')
+    Tag.objects.get_or_create(name='cancelled',
+                              details='Events that were supposed to happen but'
+                                      ' due to some circumstances got '
+                                      'cancelled')
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0112_sponsorship_contact'),
+    ]
+
+    operations = [
+        migrations.RunPython(add_tag_cancelled),
+        migrations.AlterField(
+            model_name='event',
+            name='tags',
+            field=models.ManyToManyField(help_text="<ul><li><i>stalled</i> — for events with lost contact with the host or TTT events that aren't running.</li><li><i>unresponsive</i> – for events whose hosts and/or organizers aren't going to send us attendance data.</li><li><i>cancelled</i> — for events that were supposed to happen, but due to some circumstances got cancelled.</li></ul>", to='workshops.Tag'),
+        ),
+    ]
diff --git a/workshops/models.py b/workshops/models.py
--- a/workshops/models.py
+++ b/workshops/models.py
@@ -701,8 +701,8 @@ def ongoing_events(self):
 
     def unpublished_conditional(self):
         """Return conditional for events without: start OR country OR venue OR
-        url (ie. unpublished events). This will be used in
-        `self.published_events`, too."""
+        url OR are marked as 'cancelled' (ie. unpublished events). This will be
+        used in `self.published_events`, too."""
         unknown_start = Q(start__isnull=True)
         no_country = Q(country__isnull=True)
         no_venue = Q(venue__exact='')
@@ -710,9 +710,10 @@ def unpublished_conditional(self):
         no_latitude = Q(latitude__isnull=True)
         no_longitude = Q(longitude__isnull=True)
         no_url = Q(url__isnull=True)
+        cancelled = Q(tags__name='cancelled')
         return (
             unknown_start | no_country | no_venue | no_address | no_latitude |
-            no_longitude | no_url
+            no_longitude | no_url | cancelled
         )
 
     def unpublished_events(self):
@@ -761,6 +762,8 @@ class Event(AssignmentMixin, models.Model):
                   'the host or TTT events that aren\'t running.</li>'
                   '<li><i>unresponsive</i> – for events whose hosts and/or '
                   'organizers aren\'t going to send us attendance data.</li>'
+                  '<li><i>cancelled</i> — for events that were supposed to '
+                  'happen, but due to some circumstances got cancelled.</li>'
                   '</ul>',
     )
     administrator = models.ForeignKey(


Test patch:
diff --git a/workshops/test/base.py b/workshops/test/base.py
--- a/workshops/test/base.py
+++ b/workshops/test/base.py
@@ -298,6 +298,17 @@ def _setUpEvents(self):
                              host=test_host,
                              admin_fee=100)
 
+        # create a full-blown event that got cancelled
+        e = Event.objects.create(start=event_start, end=event_end,
+                                 slug='starts-today-cancelled',
+                                 url='http://example.org/cancelled-event',
+                                 latitude=-10.0, longitude=10.0,
+                                 country='US', venue='University',
+                                 address='Phenomenal Street',
+                                 host=test_host)
+        tags = Tag.objects.filter(name__in=['SWC', 'cancelled'])
+        e.tags.set(tags)
+
         # Record some statistics about events.
         self.num_uninvoiced_events = 0
         self.num_upcoming = 0
diff --git a/workshops/test/test_commands.py b/workshops/test/test_commands.py
--- a/workshops/test/test_commands.py
+++ b/workshops/test/test_commands.py
@@ -70,7 +70,7 @@ def test_new_tags_added(self):
         """Make sure we add tags that are hard-coded. They'll end up in
         fixtures in future (see #626)."""
         tags = ['SWC', 'DC', 'LC', 'WiSE', 'TTT', 'online', 'stalled',
-                'unresponsive', 'hackathon']
+                'unresponsive', 'hackathon', 'cancelled']
         self.assertNotEqual(set(tags),
                             set(Tag.objects.values_list('name', flat=True)))