From 51038a819ba7c203207466bf263dd17b6a9b8b6b Mon Sep 17 00:00:00 2001 From: Brian Moe <lars@moe.phys.uwm.edu> Date: Wed, 25 Nov 2009 14:41:08 -0600 Subject: [PATCH] Added user profile facility --- gracedb/alert.py | 53 ++++++++- gracedb/templatetags/flash.py | 139 ++++++++++++++++++++++ gracedb/views.py | 12 +- settings.py | 2 + settings_dev.py | 2 + static/css/admin-nav.css | 2 + static/css/style.css | 2 + templates/base.html | 7 ++ templates/profile/createNotification.html | 15 +++ templates/profile/notifications.html | 34 ++++++ urls.py | 1 + userprofile/__init__.py | 0 userprofile/admin.py | 21 ++++ userprofile/forms.py | 17 +++ userprofile/models.py | 61 ++++++++++ userprofile/tests.py | 23 ++++ userprofile/urls.py | 22 ++++ userprofile/views.py | 116 ++++++++++++++++++ 18 files changed, 521 insertions(+), 8 deletions(-) create mode 100644 gracedb/templatetags/flash.py create mode 100644 templates/profile/createNotification.html create mode 100644 templates/profile/notifications.html create mode 100644 userprofile/__init__.py create mode 100644 userprofile/admin.py create mode 100644 userprofile/forms.py create mode 100644 userprofile/models.py create mode 100644 userprofile/tests.py create mode 100644 userprofile/urls.py create mode 100644 userprofile/views.py diff --git a/gracedb/alert.py b/gracedb/alert.py index 96c7a1678..dad9aee1b 100644 --- a/gracedb/alert.py +++ b/gracedb/alert.py @@ -4,11 +4,13 @@ import time from subprocess import Popen, PIPE, STDOUT import StringIO -from django.core.mail import send_mail +from django.core.mail import send_mail, EmailMessage from django.conf import settings from django.contrib.sites.models import Site from django.core.urlresolvers import reverse, get_script_prefix +from gracedb.userprofile.models import Trigger, AnalysisType + import glue.ligolw.utils import glue.lvalert.utils @@ -32,13 +34,49 @@ def prepareSummary(event): # XXX TBD what exactly this summary is. return "GPS Time: %s" % event.gpstime + +def issueEmailAlertForLabel(event, label): + profileRecips = [] + atype = AnalysisType.objects.filter(code=event.analysisType)[0] + triggers = label.trigger_set.filter(atypes=atype) + for trigger in triggers: + for recip in trigger.contacts.all(): + profileRecips.append(recip.email) + + subject = "[gracedb] %s / %s / %s" % (label.name, event.get_analysisType_display(), event.graceid()) + + message = "A %s event with graceid %s was labelled with %s" % \ + (event.get_analysisType_display(), event.graceid(), label.name) + + if event.group.name == "Test": + fromaddress = settings.ALERT_TEST_EMAIL_FROM + toaddresses = settings.ALERT_TEST_EMAIL_TO + message += "\n\nWould have send email to: %s" % str(profileRecips) + else: + fromaddress = settings.ALERT_EMAIL_FROM + toaddresses = profileRecips + + if toaddresses: + email = EmailMessage(subject, message, fromaddress, [], toaddresses) + email.send() + + def issueEmailAlert(event, location): + + # Gather Recipients if event.group.name == 'Test': fromaddress = settings.ALERT_TEST_EMAIL_FROM - toaddress = settings.ALERT_TEST_EMAIL_TO + toaddresses = settings.ALERT_TEST_EMAIL_TO else: fromaddress = settings.ALERT_EMAIL_FROM - toaddress = settings.ALERT_EMAIL_TO + toaddresses = settings.ALERT_EMAIL_TO + + atype = AnalysisType.objects.filter(code=event.analysisType)[0] + triggers = atype.trigger_set.filter(labels=None) + for trigger in triggers: + for recip in trigger.contacts.all(): + toaddresses.append(recip.email) + subject = "[gracedb] %s event. ID: %s" % (event.get_analysisType_display(), event.graceid()) message = """ New Event @@ -58,8 +96,13 @@ Event Summary: event.weburl(), event.wikiurl(), event.submitter.name, - indent(3, prepareSummary(event))) - send_mail(subject, message, fromaddress, toaddress) + indent(3, prepareSummary(event)) + ) + + email = EmailMessage(subject, message, fromaddress, [], toaddresses) + email.send() + + #send_mail(subject, message, fromaddress, toaddresses) def issueXMPPAlert(event, location, temp_data_loc): nodename = "%s_%s"% (event.group.name, event.get_analysisType_display()) diff --git a/gracedb/templatetags/flash.py b/gracedb/templatetags/flash.py new file mode 100644 index 000000000..4093b2db2 --- /dev/null +++ b/gracedb/templatetags/flash.py @@ -0,0 +1,139 @@ +""" +To function this requires the following to be installed: + +TEMPLATE_CONTEXT_PROCESSORS +django.core.context_processors.request + +MIDDLEWARE_CLASSES +django.contrib.sessions.middleware.SessionMiddleware + +@author: Robert Conner (rtconner) +""" + +# It's pretty simple. Do something like this in your view .. + +# >>>request.session['flash_msg'] = 'Your changes have been save' +# >>>request.session['flash_params'] = {'type': 'success'} + +# And maybe put something like this in your template +# +# {% load flash %} +# {% flash %} +# <h2>{{ params.type }}</h2> +# {{ msg }} +# {% endflash %} + +# It also support a flash template, you can specify a file FLASH_TEMPLATE in +# your settings file and then that file will be rendered with msg and params as +# available variable. Usage for this would simply be {% flash_template %} and +# then you gotta make a template file that does whatever you like. + +# Outside of that just be aware you need the Django session middleware and +# request context installed in your app to use this. + + +from django import template +from django.template import resolve_variable, Context +import datetime +from django.template.loader import render_to_string +from django.contrib.sessions.models import Session +from django.conf import settings + +register = template.Library() + + +def session_clear(session): + """ + Private function, clear flash msgsfrom the session + """ + try: + del session['flash_msg'] + except KeyError: + pass + + try: + del session['flash_params'] + except KeyError: + pass + + # Save changes to session + if(session.session_key): + Session.objects.save(session.session_key, session._session, + datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)) + + +class RunFlashBlockNode(template.Node): + def __init__(self, nodelist): + self.nodelist = nodelist + + def render(self, context): + + session = context['request'].session + ret = None + if session.get('flash_msg', False): + ret = {'msg': session['flash_msg']} + if 'flash_params' in session: + ret['params'] = session.get('flash_params', False) + session_clear(session); + + if ret is not None: + context.update(ret) + return self.nodelist.render(context) + return '' + + +class RunFlashTemplateNode(template.Node): + def __init__(self): + pass + + def render(self, context): + session = context['request'].session + if session.get('flash_msg', False): + ret = {'msg': session['flash_msg']} + if 'flash_params' in session: + ret['params'] = session.get('flash_params', False) + + session_clear(session); + try: + template = settings.FLASH_TEMPLATE + except AttributeError: + template = 'elements/flash.html' + return render_to_string(template, dictionary=ret) + return '' + +@register.tag(name="flash_template") +def do_flash_template(parser, token): + """ + Call template if there is flash message in session + + Runs a check if there is a flash message in the session. + If the flash message exists it calls settings.FLASH_TEMPLATE + and passes the template the variables 'msg' and 'params'. + Calling this clears the flash from the session automatically + + To set a flash msg, in a view call: + request.session['flash_msg'] = 'sometihng' + request.session[flash_'params'] = {'note': 'remember me'} + + In the template {{ msg }} and {{ params.note }} are available + """ + return RunFlashTemplateNode() + +@register.tag(name="flash") +def do_flash_block(parser, token): + """ + A block section where msg and params are both available. + Calling this clears the flash from the session automatically + + If there is no flash msg, then nothing inside this block + gets rendered + + Example: + {% flash %} + {{msg}}<br /> + {{params.somekey}} + {% endflash %} + """ + nodelist = parser.parse(('endflash',)) + parser.delete_first_token() + return RunFlashBlockNode(nodelist) diff --git a/gracedb/views.py b/gracedb/views.py index 91c9173ba..ed906098b 100644 --- a/gracedb/views.py +++ b/gracedb/views.py @@ -10,7 +10,7 @@ from django.views.generic.list_detail import object_detail, object_list from models import Event, Group, EventLog, Labelling, Label from forms import CreateEventForm, EventSearchForm -from alert import issueAlert +from alert import issueAlert, issueEmailAlertForLabel from translator import handle_uploaded_data import os @@ -140,7 +140,7 @@ def _createEventFromForm(request, form): os.path.join(event.clusterurl(), "private", f.name), temp_data_loc) except Exception, e: - warnings += ["Problem handling event creation (%s)" % e] + warnings += ["Problem issuing an alert (%s)" % e] #return HttpResponseRedirect(reverse(view, args=[event.graceid()])) except Exception, e: # something went wrong. @@ -262,6 +262,7 @@ def cli_label(request): graceid = request.POST.get('graceid') labelName = request.POST.get('label') + d = {} event = graceid and Event.getByGraceid(graceid) try: @@ -281,7 +282,12 @@ def cli_label(request): log = EventLog(event=event, issuer=request.ligouser, comment=message) log.save() - msg = str({}) + try: + issueEmailAlertForLabel(event, label) + except Exception, e: + d['warning'] = "Problem issuing email alert (%s)" % str(e) + + msg = str(d) response = HttpResponse(mimetype='application/json') response.write(msg) response['Content-length'] = len(msg) diff --git a/settings.py b/settings.py index 50f7bcd01..29f250679 100644 --- a/settings.py +++ b/settings.py @@ -83,6 +83,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( "django.core.context_processors.debug", "django.core.context_processors.i18n", "django.core.context_processors.media", + "django.core.context_processors.request", "gracedb.middleware.auth.LigoAuthContext", ) @@ -113,4 +114,5 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.sites', 'gracedb.gracedb', + 'gracedb.userprofile', ) diff --git a/settings_dev.py b/settings_dev.py index 3c03f0fe5..67ef59ec6 100644 --- a/settings_dev.py +++ b/settings_dev.py @@ -75,6 +75,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( "django.core.context_processors.debug", "django.core.context_processors.i18n", "django.core.context_processors.media", + "django.core.context_processors.request", "gracedb.middleware.auth.LigoAuthContext", ) @@ -105,4 +106,5 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.sites', 'gracedb.gracedb', + 'gracedb.userprofile', ) diff --git a/static/css/admin-nav.css b/static/css/admin-nav.css index 3a309536f..d19d736a5 100644 --- a/static/css/admin-nav.css +++ b/static/css/admin-nav.css @@ -61,6 +61,7 @@ #archive #nav-archive a, #lab #nav-lab a, #reviews #nav-reviews a, +#userprofile #nav-userprofile a, #contact #nav-contact a { background: #a9b0ba; /* Nav selected color */ /* color:#fff; / * Use if bg is dark */ @@ -74,6 +75,7 @@ #archive #nav-archive a:hover, #lab #nav-lab a:hover, #reviews #nav-reviews a:hover, +#userprofile #nav-userprofile a:hover, #contact #nav-contact a:hover { /* background:#e35a00; */ background: #a9b0ba; /* Nav selected color */ diff --git a/static/css/style.css b/static/css/style.css index c0d737efd..0d82bfe5f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -125,6 +125,7 @@ a.link, a, a.active { #archive #nav-archive a, #lab #nav-lab a, #reviews #nav-reviews a, +#userprofile #nav-userprofile a, #contact #nav-contact a { background: #a9b0ba; /* Nav selected color */ /* color:#fff; / * Use if bg is dark */ @@ -138,6 +139,7 @@ a.link, a, a.active { #archive #nav-archive a:hover, #lab #nav-lab a:hover, #reviews #nav-reviews a:hover, +#userprofile #nav-userprofile a:hover, #contact #nav-contact a:hover { /* background:#e35a00; */ background: #a9b0ba; /* Nav selected color */ diff --git a/templates/base.html b/templates/base.html index 327c19629..893af0f37 100644 --- a/templates/base.html +++ b/templates/base.html @@ -61,12 +61,19 @@ function changeTime(obj, label) { <li id="nav-search"><a href="{% url search %}">Search</a></li> <li id="nav-create"><a href="{% url create %}">Create</a></li> <li id="nav-feeds"><a href="{% url feeds %}">RSS</a></li> + <li id="nav-userprofile"><a href="{% url userprofile-home %}">Options</a></li> {% if ligouser %}<li id="nav-user">Authenticated as: {{ ligouser.name }}</li>{% endif %} </ul> {% endblock %} <p> </p> <!-- bad way to create vertical space --> +{% load flash %} +{% flash %} + <div id="status_block" class="{{ params.class }}">{{ msg }}</div> +{% endflash %} + + <h2>{% block heading %}Title{% endblock %}</h2> diff --git a/templates/profile/createNotification.html b/templates/profile/createNotification.html new file mode 100644 index 000000000..a5a81e1d9 --- /dev/null +++ b/templates/profile/createNotification.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block title %}Options | Create {{ creating }}{% endblock %} +{% block heading %}Create {{ creating }}{% endblock %} +{% block pageid %}userprofile{% endblock %} + +{% block content %} +{{ explanation }} +<form method="POST"> + <table> + {{ form.as_table }} + </table> + <input type="submit" value="Submit"/> +</form> +{% endblock %} diff --git a/templates/profile/notifications.html b/templates/profile/notifications.html new file mode 100644 index 000000000..ea4105918 --- /dev/null +++ b/templates/profile/notifications.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block title %}Options | Notifications{% endblock %} +{% block heading %}Notifications{% endblock %} +{% block pageid %}userprofile{% endblock %} + +{% block content %} + +{% for trigger in triggers %} + <ul> + <li> + <!-- <a href="{% url userprofile-edit trigger.id %}">Edit</a> --> + <a href="{% url userprofile-delete trigger.id %}">Delete</a> + {{ trigger.userlessDisplay }} + </li> + </ul> +{% endfor %} + +<a href="{% url userprofile-create %}">Create New Notification</a> + +<h2>Contacts</h2> +{% for contact in contacts %} + <ul> + <li> + <!-- <a href="{% url userprofile-edit-contact contact.id %}">Edit</a> --> + <a href="{% url userprofile-delete-contact contact.id %}">Delete</a> + {{ contact.desc }} / {{ contact.email }} + </li> + </ul> +{% endfor %} + +<a href="{% url userprofile-create-contact %}">Create New Contact</a> + +{% endblock %} diff --git a/urls.py b/urls.py index 2cdb02cbd..1f3f70127 100644 --- a/urls.py +++ b/urls.py @@ -14,6 +14,7 @@ urlpatterns = patterns('', url (r'^$', 'gracedb.gracedb.views.index', name="home"), (r'^events/', include('gracedb.gracedb.urls')), + (r'^options/', include('gracedb.userprofile.urls')), (r'^cli/create', 'gracedb.gracedb.views.create'), (r'^cli/ping', 'gracedb.gracedb.views.ping'), (r'^cli/log', 'gracedb.gracedb.views.log'), diff --git a/userprofile/__init__.py b/userprofile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/userprofile/admin.py b/userprofile/admin.py new file mode 100644 index 000000000..95ff41db4 --- /dev/null +++ b/userprofile/admin.py @@ -0,0 +1,21 @@ + +from models import AnalysisType, Contact, Trigger + +from django.contrib import admin + +class AnalysisTypeManager(admin.ModelAdmin): + list_display = [ 'display' ] + +class ContactManager(admin.ModelAdmin): + pass +# list_display = [ 'user', 'desc' ] + +class TriggerManager(admin.ModelAdmin): + pass +# exclude = [ 'labels' ] +# list_display = [ 'user', ] + +admin.site.register(AnalysisType, AnalysisTypeManager) +admin.site.register(Contact, ContactManager) +admin.site.register(Trigger, TriggerManager) + diff --git a/userprofile/forms.py b/userprofile/forms.py new file mode 100644 index 000000000..e26ddf67b --- /dev/null +++ b/userprofile/forms.py @@ -0,0 +1,17 @@ +from django import forms +from django.db import models +from models import Trigger, Contact + +from django.forms.models import modelformset_factory + +class TriggerForm(forms.ModelForm): + class Meta: + model = Trigger + exclude = ['user', 'triggerType'] + + +class ContactForm(forms.ModelForm): + class Meta: + model = Contact + exclude = ['user'] + diff --git a/userprofile/models.py b/userprofile/models.py new file mode 100644 index 000000000..ba3ad70dc --- /dev/null +++ b/userprofile/models.py @@ -0,0 +1,61 @@ + +from django.db import models + +from gracedb.gracedb.models import User, Label, Event + + +#class Notification(models.Model): +# user = models.ForeignKey(User, null=False) +# onLabel = models.ManyToManyField(Label, blank=True) +# onTypeCreate = models.CharField(max_length=20, choices=TYPES, blank=True) +# onTypeChange = models.CharField(max_length=20, choices=TYPES, blank=True) +# email = models.EmailField() + +class AnalysisType(models.Model): + # XXX Event.analysisType should probably point to this. + # The choice list thing is obnoxious for notifications to track + code = models.CharField(max_length=20, unique=True) + display = models.CharField(max_length=20, unique=True) + + def __unicode__(self): + return self.display + +def populateAnalysisType(): + lastError = None + for code, display in Event.ANALYSIS_TYPE_CHOICES: + try: + atype = AnalysisType(code=code, display=display) + atype.save() + except Exception, e: + lastError = e + if lastError is not None: + raise lastError + +class Contact(models.Model): + user = models.ForeignKey(User, null=False) + desc = models.CharField(max_length=20) + email = models.EmailField() + + def __unicode__(self): + return "%s: %s" % (self.user.name, self.desc) + +class Trigger(models.Model): + TYPES = ( ("create", "create"), ("change","change"), ("label","label") ) + user = models.ForeignKey(User, null=False) + triggerType = models.CharField(max_length=20, choices=TYPES, blank=True) + labels = models.ManyToManyField(Label, blank=True) + atypes = models.ManyToManyField(AnalysisType, blank=True, verbose_name="Analysis Types") + contacts = models.ManyToManyField(Contact, blank=True) + + def __unicode__(self): + return ("%s: %s") % ( + self.user.name, + self.userlessDisplay() + ) + + def userlessDisplay(self): + return ("(%s) & (%s) -> %s") % ( + "|".join([a.display for a in self.atypes.all()]) or "any type", + "|".join([a.name for a in self.labels.all()]) or "creating", + ",".join([x.desc for x in self.contacts.all()]) + ) diff --git a/userprofile/tests.py b/userprofile/tests.py new file mode 100644 index 000000000..2247054b3 --- /dev/null +++ b/userprofile/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/userprofile/urls.py b/userprofile/urls.py new file mode 100644 index 000000000..00f08d595 --- /dev/null +++ b/userprofile/urls.py @@ -0,0 +1,22 @@ + +from django.conf.urls.defaults import * + + +urlpatterns = patterns('gracedb.userprofile.views', + url (r'^$', 'index', name="userprofile-home"), + url (r'^contact/create$', 'createContact', name="userprofile-create-contact"), + url (r'^contact/delete/(?P<id>[\d]+)$', 'deleteContact', name="userprofile-delete-contact"), + url (r'^contact/edit/(?P<id>[\d]+)$', 'editContact', name="userprofile-edit-contact"), + + url (r'^trigger/create$', 'create', name="userprofile-create"), + url (r'^trigger/delete/(?P<id>[\d]+)$', 'delete', name="userprofile-delete"), + url (r'^trigger/edit/(?P<id>[\d]+)$', 'edit', name="userprofile-edit"), + +# (r'^view/(?P<uid>[\w\d]+)', 'view'), +# (r'^edit/(?P<uid>[\w\d]+)', 'edit'), +# (r'^request_archive/(?P<uid>[\w\d]+)(?P<rescind>/rescind)?', 'request_archive'), +# (r'^approve_archive/(?P<uid>[\w\d]+)(?P<rescind>/rescind)?', 'approve_archive'), +# url (r'^query', 'query', name="search"), +# url (r'^mine/$', 'mine', name="mine"), +# url (r'^myapprovals/$', 'myapprovals', name="myapprovals"), +) diff --git a/userprofile/views.py b/userprofile/views.py new file mode 100644 index 000000000..244ecb81e --- /dev/null +++ b/userprofile/views.py @@ -0,0 +1,116 @@ + +from django.http import HttpResponse +from django.http import HttpResponseRedirect, HttpResponseNotFound +from django.http import Http404, HttpResponseForbidden + +from django.core.urlresolvers import reverse + +from django.template import RequestContext +from django.shortcuts import render_to_response + +from gracedb.userprofile.models import Trigger, Contact + +from forms import TriggerForm, ContactForm + +def index(request): + triggers = Trigger.objects.filter(user=request.ligouser) + contacts = Contact.objects.filter(user=request.ligouser) + d = { 'triggers' : triggers, 'contacts': contacts } + return render_to_response('profile/notifications.html', + d, + context_instance=RequestContext(request)) + +def create(request): + explanation = "" + message = "" + if request.method == "POST": + form = TriggerForm(request.POST) + if form.is_valid(): + # Create the Trigger + t = Trigger(user=request.ligouser) + labels = form.cleaned_data['labels'] + atypes = form.cleaned_data['atypes'] + contacts = form.cleaned_data['contacts'] + + if contacts and (labels or atypes): + t.save() # Need an id before relations can be set. + try: + t.labels = labels + t.atypes = atypes + t.contacts = contacts + except: + t.delete() + t.save() + request.session['flash_msg'] = "Created: %s" % t.userlessDisplay() + return HttpResponseRedirect(reverse(index)) + + # Data was bad + if not contacts: + message += "You must specify at least one contact. " + if not (labels or atypes): + message += "You need to indicate label(s) and/or analysis type(s)." + else: + form = TriggerForm() + if message: + request.session['flash_msg'] = message + return render_to_response('profile/createNotification.html', + { "form" : form, + "creating":"Notification", + "explanation": explanation, + }, + context_instance=RequestContext(request)) + +def edit(request, id): + raise Http404 + +def delete(request, id): + try: + t = Trigger.objects.get(id=id) + except Trigger.DoesNotExist: + raise Http404 + if request.ligouser != t.user: + return HttpResponseForbidden("NO!") + request.session['flash_msg'] = "Notification Deleted: %s" % t.userlessDisplay() + t.delete() + return index(request) + +#-------------- +#-- Contacts -- +#-------------- + +def createContact(request): + if request.method == "POST": + form = ContactForm(request.POST) + if form.is_valid(): + # Create the Contact + c = Contact( + user=request.ligouser, + desc = form.cleaned_data['desc'], + email = form.cleaned_data['email'] + ) + c.save() + request.session['flash_msg'] = "Created: %s" % c + return HttpResponseRedirect(reverse(index)) + else: + form = ContactForm() + return render_to_response('profile/createNotification.html', + { "form" : form, + "creating":"Contact", + }, + context_instance=RequestContext(request)) + + +def editContact(request, id): + raise Http404 + +def deleteContact(request, id): + try: + c = Contact.objects.get(id=id) + except Contact.DoesNotExist: + raise Http404 + if request.ligouser != c.user: + return HttpResponseForbidden("NO!") + request.session['flash_msg'] = "Notification Deleted: %s" % c + c.delete() + return index(request) + -- GitLab