Skip to content

Commit

Permalink
Implement cached notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
teemulehtinen committed Sep 19, 2016
1 parent 746ee3b commit 51a666d
Show file tree
Hide file tree
Showing 17 changed files with 170 additions and 311 deletions.
14 changes: 7 additions & 7 deletions aplus/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,6 @@
os.path.join(BASE_DIR, 'templates'),
)

TEMPLATE_LOADERS = (
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),
)

TEMPLATE_CONTEXT_PROCESSORS = (
"django.contrib.auth.context_processors.auth",
"django.core.context_processors.debug",
Expand Down Expand Up @@ -356,3 +349,10 @@
# If debug is enabled allow basic auth for API
if DEBUG:
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] += ('rest_framework.authentication.BasicAuthentication',)
else:
TEMPLATE_LOADERS = (
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),
)
3 changes: 3 additions & 0 deletions assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ html, body {
height: 20px;
border-left: 1px solid black;
}
.glyphicon.red {
color: #ff7070;
}
.badge-danger {
background-color: #d9534f !important;
color: #fff !important;
Expand Down
9 changes: 0 additions & 9 deletions doc/initial_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,12 @@
"name": "First exercise: Hello A+",
"order": 1,
"status": "ready",
"content_time": null,
"url": "1",
"service_url": "http://localhost:8888/first_exercise/",
"model_answers": "",
"exercise_info": null,
"category": 1,
"description": "",
"content": "",
"content_head": "",
"parent": null,
"course_module": 1,
"use_wide_column": false
Expand All @@ -123,15 +120,12 @@
"name": "File exercise (assistant graded)",
"order": 2,
"status": "ready",
"content_time": null,
"url": "2",
"service_url": "http://localhost:8888/file_exercise/",
"model_answers": "",
"exercise_info": null,
"category": 1,
"description": "",
"content": "",
"content_head": "",
"parent": null,
"course_module": 1,
"use_wide_column": false
Expand All @@ -144,15 +138,12 @@
"name": "AJAX exercise",
"order": 3,
"status": "ready",
"content_time": null,
"url": "3",
"service_url": "http://localhost:8888/ajax_exercise/",
"model_answers": "",
"exercise_info": null,
"category": 1,
"description": "",
"content": "",
"content_head": "",
"parent": null,
"course_module": 1,
"use_wide_column": false
Expand Down
10 changes: 10 additions & 0 deletions exercise/cache/points.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.utils import timezone

from lib.cached import CachedAbstract
from notification.models import Notification
from ..models import LearningObject, Submission
from .hierarchy import ContentMixin

Expand Down Expand Up @@ -94,6 +95,8 @@ def r_augment(children):
})
if submission.notifications.count() > 0:
entry['notified'] = True
if submission.notifications.filter(seen=False).count() > 0:
entry['unseen'] = True

# Confirm points.
def r_check(parent, children):
Expand Down Expand Up @@ -177,7 +180,14 @@ def invalidate_content(sender, instance, **kwargs):
for profile in instance.submitters.all():
CachedPoints.invalidate(course, profile.user)

def invalidate_notification(sender, instance, **kwargs):
CachedPoints.invalidate(
instance.submission.exercise.course_instance,
instance.recipient.user
)

# Automatically invalidate cached points when submissions change.
post_save.connect(invalidate_content, sender=Submission)
post_delete.connect(invalidate_content, sender=Submission)
post_save.connect(invalidate_notification, sender=Notification)
post_delete.connect(invalidate_notification, sender=Notification)
2 changes: 1 addition & 1 deletion exercise/templates/exercise/_user_results.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ <h3 class="panel-title">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="badge">
{% if entry.notified %}
<span class="glyphicon glyphicon-comment"></span>
<span class="glyphicon glyphicon-comment{% if entry.unseen %} red{% endif %}"></span>
{% endif %}
{{ entry.submission_count }}
{% if entry.max_submissions > 0 %}
Expand Down
19 changes: 0 additions & 19 deletions lib/viewbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,22 +124,3 @@ def form_valid(self, form):

class BaseFormView(BaseFormMixin, BaseViewMixin, FormView):
pass


class PagerMixin(object):
page_kw = "page"
per_page = 10

def get_common_objects(self):
super().get_common_objects()
self.page = self._parse_page(self.page_kw)
self.note("page", "per_page")

def _parse_page(self, parameter_name):
try:
value = self.request.GET.get(parameter_name)
if value:
return max(1, int(value))
except ValueError:
pass
return 1
65 changes: 65 additions & 0 deletions notification/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from django.db.models.signals import post_save, post_delete

from lib.cached import CachedAbstract
from .models import Notification


class CachedNotifications(CachedAbstract):
KEY_PREFIX = "notifications"

def __init__(self, user):
super().__init__(user)

def _generate_data(self, user, data=None):
if not user or not user.is_authenticated():
return {
'count': 0,
'notifications': [],
}

def notification_entry(n):
exercise = n.submission.exercise if n.submission else None
return {
'id': n.id,
'submission_id': n.submission.id if n.submission else 0,
'name': "{} {}, {}".format(
n.course_instance.course.code,
(str(exercise.parent)
if exercise and exercise.parent else
n.course_instance.instance_name),
(str(exercise)
if exercise else
n.subject),
),
'link': n.get_display_url(),
}

notifications = list(
user.userprofile.received_notifications\
.filter(seen=False)\
.select_related(
'submission',
'submission__exercise',
'course_instance',
'course_instance__course',
)
)
return {
'count': len(notifications),
'notifications': [notification_entry(n) for n in notifications],
}

def count(self):
return self.data['count']

def notifications(self):
return self.data['notifications']


def invalidate_notifications(sender, instance, **kwargs):
CachedNotifications.invalidate(instance.recipient)


# Automatically invalidate cache when notifications change.
post_save.connect(invalidate_notifications, sender=Notification)
post_delete.connect(invalidate_notifications, sender=Notification)
98 changes: 24 additions & 74 deletions notification/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,84 +6,10 @@
from userprofile.models import UserProfile


class NotificationSet(object):
"""
A result set of notifications.
"""
@classmethod
def get_unread(cls, user):
qs = []
if user:
qs = user.userprofile.received_notifications.filter(
seen=False
).select_related('submission__exercise')
return NotificationSet(qs)

@classmethod
def get_course(cls, course_instance, user, per_page=30, page=1):
""" DEPRECATED, not used """
skip = max(0, page - 1) * per_page
qs = user.userprofile.received_notifications.filter(
course_instance=course_instance
).select_related('submission__exercise')[skip:(skip + per_page)]
return NotificationSet(qs)

@classmethod
def get_course_new_count(cls, course_instance, user):
""" DEPRECATED, not used """
return user.userprofile.received_notifications.filter(
course_instance=course_instance,
seen=False
).count()

def __init__(self, queryset):
self.notifications = list(queryset)

@property
def count(self):
return len(self.notifications)

def count_and_mark_unseen(self):
"""
DEPRECATED, not used, was for separate notifications page
Marks notifications seen in data base but keeps the set instances
in unseen state.
"""
count = 0
for notification in self.notifications:
if not notification.seen:
count += 1
notification.seen = True
notification.save(update_fields=["seen"])

# Return the instance to previous state without saving.
notification.seen = False
return count


class Notification(UrlMixin, models.Model):
"""
A user notification of some event, for example manual assessment.
"""

@classmethod
def send(cls, sender, submission):
for recipient in submission.submitters.all():
notification = Notification(
sender=sender,
recipient=recipient,
course_instance=submission.exercise.course_instance,
submission=submission,
)
notification.save()

@classmethod
def remove(cls, submission):
Notification.objects.filter(
submission=submission,
recipient__in=submission.submitters.all(),
).delete()

subject = models.CharField(max_length=255, blank=True)
notification = models.TextField(blank=True)
sender = models.ForeignKey(UserProfile,
Expand All @@ -105,6 +31,30 @@ def __str__(self):
+ (str(self.submission.exercise) if self.submission else self.subject)
)

@classmethod
def send(cls, sender, submission):
for recipient in submission.submitters.all():
if Notification.objects.filter(
submission=submission,
recipient=recipient,
seen=False,
).count() == 0:
notification = Notification(
sender=sender,
recipient=recipient,
course_instance=submission.exercise.course_instance,
submission=submission,
)
notification.save()

@classmethod
def remove(cls, submission):
Notification.objects.filter(
submission=submission,
recipient__in=submission.submitters.all(),
seen=False,
).delete()

ABSOLUTE_URL_NAME = "notify"

def get_url_kwargs(self):
Expand Down
35 changes: 14 additions & 21 deletions notification/templates/notification/_notification_menu.html
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
{% load i18n %}
{% load course %}
{% if unread.count > 0 %}
{% if count > 0 %}
<li role="presentation" class="menu-notification dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-envelope pull-left" aria-hidden="true"></span>
<span class="badge badge-danger">{{ unread.count }}</span> {{ unread_message }}
<span class="caret" aria-hidden="true"></span>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-envelope pull-left" aria-hidden="true"></span>
<span class="badge badge-danger">{{ count }}</span> {{ unread_message }}
<span class="caret" aria-hidden="true"></span>
</a>
<ul class="dropdown-menu">
{% for entry in notifications %}
<li>
<a href="{{ entry.link }}" class="alert-link">
{{ entry.name }}
</a>
<ul class="dropdown-menu">
{% for notification in unread.notifications %}
<li>
<a href="{{ notification|url }}" class="alert-link">
{{ notification.course_instance.course }}:
{% if notification.submission %}
{{ notification.submission.exercise }}
{% else %}
{{ notification.subject }}
{% endif %}
</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</li>
{% endif %}
11 changes: 5 additions & 6 deletions notification/templates/notification/_notification_messages.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
{% load i18n %}
{% load course %}
{% if unread.count > 0 %}
{% if count > 0 %}
<div class="alert alert-danger visible-xs">
<span class="glyphicon glyphicon-envelope pull-left" aria-hidden="true"></span>
<p>
{% trans "You have" %} {{ undread.count }} {{ unread_message }}
{% trans "You have" %} {{ count }} {{ unread_message }}
<ul>
{% for notification in unread.notifications %}
<li><a href="{{ notification.course_instance|url:'notifications' }}" class="alert-link">
{{ notification.course_instance.course }}: {{ notification.subject }}
{% for entry in notifications %}
<li><a href="{{ entry.link }}" class="alert-link">
{{ entry.name }}
</a></li>
{% endfor %}
</ul>
Expand Down
Loading

0 comments on commit 51a666d

Please sign in to comment.