Commit 9d8c6109 by Őry Máté

Merge branch 'feature-notification-2' into 'master'

Add a new Notification model

Fixes #66
parents 61cfbebd 55912feb
...@@ -7,7 +7,12 @@ from django.contrib.auth.signals import user_logged_in ...@@ -7,7 +7,12 @@ from django.contrib.auth.signals import user_logged_in
from django.db.models import ( from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField
) )
from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _, override
from model_utils.models import TimeStampedModel
from model_utils.fields import StatusField
from model_utils import Choices
from vm.models import Instance from vm.models import Instance
from acl.models import AclBase from acl.models import AclBase
...@@ -20,6 +25,32 @@ class Favourite(Model): ...@@ -20,6 +25,32 @@ class Favourite(Model):
user = ForeignKey(User) user = ForeignKey(User)
class Notification(TimeStampedModel):
STATUS = Choices(('new', _('new')),
('delivered', _('delivered')),
('read', _('read')))
status = StatusField()
to = ForeignKey(User)
subject = CharField(max_length=128)
message = TextField()
class Meta:
ordering = ['-created']
@classmethod
def send(cls, user, subject, template, context={}):
try:
language = user.profile.preferred_language
except:
language = None
with override(language):
context['user'] = user
rendered = render_to_string(template, context)
subject = unicode(subject)
return cls.objects.create(to=user, subject=subject, message=rendered)
class Profile(Model): class Profile(Model):
user = OneToOneField(User) user = OneToOneField(User)
preferred_language = CharField(verbose_name=_('preferred language'), preferred_language = CharField(verbose_name=_('preferred language'),
...@@ -31,6 +62,9 @@ class Profile(Model): ...@@ -31,6 +62,9 @@ class Profile(Model):
help_text=_('Unique identifier of the person, e.g. a student number.')) help_text=_('Unique identifier of the person, e.g. a student number.'))
instance_limit = IntegerField(default=5) instance_limit = IntegerField(default=5)
def notify(self, subject, template, context={}):
return Notification.send(self.user, subject, template, context)
class GroupProfile(AclBase): class GroupProfile(AclBase):
ACL_LEVELS = ( ACL_LEVELS = (
......
...@@ -331,3 +331,30 @@ a.hover-black { ...@@ -331,3 +331,30 @@ a.hover-black {
display: block; display: block;
} }
.notification-messages {
padding: 10px 8px;
width: 350px;
}
.notification-message {
margin-bottom: 10px;
padding: 0 0 4px 0;
border-bottom: 1px dotted #D3D3D3;
}
.notification-messages .notification-message:last-child {
margin-bottom: 0px;
padding: 0px;
border-bottom: none;
}
.notification-message-text {
padding: 8px 15px;
display: none;
}
.notification-message .notification-message-subject {
cursor: pointer;
}
...@@ -205,7 +205,16 @@ $(function () { ...@@ -205,7 +205,16 @@ $(function () {
window.location.href = "/dashboard/vm/list/?s=" + input; window.location.href = "/dashboard/vm/list/?s=" + input;
} }
}); });
/* notification message toggle */
$(document).on('click', ".notification-message-subject", function() {
$(".notification-message-text", $(this).parent()).slideToggle();
return false;
});
$("#notification-button").click(function() {
$('.notification-messages').load("/dashboard/notifications/");
});
}); });
function generateVmHTML(pk, name, fav) { function generateVmHTML(pk, name, fav) {
......
{% load i18n %}
{% for n in notifications %}
<li class="notification-message">
<span class="notification-message-subject">
{% if n.status == "new" %}<i class="icon-envelope-alt"></i> {% endif %}
{{ n.subject }}
</span>
<span class="notification-message-date pull-right">
{{ n.created|timesince }}
</span>
<div style="clear: both;"></div>
<div class="notification-message-text">
{{ n.message }}
</div>
</li>
{% empty %}
<li class="notification-message">
{% trans "You have no notifications." %}
</li>
{% endfor %}
...@@ -23,16 +23,28 @@ ...@@ -23,16 +23,28 @@
<body> <body>
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar navbar-inverse navbar-fixed-top">
<a class="navbar-brand" href="/dashboard/">{% block header-site %}CIRCLE{% endblock %}</a> <div class="navbar-header">
<!-- temporarily --> <a class="navbar-brand" href="{% url "dashboard.index" %}">CIRCLE</a>
<a class="navbar-brand pull-right" href="/network/" style="color: white; font-size: 10px;">Network</a> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;">Admin</a> <span class="icon-bar"></span>
{% if user.is_authenticated %} <span class="icon-bar"></span>
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;">Log out {{user}}</a> <span class="icon-bar"></span>
{% else %} </button>
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;">Login</a> </div><!-- .navbar-header -->
{% endif %} <div class="collapse navbar-collapse">
</div> <ul class="nav navbar-nav pull-right">
{% block navbar-ul %}
{% endblock %}
</ul>
<a class="navbar-brand pull-right" href="/network/" style="color: white; font-size: 10px;">Network</a>
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;">Admin</a>
{% if user.is_authenticated %}
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;">Log out {{user}}</a>
{% else %}
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;">Login</a>
{% endif %}
</div><!-- .collapse .navbar-collapse -->
</div><!-- navbar navbar-inverse navbar-fixed-top -->
<div class="container"> <div class="container">
{% block messages %} {% block messages %}
......
...@@ -3,6 +3,17 @@ ...@@ -3,6 +3,17 @@
{% block title-page %}{% trans "Dashboard" %}{% endblock %} {% block title-page %}{% trans "Dashboard" %}{% endblock %}
{% block navbar-ul %}
<li class="dropdown" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;" class="dropdown-toggle" data-toggle="dropdown">
Notifications{% if new_notifications %} <span class="badge">{{ new_notifications }}</span>{% endif %}
</a>
<ul class="dropdown-menu notification-messages">
<li>{% trans "Loading..." %}</li>
</ul>
</li>
{% endblock %}
{% block content %} {% block content %}
<div class="body-content dashboard-index"> <div class="body-content dashboard-index">
<div class="row"> <div class="row">
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="icon-desktop"></i> {% trans "Notifications" %}</h3>
</div>
<div class="panel-body">
<ul style="list-style: none;">
{% include "dashboard/_notifications-timeline.html" %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
This is a test for {{user.username}}.
Var: {{var}}.
from django.contrib.auth.models import User
from django.test import TestCase
from ..models import Profile
class NotificationTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
Profile.objects.get_or_create(user=self.u1)
self.u2 = User.objects.create(username='user2')
Profile.objects.get_or_create(user=self.u2)
def test_notification_send(self):
c1 = self.u1.notification_set.count()
c2 = self.u1.notification_set.count()
profile = self.u1.profile
msg = profile.notify('subj',
'dashboard/test_message.txt',
{'var': 'testme'})
assert self.u1.notification_set.count() == c1 + 1
assert self.u2.notification_set.count() == c2
assert 'user1' in msg.message
assert 'testme' in msg.message
assert msg in self.u1.notification_set.all()
...@@ -224,3 +224,13 @@ class VmDetailTest(TestCase): ...@@ -224,3 +224,13 @@ class VmDetailTest(TestCase):
'disk-size': 1}) 'disk-size': 1})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(disks + 1, inst.disks.count()) self.assertEqual(disks + 1, inst.disks.count())
def test_notification_read(self):
c = Client()
self.login(c, "user1")
self.u1.profile.notify('subj', 'dashboard/test_message.txt',
{'var': 'testme'})
assert self.u1.notification_set.get().status == 'new'
response = c.get("/dashboard/notifications/")
self.assertEqual(response.status_code, 200)
assert self.u1.notification_set.get().status == 'read'
...@@ -8,7 +8,7 @@ from .views import ( ...@@ -8,7 +8,7 @@ from .views import (
TemplateList, LeaseDetail, NodeCreate, LeaseCreate, TemplateCreate, TemplateList, LeaseDetail, NodeCreate, LeaseCreate, TemplateCreate,
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete, FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete, VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete,
GroupAclUpdateView, GroupUserDelete, NodeGraphView GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -81,4 +81,7 @@ urlpatterns = patterns( ...@@ -81,4 +81,7 @@ urlpatterns = patterns(
name='dashboard.views.group-acl'), name='dashboard.views.group-acl'),
url(r'^groupuser/delete/(?P<pk>\d+)/$', GroupUserDelete.as_view(), url(r'^groupuser/delete/(?P<pk>\d+)/$', GroupUserDelete.as_view(),
name="dashboard.views.delete-groupuser"), name="dashboard.views.delete-groupuser"),
url(r'^notifications/$', NotificationView.as_view(),
name="dashboard.views.notifications"),
) )
...@@ -81,6 +81,10 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -81,6 +81,10 @@ class IndexView(LoginRequiredMixin, TemplateView):
'more_instances': instances.count() - len(instances[:5]) 'more_instances': instances.count() - len(instances[:5])
}) })
if user is not None:
context['new_notifications'] = user.notification_set.filter(
status="new").count()
nodes = Node.objects.all() nodes = Node.objects.all()
groups = Group.objects.all() groups = Group.objects.all()
context.update({ context.update({
...@@ -1652,3 +1656,31 @@ class NodeGraphView(SuperuserRequiredMixin, GraphViewBase): ...@@ -1652,3 +1656,31 @@ class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
def get_object(self, request, pk): def get_object(self, request, pk):
return self.model.objects.get(id=pk) return self.model.objects.get(id=pk)
class NotificationView(LoginRequiredMixin, TemplateView):
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_notifications-timeline.html']
else:
return ['dashboard/notifications.html']
def get_context_data(self, *args, **kwargs):
context = super(NotificationView, self).get_context_data(
*args, **kwargs)
# we need to convert it to list, otherwise it's gonna be
# similar to a QuerySet and update everything to
# read status after get
n = 10 if self.request.is_ajax() else 1000
context['notifications'] = list(
self.request.user.notification_set.values()[:n])
return context
def get(self, *args, **kwargs):
response = super(NotificationView, self).get(*args, **kwargs)
un = self.request.user.notification_set.filter(status="new")
for u in un:
u.status = "read"
u.save()
return response
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment