From 81cfb043f06ab71da7c021a063f80f6df58305cc Mon Sep 17 00:00:00 2001 From: Parker Berberian Date: Wed, 24 Oct 2018 15:12:32 -0400 Subject: Rewrite Notification subsystem In this commit: - delete a lot of really bad and / or unused code - redesign a much simpler Notification model - create and send notifications to the user's inbox on booking start & end - migrations - emails user when booking is ready and when it ends Not in this commit: - Creating notifications from lab messages - warning messages when a booking is about to end - creating "summary" notifications when e.g. a booking has been fulfilled by a lab Change-Id: I69b4dc36c3f2bce76d810106baadeef5a562cc7d Signed-off-by: Parker Berberian --- dashboard/src/api/models.py | 12 ++ dashboard/src/api/serializers/old_serializers.py | 7 - dashboard/src/api/urls.py | 1 - dashboard/src/api/views.py | 10 +- dashboard/src/dashboard/tasks.py | 9 +- dashboard/src/notifier/admin.py | 6 +- dashboard/src/notifier/dispatchers.py | 33 ---- dashboard/src/notifier/manager.py | 181 ++++++++++++--------- .../notifier/migrations/0002_auto_20181102_1631.py | 44 +++++ dashboard/src/notifier/models.py | 40 +---- dashboard/src/notifier/views.py | 11 +- dashboard/src/pharos_dashboard/settings.py | 4 - dashboard/src/templates/notifier/email_ended.txt | 21 +++ .../src/templates/notifier/email_fulfilled.txt | 17 ++ dashboard/src/templates/notifier/end_booking.html | 36 ++++ dashboard/src/templates/notifier/inbox.html | 2 +- dashboard/src/templates/notifier/new_booking.html | 34 ++++ dashboard/src/workflow/models.py | 7 +- 18 files changed, 291 insertions(+), 184 deletions(-) delete mode 100644 dashboard/src/notifier/dispatchers.py create mode 100644 dashboard/src/notifier/migrations/0002_auto_20181102_1631.py create mode 100644 dashboard/src/templates/notifier/email_ended.txt create mode 100644 dashboard/src/templates/notifier/email_fulfilled.txt create mode 100644 dashboard/src/templates/notifier/end_booking.html create mode 100644 dashboard/src/templates/notifier/new_booking.html (limited to 'dashboard/src') diff --git a/dashboard/src/api/models.py b/dashboard/src/api/models.py index 7448ac4..9a7f775 100644 --- a/dashboard/src/api/models.py +++ b/dashboard/src/api/models.py @@ -217,6 +217,18 @@ class Job(models.Model): tasklist += list(cls.objects.filter(job=self).filter(status=status)) return tasklist + def is_fulfilled(self): + """ + This method should return true if all of the job's tasks are done, + and false otherwise + """ + my_tasks = self.get_tasklist() + for task in my_tasks: + if task.status != JobStatus.DONE: + return False + return True + + def get_delta(self, status): d = {} j = {} diff --git a/dashboard/src/api/serializers/old_serializers.py b/dashboard/src/api/serializers/old_serializers.py index f50b90b..0944881 100644 --- a/dashboard/src/api/serializers/old_serializers.py +++ b/dashboard/src/api/serializers/old_serializers.py @@ -11,13 +11,6 @@ from rest_framework import serializers from account.models import UserProfile -from notifier.models import Notifier - - -class NotifierSerializer(serializers.ModelSerializer): - class Meta: - model = Notifier - fields = ('id', 'title', 'content', 'user', 'sender', 'message_type', 'msg_sent') class UserSerializer(serializers.ModelSerializer): diff --git a/dashboard/src/api/urls.py b/dashboard/src/api/urls.py index 94f8279..4b1fe40 100644 --- a/dashboard/src/api/urls.py +++ b/dashboard/src/api/urls.py @@ -32,7 +32,6 @@ from api.views import * router = routers.DefaultRouter() router.register(r'bookings', BookingViewSet) -router.register(r'notifier', NotifierViewSet) router.register(r'user', UserViewSet) urlpatterns = [ diff --git a/dashboard/src/api/views.py b/dashboard/src/api/views.py index 072354f..cc3a668 100644 --- a/dashboard/src/api/views.py +++ b/dashboard/src/api/views.py @@ -21,11 +21,11 @@ from django.views.decorators.csrf import csrf_exempt import json from api.serializers.booking_serializer import * -from api.serializers.old_serializers import NotifierSerializer, UserSerializer +from api.serializers.old_serializers import UserSerializer from account.models import UserProfile from booking.models import Booking -from notifier.models import Notifier from api.models import * +from notifier.manager import NotificationHandler class BookingViewSet(viewsets.ModelViewSet): @@ -34,11 +34,6 @@ class BookingViewSet(viewsets.ModelViewSet): filter_fields = ('resource', 'id') -class NotifierViewSet(viewsets.ModelViewSet): - queryset = Notifier.objects.none() - serializer_class = NotifierSerializer - - class UserViewSet(viewsets.ModelViewSet): queryset = UserProfile.objects.all() serializer_class = UserSerializer @@ -87,6 +82,7 @@ def specific_task(request, lab_name="", job_id="", task_id=""): if 'message' in request.POST: task.message = request.POST.get('message') task.save() + NotificationHandler.task_updated(task) d = {} d['task'] = task.config.get_delta() m = {} diff --git a/dashboard/src/dashboard/tasks.py b/dashboard/src/dashboard/tasks.py index 48008b6..c619642 100644 --- a/dashboard/src/dashboard/tasks.py +++ b/dashboard/src/dashboard/tasks.py @@ -13,17 +13,11 @@ from celery import shared_task from django.utils import timezone from django.db.models import Q from booking.models import Booking -from notifier.manager import * -from notifier.models import * +from notifier.manager import NotificationHandler from api.models import * from resource_inventory.resource_manager import ResourceManager -@shared_task -def conjure_aggregate_notifiers(): - NotifyPeriodic.task() - - @shared_task def booking_poll(): def cleanup_hardware(qs): @@ -86,6 +80,7 @@ def booking_poll(): cleanup_access(AccessRelation.objects.filter(job=job)) job.complete = True job.save() + NotificationHandler.notify_booking_end(booking) @shared_task diff --git a/dashboard/src/notifier/admin.py b/dashboard/src/notifier/admin.py index d3e8be5..4a2984c 100644 --- a/dashboard/src/notifier/admin.py +++ b/dashboard/src/notifier/admin.py @@ -9,8 +9,6 @@ from django.contrib import admin -from notifier.models import * +from notifier.models import Notification -admin.site.register(Notifier) -admin.site.register(MetaBooking) -admin.site.register(LabMessage) +admin.site.register(Notification) diff --git a/dashboard/src/notifier/dispatchers.py b/dashboard/src/notifier/dispatchers.py deleted file mode 100644 index 1b66b37..0000000 --- a/dashboard/src/notifier/dispatchers.py +++ /dev/null @@ -1,33 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - -from django.db.models.signals import pre_save -from django.dispatch import receiver -from django.contrib import messages -from django.core.mail import send_mail - -class DispatchHandler(): - - @receiver(pre_save, sender='notifier.Notifier') - def dispatch(sender, instance, *args, **kwargs): - try: - msg_type = getattr(DispatchHandler, instance.message_type) - msg_type(instance) - except AttributeError: - instance.msg_sent = 'no dispatcher by given name exists: sending by email' - email(instance) - - def email(instance): - if instance.msg_sent != 'no dispatcher by given name exists: sending by email': - instance.msg_sent = 'by email' - send_mail(instance.title,instance.content, - instance.sender,[instance.user.email_addr], fail_silently=False) - - def webnotification(instance): - instance.msg_sent='by web notification' diff --git a/dashboard/src/notifier/manager.py b/dashboard/src/notifier/manager.py index a705d00..cc1aa16 100644 --- a/dashboard/src/notifier/manager.py +++ b/dashboard/src/notifier/manager.py @@ -1,98 +1,125 @@ ############################################################################## -# Copyright (c) 2018 Sawyer Bergeron and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## +import os +from notifier.models import Notification -from booking.models import * -from notifier.models import Notifier, MetaBooking, LabMessage -from django.utils import timezone -from datetime import timedelta -from django.template import Template, Context -from account.models import UserProfile +from django.core.mail import send_mail +from django.template.loader import render_to_string -from django.db import models -class NotifyPeriodic(object): - def task(): - bookings_new = Booking.objects.filter(metabooking__isnull=True) - bookings_old = Booking.objects.filter(end__lte=timezone.now() + timedelta(hours=24)).filter(metabooking__ended_notified=False) +class NotificationHandler(object): - for booking in bookings_old: - metabooking = booking.metabooking - if booking.end <= timezone.now() + timedelta(hours=24): - if not metabooking.ending_notified: - Notify().notify(Notify.TOCLEAN, booking) - metabooking.ending_notified = True - metabooking.save() - if booking.end <= timezone.now(): - metabooking = booking.metabooking - if not metabooking.ended_notified: - Notify().notify(Notify.CLEANED, booking) - metabooking.ended_notified = True - metabooking.save() + @classmethod + def notify_new_booking(cls, booking): + template = "notifier/new_booking.html" + titles = ["You have a new Booking", "You have been added to a Booking"] + cls.booking_notify(booking, template, titles) - for booking in bookings_new: - metabooking = MetaBooking() - metabooking.booking = booking - metabooking.created_notified = True - metabooking.save() + @classmethod + def notify_booking_end(cls, booking): + template = "notifier/end_booking.html" + titles = ["Your booking has ended", "A booking you collaborate on has ended"] + cls.booking_notify(booking, template, titles) - Notify().notify(Notify.CREATED, booking) + @classmethod + def booking_notify(cls, booking, template, titles): + """ + Creates a notification for a booking owner and collaborators + using the template. + titles is a list - the first is the title for the owner's notification, + the last is the title for the collaborators' + """ + owner_notif = Notification.objects.create( + title=titles[0], + content=render_to_string(template, context={ + "booking": booking, + "owner": True + }) + ) + owner_notif.recipients.add(booking.owner) + if not booking.collaborators.all().exists(): + return # no collaborators - were done + collab_notif = Notification.objects.create( + title=titles[-1], + content=render_to_string(template, context={ + "booking": booking, + "owner": False + }) + ) + for c in booking.collaborators.all(): + collab_notif.recipients.add(c) -class Notify(object): + @classmethod + def email_job_fulfilled(cls, job): + template_name = "notifier/email_fulfilled.txt" + all_tasks = job.get_tasklist() + users = list(job.booking.collaborators.all()) + users.append(job.booking.owner) + for user in users: + user_tasklist = [] + # gather up all the relevant messages from the lab + for task in all_tasks: + if (not hasattr(task, "user")) or task.user == user: + user_tasklist.append({ + "title": task.type_str + " Message: ", + "content": task.message + }) + # gather up all the other needed info + context = { + "user_name": user.userprofile.full_name, + "messages": user_tasklist, + "booking_url": os.environ.get("DASHBOARD_URL", "") + "/booking/detail/" + str(job.booking.id) + "/" + } - CREATED = "created" - TOCLEAN = "toclean" - CLEANED = "cleaned" + # render email template + message = render_to_string(template_name, context) - TITLES = {} - TITLES["created"] = "Your booking has been confirmed" - TITLES["toclean"] = "Your booking is ending soon" - TITLES["cleaned"] = "Your booking has ended" + # finally, send the email + send_mail( + "Your Booking is Ready", + message, + os.environ.get("DEFAULT_FROM_EMAIL", "opnfv@pharos-dashboard"), + user.userprofile.email_addr, + fail_silently=False + ) - """ - Lab message is provided with the following context elements: - * if is for owner or for collaborator (if owner) - * recipient username (.username) - * recipient full name (.userprofile.full_name) - * booking it pertains to (booking) - * status message should convey (currently "created", "toclean" and "cleaned" as strings) - It should be a django template that can be rendered with these context elements - and should generally use all of them in one way or another. - It should be applicable to email, the web based general view, and should be scalable for - all device formats across those mediums. - """ - def notify(self, notifier_type, booking): - template = Template(LabMessage.objects.filter(lab=booking.lab).first().msg) + @classmethod + def email_booking_over(cls, booking): + template_name = "notifier/email_ended.txt" + hostnames = [host.template.resource.name for host in booking.resource.hosts.all()] + users = list(booking.collaborators.all()) + users.append(booking.owner) + for user in users: + context = { + "user_name": user.userprofile.full_name, + "booking": booking, + "hosts": hostnames, + "booking_url": os.environ.get("DASHBOARD_URL", "") + "/booking/detail/" + str(booking.id) + "/" + } - context = {} - context["owner"] = booking.owner - context["notify_type"] = notifier_type - context["booking"] = booking - message = template.render(Context(context)) - notifier = Notifier() - notifier.title = self.TITLES[notifier_type] - notifier.content = message - notifier.user = booking.owner.userprofile - notifier.sender = str(booking.lab) - notifier.save() - notifier.send() + message = render_to_string(template_name, context) + send_mail( + "Your Booking has Expired", + message, + os.environ.get("DEFAULT_FROM_EMAIL", "opnfv@pharos-dashboard"), + user.userprofile.email_addr, + fail_silently=False + ) - context["owner"] = False - - for user in booking.collaborators.all(): - context["collaborator"] = user - message = template.render(Context(context)) - notifier = Notifier() - notifier.title = self.TITLES[notifier_type] - notifier.content = message - notifier.user = UserProfile.objects.get(user=user) - notifier.sender = str(booking.lab) - notifier.save() - notifier.send() + @classmethod + def task_updated(cls, task): + """ + called every time a lab updated info about a task. + currently only checks if the job is now done so I can send an email, + may add more functionality later + """ + if task.job.is_fulfilled(): + cls.email_job_fulfilled(task.job) diff --git a/dashboard/src/notifier/migrations/0002_auto_20181102_1631.py b/dashboard/src/notifier/migrations/0002_auto_20181102_1631.py new file mode 100644 index 0000000..e5fef89 --- /dev/null +++ b/dashboard/src/notifier/migrations/0002_auto_20181102_1631.py @@ -0,0 +1,44 @@ +# Generated by Django 2.1 on 2018-11-02 16:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0003_publicnetwork'), + ('notifier', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=150)), + ('content', models.TextField()), + ('recipients', models.ManyToManyField(to='account.UserProfile')), + ], + ), + migrations.RemoveField( + model_name='labmessage', + name='lab', + ), + migrations.RemoveField( + model_name='metabooking', + name='booking', + ), + migrations.RemoveField( + model_name='notifier', + name='user', + ), + migrations.DeleteModel( + name='LabMessage', + ), + migrations.DeleteModel( + name='MetaBooking', + ), + migrations.DeleteModel( + name='Notifier', + ), + ] diff --git a/dashboard/src/notifier/models.py b/dashboard/src/notifier/models.py index ed0edeb..5e7c60e 100644 --- a/dashboard/src/notifier/models.py +++ b/dashboard/src/notifier/models.py @@ -8,44 +8,16 @@ ############################################################################## from django.db import models -from booking.models import Booking from account.models import UserProfile -from fernet_fields import EncryptedTextField -from account.models import Lab -class MetaBooking(models.Model): - id = models.AutoField(primary_key=True) - booking = models.OneToOneField(Booking, on_delete=models.CASCADE, related_name="metabooking") - ending_notified = models.BooleanField(default=False) - ended_notified = models.BooleanField(default=False) - created_notified = models.BooleanField(default=False) - - -class LabMessage(models.Model): - lab = models.ForeignKey(Lab, on_delete=models.CASCADE) - msg = models.TextField() # django template should be put here - - -class Notifier(models.Model): - id = models.AutoField(primary_key=True) - title = models.CharField(max_length=240) - content = EncryptedTextField() - user = models.ForeignKey(UserProfile, on_delete=models.CASCADE, null=True, blank=True) - sender = models.CharField(max_length=240, default='unknown') - message_type = models.CharField(max_length=240, default='email', choices=( - ('email', 'Email'), - ('webnotification', 'Web Notification'))) - msg_sent = '' +class Notification(models.Model): + title = models.CharField(max_length=150) + content = models.TextField() + recipients = models.ManyToManyField(UserProfile) def __str__(self): return self.title - """ - Implement for next PR: send Notifier by media agreed to by user - """ - def send(self): - pass - - def getEmail(self): - return self.user.email_addr + def to_preview_html(self): + return "

" + self.title + "

" # TODO - template? diff --git a/dashboard/src/notifier/views.py b/dashboard/src/notifier/views.py index 026894a..c1a2f7e 100644 --- a/dashboard/src/notifier/views.py +++ b/dashboard/src/notifier/views.py @@ -7,28 +7,27 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## -from notifier.models import * +from notifier.models import Notification from django.shortcuts import render + def InboxView(request): if request.user.is_authenticated: user = request.user else: return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) - return render(request, "notifier/inbox.html", {'notifier_messages': Notifier.objects.filter(user=user.userprofile)}) + return render(request, "notifier/inbox.html", {'notifications': Notification.objects.filter(recipient=user.userprofile)}) def NotificationView(request, notification_id): - if notification_id == 0: - pass if request.user.is_authenticated: user = request.user else: return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) - notification = Notifier.objects.get(id=notification_id) - if not notification.user.user.username == user.username: + notification = Notification.objects.get(id=notification_id) + if user not in notification.recipients: return render(request, "dashboard/login.html", {'title': 'Access Denied'}) return render(request, "notifier/notification.html", {'notification': notification}) diff --git a/dashboard/src/pharos_dashboard/settings.py b/dashboard/src/pharos_dashboard/settings.py index 7fccb32..91fc93e 100644 --- a/dashboard/src/pharos_dashboard/settings.py +++ b/dashboard/src/pharos_dashboard/settings.py @@ -194,10 +194,6 @@ CELERYBEAT_SCHEDULE = { 'task': 'dashboard.tasks.free_hosts', 'schedule': timedelta(minutes=1) }, - 'conjure_notifiers': { - 'task': 'dashboard.tasks.conjure_aggregate_notifiers', - 'schedule': timedelta(seconds=30) - }, } # Notifier Settings diff --git a/dashboard/src/templates/notifier/email_ended.txt b/dashboard/src/templates/notifier/email_ended.txt new file mode 100644 index 0000000..7467a0e --- /dev/null +++ b/dashboard/src/templates/notifier/email_ended.txt @@ -0,0 +1,21 @@ +{{user_name|default:"Developer"}}, + +The booking you requested of the OPNFV Lab as a Service has ended. + +booking information: + start: {{booking.start}} + end: {{booking.end}} + machines: + {% for host in hosts %} + - {{host}} + {% endfor %} + purpose: {{booking.purpose}} + +You may visit the following link for more information: +{{booking_url}} + +Feel free to create another booking with us! + +Thank you for contributing to the OPNFV platform! + + - The Lab-as-a-Service team diff --git a/dashboard/src/templates/notifier/email_fulfilled.txt b/dashboard/src/templates/notifier/email_fulfilled.txt new file mode 100644 index 0000000..d473961 --- /dev/null +++ b/dashboard/src/templates/notifier/email_fulfilled.txt @@ -0,0 +1,17 @@ +{{user_name|default:"Developer"}}, + +The booking you requested of the OPNFV Lab as a Service has finished deploying and is ready for you to use. + +The lab that fulfilled your booking request has sent you the following messages: + {% for message in messages %} + {% message.title %} + {% message.content %} + -------------------- + {% endfor %} + +You may visit the following link for more information: +{{booking_url}} + +Thank you for contributing to the OPNFV platform! + + - The Lab-as-a-Service team diff --git a/dashboard/src/templates/notifier/end_booking.html b/dashboard/src/templates/notifier/end_booking.html new file mode 100644 index 0000000..22014fb --- /dev/null +++ b/dashboard/src/templates/notifier/end_booking.html @@ -0,0 +1,36 @@ + + +
+ {% if owner %} +

Your booking has expired

+

Your booking has ended and the machines have been cleaned up.

+

Thank you for working on OPNFV, and feel free to book more machines if you need them.

+ {% else %} +

A booking that you collaborated on has expired

+

The booking owned by {{booking.owner.username}} that you worked on has ended

+

Thank you for contributing to OPNFV!

+ {% endif %} +

Booking information:

+
    +
  • owner: {{booking.owner.username}}
  • +
  • id: {{booking.id}}
  • +
  • lab: {{booking.resource.template.lab.lab_user.username}}
  • +
  • resource: {{booking.resource.template.name}}
  • +
  • start: {{booking.start}}
  • +
  • end: {{booking.end}}
  • +
  • purpose: {{booking.purpose}}
  • +
  • collaborators: +
      + {% for user in booking.collaborators.all %} +
    • user.username
    • + {% empty %} +
    • No collaborators
    • + {% endfor %} +
    +
  • +
+ +

You can find more detailed information Here

+
+ + diff --git a/dashboard/src/templates/notifier/inbox.html b/dashboard/src/templates/notifier/inbox.html index ee0f27a..c0ee1ba 100644 --- a/dashboard/src/templates/notifier/inbox.html +++ b/dashboard/src/templates/notifier/inbox.html @@ -65,7 +65,7 @@
- +
diff --git a/dashboard/src/templates/notifier/new_booking.html b/dashboard/src/templates/notifier/new_booking.html new file mode 100644 index 0000000..4b53875 --- /dev/null +++ b/dashboard/src/templates/notifier/new_booking.html @@ -0,0 +1,34 @@ + + +
+ {% if owner %} +

You have created a new booking

+

We have recieved your booking request and will start working on it right away.

+ {% else %} +

You have been added as a collaborator to a booking

+

{{booking.owner.username}} has given you access to thier booking.

+ {% endif %} +

Booking information:

+
    +
  • owner: {{booking.owner.username}}
  • +
  • id: {{booking.id}}
  • +
  • lab: {{booking.resource.template.lab.lab_user.username}}
  • +
  • resource: {{booking.resource.template.name}}
  • +
  • start: {{booking.start}}
  • +
  • end: {{booking.end}}
  • +
  • purpose: {{booking.purpose}}
  • +
  • collaborators: +
      + {% for user in booking.collaborators.all %} +
    • user.username
    • + {% empty %} +
    • No collaborators
    • + {% endfor %} +
    +
  • +
+ +

You can find more detailed information Here

+
+ + diff --git a/dashboard/src/workflow/models.py b/dashboard/src/workflow/models.py index e862957..e5a23b2 100644 --- a/dashboard/src/workflow/models.py +++ b/dashboard/src/workflow/models.py @@ -14,8 +14,6 @@ from django.shortcuts import render from django.contrib import messages import yaml -import json -import traceback import requests from workflow.forms import ConfirmationForm @@ -23,6 +21,7 @@ from api.models import * from dashboard.exceptions import * from resource_inventory.models import * from resource_inventory.resource_manager import ResourceManager +from notifier.manager import NotificationHandler class BookingAuthManager(): @@ -282,6 +281,9 @@ class Repository(): errors = self.make_booking() if errors: return errors + # create notification + booking = self.el[self.BOOKING_MODELS]['booking'] + NotificationHandler.notify_new_booking(booking) def make_snapshot(self): @@ -465,7 +467,6 @@ class Repository(): for collaborator in collaborators: booking.collaborators.add(collaborator) - try: JobFactory.makeCompleteJob(booking) except Exception as e: -- cgit 1.2.3-korg