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