Implement a send started email feature.
authorDiane Trout <diane@caltech.edu>
Thu, 20 Aug 2009 23:40:12 +0000 (23:40 +0000)
committerDiane Trout <diane@caltech.edu>
Thu, 20 Aug 2009 23:40:12 +0000 (23:40 +0000)
The UI is linked to from the admin page by overriding the django
admin change_form for experments/flowcell to include an new link
in the object-tools block.

The actual page is implemented as a custom view. Its designed to
render the emails to a preview page, and then, at the bottom
of the page there's a button to actually send the emails.

It'll re-load the page but with a small status message about
what happened with the email in question.

Additionally this requires that users be attached to affiliations
and that users have email addresses.

I forgot we were also going to extract an email address from the
affiliation as well, if it was set.

htsworkflow/frontend/experiments/experiments.py
htsworkflow/frontend/experiments/urls.py
htsworkflow/frontend/experiments/views.py
htsworkflow/frontend/samples/models.py
htsworkflow/frontend/settings.py
htsworkflow/frontend/templates/admin/experiments/flowcell/change_form.html [new file with mode: 0644]
htsworkflow/frontend/templates/experiments/email_preview.html [new file with mode: 0644]
htsworkflow/frontend/templates/experiments/index.html
htsworkflow/frontend/templates/experiments/started_email.html [new file with mode: 0644]

index 0d3f81f7490dc05a39faf593db2f591afccba65f..cfea75f895e31ca2fcb2c4aba2559f768678e337 100755 (executable)
@@ -1,9 +1,9 @@
 # some core functions of the exp tracker module
-from django.http import HttpResponse
-from datetime import datetime
-from string import *
+from datetime import datetime, timedelta
 import os
 import re
+
+from django.http import HttpResponse
 from htsworkflow.frontend import settings
 from htsworkflow.frontend.experiments.models import FlowCell, DataRun
 from htsworkflow.frontend.samples.models import Library
@@ -178,3 +178,55 @@ def getLaneLibs(req):
     else: outputfile = 'Missing input: flowcell id'
 
     return HttpResponse(outputfile, mimetype='text/plain')
+
+def estimateFlowcellDuration(flowcell):
+    """
+    Attempt to estimate how long it will take to run a flowcell
+
+    """
+    # (3600 seconds * 1.5 hours per cycle )
+    sequencing_seconds_per_cycle= 3600 * 1.5
+    # 800 is a rough guess
+    pipeline_seconds_per_cycle = 800
+    
+    cycles = flowcell.read_length
+    if flowcell.paired_end:
+        cycles *= 2
+    sequencing_time = timedelta(0, cycles * sequencing_seconds_per_cycle)
+    analysis_time = timedelta(0, cycles * pipeline_seconds_per_cycle)
+    estimate_mid = sequencing_time + analysis_time
+    # floor estimate_mid
+    estimate_low = timedelta(estimate_mid.days, 0)
+    # floor estimate_mid and add a day
+    estimate_high = timedelta(estimate_mid.days+1, 0)
+    
+    return (estimate_low, estimate_high)
+    
+
+def makeUserLaneMap(flowcell):
+    """
+    Given a flowcell return a mapping of users interested in
+    the libraries on those lanes.
+    """
+    users = {}
+
+    for lane in flowcell.lane_set.all():
+        for affiliation in lane.library.affiliations.all():
+            for user in affiliation.users.all():
+                users.setdefault(user,[]).append(lane)
+
+    return users
+
+def makeUserLibrarMap(libraries):
+    """
+    Given an interable set of libraries return a mapping or
+    users interested in those libraries.
+    """
+    users = {}
+    
+    for library in libraries:
+        for affiliation in library.affiliations.all():
+            for user in affiliation.users.all():
+                users.setdefault(user,[]).append(library)
+                
+    return users
index c4df6a889220ce3e60b92530c844e52d4d6b0d1a..aaefba3b4cc2ca7cbf5987e0b6f961ed746a808a 100755 (executable)
@@ -1,12 +1,17 @@
 from django.conf.urls.defaults import *
 
 urlpatterns = patterns('',
-                                                                                                      
     (r'^$', 'htsworkflow.frontend.experiments.views.index'),
     #(r'^liblist$', 'htsworkflow.frontend.experiments.views.test_Libs'),
     #(r'^(?P<run_folder>.+)/$', 'gaworkflow.frontend.experiments.views.detail'),
-    (r'^(?P<fcid>.+)/$', 'htsworkflow.frontend.experiments.views.makeFCSheet'),
+    (r'^fcsheet/(?P<fcid>.+)/$', 'htsworkflow.frontend.experiments.views.makeFCSheet'),
     (r'^updStatus$', 'htsworkflow.frontend.experiments.experiments.updStatus'),
     (r'^getConfile$', 'htsworkflow.frontend.experiments.experiments.getConfile'),
-    (r'^getLanesNames$', 'htsworkflow.frontend.experiments.experiments.getLaneLibs')   
+    (r'^getLanesNames$', 'htsworkflow.frontend.experiments.experiments.getLaneLibs'),
+    # for the following two URLS I have to pass in the primary key
+    # because I link to the page from an overridden version of the admin change_form
+    # which only makes the object primary key available in the form.
+    # (Or at least as far as I could tell)
+    (r'^started/(?P<pk>.+)/$', 'htsworkflow.frontend.experiments.views.startedEmail'),
+    (r'^finished/(?P<pk>.+)/$', 'htsworkflow.frontend.experiments.views.finishedEmail'),
 )
index a2d14bb3823f487bf5719b1be0b05c1b33621af0..92f6cbd9227bcffa2eb465177f4d226c3ccc30ba 100755 (executable)
@@ -1,10 +1,18 @@
 # Create your views here.
 #from django.template import Context, loader
 #shortcut to the above modules
+from django.contrib.auth.decorators import user_passes_test
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.mail import EmailMessage, mail_managers
+from django.http import HttpResponse
 from django.shortcuts import render_to_response, get_object_or_404
+from django.template import Context
+from django.template.loader import get_template
+
 from htsworkflow.frontend.experiments.models import *
-from django.http import HttpResponse
-from django.core.exceptions import ObjectDoesNotExist
+from htsworkflow.frontend.experiments.experiments import \
+     estimateFlowcellDuration, \
+     makeUserLaneMap
 
 def index(request):
     all_runs = DataRun.objects.order_by('-run_start_time')
@@ -32,3 +40,82 @@ def makeFCSheet(request,fcid):
     pass
   lanes = ['1','2','3','4','5','6','7','8']
   return render_to_response('experiments/flowcellSheet.html',{'fc': rec})
+
+
+@user_passes_test(lambda u: u.is_staff)
+def startedEmail(request, pk):
+    """
+    Create the we have started processing your samples email
+    """
+    fc = get_object_or_404(FlowCell, id=pk)
+
+    send = request.REQUEST.get('send',False)
+    if send in ('1', 'on', 'True', 'true', True):
+        send = True
+    else:
+        send = False
+
+    bcc_managers = request.REQUEST.get('bcc', False)
+    if bcc_managers in ('on', '1', 'True', 'true'):
+        bcc_managers = True
+    else:
+        bcc_managers = False
+
+    user_lane = makeUserLaneMap(fc)
+    estimate_low, estimate_high = estimateFlowcellDuration(fc)
+    email_verify = get_template('experiments/email_preview.html')
+    email_template = get_template('experiments/started_email.html')
+    sender = settings.NOTIFICATION_SENDER
+
+    warnings = []
+    emails = []
+    
+    for user in user_lane.keys():
+        sending = ""
+        # build body
+        context = Context({u'flowcell': fc,
+                   u'lanes': user_lane[user],
+                   u'runfolder': 'blank',
+                   u'finish_low': estimate_low,
+                   u'finish_high': estimate_high,
+                   u'user_admin': user.admin_url(),
+                  })
+
+        # build view
+        subject = "Flowcell  %s" % ( fc.flowcell_id )
+        body = email_template.render(context)
+
+        # provide warning
+        has_email = True
+        if user.email is None or len(user.email) == 0:
+            warnings.append((user.admin_url(), user.username))
+            has_email = False
+            
+        if send:
+            if has_email:
+                email = EmailMessage(subject, body, sender, to=[user.email])
+                if bcc_managers:
+                    print 'bcc_managers', bcc_managers
+                    email.bcc = settings.MANAGERS
+                print email.to, email.bcc
+                email.send()
+                sending = "sent"
+            else:
+                print settings.MANAGERS
+                mail_managers("Couldn't send to "+user.username, body)
+                sending = "bounced to managers"
+
+        emails.append((user.email, subject, body, sending))
+
+    verify_context = Context({
+        'send': send,
+        'warnings': warnings,
+        'emails': emails,
+        'from': sender,
+        })
+    return HttpResponse(email_verify.render(verify_context))
+    
+def finishedEmail(request, pk):
+    """
+    """
+    return HttpResponse("I've got nothing.")
index 4c5df1c1cb11d51101ab471a07fa8ddeebfa88e4..38c1863bb84e806b7ffdbbd93054420cad4b09b7 100644 (file)
@@ -269,4 +269,6 @@ class HTSUser(User):
 
     class Meta:
         ordering = ['username']
-        
+
+    def admin_url(self):
+        return '/admin/%s/%s/%d' % (self._meta.app_label, self._meta.module_name, self.id)
index 675da571863af24483f254b7f2ec5dbf38398751..91eccf967e301615ae535fa6736df7c53d26ed3c 100644 (file)
@@ -26,17 +26,18 @@ The options understood by this module are (with their defaults):
 """
 import ConfigParser
 import os
+import shlex
 
 # make epydoc happy
 __docformat__ = "restructuredtext en"
 
-def options_to_list(dest, section_name):
+def options_to_list(options, dest, section_name, option_name):
   """
   Load a options from section_name and store in a dictionary
   """
-  if options.has_section(section_name):
-    for name in options.options(section_name):
-      dest.append( options.get(section_name, name) )
+  if options.has_option(section_name, option_name):
+    opt = options.get(section_name, option_name)
+    dest.extend( shlex.split(opt) )
       
 def options_to_dict(dest, section_name):
   """
@@ -80,9 +81,10 @@ DEBUG = True
 TEMPLATE_DEBUG = DEBUG
 
 ADMINS = []
-options_to_list(ADMINS, 'admins')
+options_to_list(options, ADMINS, 'frontend', 'admins')
 
-MANAGERS = ADMINS
+MANAGERS = []
+options_to_list(options, MANAGERS, 'frontend', 'managers')
 
 AUTHENTICATION_BACKENDS = ( 'samples.auth_backend.HTSUserModelBackend', )
 CUSTOM_USER_MODEL = 'samples.HTSUser' 
@@ -90,6 +92,11 @@ CUSTOM_USER_MODEL = 'samples.HTSUser'
 EMAIL_HOST = options.get('frontend', 'email_host')
 EMAIL_PORT = int(options.get('frontend', 'email_port'))
 
+if options.has_option('frontend', 'notification_sender'):
+    NOTIFICATION_SENDER = options.get('frontend', 'notification_sender')
+else:
+    NOTIFICATION_SENDER = "noreply@example.com"
+
 # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
 DATABASE_ENGINE = options.get('frontend', 'database_engine')
 
diff --git a/htsworkflow/frontend/templates/admin/experiments/flowcell/change_form.html b/htsworkflow/frontend/templates/admin/experiments/flowcell/change_form.html
new file mode 100644 (file)
index 0000000..2a44ee2
--- /dev/null
@@ -0,0 +1,12 @@
+{% extends "admin/change_form.html" %}
+{% load i18n %}
+{% block object-tools %}
+{% if change %}{% if not is_popup %}
+  <ul class="object-tools">
+    <li><a href="../../../../{{ app_label }}/started/{{ object_id }}/">{% trans "Started Email" %}</a></li>
+    <li><a href="history/" class="historylink">{% trans "History" %}</a></li>
+  {% if has_absolute_url %}<li><a href="../../../r/{{ content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans
+ "View on site" %}</a></li>{% endif%}
+  </ul>
+{% endif %}{% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/htsworkflow/frontend/templates/experiments/email_preview.html b/htsworkflow/frontend/templates/experiments/email_preview.html
new file mode 100644 (file)
index 0000000..a045c74
--- /dev/null
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html> <head>
+<title></title>
+</head>
+<body>
+<p>
+{% for user_admin_url, username in warnings %}
+Warning: User <a href="{{ user_admin_url}}">{{ username }}</a> has no
+email address <br/>
+{% endfor %}
+</p>
+{% for to, subject, body, sending in emails %}
+<hr/>
+{% if sending %}<b>Message:</b> {{ sending }}<br/>{% endif %}
+<b>From:</b> {{ from }}<br/>
+<b>To:</b> {{ to }}<br/>
+<b>Subject:</b> {{ subject }}<br/>
+{{ body }}
+{% endfor %}<hr/>
+<form method="get">
+<label for="bcc">BCC Managers?</label>
+<input type="checkbox" id="bcc" name="bcc" checked="on"/><br/>
+<input type="hidden" name="send" value="1"/>
+<input type="submit" value="Send Email"/>
+</body>
+</html>
index 0d817d20f70c245def4b0123f35a72d5eb679529..28c24bff13c13d5a13d8fe5e540268cd13e706cb 100644 (file)
@@ -1,9 +1,14 @@
 {% if data_run_list %}
-    <ul>
-    {% for run in data_run_list %}
-        <li><a href="{{ run.fcid }}">{{ run.run_folder }}</a></li>
-    {% endfor %}
-    </ul>
+    <table>
+      {% for run in data_run_list %}
+        <tr>
+          <td>{{run.run_folder}}</td>
+          <td><a href="fcsheet/{{run.fcid}}">sheet</td>
+          <td><a href="started/{{run.fcid}}">started email</td>
+          <td><a href="finished/{{run.fcid}}">finished email</td>
+       </tr>
+      {% endfor %}
+   </table>
 {% else %}
     <p>No data runs are available.</p>
 {% endif %}
diff --git a/htsworkflow/frontend/templates/experiments/started_email.html b/htsworkflow/frontend/templates/experiments/started_email.html
new file mode 100644 (file)
index 0000000..9434c93
--- /dev/null
@@ -0,0 +1,19 @@
+<p>
+The following libraries are on the flowcell {{ flowcell.flowcell_id }}
+which is a {{ flowcell.read_length }} base pair {% if flowcell.paired_end %}paired end{% else %}single ended{% endif %} flowcell.
+</p>
+<p>{% for lane in lanes %}
+Lane #{{ lane.lane_number }} : 
+<a href="https://jumpgate.caltech.edu/library/{{lane.library.library_id}}">
+{{ lane.library.library_id }}</a>
+{{ lane.library.library_name }}<br/>
+{% endfor %}</p>
+<p>
+The data should be available at the following link when
+the pipeline finishes, probably in about
+{{ finish_low.days }} to {{ finish_high.days }} days after the flowcell is started.
+</p>
+<p>
+<a href="https://jumpgate.caltech.edu/runfolders/cellcenter/">
+https://jumpgate.caltech.edu/runfolders/cellcenter/
+</a></p>