Repository: carpentries/amy

Issue:
Modify "send us attendance figures" email to include host (if any)
The mailto: list for the message asking for attendance figures should include the hosts as well as the instructors.



Discussion:
@gvwilson:
1. We don't have any contact details in the Host object
2. But we do have Event.contact - text field for storing contact point(s) for the event
3. And yet it doesn't have a specified format, so it can even be a telephone number.

So I can see 3 options here:
1. Add contact to the Host model, or
2. Enforce some format on the Event.contact field, or
3. Find email addresses in the Event.contact field and add them to the attendance email

Any thoughts, @gvwilson?

We should have at least one Person awarded an Organizer badge for each 
Event.  I've been slack about doing this, but if we can get in the 
habit, that'll be simpler than trying to record contact info in Host 
organizations.

So what do you decide on this issue, @gvwilson?

I need to be better at doing this consistently but....
- for standard workshops I have been adding names to Persons in AMY and listing their tasks as Host for specific events.   This Person is not connected to the Host model though. I've been wanting to put in an issue for that.
- for self-organized workshops I've been naming the Organizer (usually also the instructor).

If we include everyone marked as Instructor or Organizer for an Event, that'll be good enough.

I'd actually like it to include Host as well.  Sometimes they are the ones managing registration & attendance and so they're the ones who actually have that information.


Resolving patch:
diff --git a/workshops/models.py b/workshops/models.py
--- a/workshops/models.py
+++ b/workshops/models.py
@@ -585,6 +585,30 @@ def uninvoiced(self):
         """Indicate if the event has been invoiced or not."""
         return self.invoice_status == 'not-invoiced'
 
+    @property
+    def mailto(self):
+        """Return list of emails we can contact about workshop details, like
+        attendance."""
+        from workshops.util import find_emails
+
+        emails = Task.objects \
+            .filter(event=self) \
+            .filter(
+                # we only want hosts, organizers and instructors
+                Q(role__name='host') | Q(role__name='organizer') |
+                Q(role__name='instructor')
+            ) \
+            .filter(person__may_contact=True) \
+            .exclude(Q(person__email='') | Q(person__email=None)) \
+            .values_list('person__email', flat=True)
+
+        additional_emails = find_emails(self.contact)
+        # Emails will become an iterator in 1.9 (ValuesListQuerySet previously)
+        # so we need a normal list that will be extended by that iterator.
+        # Bonus points: it works in 1.8.x too!
+        additional_emails.extend(emails)
+        return ','.join(additional_emails)
+
     def get_invoice_form_url(self):
         from .util import universal_date_format
 
diff --git a/workshops/util.py b/workshops/util.py
--- a/workshops/util.py
+++ b/workshops/util.py
@@ -13,6 +13,9 @@
 
 from workshops.models import Event, Role, Person, Task, Award, Badge
 
+WORD_SPLIT = re.compile(r'''([\s<>"']+)''')
+SIMPLE_EMAIL = re.compile(r'^\S+@\S+\.\S+$')
+
 
 class InternalError(Exception):
     pass
@@ -586,3 +589,22 @@ def default_membership_cutoff():
     earliest = datetime.date.today() - 2 * datetime.timedelta(days=365)
     latest = datetime.date.today()
     return earliest, latest
+
+
+def find_emails(text):
+    """Find emails in the text.  This is based on Django's own
+    `django.utils.html.urlize`."""
+    # Split into tokens in case someone uses for example
+    # 'Name <name@gmail.com>' format.
+    emails = []
+
+    for word in WORD_SPLIT.split(text):
+        if SIMPLE_EMAIL.match(word):
+            local, domain = word.rsplit('@', 1)
+            try:
+                domain = domain.encode('idna').decode('ascii')
+            except UnicodeError:
+                continue
+            emails.append('{}@{}'.format(local, domain))
+
+    return emails
diff --git a/workshops/views.py b/workshops/views.py
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -1534,8 +1534,6 @@ def instructor_num_taught(request):
 def workshop_issues(request):
     '''Display workshops in the database whose records need attention.'''
 
-    host = Role.objects.get(name='host')
-    instructor = Role.objects.get(name='instructor')
     events = Event.objects.past_events().filter(
         Q(attendance=None) | Q(attendance=0) |
         Q(country=None) |
@@ -1544,16 +1542,15 @@ def workshop_issues(request):
         Q(latitude=None) | Q(longitude=None) |
         Q(start__gt=F('end'))
     )
+
     for e in events:
-        tasks = Task.objects.filter(event=e).\
-            filter(Q(role=host) | Q(role=instructor))
-        e.mailto_ = ','.join([t.person.email for t in tasks if t.person.email])
         e.missing_attendance_ = (not e.attendance)
         e.missing_location_ = (
-            not e.country or not e.venue or not e.address or not e.latitude
-            or not e.longitude
+            not e.country or not e.venue or not e.address or not e.latitude or
+            not e.longitude
         )
         e.bad_dates_ = e.start and e.end and (e.start > e.end)
+
     context = {'title': 'Workshops with Issues',
                'events': events}
     return render(request, 'workshops/workshop_issues.html', context)


Test patch: