Browse files

First version based on django-activity-stream v0.4.3

  • Loading branch information...
1 parent 4c6d21b commit da17a0491188a97e9350182187a85bccecae0e26 Brant Young committed Jul 22, 2012
View
5 .gitignore
@@ -0,0 +1,5 @@
+build
+dist
+*.pyc
+.DS_Store
+MANIFEST
View
26 AUTHORS.txt
@@ -0,0 +1,26 @@
+Justin Quick <justquick@gmail.com>
+Aaron Williamson
+Jordan Reiter
+Manuel Aristaran
+Darrell Hoy
+Ken "Elf" Mathieu Sternberg
+Josh Ourisman
+Trever Shick
+Sandip Agrawal
+Piet Delport
+Steve Ivy
+Ben Slavin
+Jason Culverhouse
+Dave Harrington
+Pedro Bur�n
+Ryan Quigley
+Neelesh Shastry
+David Gouldin <david@gould.in>
+Donald Stufft
+Nolan Brubaker
+Herman Schaaf
+Walter Scheper
+Chris Beaven
+Vineet Naik
+Walter Scheper
+Brant Young
View
8 CHANGELOG.rst
@@ -0,0 +1,8 @@
+Changelog
+==========
+
+0.5
+-----
+
+First version based on `django-activity-stream <https://github.com/justquick/django-activity-stream>`_ v0.4.3
+
View
29 LICENSE.txt
@@ -0,0 +1,29 @@
+Copyright (c) 2011, Justin Quick
+Copyright (c) 2012, Brant Young
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of the author nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
View
2 MANIFEST.in
@@ -0,0 +1,2 @@
+include setup.py README.md AUTHORS.txt LICENSE.txt CHANGELOG.rst
+recursive-include actstream *.py *.html *.txt *.po
View
4 README.md
@@ -1,4 +0,0 @@
-django-notifications
-====================
-
-A github alike notification app for Django
View
114 README.rst
@@ -0,0 +1,114 @@
+Django Notifications Documentation
+===================================
+
+`django-notifications <https://github.com/brantyoung/django-notifications>`_ is a GitHub notification alike app for Django, it was derived from `django-activity-stream <https://github.com/justquick/django-activity-stream>`_
+
+Notifications are actually actions events, which are categorized by four main components.
+
+ * ``Actor``. The object that performed the activity.
+ * ``Verb``. The verb phrase that identifies the action of the activity.
+ * ``Action Object``. *(Optional)* The object linked to the action itself.
+ * ``Target``. *(Optional)* The object to which the activity was performed.
+
+``Actor``, ``Action Object`` and ``Target`` are ``GenericForeignKeys`` to any arbitrary Django object.
+An action is a description of an action that was performed (``Verb``) at some instant in time by some ``Actor`` on some optional ``Target`` that results in an ``Action Object`` getting created/updated/deleted.
+
+For example: `justquick <https://github.com/justquick/>`_ ``(actor)`` *closed* ``(verb)`` `issue 2 <https://github.com/justquick/django-activity-stream/issues/2>`_ ``(object)`` on `activity-stream <https://github.com/justquick/django-activity-stream/>`_ ``(target)`` 12 hours ago
+
+Nomenclature of this specification is based on the Activity Streams Spec: `<http://activitystrea.ms/specs/atom/1.0/>`_
+
+Installation
+============
+
+Installation is easy using ``pip`` and the only requirement is a recent version of Django.
+
+::
+
+ $ pip install django-notifications-hq
+
+or get it from source
+
+::
+
+ $ git clone https://github.com/brantyoung/django-notifications
+ $ cd django-notifications
+ $ python setup.py install
+
+Then to add the Django Notifications to your project add the app ``notifications`` to your ``INSTALLED_APPS`` and urlconf.
+
+The app should go somewhere after all the apps that are going to be generating notifications like ``django.contrib.auth``::
+
+ INSTALLED_APPS = (
+ 'django.contrib.auth',
+ ...
+ 'notifications',
+ ...
+ )
+
+Add the notifications urls to your urlconf::
+
+ urlpatterns = patterns('',
+ ...
+ ('^inbox/notifications/', include('notifications.urls')),
+ ...
+ )
+
+Generating Notifications
+=========================
+
+Generating notifications is probably best done in a separate signal.
+
+::
+
+ from django.db.models.signals import post_save
+ from notifications import notify
+ from myapp.models import MyModel
+
+ def my_handler(sender, instance, created, **kwargs):
+ notify.send(instance, verb='was saved')
+
+ post_save.connect(my_handler, sender=MyModel)
+
+To generate an notification anywhere in your code, simply import the notify signal and send it with your actor, verb, and target.
+
+::
+
+ from notifications import notify
+
+ notify.send(request.user, verb='reached level 10')
+ notify.send(request.user, verb='joined', target=group)
+ notify.send(request.user, verb='created comment', action_object=comment, target=group)
+
+API
+====
+
+``Notification.objects.mark_all_as_read(recipient)``
+-------------------------------------------------------
+
+Mark all unread notifications received by ``recipient``.
+
+
+``Notification.objects.get().mark_as_read()``
+---------------------------------------------
+
+Mark current notification as read.
+
+
+``notifications_unread`` templatetags
+--------------------------------------
+
+::
+
+ {% notifications_unread %}
+
+Give the number of unread notifications for a user, or nothing (an empty string) for an anonymous user.
+
+Storing the count in a variable for further processing is advised, such as::
+
+ {% notifications_unread as unread_count %}
+ ...
+ {% if unread_count %}
+ You have <strong>{{ unread_count }}</strong> unread notifications.
+ {% endif %}
+
+
View
25 notifications/__init__.py
@@ -0,0 +1,25 @@
+try:
+ from notifications.signals import notify
+except ImportError:
+ pass
+
+__version_info__ = {
+ 'major': 0,
+ 'minor': 5,
+ 'micro': 1,
+ 'releaselevel': 'final',
+ 'serial': 0
+}
+
+
+def get_version(release_level=True):
+ """
+ Return the formatted version information
+ """
+ vers = ["%(major)i.%(minor)i.%(micro)i" % __version_info__]
+ if release_level and __version_info__['releaselevel'] != 'final':
+ vers.append('%(releaselevel)s%(serial)i' % __version_info__)
+ return ''.join(vers)
+
+
+__version__ = get_version()
View
9 notifications/admin.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+from django.contrib import admin
+from notifications.models import Notification
+
+class NotificationAdmin(admin.ModelAdmin):
+ pass
+
+admin.site.register(Notification, NotificationAdmin)
View
9 notifications/managers.py
@@ -0,0 +1,9 @@
+from django.db import models
+
+
+class NotificationManager(models.Manager):
+ def unread_count(self, user):
+ return self.filter(recipient=user, readed=False).count()
+
+ def mark_all_as_read(self, recipient):
+ return self.filter(recipient=recipient, readed=False).update(readed=True)
View
140 notifications/models.py
@@ -0,0 +1,140 @@
+import datetime
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.db import models
+from django.utils.timezone import utc
+from .utils import id2slug
+from notifications.managers import NotificationManager
+from notifications.signals import notify
+
+try:
+ from django.utils import timezone
+ now = timezone.now
+except ImportError:
+ now = datetime.datetime.now
+
+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
+
+ HTML Representation::
+
+ <a href="http://oebfare.com/">brosner</a> commented on <a href="http://github.com/pinax/pinax">pinax/pinax</a> 2 hours ago
+
+ """
+ recipient = models.ForeignKey(User, blank=False, related_name='notifications')
+ readed = models.BooleanField(default=False, blank=False)
+
+ actor_content_type = models.ForeignKey(ContentType, related_name='notify_actor')
+ actor_object_id = models.CharField(max_length=255)
+ actor = generic.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 = generic.GenericForeignKey('target_content_type',
+ 'target_object_id')
+
+ action_object_content_type = models.ForeignKey(ContentType,
+ related_name='notify_action_object', blank=True, null=True)
+ action_object_object_id = models.CharField(max_length=255, blank=True,
+ null=True)
+ action_object = generic.GenericForeignKey('action_object_content_type',
+ 'action_object_object_id')
+
+ timestamp = models.DateTimeField(default=now)
+
+ public = models.BooleanField(default=True)
+
+ objects = NotificationManager()
+
+ class Meta:
+ ordering = ('-timestamp', )
+
+ 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 '%(actor)s %(verb)s %(action_object)s on %(target)s %(timesince)s ago' % ctx
+ return '%(actor)s %(verb)s %(target)s %(timesince)s ago' % ctx
+ if self.action_object:
+ return '%(actor)s %(verb)s %(action_object)s %(timesince)s ago' % ctx
+ return '%(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):
+ return id2slug(self.id)
+
+ def mark_as_read(self):
+ if not self.readed:
+ self.readed = True
+ self.save()
+
+def notify_handler(verb, **kwargs):
+ """
+ Handler function to create Notification instance upon action signal call.
+ """
+
+ kwargs.pop('signal', None)
+ recipient = kwargs.pop('recipient')
+ actor = kwargs.pop('sender')
+ newnotify = Notification(
+ recipient = recipient,
+ actor_content_type=ContentType.objects.get_for_model(actor),
+ actor_object_id=actor.pk,
+ verb=unicode(verb),
+ public=bool(kwargs.pop('public', True)),
+ description=kwargs.pop('description', None),
+ timestamp=kwargs.pop('timestamp', datetime.datetime.utcnow().replace(tzinfo=utc))
+ )
+
+ for opt in ('target', 'action_object'):
+ obj = kwargs.pop(opt, None)
+ if not obj is None:
+ setattr(newnotify, '%s_object_id' % opt, obj.pk)
+ setattr(newnotify, '%s_content_type' % opt,
+ ContentType.objects.get_for_model(obj))
+
+ newnotify.save()
+
+
+# connect the signal
+notify.connect(notify_handler, dispatch_uid='notifications.models.notification')
View
4 notifications/signals.py
@@ -0,0 +1,4 @@
+from django.dispatch import Signal
+
+notify = Signal(providing_args=['recipient', 'actor', 'verb', 'action_object', 'target',
+ 'description', 'timestamp'])
View
43 notifications/templates/notifications/list.html
@@ -0,0 +1,43 @@
+{% extends "base.html" %}
+{% load i18n notifications_unread %}
+
+{% notifications_unread as unread_count %}
+
+{% block title %}Notifications ({{ unread_count }}){% endblock %}
+
+{% block content %}
+ <h1 class="header">
+ Notifications ({{ unread_count }}) <a class="btn" href="{% url notifications_read_all %}">Mark all as read</a>
+ </h1>
+
+ <div class="activities">
+ {% for action in action_list %}
+ {# display action only when action_object exists #}
+ {% if action.action_object %}
+ <div class="act">
+ <div class="body">
+ <div class="title">
+ <i class="{% if action.readed %}icon-mail{% else %}icon-mail-alt{% endif %}"></i>
+ {% if action.actor.get_absolute_url %}
+ <a href="{{ action.actor.get_absolute_url }}">{{ action.actor }}</a>
+ {% endif %}
+
+ {{ action.verb }}
+
+ <a href="{% url notifications_read action.slug %}?next={{ action.target.get_absolute_url }}">{{ action.target }}</a>
+ </div>
+ <div class="details">
+ {% if action.description %}
+ <div class="message">
+ <a href="{% url notifications_read action.slug %}?next={{ action.action_object.get_absolute_url }}">{{ action.description|linebreaksbr|atmention_str }}</a>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ {% empty %}
+ No notification.
+ {% endfor %}
+ </div>
+{% endblock %}
View
0 notifications/templatetags/__init__.py
No changes.
View
54 notifications/templatetags/notifications_tags.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+from django.template import Library
+from django.template.base import TemplateSyntaxError
+from notifications.models import Notification
+from django.template import Node
+
+register = Library()
+
+class InboxCountNode(Node):
+ "For use in the notifications_unread tag"
+ def __init__(self, asvar=None):
+ self.asvar = asvar
+
+ def render(self, context):
+ """
+ Return the count of unread messages for the user found in context,
+ (may be 0) or an empty string.
+ """
+ try:
+ user = context['user']
+ if user.is_anonymous():
+ count = ''
+ else:
+ count = Notification.objects.unread_count(user)
+ except (KeyError, AttributeError):
+ count = ''
+ if self.asvar:
+ context[self.asvar] = count
+ return ''
+ return count
+
+@register.tag
+def notifications_unread(parser, token):
+ """
+ Give the number of unread notifications for a user,
+ or nothing (an empty string) for an anonymous user.
+
+ Storing the count in a variable for further processing is advised, such as::
+
+ {% notifications_unread as unread_count %}
+ ...
+ {% if unread_count %}
+ You have <strong>{{ unread_count }}</strong> unread notifications.
+ {% endif %}
+ """
+ bits = token.split_contents()
+ if len(bits) > 1:
+ if len(bits) != 3:
+ raise TemplateSyntaxError("'{0}' tag takes no argument or exactly two arguments".format(bits[0]))
+ if bits[1] != 'as':
+ raise TemplateSyntaxError("First argument to '{0}' tag must be 'as'".format(bits[0]))
+ return InboxCountNode(bits[2])
+ else:
+ return InboxCountNode()
View
16 notifications/tests.py
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.assertEqual(1 + 1, 2)
View
9 notifications/urls.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+from django.conf.urls import *
+
+urlpatterns = patterns('notifications.views',
+ url(r'^$', 'list', name='notifications_list'),
+ url(r'^read_all/$', 'read_all', name='notifications_read_all'),
+ url(r'^read/(?P<slug>\d+)/$', 'read', name='notifications_read'),
+)
View
7 notifications/utils.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+def slug2id(slug):
+ return long(slug) - 110909
+
+def id2slug(id):
+ return id + 110909
View
52 notifications/views.py
@@ -0,0 +1,52 @@
+# Create your views here.
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.shortcuts import get_object_or_404, render_to_response, redirect
+from django.template.context import RequestContext
+from .utils import slug2id
+from notifications.models import Notification
+
+@login_required
+def list(request):
+ """
+ Index page for authenticated user
+ """
+ actions = Notification.objects.filter(recipient=request.user)
+
+ paginator = Paginator(actions, 16) # Show 16 notifications per page
+ page = request.GET.get('p')
+
+ try:
+ action_list = paginator.page(page)
+ except PageNotAnInteger:
+ # If page is not an integer, deliver first page.
+ action_list = paginator.page(1)
+ except EmptyPage:
+ # If page is out of range (e.g. 9999), deliver last page of results.
+ action_list = paginator.page(paginator.num_pages)
+
+ return render_to_response('notifications/list.html', {
+ 'member': request.user,
+ 'action_list': action_list,
+ }, context_instance=RequestContext(request))
+
+@login_required
+def read_all(request):
+ Notification.objects.mark_all_as_read(request.user)
+
+ return redirect('notifications_list')
+
+@login_required
+def read(request, slug=None):
+ id = slug2id(slug)
+
+ notification = get_object_or_404(Notification, recipient=request.user, id=id)
+ notification.mark_as_read()
+
+ next = request.REQUEST.get('next')
+
+ if next:
+ return redirect(next)
+
+ return redirect('notifications_list')
View
23 setup.py
@@ -0,0 +1,23 @@
+from distutils.core import setup
+from notifications import __version__
+
+setup(name='django-notifications-hq',
+ version=__version__,
+ description='GitHub notifications alike app for Django.',
+ long_description=open('README.rst').read(),
+ author='Brant Young',
+ author_email='brant.young@gmail.com',
+ url='http://github.com/brantyoung/django-notifications',
+ packages=['notifications',
+ 'notifications.templatetags'],
+ package_data={'notifications': [
+ 'templates/notifications/*.html']},
+ classifiers=['Development Status :: 5 - Production/Stable',
+ 'Environment :: Web Environment',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Utilities'],
+ )

0 comments on commit da17a04

Please sign in to comment.