Rewrite Notification subsystem 67/63967/5
authorParker Berberian <pberberian@iol.unh.edu>
Wed, 24 Oct 2018 19:12:32 +0000 (15:12 -0400)
committerParker Berberian <pberberian@iol.unh.edu>
Wed, 7 Nov 2018 15:32:56 +0000 (10:32 -0500)
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 <pberberian@iol.unh.edu>
23 files changed:
dashboard/booking_communication_agent.py [deleted file]
dashboard/dashboard_api/__init__.py [deleted file]
dashboard/dashboard_api/api.py [deleted file]
dashboard/dashboard_notification/__init__.py [deleted file]
dashboard/dashboard_notification/notification.py [deleted file]
dashboard/src/api/models.py
dashboard/src/api/serializers/old_serializers.py
dashboard/src/api/urls.py
dashboard/src/api/views.py
dashboard/src/dashboard/tasks.py
dashboard/src/notifier/admin.py
dashboard/src/notifier/dispatchers.py [deleted file]
dashboard/src/notifier/manager.py
dashboard/src/notifier/migrations/0002_auto_20181102_1631.py [new file with mode: 0644]
dashboard/src/notifier/models.py
dashboard/src/notifier/views.py
dashboard/src/pharos_dashboard/settings.py
dashboard/src/templates/notifier/email_ended.txt [new file with mode: 0644]
dashboard/src/templates/notifier/email_fulfilled.txt [new file with mode: 0644]
dashboard/src/templates/notifier/end_booking.html [new file with mode: 0644]
dashboard/src/templates/notifier/inbox.html
dashboard/src/templates/notifier/new_booking.html [new file with mode: 0644]
dashboard/src/workflow/models.py

diff --git a/dashboard/booking_communication_agent.py b/dashboard/booking_communication_agent.py
deleted file mode 100644 (file)
index dc4ea4d..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-##############################################################################
-# Copyright (c) 2018 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
-##############################################################################
-from dashboard_notification.notification import Notification
-from dashboard_api.api import DashboardAPI
-
-CONFIG = {
-    'dashboard_ip': '127.0.0.1',
-    'dashboard_url': 'http://127.0.0.1',
-    'api_token': 'f33ff43c85ecb13f5d0632c05dbb0a7d85a5a8d1',
-    'user': 'opnfv',
-    'password': 'opnfvopnfv'
-}
-
-api = DashboardAPI(CONFIG['dashboard_url'], api_token=CONFIG['api_token'], verbose=True)
-
-
-def booking_start(message):
-    content = message.content
-    booking = api.get_booking(id=content['booking_id'])
-
-    # do something here...
-
-    # notify dashboard
-    api.post_resource_status(resource_id=booking['resource_id'], type='info', title='pod setup',
-                             content='details')
-
-
-def booking_end(message):
-    # do something here...
-
-    # notify dashboard
-    api.post_resource_status(resource_id=message.content['resource_id'], type='info',
-                             title='booking end', content='details')
-
-
-def main():
-    with Notification(CONFIG['dashboard_ip'], CONFIG['user'], CONFIG['password']) as notification:
-        notification.register(booking_start, 'Arm POD 2', 'booking_start')
-        notification.register(booking_end, 'Arm POD 2', 'booking_end')
-        notification.receive()  # wait for notifications
-
-
-if __name__ == "__main__":
-    main()
diff --git a/dashboard/dashboard_api/__init__.py b/dashboard/dashboard_api/__init__.py
deleted file mode 100644 (file)
index ce1acf3..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-##############################################################################
-# Copyright (c) 2016 Max Breitenfeldt 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
-##############################################################################
\ No newline at end of file
diff --git a/dashboard/dashboard_api/api.py b/dashboard/dashboard_api/api.py
deleted file mode 100644 (file)
index d40e0aa..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-##############################################################################
-# Copyright (c) 2016 Max Breitenfeldt 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 logging
-
-import requests
-
-URLS = {
-    'resources': '/api/resources/',
-    'servers': '/api/servers/',
-    'bookings': '/api/bookings',
-    'resource_status': '/api/resource_status/',
-}
-
-class DashboardAPI(object):
-    def __init__(self, dashboard_url, api_token='', verbose=False):
-        self._api_token = api_token
-        self._verbose = verbose
-        self._resources_url = dashboard_url + URLS['resources']
-        self._servers_url = dashboard_url + URLS['servers']
-        self._bookings_url = dashboard_url + URLS['bookings']
-        self._resources_status_url = dashboard_url + URLS['resource_status']
-        self._logger = logging.getLogger(__name__)
-
-    def get_all_resources(self):
-        return self._get_json(self._resources_url)
-
-    def get_resource(self, id='', name='', url=''):
-        if url != '':
-            return self._get_json(url)[0]
-        url = self._resources_url + self._url_parameter(id=id, name=name)
-        return self._get_json(url)[0]
-
-    def get_all_bookings(self):
-        return self._get_json(self._bookings_url)
-
-    def get_resource_bookings(self, resource_id):
-        url = self._bookings_url + self._url_parameter(resource_id=resource_id)
-        return self._get_json(url)
-
-    def get_booking(self, id):
-        url = self._bookings_url + self._url_parameter(id=id)
-        return self._get_json(url)[0]
-
-    def post_resource_status(self, resource_id, type, title, content):
-        data = {
-            'resource': resource_id,
-            'type': type,
-            'title': title,
-            'content': content
-        }
-        return self._post_json(self._resources_status_url, data)
-
-    def get_url(self, url):
-        return self._get_json(url)
-
-    def _url_parameter(self, **kwargs):
-        res = ''
-        prefix = '?'
-        for key, val in kwargs.items():
-            res += prefix + key + '=' + str(val)
-            prefix = '&'
-        return res
-
-    def _get_json(self, url):
-        try:
-            response = requests.get(url)
-            if self._verbose:
-                print('Get JSON: ' + url)
-                print(response.status_code, response.content)
-            return response.json()
-        except requests.exceptions.RequestException as e:
-            self._logger.exception(e)
-        except ValueError as e:
-            self._logger.exception(e)
-
-    def _post_json(self, url, json):
-        if self._api_token == '':
-            raise Exception('Need api token to POST data.')
-        response = requests.post(url, json, headers={'Authorization': 'Token ' + self._api_token})
-        if self._verbose:
-            print('Post JSON: ' + url)
-            print(response.status_code, response.content)
-        return response.status_code
diff --git a/dashboard/dashboard_notification/__init__.py b/dashboard/dashboard_notification/__init__.py
deleted file mode 100644 (file)
index b5914ce..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-##############################################################################
-# Copyright (c) 2016 Max Breitenfeldt 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
-##############################################################################
-
-
diff --git a/dashboard/dashboard_notification/notification.py b/dashboard/dashboard_notification/notification.py
deleted file mode 100644 (file)
index 6843c76..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-##############################################################################
-# Copyright (c) 2016 Max Breitenfeldt 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 jsonpickle
-import pika
-
-
-class Message(object):
-    def __init__(self, type, topic, content):
-        self.type = type
-        self.topic = topic
-        self.content = content
-
-
-class Notification(object):
-    """
-    This class can be used by the dashboard and the labs to exchange notifications about booking
-    events and pod status. It utilizes rabbitmq to communicate.
-
-    Notifications are associated to an event and to a topic.
-    Events are:
-    [ 'booking_start', 'booking_end']
-    The topic is usually a POD name, ie:
-    'Intel POD 2'
-    """
-
-    def __init__(self, dashboard_url, user=None, password=None, verbose=False):
-        self.rabbitmq_broker = dashboard_url
-        self.verbose = verbose
-        if user is None and password is None:
-            self._connection = pika.BlockingConnection(pika.ConnectionParameters(
-                host=self.rabbitmq_broker))
-        else:
-            self.credentials = pika.PlainCredentials(user, password)
-            self._connection = pika.BlockingConnection(pika.ConnectionParameters(
-                credentials=self.credentials,
-                host=self.rabbitmq_broker))
-        self._registry = {}
-        self._channel = self._connection.channel()
-        self._channel.exchange_declare(exchange='notifications', type='topic')
-        self._result = self._channel.queue_declare(exclusive=True, durable=True)
-        self._queue_name = self._result.method.queue
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        self._connection.close()
-
-    def register(self, function, topic, type='all'):
-        """
-        Registers a function to be called for the specified event.
-        :param function: the function to register
-        :param event: the event type
-        :param regex: a regex to specify for wich topics the function will be called. Some
-        possible Expressions can be:
-        'Intel POD 2' : Intel POD 2
-        """
-
-        if topic not in self._registry:
-            self._registry[topic] = [(function, type)]
-        else:
-            self._registry[topic].append((function, type))
-
-    def receive(self):
-        """
-        Start receiving notifications. This is a blocking operation, if a notification is received,
-        the registered functions will be called.
-        """
-        if self.verbose:
-            print('Start receiving Notifications. Keys: ', self._registry.keys())
-        self._receive_message(self._registry.keys())
-
-    def send(self, message):
-        """
-        Send an event notification.
-        :param event: the event type
-        :param topic: the pod name
-        :param content: a JSON-serializable dictionary
-        """
-        self._send_message(message)
-
-    def _send_message(self, message):
-        routing_key = message.topic
-        message_json = jsonpickle.encode(message)
-        self._channel.basic_publish(exchange='notifications',
-                                    routing_key=routing_key,
-                                    body=message_json,
-                                    properties=pika.BasicProperties(
-                                        content_type='application/json',
-                                        delivery_mode=2,  # make message persistent
-                                    ))
-        if self.verbose:
-            print(" [x] Sent %r:%r" % (routing_key, message_json))
-
-    def _receive_message(self, binding_keys):
-        for key in binding_keys:
-            self._channel.queue_bind(exchange='notifications',
-                                     queue=self._queue_name,
-                                     routing_key=key)
-        self._channel.basic_consume(self._message_callback,
-                                    queue=self._queue_name)
-        self._channel.start_consuming()
-
-    def _message_callback(self, ch, method, properties, body):
-        if self.verbose:
-            print(" [x] Got %r:%r" % (method.routing_key, body))
-        if method.routing_key not in self._registry:
-            return
-        for func, type in self._registry[method.routing_key]:
-            message = jsonpickle.decode(body.decode())
-            if message.type == type:
-                func(message)
-        ch.basic_ack(delivery_tag=method.delivery_tag)
index 7448ac4..9a7f775 100644 (file)
@@ -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 = {}
index f50b90b..0944881 100644 (file)
 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):
index 94f8279..4b1fe40 100644 (file)
@@ -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 = [
index 072354f..cc3a668 100644 (file)
@@ -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 = {}
index 48008b6..c619642 100644 (file)
@@ -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
index d3e8be5..4a2984c 100644 (file)
@@ -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 (file)
index 1b66b37..0000000
+++ /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'
index a705d00..cc1aa16 100644 (file)
 ##############################################################################
-# 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", "<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 (<owner, collaborator>.username)
-    * recipient full name (<owner, collaborator>.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", "<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 (file)
index 0000000..e5fef89
--- /dev/null
@@ -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',
+        ),
+    ]
index ed0edeb..5e7c60e 100644 (file)
@@ -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 "<h3>" + self.title + "</h3>"  # TODO - template?
index 026894a..c1a2f7e 100644 (file)
@@ -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})
index 7fccb32..91fc93e 100644 (file)
@@ -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 (file)
index 0000000..7467a0e
--- /dev/null
@@ -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 (file)
index 0000000..d473961
--- /dev/null
@@ -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 (file)
index 0000000..22014fb
--- /dev/null
@@ -0,0 +1,36 @@
+<html>
+    <body>
+        <div id="message_content_wrapper">
+            {% if owner %}
+            <h3>Your booking has expired</h3>
+            <p>Your booking has ended and the machines have been cleaned up.</p>
+            <p>Thank you for working on OPNFV, and feel free to book more machines if you need them.</p>
+            {% else %}
+            <h3>A booking that you collaborated on has expired</h3>
+            <p>The booking owned by {{booking.owner.username}} that you worked on has ended</p>
+            <p>Thank you for contributing to OPNFV!</p>
+            {% endif %}
+            <p>Booking information:</p>
+            <ul>
+                <li>owner: {{booking.owner.username}}</li>
+                <li>id: {{booking.id}}</li>
+                <li>lab: {{booking.resource.template.lab.lab_user.username}}</li>
+                <li>resource: {{booking.resource.template.name}}</li>
+                <li>start: {{booking.start}}</li>
+                <li>end: {{booking.end}}</li>
+                <li>purpose: {{booking.purpose}}</li>
+                <li>collaborators:
+                    <ul>
+                        {% for user in booking.collaborators.all %}
+                        <li>user.username</li>
+                        {% empty %}
+                        <li>No collaborators</li>
+                        {% endfor %}
+                    </ul>
+                </li>
+            </ul>
+
+            <p>You can find more detailed information <a href=/booking/detail/{{booking.id/>Here</a></p>
+        </div>
+    </body>
+</html>
index ee0f27a..c0ee1ba 100644 (file)
@@ -65,7 +65,7 @@
   </div>
   <div class="iframe-panel inbox-expanded-view">
       <div class="inbox-iframe-div">
-        <iframe id="inbox-iframe" src="notification/0" frameBorder="0" width="100%" height="100vh" scrolling="yes" onload="sizetoiframe(this);">Please select a notification</iframe>
+        <iframe id="inbox-iframe" frameBorder="0" width="100%" height="100vh" scrolling="yes" onload="sizetoiframe(this);">Please select a notification</iframe>
       </div>
   </div>
 </div>
diff --git a/dashboard/src/templates/notifier/new_booking.html b/dashboard/src/templates/notifier/new_booking.html
new file mode 100644 (file)
index 0000000..4b53875
--- /dev/null
@@ -0,0 +1,34 @@
+<html>
+    <body>
+        <div id="message_content_wrapper">
+            {% if owner %}
+            <h3>You have created a new booking</h3>
+            <p>We have recieved your booking request and will start working on it right away.</p>
+            {% else %}
+            <h3>You have been added as a collaborator to a booking</h3>
+            <p>{{booking.owner.username}} has given you access to thier booking.</p>
+            {% endif %}
+            <p>Booking information:</p>
+            <ul>
+                <li>owner: {{booking.owner.username}}</li>
+                <li>id: {{booking.id}}</li>
+                <li>lab: {{booking.resource.template.lab.lab_user.username}}</li>
+                <li>resource: {{booking.resource.template.name}}</li>
+                <li>start: {{booking.start}}</li>
+                <li>end: {{booking.end}}</li>
+                <li>purpose: {{booking.purpose}}</li>
+                <li>collaborators:
+                    <ul>
+                        {% for user in booking.collaborators.all %}
+                        <li>user.username</li>
+                        {% empty %}
+                        <li>No collaborators</li>
+                        {% endfor %}
+                    </ul>
+                </li>
+            </ul>
+
+            <p>You can find more detailed information <a href=/booking/detail/{{booking.id/>Here</a></p>
+        </div>
+    </body>
+</html>
index e862957..e5a23b2 100644 (file)
@@ -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: