SciPost Code Repository

Skip to content
Snippets Groups Projects
Commit 6729150b authored by Jorran de Wit's avatar Jorran de Wit
Browse files

Notification core set up

parent 2cb30a06
No related branches found
No related tags found
No related merge requests found
Showing
with 431 additions and 6 deletions
......@@ -90,6 +90,7 @@ INSTALLED_APPS = (
'mails',
'mailing_lists',
'news',
'notifications',
'scipost',
'submissions',
'theses',
......@@ -107,11 +108,6 @@ HAYSTACK_CONNECTIONS = {
'PATH': 'local_files/haystack/',
'EXCLUDED_INDEXES': ['sphinxdoc.search_indexes.DocumentIndex'],
},
# 'scipost': {
# 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
# 'PATH': 'local_files/haystack_scipost/',
# 'EXCLUDED_INDEXES': ['sphinxdoc.search_indexes.DocumentIndex'],
# },
}
# Brute force automatically re-index Haystack using post_save signals on all models.
......
......@@ -41,6 +41,7 @@ urlpatterns = [
url(r'^thesis/', include('theses.urls', namespace="_theses")),
url(r'^meetings/', include('virtualmeetings.urls', namespace="virtualmeetings")),
url(r'^news/', include('news.urls', namespace="news")),
url(r'^notifications/', include('notifications.urls', namespace="notifications")),
url(r'^production/', include('production.urls', namespace="production")),
url(r'^partners/', include('partners.urls', namespace="partners")),
......
from django.contrib import admin
from .models import Notification
class NotificationAdmin(admin.ModelAdmin):
raw_id_fields = ('recipient', )
list_display = ('recipient', 'actor',
'level', 'target', 'unread', 'public')
list_filter = ('level', 'unread', 'public', 'created', )
admin.site.register(Notification, NotificationAdmin)
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
name = 'notifications'
from django.db import models
class NotificationQuerySet(models.query.QuerySet):
def unsent(self):
return self.filter(emailed=False)
def sent(self):
return self.filter(emailed=True)
def unread(self):
"""Return only unread items in the current queryset"""
return self.filter(unread=True)
def read(self):
"""Return only read items in the current queryset"""
return self.filter(unread=False)
def mark_all_as_read(self, recipient=None):
"""Mark as read any unread messages in the current queryset."""
# We want to filter out read ones, as later we will store
# the time they were marked as read.
qs = self.unread()
if recipient:
qs = qs.filter(recipient=recipient)
return qs.update(unread=False)
def mark_all_as_unread(self, recipient=None):
"""Mark as unread any read messages in the current queryset."""
qs = self.read()
if recipient:
qs = qs.filter(recipient=recipient)
return qs.update(unread=True)
def deleted(self):
"""Return only deleted items in the current queryset"""
raise DeprecationWarning
return self.filter(deleted=True)
def active(self):
"""Return only active(un-deleted) items in the current queryset"""
raise DeprecationWarning
return self.filter(deleted=False)
def mark_all_as_deleted(self, recipient=None):
"""Mark current queryset as deleted."""
raise DeprecationWarning
qs = self.active()
if recipient:
qs = qs.filter(recipient=recipient)
return qs.update(deleted=True)
def mark_all_as_active(self, recipient=None):
"""Mark current queryset as active(un-deleted)."""
raise DeprecationWarning
qs = self.deleted()
if recipient:
qs = qs.filter(recipient=recipient)
return qs.update(deleted=False)
def mark_as_unsent(self, recipient=None):
qs = self.sent()
if recipient:
qs = self.filter(recipient=recipient)
return qs.update(emailed=False)
def mark_as_sent(self, recipient=None):
qs = self.unsent()
if recipient:
qs = self.filter(recipient=recipient)
return qs.update(emailed=True)
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-11 18:33
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('level', models.CharField(choices=[('success', 'Success'), ('info', 'Info'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=20)),
('unread', models.BooleanField(default=True)),
('actor_object_id', models.CharField(max_length=255)),
('verb', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('target_object_id', models.CharField(blank=True, max_length=255, null=True)),
('action_object_object_id', models.CharField(blank=True, max_length=255, null=True)),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('public', models.BooleanField(default=True)),
('emailed', models.BooleanField(default=False)),
('action_object_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notify_action_object', to='contenttypes.ContentType')),
('actor_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notify_actor', to='contenttypes.ContentType')),
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
('target_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notify_target', to='contenttypes.ContentType')),
],
options={
'ordering': ('-created',),
},
),
]
from django.db import models
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.utils import timezone
from .managers import NotificationQuerySet
class Notification(models.Model):
"""
Action model describing the actor acting out a verb (on an optional
target).
Nomenclature based on http://activitystrea.ms/specs/atom/1.0/
Generalized Format::
<actor> <verb> <time>
<actor> <verb> <target> <time>
<actor> <verb> <action_object> <target> <time>
Examples::
<justquick> <reached level 60> <1 minute ago>
<brosner> <commented on> <pinax/pinax> <2 hours ago>
<washingtontimes> <started follow> <justquick> <8 minutes ago>
<mitsuhiko> <closed> <issue 70> on <mitsuhiko/flask> <about 2 hours ago>
Unicode Representation::
justquick reached level 60 1 minute ago
mitsuhiko closed issue 70 on mitsuhiko/flask 3 hours ago
"""
LEVELS = (('success', 'Success'), ('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'))
level = models.CharField(choices=LEVELS, default='info', max_length=20)
recipient = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False,
related_name='notifications')
unread = models.BooleanField(default=True)
actor_content_type = models.ForeignKey(ContentType, related_name='notify_actor')
actor_object_id = models.CharField(max_length=255)
actor = GenericForeignKey('actor_content_type', 'actor_object_id')
verb = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
target_content_type = models.ForeignKey(ContentType, related_name='notify_target',
blank=True, null=True)
target_object_id = models.CharField(max_length=255, blank=True, null=True)
target = GenericForeignKey('target_content_type', 'target_object_id')
action_object_content_type = models.ForeignKey(ContentType, blank=True, null=True,
related_name='notify_action_object')
action_object_object_id = models.CharField(max_length=255, blank=True, null=True)
action_object = GenericForeignKey('action_object_content_type', 'action_object_object_id')
created = models.DateTimeField(default=timezone.now)
public = models.BooleanField(default=True)
emailed = models.BooleanField(default=False)
objects = NotificationQuerySet.as_manager()
class Meta:
ordering = ('-created', )
def __unicode__(self):
ctx = {
'actor': self.actor,
'verb': self.verb,
'action_object': self.action_object,
'target': self.target,
'timesince': self.timesince()
}
if self.target:
if self.action_object:
return u'%(actor)s %(verb)s %(action_object)s on %(target)s %(timesince)s ago' % ctx
return u'%(actor)s %(verb)s %(target)s %(timesince)s ago' % ctx
if self.action_object:
return u'%(actor)s %(verb)s %(action_object)s %(timesince)s ago' % ctx
return u'%(actor)s %(verb)s %(timesince)s ago' % ctx
def timesince(self, now=None):
"""
Shortcut for the ``django.utils.timesince.timesince`` function of the
current timestamp.
"""
from django.utils.timesince import timesince as timesince_
return timesince_(self.timestamp, now)
@property
def slug(self):
from .utils import id2slug
return id2slug(self.id)
def mark_as_read(self):
if self.unread:
self.unread = False
self.save()
def mark_as_unread(self):
if not self.unread:
self.unread = True
self.save()
{% extends 'scipost/_personal_page_base.html' %}
{% block breadcrumb_items %}
{{block.super}}
<span class="breadcrumb-item">My notifications ({{notifications|length}})</span>
{% endblock %}
{% block pagetitle %}: My notifications ({{notifications|length}}){% endblock pagetitle %}
{% block content %}
<ul class="list-group notifications">
{% for notice in notifications %}
{% include 'notifications/partials/notice.html' %}
{% endfor %}
</ul>
{% endblock %}
<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">&times;</a>
<h4 class="mb-1x">
<strong>{{ notice.actor }}</strong>
{{ notice.verb }}
{% if notice.target %}
{{ notice.target }}
{% endif %}
</h4>
<div class="text-muted">{{ notice.created|timesince }} ago</div>
</div>
<p class="mb-1">{{ notice.description|linebreaksbr }}</p>
{% if notice.target %}
<a href="{{notice.target.get_absolute_url}}">Go to {{notice.target_content_type}}</a>
{% endif %}
</div>
from django.test import TestCase
# Create your tests here.
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.AllNotificationsList.as_view(), name='all'),
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'^delete/(?P<slug>\d+)/$', views.delete, name='delete'),
url(r'^api/unread_count/$', views.live_unread_notification_count,
name='live_unread_notification_count'),
url(r'^api/unread_list/$', views.live_unread_notification_list,
name='live_unread_notification_list'),
]
def slug2id(slug):
return int(slug) - 9631
def id2slug(id):
return id + 9631
from django.contrib.auth.decorators import login_required
from django.forms import model_to_dict
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.views.generic import ListView
from .models import Notification
from .utils import id2slug, slug2id
@method_decorator(login_required, name='dispatch')
class NotificationViewList(ListView):
context_object_name = 'notifications'
class AllNotificationsList(NotificationViewList):
"""
Index page for authenticated user
"""
def get_queryset(self):
return self.request.user.notifications.all()
class UnreadNotificationsList(NotificationViewList):
def get_queryset(self):
return self.request.user.notifications.unread()
@login_required
def mark_all_as_read(request):
request.user.notifications.mark_all_as_read()
_next = request.GET.get('next')
if _next:
return redirect(_next)
return redirect('notifications:unread')
@login_required
def mark_as_read(request, slug=None):
id = slug2id(slug)
notification = get_object_or_404(Notification, recipient=request.user, id=id)
notification.mark_as_read()
_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 _next:
return redirect(_next)
return redirect('notifications:unread')
@login_required
def delete(request, slug=None):
id = slug2id(slug)
notification = get_object_or_404(Notification, recipient=request.user, id=id)
notification.delete()
_next = request.GET.get('next')
if _next:
return redirect(_next)
return redirect('notifications:all')
def live_unread_notification_count(request):
if not request.user.is_authenticated():
data = {'unread_count': 0}
else:
data = {'unread_count': request.user.notifications.unread().count()}
return JsonResponse(data)
def live_unread_notification_list(request):
if not request.user.is_authenticated():
data = {
'unread_count': 0,
'unread_list': []
}
return JsonResponse(data)
try:
# Default to 5 as a max number of notifications
num_to_fetch = max(int(request.GET.get('max', 5)))
num_to_fetch = min(num_to_fetch, 100)
except ValueError:
num_to_fetch = 5
unread_list = []
for n in request.user.notifications.unread()[:num_to_fetch]:
struct = model_to_dict(n)
struct['slug'] = id2slug(n.id)
if n.actor:
struct['actor'] = str(n.actor)
if n.target:
struct['target'] = str(n.target)
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()
data = {
'unread_count': request.user.notifications.unread().count(),
'unread_list': unread_list
}
return JsonResponse(data)
......@@ -93,6 +93,9 @@ $list-group-bg: transparent;
$list-group-item-padding-x: 0;
$list-group-item-padding-y: 0;
$list-group-border-color: #ddd;
$list-group-active-color: $scipost-darkblue;
$list-group-active-bg: $scipost-lightestblue;
$list-group-active-border-color: $scipost-lightestblue;
// Fonts
//
......
.alert {
padding: 0.75rem 3rem 0.75rem 1.25rem;
padding: 0.75rem 1.25rem;
margin-bottom: 0.5rem;
position: relative;
clear: both;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment