Skip to content
This repository has been archived by the owner on Jan 8, 2021. It is now read-only.

Commit

Permalink
Merge pull request #240 from mmiha/notifications_#165
Browse files Browse the repository at this point in the history
Notifications - issue #165
  • Loading branch information
mitar committed Sep 24, 2012
2 parents 2e143e5 + 2bc2865 commit b22610a
Show file tree
Hide file tree
Showing 16 changed files with 274 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ celerybeat-schedule
*.celerybeat-schedule

# Celerybeat
*.pid
*.pid
9 changes: 9 additions & 0 deletions piplmesh/account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import mongoengine
from mongoengine.django import auth

import uuid

from . import fields, utils
from .. import panels

Expand All @@ -27,6 +29,9 @@ def upper_birthdate_limit():
def lower_birthdate_limit():
return timezone.now().date() - datetime.timedelta(LOWER_DATE_LIMIT)

def generate_channel_id():
return uuid.uuid4()

class Connection(mongoengine.EmbeddedDocument):
http_if_none_match = mongoengine.StringField()
http_if_modified_since = mongoengine.StringField()
Expand Down Expand Up @@ -62,6 +67,7 @@ class User(auth.User):
birthdate = fields.LimitedDateTimeField(upper_limit=upper_birthdate_limit, lower_limit=lower_birthdate_limit)
gender = fields.GenderField()
language = fields.LanguageField()
channel_id = mongoengine.UUIDField(binary=False, default=generate_channel_id)

facebook_access_token = mongoengine.StringField(max_length=150)
facebook_profile_data = mongoengine.DictField()
Expand Down Expand Up @@ -155,6 +161,9 @@ def get_image_url(self):
else:
return staticfiles_storage.url(settings.DEFAULT_USER_IMAGE)

def get_user_channel(self):
return "user/%s" % self.channel_id

@classmethod
def create_user(cls, username, email=None, password=None):
now = timezone.now()
Expand Down
9 changes: 9 additions & 0 deletions piplmesh/api/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,13 @@ def apply_limits(self, request, object_list):
else:
object_list = object_list.filter(is_published=True)

return object_list

class NotificationAuthorization(tastypie_authorization.Authorization):
def apply_limits(self, request, object_list):
if request and hasattr(request, 'user'):
object_list = object_list.filter(recipient=request.user)
else:
object_list = []

return object_list
19 changes: 18 additions & 1 deletion piplmesh/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import mongoengine

from pushserver import utils
from piplmesh.account import models as account_models
from . import base

POST_MESSAGE_MAX_LENGTH = 500
Expand Down Expand Up @@ -33,6 +35,8 @@ class Post(base.AuthoredDocument):
comments = mongoengine.ListField(mongoengine.EmbeddedDocumentField(Comment), default=lambda: [], required=False)
attachments = mongoengine.ListField(mongoengine.EmbeddedDocumentField(Attachment), default=lambda: [], required=False)

subscribers = mongoengine.ListField(mongoengine.ReferenceField(account_models.User), default=lambda: [], required=False)

# TODO: Prevent posting comments if post is not published
# TODO: Prevent adding attachments if post is published
# TODO: Prevent marking post as unpublished once it was published
Expand All @@ -42,6 +46,19 @@ def save(self, *args, **kwargs):
self.updated_time = timezone.now()
return super(Post, self).save(*args, **kwargs)

class Notification(mongoengine.Document):
"""
This class defines document type for notifications.
"""

recipient = mongoengine.ReferenceField(account_models.User, required=True)
created_time = mongoengine.DateTimeField(default=timezone.now, required=True)
read = mongoengine.BooleanField(default=False)
post = mongoengine.ReferenceField(Post)

# TODO: This is probably not the best approach.
comment = mongoengine.IntField()

class UploadedFile(base.AuthoredDocument):
"""
This class document type for uploaded files.
Expand All @@ -65,4 +82,4 @@ class LinkAttachment(Attachment):

link_url = mongoengine.URLField(required=True)
link_caption = mongoengine.StringField(default='', required=True)
link_description = mongoengine.StringField(default='', required=True)
link_description = mongoengine.StringField(default='', required=True)
37 changes: 36 additions & 1 deletion piplmesh/api/resources.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from django.conf import settings

from tastypie import authorization as tastypie_authorization, fields as tastypie_fields

from tastypie_mongoengine import fields, paginator, resources

from pushserver.utils import updates

from piplmesh.account import models as account_models
from piplmesh.api import authorization, models as api_models, signals

Expand All @@ -26,12 +30,41 @@ def hydrate(self, bundle):
return bundle

class CommentResource(AuthoredResource):
def obj_create(self, bundle, request=None, **kwargs):
bundle = super(CommentResource, self).obj_create(bundle, request=request, **kwargs)

for subscriber in self.instance.subscribers:
if subscriber != bundle.obj.author:
notification = api_models.Notification.objects.create(recipient=subscriber, post=self.instance, comment=bundle.obj.pk)
signals.notification_created.send(sender=self, notification=notification, request=request or bundle.request)

if bundle.obj.author not in self.instance.subscribers:
self.instance.subscribers.append(bundle.obj.author)
self.instance.save()

return bundle

class Meta:
object_class = api_models.Comment
allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
# TODO: Make proper authorization, current implementation is for development use only
authorization = tastypie_authorization.Authorization()

class NotificationResource(resources.MongoEngineResource):
comment_message = tastypie_fields.CharField(default='', null=False, blank=True)
comment_author = tastypie_fields.CharField(default='', null=False, blank=True)

def dehydrate_comment_author(self, bundle):
return bundle.obj.post.comments[bundle.obj.comment].author

def dehydrate_comment_message(self, bundle):
return bundle.obj.post.comments[bundle.obj.comment].message

class Meta:
queryset = api_models.Notification.objects.all()
allowed_methods = ('get')
authorization = authorization.NotificationAuthorization()

class ImageAttachmentResource(AuthoredResource):
image_file = fields.ReferenceField(to='piplmesh.api.resources.UploadedFileResource', attribute='image_file', null=False, full=True)
image_description = tastypie_fields.CharField(attribute='image_description', default='', null=False, blank=True)
Expand Down Expand Up @@ -74,11 +107,13 @@ class PostResource(AuthoredResource):

def obj_create(self, bundle, request=None, **kwargs):
bundle = super(PostResource, self).obj_create(bundle, request=request, **kwargs)
bundle.obj.subscribers.append(bundle.request.user)
bundle.obj.save()
signals.post_created.send(sender=self, post=bundle.obj, request=request or bundle.request, bundle=bundle)
return bundle

class Meta:
queryset = api_models.Post.objects.all().order_by('-updated_time')
allowed_methods = ('get', 'post', 'put', 'patch', 'delete')
authorization = authorization.PostAuthorization()
paginator_class = paginator.Paginator
paginator_class = paginator.Paginator
1 change: 1 addition & 0 deletions piplmesh/api/signals.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django import dispatch

post_created = dispatch.Signal(providing_args=('post', 'request', 'bundle'))
notification_created = dispatch.Signal(providing_args=('notification', 'request'))
58 changes: 56 additions & 2 deletions piplmesh/frontend/static/piplmesh/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -188,21 +188,75 @@ input.gsc-search-button:hover,
right: 0;
}

#header .user ul {
#header .user > ul {
font-size: 10px;
list-style: none;
margin: 0;
text-align: right;
}

#header .user li {
#header .user > li {
float: left;
border-right: 1px solid #d7d7d7;
display: inline;
padding: 0 .75em;
white-space: nowrap;
}

#notifications_count {
border: 1px solid #999;
cursor: pointer;
padding: 5px;
}

.close_notification_box {
background: #eee;
clear: both;
cursor: pointer;
}

.notification_element {
display: block;
}

.notification_message {
font-size: 1.1em;
}

#notifications_box {
border: 1px solid #999;
background: #ffffff;
position: absolute;
margin-top: -1px;
display: none;
width: 270px;
right: 0px;
}

ul.notification_list {
list-style: none;
margin: 0px;

}

li.notification {
border-bottom: 1px solid #68AE2C;
text-align: left;
padding: 0 4px 0 4px;
margin-left: -40px;
cursor: pointer;
width: 262px;
}

li.notification:hover {
background-color: #e5f5d5;
}

.close_notification_box {
text-align: right;
padding-right: 5px;
}

#header .user li.last {
border-right: none;
}
Expand Down
79 changes: 79 additions & 0 deletions piplmesh/frontend/static/piplmesh/js/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,67 @@ function showLastPosts(offset) {
});
}

function addNewNotification(newNotification) {
var notification_counter = parseInt($('#notifications_count').text()) + 1;
$('#notifications_count').html(notification_counter);
$('.notification_list').prepend(buildNotification(newNotification.notification));
}

function buildNotification(notification) {
var format = gettext("%(author)s commented on post.");
var author = interpolate(format, {'author': notification.comment_author}, true);

var new_notification = $('<li/>').addClass('notification').append(
$('<span/>').addClass('notification_element').text(author)
).append(
$('<span/>').addClass('notification_message').addClass('notification_element').text(notification.comment_message)
).append(
$('<span/>').addClass('notification_element').addClass('notification_created_time').text(formatDiffTime(notification.created_time))
);
new_notification.data('notification', notification);

return new_notification;
}

function updateNotificationDate(element) {
$(element).find('.notification_created_time').text(formatDiffTime(element.data('notification').created_time));
}

function loadNotifications() {
$.getJSON(URLS.notifications, function (notifications, textStatus, jqXHR) {
var unread_counter = 0;

var content = $('<ul/>').addClass('notification_list');

$.each(notifications.objects, function (i, notification) {
if (!notification.read) {
unread_counter++;
}
content.prepend(buildNotification(notification));
})

$('#notifications_content').html(content);
$('#notifications_count').text(unread_counter);
});
}

// This is just for testing purposes. It can be base for future development.
function addComment(comment) {
// TODO: Change this for any post
var post_url = $('.post').first().data('post').resource_uri;

$.ajax({
type: 'POST',
url: post_url + 'comments/',
data: JSON.stringify({'message': comment}),
contentType: 'application/json',
dataType: 'json',
success: function (data, textStatus, jqXHR) {
alert("Comment posted.");
},
});
}

$(document).ready(function () {
initializePanels();

Expand All @@ -216,6 +277,21 @@ $(document).ready(function () {
});
});

// Notifications
$('#notifications_count').click(function () {
$('#notifications_box').slideToggle('fast');
});
$('.close_notifications_box').click(function (event) {
$('#notifications_box').slideToggle('fast');
});
$('#add_comment').click(function (event) {
addComment("Test comment");
});

$.updates.registerProcessor('user_channel', 'notification', addNewNotification);

loadNotifications();

// TODO: Ajax request to store panels state is currently send many times while resizing, it should be send only at the end
$(window).resize(function (event) {
initializePanels();
Expand Down Expand Up @@ -283,5 +359,8 @@ $(document).ready(function () {
$('.post').each(function (i, post) {
$(post).data('post').updateDate(this);
});
$('.notification').each(function (i, notification) {
updateNotificationDate($(notification));
});
}, POSTS_DATE_UPDATE_INTERVAL);
});
6 changes: 5 additions & 1 deletion piplmesh/frontend/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def check_online_users():
connections__in=([], None), # None if field is missing altogether
connection_last_unsubscribe__lt=timezone.now() - datetime.timedelta(seconds=CHECK_ONLINE_USERS_RECONNECT_TIMEOUT),
).update(set__is_online=False):
user.reload()
# On user disconnect new channel_id is generated for safety reasons
user.channel_id = models.generate_channel_id()
user.save()
updates.send_update(
views.HOME_CHANNEL_ID,
{
Expand All @@ -56,4 +60,4 @@ def check_online_users():
'image_url': user.get_image_url(),
},
}
)
)
10 changes: 10 additions & 0 deletions piplmesh/frontend/templates/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@
{% user_image %}
</li>
<li class="username"><a href="{% url "profile" username=user.username %}">{{ user.username }}</a></li>
<li>
<div id="notifications_count"></div>
<div id="notifications_box" class="hide">
<div id="notifications_content"></div>
<div class='close_notification_box'>
<div id="add_comment">Add test comment</div>
Close
</div>
</div>
</li>
{% if user.is_authenticated %}
<li class="last">
<form method="post" action="{% url "logout" %}" id="logout_form">
Expand Down
Loading

0 comments on commit b22610a

Please sign in to comment.