diff --git a/notifications/managers.py b/notifications/managers.py index 31346a0c7d4bde14029fe817ebe24138d81a5cb9..4ec6b22b904ec620247661c0314472a25ade2ba7 100644 --- a/notifications/managers.py +++ b/notifications/managers.py @@ -2,6 +2,7 @@ from django.db import models class NotificationQuerySet(models.query.QuerySet): + def unsent(self): return self.filter(emailed=False) diff --git a/notifications/models.py b/notifications/models.py index 49c6bdd1c11b1336bb84fc1fba934a77f5e48826..ecec80cfea09c80841445b49b984d9b8cde60c25 100644 --- a/notifications/models.py +++ b/notifications/models.py @@ -92,6 +92,14 @@ class Notification(models.Model): from .utils import id2slug return id2slug(self.id) + def mark_toggle(self): + if self.unread: + self.unread = False + self.save() + else: + self.unread = True + self.save() + def mark_as_read(self): if self.unread: self.unread = False diff --git a/notifications/templates/notifications/notification_list.html b/notifications/templates/notifications/notification_list.html index 6b3192b643d838bbb37bc8d549bd3a2bf9638233..a2288eb3a70f25d59f831a98d4106600c140ab2a 100644 --- a/notifications/templates/notifications/notification_list.html +++ b/notifications/templates/notifications/notification_list.html @@ -2,15 +2,24 @@ {% block breadcrumb_items %} {{block.super}} - <span class="breadcrumb-item">My notifications ({{notifications|length}})</span> + <span class="breadcrumb-item">My notifications</span> {% endblock %} -{% block pagetitle %}: My notifications ({{notifications|length}}){% endblock pagetitle %} +{% block pagetitle %}: My notifications{% endblock pagetitle %} {% block content %} + <h1>My notifications</h1> + <div class="mb-3"> + <a href="{% url 'notifications:mark_all_as_read' %}">Mark all as read</a> + </div> + <ul class="list-group notifications"> {% for notice in notifications %} {% include 'notifications/partials/notice.html' %} + {% empty %} + <li class="list-group-item"> + <div class="text-center px-2 py-3"><i class="fa fa-star-o fa-2x" aria-hidden="true"></i><h3>You have no new notifications</h3></div> + </li> {% endfor %} </ul> {% endblock %} diff --git a/notifications/templates/notifications/partials/notice.html b/notifications/templates/notifications/partials/notice.html index 2855a53ed7a4bfb4a2436008a54a2ee549d186e6..642acde443cb311d1726ee30fbbe899a4fab3680 100644 --- a/notifications/templates/notifications/partials/notice.html +++ b/notifications/templates/notifications/partials/notice.html @@ -1,17 +1,33 @@ <div class="list-group-item list-group-item-action flex-column align-items-start{% if notice.unread %} active{% endif %} px-3 py-2"> - <div class=""> - <a href="{% url 'notifications:mark_as_read' notice.slug %}" class="close" aria-hidden="true">×</a> - <h4 class="mb-1x"> - <strong>{{ notice.actor }}</strong> + <div> + <div class="actions"> + <a href="{% url 'notifications:mark_toggle' notice.slug %}"> + {% if notice.unread %} + <i class="fa fa-circle" data-toggle="tooltip" data-placement="auto" title="Mark as unread" aria-hidden="true"></i> + {% else %} + <i class="fa fa-circle-o" data-toggle="tooltip" data-placement="auto" title="Mark as read" aria-hidden="true"></i> + {% endif %} + </a> + </div> + <h4> + <strong> + {% if notice.actor.first_name and notice.actor.last_name %} + {{ notice.actor.first_name}} {{ notice.actor.last_name }} + {% else %} + {{ notice.actor }} + {% endif %} + </strong> {{ notice.verb }} {% if notice.target %} {{ notice.target }} {% endif %} </h4> - <div class="text-muted">{{ notice.created|timesince }} ago</div> + <div class="text-muted mb-1">{{ notice.created|timesince }} ago</div> </div> - <p class="mb-1">{{ notice.description|linebreaksbr }}</p> + {% if notice.description %} + <p class="mb-1">{{ notice.description|linebreaksbr }}</p> + {% endif %} {% if notice.target %} <a href="{{notice.target.get_absolute_url}}">Go to {{notice.target_content_type}}</a> {% endif %} diff --git a/notifications/urls.py b/notifications/urls.py index e71492d47e0ec927823a4354d300f4988bd875b5..23bb20e9e1e4aedb2bc6ec2191bf9dbf2f4c5101 100644 --- a/notifications/urls.py +++ b/notifications/urls.py @@ -6,10 +6,8 @@ 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'), - url(r'^mark-as-unread/(?P<slug>\d+)/$', views.mark_as_unread, name='mark_as_unread'), + url(r'^mark-toggle/(?P<slug>\d+)/$', views.mark_toggle, name='mark_toggle'), url(r'^delete/(?P<slug>\d+)/$', views.delete, name='delete'), url(r'^api/unread_count/$', views.live_unread_notification_count, name='live_unread_notification_count'), diff --git a/notifications/views.py b/notifications/views.py index 2716461f857a1b8188fda7eedee03aba75fc8fab..38dd0329b46e900f6b569d04e50cbb586c2fb517 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -1,4 +1,4 @@ -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import User from django.forms import model_to_dict from django.http import JsonResponse @@ -10,7 +10,12 @@ from .models import Notification from .utils import id2slug, slug2id +def is_test_user(user): + return user.groups.filter(name='Testers').exists() + + @method_decorator(login_required, name='dispatch') +@method_decorator(user_passes_test(is_test_user), name='dispatch') class NotificationViewList(ListView): context_object_name = 'notifications' @@ -23,12 +28,8 @@ class AllNotificationsList(NotificationViewList): return self.request.user.notifications.all() -class UnreadNotificationsList(NotificationViewList): - def get_queryset(self): - return self.request.user.notifications.unread() - - @login_required +@user_passes_test(is_test_user) def forward(request, slug): """ Open the url of the target object of the notification and redirect. @@ -40,6 +41,7 @@ def forward(request, slug): @login_required +@user_passes_test(is_test_user) def mark_all_as_read(request): request.user.notifications.mark_all_as_read() @@ -47,40 +49,29 @@ def mark_all_as_read(request): if _next: return redirect(_next) - return redirect('notifications:unread') + return redirect('notifications:all') @login_required -def mark_as_read(request, slug=None): +@user_passes_test(is_test_user) +def mark_toggle(request, slug=None): id = slug2id(slug) notification = get_object_or_404(Notification, recipient=request.user, id=id) - notification.mark_as_read() + notification.mark_toggle() _next = request.GET.get('next') - if _next: return redirect(_next) - return redirect('notifications:unread') - - -@login_required -def mark_as_unread(request, slug=None): - id = slug2id(slug) - - notification = get_object_or_404(Notification, recipient=request.user, id=id) - notification.mark_as_unread() - - _next = request.GET.get('next') + if request.GET.get('json'): + return JsonResponse({'unread': notification.unread}) - if _next: - return redirect(_next) - - return redirect('notifications:unread') + return redirect('notifications:all') @login_required +@user_passes_test(is_test_user) def delete(request, slug=None): id = slug2id(slug) @@ -113,7 +104,7 @@ def live_notification_list(request): try: # Default to 5 as a max number of notifications - num_to_fetch = max(int(request.GET.get('max', 5)), 100) + num_to_fetch = max(int(request.GET.get('max', 5)), 1) num_to_fetch = min(num_to_fetch, 100) except ValueError: num_to_fetch = 5 @@ -125,6 +116,7 @@ def live_notification_list(request): struct['slug'] = id2slug(n.id) if n.actor: if isinstance(n.actor, User): + # Humanize if possible struct['actor'] = '{f} {l}'.format(f=n.actor.first_name, l=n.actor.last_name) else: struct['actor'] = str(n.actor) diff --git a/scipost/static/scipost/assets/css/_popover.scss b/scipost/static/scipost/assets/css/_popover.scss index f0a381ddaaf9d88bbf2a2adea907fdad0378dd49..6cc116b60479e5d4661291068b077b55cec1d9c6 100644 --- a/scipost/static/scipost/assets/css/_popover.scss +++ b/scipost/static/scipost/assets/css/_popover.scss @@ -3,12 +3,12 @@ box-shadow: #ccc 0px 1px 2px 1px; } -.popover.notifications { +.notifications { .popover-body { padding: 0; } - .list-group-item { + &.popover .list-group-item { padding: 9px 14px; border-radius: 0; border-top: 1px solid #fff; diff --git a/scipost/static/scipost/assets/css/_type.scss b/scipost/static/scipost/assets/css/_type.scss index 6aba22c09b1129e47f6037081f719db50a8b3812..9f21c44547325bbbf1055366f00574a18cc4a3b2 100644 --- a/scipost/static/scipost/assets/css/_type.scss +++ b/scipost/static/scipost/assets/css/_type.scss @@ -26,7 +26,8 @@ h1 > a { color: $scipost-darkblue; } -h3 { +h3, +h4 { line-height: normal; } diff --git a/scipost/static/scipost/assets/js/notifications.js b/scipost/static/scipost/assets/js/notifications.js index 33acc8db84bc4daf1baa9d9f4fbfdb7909a184b1..533c57a74a540a983911e7a9e267f2b03e6b34d9 100644 --- a/scipost/static/scipost/assets/js/notifications.js +++ b/scipost/static/scipost/assets/js/notifications.js @@ -2,12 +2,69 @@ 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/list/'; +var notify_api_url_toggle_read = '/notifications/mark-toggle'; var notify_fetch_count = '5'; var notify_refresh_period = 15000; var consecutive_misfires = 0; var registered_functions = [fill_notification_badge]; +function initiate_popover(reinitiate=false) { + var notification_template = '<div class="popover notifications" role="tooltip"><div class="arrow"></div><h3 class="popover-header h2"></h3><div class="popover-body"></div></div>'; + + var get_notifications_title = function() { + return 'New notifications <div class="badge badge-warning live_notify_badge"></div><div class="mt-1"><small><a href="/notifications">See all my notifications</a></small></div>'; + } + + var get_notifications = function() { + var _str = '<ul id="notification-list" class="update_notifications list-group"><div class="w-100 text-center py-4"><i class="fa fa-circle-o-notch fa-2x fa-spin fa-fw"></i><span class="sr-only">Loading...</span></div></ul>'; + get_notification_list(); + return _str; + } + + $('.popover [data-toggle="tooltip"]').tooltip('dispose') + $('#notifications_badge').popover('dispose').popover({ + animation: false, + trigger: 'click', + title: get_notifications_title, + template: notification_template, + content: get_notifications, + container: 'body', + offset: '0, 9px', + placement: "bottom", + html: true + }).on('inserted.bs.popover', function() { + // Bloody js + setTimeout(function() { + $('.popover [data-toggle="tooltip"]').tooltip({ + animation: false, + fallbackPlacement: 'clockwise', + placement: 'bottom' + }); + $('.popover .actions a').on('click', function() { + mark_toggle(this) + }) + }, 1000); + }); + if (reinitiate) { + $('#notifications_badge').popover('show') + } +} + + +function mark_toggle(el) { + var r = new XMLHttpRequest(); + r.addEventListener('readystatechange', function(event){ + if (this.readyState == 4 && this.status == 200) { + fetch_api_data() + initiate_popover(reinitiate=true) + } + }) + r.open("GET", notify_api_url_toggle_read +'/' + $(el).data('slug') + '/?json=1', true); + r.send(); +} + + function fill_notification_badge(data) { var badges = document.getElementsByClassName(notify_badge_class); if (badges) { @@ -21,12 +78,12 @@ function get_notification_list() { var data = fetch_api_data(notify_api_url_list, function(data) { var messages = data.list.map(function (item) { - var message = ""; + var message = ''; if(typeof item.actor !== 'undefined'){ - message = '<strong>' + item.actor + '</strong>'; + message += '<strong>' + item.actor + '</strong>'; } if(typeof item.verb !== 'undefined'){ - message = message + " " + item.verb; + message += " " + item.verb; } if(typeof item.target !== 'undefined'){ if(typeof item.forward_link !== 'undefined') { @@ -36,18 +93,20 @@ function get_notification_list() { } } if(typeof item.timesince !== 'undefined'){ - message = message + " <div class='text-muted'>" + item.timesince + " ago</div>"; + message += " <div class='text-muted'>" + item.timesince + " ago</div>"; } if(item.unread) { - var mark_as_read = '<div class="actions"><a href="#"><i class="fa fa-circle" data-toggle="tooltip" data-placement="auto" title="Mark as unread" aria-hidden="true"></i></a></div>'; + var mark_as_read = '<div class="actions"><a href="#" data-slug="' + item.slug + '"><i class="fa fa-circle" data-toggle="tooltip" data-placement="auto" title="Mark as unread" aria-hidden="true"></i></a></div>'; } else { - var mark_as_read = '<div class="actions"><a href="#"><i class="fa fa-circle-o" data-toggle="tooltip" data-placement="auto" title="Mark as read" aria-hidden="true"></i></a></div>'; + var mark_as_read = '<div class="actions"><a href="#" data-slug="' + item.slug + '"><i class="fa fa-circle-o" data-toggle="tooltip" data-placement="auto" title="Mark as read" aria-hidden="true"></i></a></div>'; } return '<li class="list-group-item ' + (item.unread ? ' active' : '') + '">' + mark_as_read + message + '</li>'; }).join(''); - if (messages == '') { messages = 'You have no notifications.' } + if (messages == '') { + messages = '<div class="text-center px-2 py-3"><i class="fa fa-star-o fa-2x" aria-hidden="true"></i><h3>You have no new notifications</h3></div>' + } document.getElementById('notification-list').innerHTML = messages; }); @@ -94,34 +153,5 @@ function fetch_api_data(url=null, callback=null) { setTimeout(fetch_api_data, 1000); $(function(){ - var notification_template = '<div class="popover notifications" role="tooltip"><div class="arrow"></div><h3 class="popover-header h2"></h3><div class="popover-body"></div></div>'; - - var get_notifications_title = function() { - return 'New notifications <div class="badge badge-warning live_notify_badge"></div><div class="mt-1"><small><a href="/notifications">See all my notifications</a></small></div>'; - } - - var get_notifications = function() { - var _str = '<ul id="notification-list" class="update_notifications list-group"><div class="w-100 text-center py-4"><i class="fa fa-circle-o-notch fa-2x 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 - }).on('inserted.bs.popover', function() { - console.log($('.popover [data-toggle="tooltip"]')) - $('.popover [data-toggle="tooltip"]').tooltip({ - animation: false, - fallbackPlacement: 'clockwise', - placement: 'auto' - }); - }); + initiate_popover(); });