From 6dbc0fee35541b1d0eef32518466d359151d2232 Mon Sep 17 00:00:00 2001
From: Jorran de Wit <jorrandewit@outlook.com>
Date: Wed, 13 Sep 2017 12:28:58 +0200
Subject: [PATCH] Notifications live updated and show on all pages

---
 .bootstraprc                                  |   2 +
 SciPost_v1/settings/base.py                   |   4 +-
 notifications/__init__.py                     |   2 +-
 notifications/context_processors.py           |  14 ---
 notifications/middleware.py                   |  15 ---
 notifications/models.py                       |   6 +-
 .../templatetags/notifications_tags.py        |  44 +++++++
 notifications/urls.py                         |   1 +
 notifications/views.py                        |  17 ++-
 .../scipost/assets/config/preconfig.scss      |  11 ++
 scipost/static/scipost/assets/css/_badge.scss |   8 ++
 .../static/scipost/assets/css/_popover.scss   |  15 +++
 scipost/static/scipost/assets/css/style.scss  |   2 +
 .../static/scipost/assets/js/notifications.js | 112 ++++++++++++++++++
 scipost/static/scipost/assets/js/scripts.js   |   3 +-
 scipost/templates/scipost/navbar.html         |  32 ++---
 webpack.config.js                             |  25 ++--
 17 files changed, 254 insertions(+), 59 deletions(-)
 delete mode 100644 notifications/context_processors.py
 delete mode 100644 notifications/middleware.py
 create mode 100644 notifications/templatetags/notifications_tags.py
 create mode 100644 scipost/static/scipost/assets/css/_badge.scss
 create mode 100644 scipost/static/scipost/assets/css/_popover.scss
 create mode 100644 scipost/static/scipost/assets/js/notifications.js

diff --git a/.bootstraprc b/.bootstraprc
index 1d0fc6d98..e579effca 100644
--- a/.bootstraprc
+++ b/.bootstraprc
@@ -36,6 +36,7 @@
         "type": true,
         "tooltip": true,
         "utilities": true,
+        "popover": true,
     },
     "scripts": {
         "alert": true,
@@ -46,5 +47,6 @@
         "tab": true,
         "tooltip": true,
         "util": true,
+        "popover": true,
     }
 }
diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py
index 2768b0821..8fd730d74 100644
--- a/SciPost_v1/settings/base.py
+++ b/SciPost_v1/settings/base.py
@@ -154,8 +154,7 @@ MIDDLEWARE = (
     'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
-    'django.middleware.security.SecurityMiddleware',
-    'notifications.middleware.NotificationMiddleware'
+    'django.middleware.security.SecurityMiddleware'
 )
 
 ROOT_URLCONF = 'SciPost_v1.urls'
@@ -173,7 +172,6 @@ TEMPLATES = [
                 'django.template.context_processors.media',
                 'django.contrib.auth.context_processors.auth',
                 'django.contrib.messages.context_processors.messages',
-                'notifications.context_processors.notifications',
             ],
         },
     },
diff --git a/notifications/__init__.py b/notifications/__init__.py
index 41f694b45..d51321cad 100644
--- a/notifications/__init__.py
+++ b/notifications/__init__.py
@@ -1 +1 @@
-default_app_config = 'notifications.apps.Config'
+default_app_config = 'notifications.apps.NotificationsConfig'
diff --git a/notifications/context_processors.py b/notifications/context_processors.py
deleted file mode 100644
index 3f99f392b..000000000
--- a/notifications/context_processors.py
+++ /dev/null
@@ -1,14 +0,0 @@
-def get_notifications(request):
-    return getattr(request, 'notifications', [])
-
-
-def get_unread_count(request):
-    return getattr(request, 'notifications_unread', 0)
-
-
-def notifications(request):
-    """ Return a lazy 'notifications' context variable. """
-    return {
-        'notifications': get_notifications(request),
-        'notifications_unread': get_unread_count(request)
-    }
diff --git a/notifications/middleware.py b/notifications/middleware.py
deleted file mode 100644
index 9faf75859..000000000
--- a/notifications/middleware.py
+++ /dev/null
@@ -1,15 +0,0 @@
-class NotificationMiddleware(object):
-    def __init__(self, get_response):
-        self.get_response = get_response
-
-    def __call__(self, request):
-        response = self.get_response(request)
-        request.notifications = ['123']
-        return response
-
-    def process_view(self, request, view_func, view_args, view_kwargs):
-        """ Add notifications in the request if user is logged in! """
-
-        if request.user.is_authenticated:
-            request.notifications = request.user.notifications.all()[:5]
-            request.notifications_unread = request.user.notifications.unread().count()
diff --git a/notifications/models.py b/notifications/models.py
index 04e2aff1e..e3205f9c4 100644
--- a/notifications/models.py
+++ b/notifications/models.py
@@ -1,4 +1,5 @@
 from django.db import models
+from django.core.urlresolvers import reverse
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.fields import GenericForeignKey
@@ -59,7 +60,7 @@ class Notification(models.Model):
     class Meta:
         ordering = ('-created', )
 
-    def __unicode__(self):
+    def __str__(self):
         ctx = {
             'actor': self.actor,
             'verb': self.verb,
@@ -75,6 +76,9 @@ class Notification(models.Model):
             return u'%(actor)s %(verb)s %(action_object)s %(timesince)s ago' % ctx
         return u'%(actor)s %(verb)s %(timesince)s ago' % ctx
 
+    def get_absolute_url(self):
+        return reverse('notifications:forward', args=(self.slug,))
+
     def timesince(self, now=None):
         """
         Shortcut for the ``django.utils.timesince.timesince`` function of the
diff --git a/notifications/templatetags/notifications_tags.py b/notifications/templatetags/notifications_tags.py
new file mode 100644
index 000000000..16c08e260
--- /dev/null
+++ b/notifications/templatetags/notifications_tags.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+from django.template import Library
+from django.utils.html import format_html
+
+register = Library()
+
+
+@register.assignment_tag(takes_context=True)
+def notifications_unread(context):
+    user = user_context(context)
+    if not user:
+        return ''
+    return user.notifications.unread().count()
+
+
+@register.simple_tag(takes_context=True)
+def live_notify_badge(context, badge_class='live_notify_badge', classes=''):
+    user = user_context(context)
+    if not user:
+        return ''
+
+    html = "<span class='{badge_class} {classes}'>{unread}</span>".format(
+        badge_class=badge_class, unread=user.notifications.unread().count(),
+        classes=classes
+    )
+    return format_html(html)
+
+
+@register.simple_tag
+def live_notify_list(list_class='live_notify_list', classes=''):
+    html = "<ul class='{list_class} {classes}'></ul>".format(list_class=list_class,
+                                                             classes=classes)
+    return format_html(html)
+
+
+def user_context(context):
+    if 'user' not in context:
+        return None
+
+    request = context['request']
+    user = request.user
+    if user.is_anonymous():
+        return None
+    return user
diff --git a/notifications/urls.py b/notifications/urls.py
index 20f0e4db9..7386359ef 100644
--- a/notifications/urls.py
+++ b/notifications/urls.py
@@ -5,6 +5,7 @@ from . import views
 
 urlpatterns = [
     url(r'^$', views.AllNotificationsList.as_view(), name='all'),
+    url(r'^redirect/(?P<slug>\d+)$', views.forward, name='forward'),
     url(r'^unread/$', views.UnreadNotificationsList.as_view(), name='unread'),
     url(r'^mark-all-as-read/$', views.mark_all_as_read, name='mark_all_as_read'),
     url(r'^mark-as-read/(?P<slug>\d+)/$', views.mark_as_read, name='mark_as_read'),
diff --git a/notifications/views.py b/notifications/views.py
index d583504aa..a3b88a85a 100644
--- a/notifications/views.py
+++ b/notifications/views.py
@@ -27,6 +27,17 @@ class UnreadNotificationsList(NotificationViewList):
         return self.request.user.notifications.unread()
 
 
+@login_required
+def forward(request, slug):
+    """
+    Open the url of the target object of the notification and redirect.
+    In addition, mark the notification as read.
+    """
+    notification = get_object_or_404(Notification, recipient=request.user, id=slug2id(slug))
+    notification.mark_as_read()
+    return redirect(notification.target.get_absolute_url())
+
+
 @login_required
 def mark_all_as_read(request):
     request.user.notifications.mark_all_as_read()
@@ -101,7 +112,7 @@ def live_unread_notification_list(request):
 
     try:
         # Default to 5 as a max number of notifications
-        num_to_fetch = max(int(request.GET.get('max', 5)))
+        num_to_fetch = max(int(request.GET.get('max', 5)), 100)
         num_to_fetch = min(num_to_fetch, 100)
     except ValueError:
         num_to_fetch = 5
@@ -115,10 +126,10 @@ def live_unread_notification_list(request):
             struct['actor'] = str(n.actor)
         if n.target:
             struct['target'] = str(n.target)
+            struct['forward_link'] = n.get_absolute_url()
         if n.action_object:
             struct['action_object'] = str(n.action_object)
-        if n.data:
-            struct['data'] = n.data
+
         unread_list.append(struct)
         if request.GET.get('mark_as_read'):
             n.mark_as_read()
diff --git a/scipost/static/scipost/assets/config/preconfig.scss b/scipost/static/scipost/assets/config/preconfig.scss
index de82cbe66..9a15bb0e3 100644
--- a/scipost/static/scipost/assets/config/preconfig.scss
+++ b/scipost/static/scipost/assets/config/preconfig.scss
@@ -7,6 +7,7 @@
 //
 $base-border-radius: 2px;
 $border-radius: 2px;
+$border-radius-lg: 2px;
 
 // Alert
 //
@@ -140,6 +141,16 @@ $blockquote-font-size: $font-size-base;
 // $blockquote-border-color: #ececec;
 
 
+// Popover
+//
+$popover-border-color:                #dddddd;
+$popover-arrow-width:                 5px;
+$popover-arrow-height:                5px;
+// $popover-arrow-color:                 $popover-bg;
+$popover-max-width:                   400px;
+
+$popover-arrow-outer-width:           ($popover-arrow-width + 1px);
+
 
 
 //  ---
diff --git a/scipost/static/scipost/assets/css/_badge.scss b/scipost/static/scipost/assets/css/_badge.scss
new file mode 100644
index 000000000..e78445df0
--- /dev/null
+++ b/scipost/static/scipost/assets/css/_badge.scss
@@ -0,0 +1,8 @@
+.badge {
+    vertical-align: bottom;
+
+    .h3 &,
+    h3 & {
+        line-height: 1.25;
+    }
+}
diff --git a/scipost/static/scipost/assets/css/_popover.scss b/scipost/static/scipost/assets/css/_popover.scss
new file mode 100644
index 000000000..31f5ec152
--- /dev/null
+++ b/scipost/static/scipost/assets/css/_popover.scss
@@ -0,0 +1,15 @@
+.popover {
+    width: 400px;
+}
+
+.popover.notifications {
+    .popover-body {
+        padding: 0;
+    }
+
+    .list-group-item {
+        padding: 9px 14px;
+        border-radius: 0;
+        border-top: 1px solid #fff;
+    }
+}
diff --git a/scipost/static/scipost/assets/css/style.scss b/scipost/static/scipost/assets/css/style.scss
index a9ca91276..5c8bb75ee 100644
--- a/scipost/static/scipost/assets/css/style.scss
+++ b/scipost/static/scipost/assets/css/style.scss
@@ -16,6 +16,7 @@
 @import "mixins_labels";
 
 @import "alert";
+@import "badge";
 @import "breadcrumb";
 @import "buttons";
 @import "cards";
@@ -30,6 +31,7 @@
 @import "navbar";
 @import "nav";
 @import "page_header";
+@import "popover";
 @import "tables";
 @import "tooltip";
 @import "type";
diff --git a/scipost/static/scipost/assets/js/notifications.js b/scipost/static/scipost/assets/js/notifications.js
new file mode 100644
index 000000000..dfc340784
--- /dev/null
+++ b/scipost/static/scipost/assets/js/notifications.js
@@ -0,0 +1,112 @@
+var notify_badge_class = 'live_notify_badge';
+var notify_menu_class = 'live_notify_list';
+var notify_api_url_count = '/notifications/api/unread_count/';
+var notify_api_url_list = '/notifications/api/unread_list/';
+var notify_fetch_count = '5';
+var notify_refresh_period = 15000;
+var consecutive_misfires = 0;
+var registered_functions = [fill_notification_badge];
+
+
+function fill_notification_badge(data) {
+    var badges = document.getElementsByClassName(notify_badge_class);
+    if (badges) {
+        for(var i = 0; i < badges.length; i++){
+            badges[i].innerHTML = data.unread_count;
+        }
+    }
+}
+
+function get_notification_list() {
+    var data = fetch_api_data(notify_api_url_list, function(data) {
+
+        var messages = data.unread_list.map(function (item) {
+            var message = "";
+            if(typeof item.actor !== 'undefined'){
+                message = '<strong>' + item.actor + '</strong>';
+            }
+            if(typeof item.verb !== 'undefined'){
+                message = message + " " + item.verb;
+            }
+            if(typeof item.target !== 'undefined'){
+                if(typeof item.forward_link !== 'undefined') {
+                    message += " <a href='" + item.forward_link + "'>" + item.target + "</a>";
+                } else {
+                    message += " " + item.target;
+                }
+            }
+            if(typeof item.timestamp !== 'undefined'){
+                message = message + " " + item.timestamp;
+            }
+            return '<li class="list-group-item ' + (item.unread ? ' active' : '') + '">' + message + '</li>';
+        }).join('')
+
+        document.getElementById('notification-list').innerHTML = messages;
+    });
+}
+
+function fetch_api_data(url=null, callback=null) {
+    if (!url) {
+        var url = notify_api_url_count;
+    }
+
+    if (registered_functions.length > 0) {
+        //only fetch data if a function is setup
+        var r = new XMLHttpRequest();
+        r.addEventListener('readystatechange', function(event){
+            if (this.readyState === 4){
+                if (this.status === 200){
+                    consecutive_misfires = 0;
+                    var data = JSON.parse(r.responseText);
+                    registered_functions.forEach(function (func) { func(data); });
+                    if (callback) {
+                        return callback(data);
+                    }
+                }else{
+                    consecutive_misfires++;
+                }
+            }
+        })
+        r.open("GET", url +'?max='+notify_fetch_count, true);
+        r.send();
+    }
+    if (consecutive_misfires < 10) {
+        setTimeout(fetch_api_data,notify_refresh_period);
+    } else {
+        var badges = document.getElementsByClassName(notify_badge_class);
+        if (badges) {
+            for (var i = 0; i < badges.length; i++){
+                badges[i].innerHTML = "!";
+                badges[i].title = "Connection lost!"
+            }
+        }
+    }
+}
+
+setTimeout(fetch_api_data, 1000);
+
+$(function(){
+    var notification_template = '<div class="popover notifications" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>';
+
+    var get_notifications_title = function() {
+        return 'New notifications <div class="badge badge-warning live_notify_badge"></div>';
+    }
+
+    var get_notifications = function() {
+        var _str = '<ul id="notification-list" class="update_notifications list-group"><div class="w-100 text-center"><i class="fa fa-circle-o-notch fa-spin fa-fw"></i><span class="sr-only">Loading...</span></div></ul>';
+        get_notification_list();
+        return _str;
+    }
+
+    $('#notifications_badge').popover({
+        animation: false,
+        trigger: 'click',
+        title: get_notifications_title,
+        template: notification_template,
+        content: get_notifications,
+        container: 'body',
+        offset: '0, 9px',
+        placement: "bottom",
+        html: true
+    });
+});
diff --git a/scipost/static/scipost/assets/js/scripts.js b/scipost/static/scipost/assets/js/scripts.js
index c17d95694..e16263ae7 100644
--- a/scipost/static/scipost/assets/js/scripts.js
+++ b/scipost/static/scipost/assets/js/scripts.js
@@ -1,4 +1,5 @@
 import tooltip from './tooltip.js';
+import notifications from './notifications.js';
 
 function hide_all_alerts() {
     $(".alert").fadeOut(300);
@@ -43,5 +44,5 @@ $(function(){
     // Auto-submit hook for general form elements
     $("form .auto-submit input").on('change', function(){
         $(this).parents('form').submit()
-    })
+    });
 });
diff --git a/scipost/templates/scipost/navbar.html b/scipost/templates/scipost/navbar.html
index fa0f19589..beb5bce93 100644
--- a/scipost/templates/scipost/navbar.html
+++ b/scipost/templates/scipost/navbar.html
@@ -1,3 +1,10 @@
+{% load staticfiles %}
+{% load notifications_tags %}
+
+{# <script src="{% static 'notifications/notify.js' %}" type="text/javascript"></script>#}
+{# {% register_notify_callbacks callbacks='fill_notification_list,fill_notification_badge' %}#}
+
+
 <nav class="navbar navbar-scroll navbar-light main-nav navbar-expand-lg">
     <div class="navbar-scroll-inner">
         <ul class="navbar-nav mr-auto">
@@ -22,18 +29,18 @@
 
           {% if user.is_authenticated %}
               <li class="nav-item highlighted dropdown navbar-counter">
-                <a href="{% url 'notifications:all' %}" class="nav-link dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                    Logged in as {{ user.username }}
-                    <i class="fa fa-bell-o" aria-hidden="true"></i>
-                    {% if notifications_unread %}
-                        <span class="badge badge-pill badge-primary">{{notifications_unread}}</span>
-                    {% endif %}
-                </a>
-                <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
-                    {% for note in notifications %}
-                        <a class="dropdown-item" href="#">Action</a>
-                    {% endfor %}
+                <div class="nav-link">{# href="{% url 'notifications:all' %}" #}
+                    <span class="user">{% if user.last_name %}{% if user.contributor %}{{ user.contributor.get_title_display }} {% endif %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}</span>
+                    <a href="#" class="d-inline dropdown-toggle" id="notifications_badge">
+                        <i class="fa fa-bell-o" aria-hidden="true"></i>
+                        {% live_notify_badge classes="badge badge-pill badge-primary" %}
+                    </a>
+                    {% live_notify_list classes="update_notifications d-none" %}
                 </div>
+
+              </li>
+              <li class="nav-item">
+                  <a class="nav-link" href="{% url 'scipost:logout' %}">Logout</a>
               </li>
               {% if perms.scipost.can_oversee_refereeing %}
                   <li class="nav-item{% if '/submissions/admin' in request.path %} active{% endif %}">
@@ -50,9 +57,6 @@
                     <a class="nav-link" href="{% url 'partners:dashboard' %}">Partner Page</a>
                   </li>
               {% endif %}
-              <li class="nav-item">
-                <a class="nav-link" href="{% url 'scipost:logout' %}">Logout</a>
-              </li>
           {% else %}
               <li class="nav-item{% if request.path == '/login/' %} active{% endif %}">
                 <a class="nav-link" href="{% url 'scipost:login' %}">Login</a>
diff --git a/webpack.config.js b/webpack.config.js
index 54f4a619a..8de385d65 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -48,10 +48,21 @@ module.exports = {
     },
     plugins: [
         new webpack.ProvidePlugin({
-            $: "jquery",
-            jQuery: "jquery",
-            "window.jQuery": "jquery",
-            Util: "exports-loader?Util!bootstrap/js/dist/util",
+            $: 'jquery',
+            jQuery: 'jquery',
+            Tether: 'tether',
+            'window.Tether': 'tether',
+            // Alert: 'exports-loader?Alert!bootstrap/js/dist/alert',
+            // Button: 'exports-loader?Button!bootstrap/js/dist/button',
+            // Carousel: 'exports-loader?Carousel!bootstrap/js/dist/carousel',
+            // Collapse: 'exports-loader?Collapse!bootstrap/js/dist/collapse',
+            // Dropdown: 'exports-loader?Dropdown!bootstrap/js/dist/dropdown',
+            // Modal: 'exports-loader?Modal!bootstrap/js/dist/modal',
+            // Popover: 'exports-loader?Popover!bootstrap/js/dist/popover',
+            // Scrollspy: 'exports-loader?Scrollspy!bootstrap/js/dist/scrollspy',
+            // Tab: 'exports-loader?Tab!bootstrap/js/dist/tab',
+            Tooltip: "exports-loader?Tooltip!bootstrap/js/dist/tooltip",
+            Util: 'exports-loader?Util!bootstrap/js/dist/util',
             Popper: ['popper.js', 'default'],
         }),
         new BundleTracker({
@@ -65,8 +76,8 @@ module.exports = {
             dry: false,
             exclude: []
         }),
-        new webpack.optimize.UglifyJsPlugin(),
-        new webpack.optimize.OccurrenceOrderPlugin(),
-        new webpack.optimize.AggressiveMergingPlugin()
+        // new webpack.optimize.UglifyJsPlugin(),
+        // new webpack.optimize.OccurrenceOrderPlugin(),
+        // new webpack.optimize.AggressiveMergingPlugin()
     ],
 }
-- 
GitLab