Permalink
Browse files

pluggable-backends: Merged from trunk up to r128.

  • Loading branch information...
1 parent e3f854e commit f4d526cb2a61a6830b4782edc4ce991e47e83035 @brosner brosner committed Jan 4, 2009
View
13 AUTHORS
@@ -0,0 +1,13 @@
+
+The PRIMARY AUTHORS are:
+
+ * James Tauber
+ * Brian Rosner
+ * Jannis Leidel
+
+ADDITIONAL CONTRIBUTORS include:
+
+ * Eduardo Padoan
+ * Fabian Neumann
+ * Juanjo Conti
+ * Michael Trier
View
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2008 James Tauber and contributors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
View
4 MANIFEST.in
@@ -0,0 +1,4 @@
+include AUTHORS
+include LICENSE
+recursive-include docs *
+recursive-include notification/templates/notification *
View
11 README
@@ -0,0 +1,11 @@
+
+Many sites need to notify users when certain events have occurred and to allow
+configurable options as to how those notifications are to be received.
+
+The project aims to provide a Django app for this sort of functionality. This
+includes:
+
+ * submission of notification messages by other apps
+ * notification messages on signing in
+ * notification messages via email (configurable by user)
+ * notification messages via feed
View
21 docs/index.txt
@@ -0,0 +1,21 @@
+
+===================
+django-notification
+===================
+
+Many sites need to notify users when certain events have occurred and to allow
+configurable options as to how those notifications are to be received.
+
+The project aims to provide a Django app for this sort of functionality. This
+includes:
+
+ * Submission of notification messages by other apps.
+ * Notification messages on signing in.
+ * Notification messages via email (configurable by user).
+ * Notification messages via feed.
+
+Contents:
+
+.. toctree::
+
+ usage
View
150 docs/usage.txt
@@ -0,0 +1,150 @@
+
+=====
+Usage
+=====
+
+Integrating notification support into your app is a simple three-step process.
+
+ * create your notice types
+ * create your notice templates
+ * send notifications
+
+Creating Notice Types
+=====================
+
+You need to call ``create_notice_type(label, display, description)`` once to
+create the notice types for your application in the database. ``label`` is just
+the internal shortname that will be used for the type, ``display`` is what the
+user will see as the name of the notification type and `description` is a
+short description.
+
+For example::
+
+ notification.create_notice_type("friends_invite", "Invitation Received", "you have received an invitation")
+
+One good way to automatically do this notice type creation is in a
+``management.py`` file for your app, attached to the syncdb signal.
+Here is an example::
+
+ from django.db.models import signals, get_app
+ from django.core.exceptions import ImproperlyConfigured
+
+ try:
+ notification = get_app("notification")
+
+ def create_notice_types(app, created_models, verbosity, **kwargs):
+ notification.create_notice_type("friends_invite", "Invitation Received", "you have received an invitation")
+ notification.create_notice_type("friends_accept", "Acceptance Received", "an invitation you sent has been accepted")
+
+ signals.post_syncdb.connect(create_notice_types, sender=notification)
+ except ImproperlyConfigured:
+ print "Skipping creation of NoticeTypes as notification app not found"
+
+Notice that the code is wrapped in a try clause so if django-notification is
+not installed, your app will proceed anyway.
+
+
+Notification templates
+======================
+
+There are four different templates that can to be written for the actual content of the notices:
+
+ * ``short.txt`` is a very short, text-only version of the notice (suitable for things like email subjects)
+ * ``full.txt`` is a longer, text-only version of the notice (suitable for things like email bodies)
+ * ``notice.html`` is a short, html version of the notice, displayed in a user's notice list on the website
+ * ``full.html`` is a long, html version of the notice (not currently used for anything)
+
+Each of these should be put in a directory on the template path called ``notification/<notice_type_label>/<template_name>``.
+If any of these are missing, a default would be used. In practice, ``notice.html`` and ``full.txt`` should be provided at a minimum.
+
+For example, ``notification/friends_invite/notice.html`` might contain::
+
+ {% load i18n %}{% url invitations as invitation_page %}{% url profile_detail username=invitation.from_user.username as user_url %}
+ {% blocktrans with invitation.from_user as invitation_from_user %}<a href="{{ user_url }}">{{ invitation_from_user }}</a> has requested to add you as a friend (see <a href="{{ invitation_page }}">invitations</a>){% endblocktrans %}
+
+and ``notification/friends_full.txt`` might contain::
+
+ {% load i18n %}{% url invitations as invitation_page %}{% blocktrans with invitation.from_user as invitation_from_user %}{{ invitation_from_user }} has requested to add you as a friend. You can accept their invitation at:
+
+ http://{{ current_site }}{{ invitation_page }}
+ {% endblocktrans %}
+
+The context variables are provided when sending the notification.
+
+
+Sending Notification
+====================
+
+There are two different ways of sending out notifications. We have support
+for blocking and non-blocking methods of sending notifications. The most
+simple way to send out a notification, for example::
+
+ notification.send([to_user], "friends_invite", {"from_user": from_user})
+
+One thing to note is that ``send`` is a proxy around either ``send_now`` or
+``queue``. They all have the same signature::
+
+ send(users, label, extra_context, on_site)
+
+The parameters are:
+
+ * ``users`` is an iterable of ``User`` objects to send the notification to.
+ * ``label`` is the label you used in the previous step to identify the notice
+ type.
+ * ``extra_content`` is a dictionary to add custom context entries to the
+ template used to render to notification. This is optional.
+ * ``on_site`` is a boolean flag to determine whether an ``Notice`` object is
+ created in the database.
+
+``send_now`` vs. ``queue`` vs. ``send``
+---------------------------------------
+
+Lets first break down what each does.
+
+``send_now``
+~~~~~~~~~~~~
+
+This is a blocking call that will check each user for elgibility of the
+notice and actually peform the send.
+
+``queue``
+~~~~~~~~~
+
+This is a non-blocking call that will queue the call to ``send_now`` to
+be executed at a later time. To later execute the call you need to use
+the ``emit_notices`` management command.
+
+``send``
+~~~~~~~~
+
+A proxy around ``send_now`` and ``queue``. It gets its behavior from a global
+setting named ``NOTIFICATION_QUEUE_ALL``. By default it is ``False``. This
+setting is meant to help control whether you want to queue any call to
+``send``.
+
+``send`` also accepts ``now`` and ``queue`` keyword arguments. By default
+each option is set to ``False`` to honor the global setting which is ``False``.
+This enables you to override on a per call basis whether it should call
+``send_now`` or ``queue``.
+
+Optional notification support
+-----------------------------
+
+To allow your app to still function without notification, you can wrap your
+import in a try clause and test that the module has been loaded before sending
+a notice.
+
+For example::
+
+ from django.db.models import get_app
+ from django.core.exceptions import ImproperlyConfigured
+
+ try:
+ notification = get_app('notification')
+ except ImproperlyConfigured:
+ notification = None
+
+and then, later::
+
+ if notification:
+ notification.send([to_user], "friends_invite", {"from_user": from_user})
View
2 notification/__init__.py
@@ -0,0 +1,2 @@
+VERSION = (0, 1, 'pre')
+__version__ = '.'.join(map(str, VERSION))
View
18 notification/engine.py
@@ -1,14 +1,18 @@
+import sys
import time
import logging
+import traceback
try:
import cPickle as pickle
except ImportError:
import pickle
from django.conf import settings
+from django.core.mail import mail_admins
from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
from lockfile import FileLock, AlreadyLocked, LockTimeout
@@ -38,16 +42,26 @@ def send_all():
try:
for queued_batch in NoticeQueueBatch.objects.all():
- notices = pickle.loads(str(queued_batch.pickled_data))
+ notices = pickle.loads(str(queued_batch.pickled_data).decode("base64"))
for user, label, extra_context, on_site in notices:
user = User.objects.get(pk=user)
logging.info("emitting notice to %s" % user)
# call this once per user to be atomic and allow for logging to
# accurately show how long each takes.
- notification.send([user], label, extra_context, on_site)
+ notification.send_now([user], label, extra_context, on_site)
sent += 1
queued_batch.delete()
batches += 1
+ except:
+ # get the exception
+ exc_class, e, t = sys.exc_info()
+ # email people
+ current_site = Site.objects.get_current()
+ subject = "[%s emit_notices] %r" % (current_site.name, e)
+ message = "%s" % ("\n".join(traceback.format_exception(*sys.exc_info())),)
+ mail_admins(subject, message, fail_silently=True)
+ # log it as critical
+ logging.critical("an exception occurred: %r" % e)
finally:
logging.debug("releasing lock...")
lock.release()
View
311 notification/lockfile.py
@@ -7,21 +7,21 @@
Usage:
->>> lock = FileLock(_testfile())
+>>> lock = FileLock('somefile')
>>> try:
... lock.acquire()
... except AlreadyLocked:
-... print _testfile(), 'is locked already.'
+... print 'somefile', 'is locked already.'
... except LockFailed:
-... print _testfile(), 'can\\'t be locked.'
+... print 'somefile', 'can\\'t be locked.'
... else:
... print 'got lock'
got lock
>>> print lock.is_locked()
True
>>> lock.release()
->>> lock = FileLock(_testfile())
+>>> lock = FileLock('somefile')
>>> print lock.is_locked()
False
>>> with lock:
@@ -46,21 +46,28 @@
UnlockError - base class for all unlocking exceptions
AlreadyUnlocked - File was not locked.
NotMyLock - File was locked but not by the current thread/process
-
-To do:
- * Write more test cases
- - verify that all lines of code are executed
- * Describe on-disk file structures in the documentation.
"""
-from __future__ import division, with_statement
+from __future__ import division
+import sys
import socket
import os
+import thread
import threading
import time
import errno
-import thread
+
+# Work with PEP8 and non-PEP8 versions of threading module.
+try:
+ threading.current_thread
+except AttributeError:
+ threading.current_thread = threading.currentThread
+ threading.Thread.get_name = threading.Thread.getName
+
+__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
+ 'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock',
+ 'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock']
class Error(Exception):
"""
@@ -149,14 +156,15 @@ class LockBase:
"""Base class for platform-specific lock classes."""
def __init__(self, path, threaded=True):
"""
- >>> lock = LockBase(_testfile())
+ >>> lock = LockBase('somefile')
+ >>> lock = LockBase('somefile', threaded=False)
"""
self.path = path
self.lock_file = os.path.abspath(path) + ".lock"
self.hostname = socket.gethostname()
self.pid = os.getpid()
if threaded:
- tname = "%x-" % thread.get_ident()
+ tname = "%s-" % threading.current_thread().get_name()
else:
tname = ""
dirname = os.path.dirname(self.lock_file)
@@ -178,211 +186,52 @@ def acquire(self, timeout=None):
* If timeout <= 0, raise AlreadyLocked immediately if the file is
already locked.
-
- >>> # As simple as it gets.
- >>> lock = FileLock(_testfile())
- >>> lock.acquire()
- >>> lock.release()
-
- >>> # No timeout test
- >>> e1, e2 = threading.Event(), threading.Event()
- >>> t = _in_thread(_lock_wait_unlock, e1, e2)
- >>> e1.wait() # wait for thread t to acquire lock
- >>> lock2 = FileLock(_testfile())
- >>> lock2.is_locked()
- True
- >>> lock2.i_am_locking()
- False
- >>> try:
- ... lock2.acquire(timeout=-1)
- ... except AlreadyLocked:
- ... pass
- ... except Exception, e:
- ... print 'unexpected exception', repr(e)
- ... else:
- ... print 'thread', threading.currentThread().getName(),
- ... print 'erroneously locked an already locked file.'
- ... lock2.release()
- ...
- >>> e2.set() # tell thread t to release lock
- >>> t.join()
-
- >>> # Timeout test
- >>> e1, e2 = threading.Event(), threading.Event()
- >>> t = _in_thread(_lock_wait_unlock, e1, e2)
- >>> e1.wait() # wait for thread t to acquire filelock
- >>> lock2 = FileLock(_testfile())
- >>> lock2.is_locked()
- True
- >>> try:
- ... lock2.acquire(timeout=0.1)
- ... except LockTimeout:
- ... pass
- ... except Exception, e:
- ... print 'unexpected exception', repr(e)
- ... else:
- ... lock2.release()
- ... print 'thread', threading.currentThread().getName(),
- ... print 'erroneously locked an already locked file.'
- ...
- >>> e2.set()
- >>> t.join()
"""
- pass
+ raise NotImplemented("implement in subclass")
def release(self):
"""
Release the lock.
If the file is not locked, raise NotLocked.
- >>> lock = FileLock(_testfile())
- >>> lock.acquire()
- >>> lock.release()
- >>> lock.is_locked()
- False
- >>> lock.i_am_locking()
- False
- >>> try:
- ... lock.release()
- ... except NotLocked:
- ... pass
- ... except NotMyLock:
- ... print 'unexpected exception', NotMyLock
- ... except Exception, e:
- ... print 'unexpected exception', repr(e)
- ... else:
- ... print 'erroneously unlocked file'
-
- >>> e1, e2 = threading.Event(), threading.Event()
- >>> t = _in_thread(_lock_wait_unlock, e1, e2)
- >>> e1.wait()
- >>> lock2 = FileLock(_testfile())
- >>> lock2.is_locked()
- True
- >>> lock2.i_am_locking()
- False
- >>> try:
- ... lock2.release()
- ... except NotMyLock:
- ... pass
- ... except Exception, e:
- ... print 'unexpected exception', repr(e)
- ... else:
- ... print 'erroneously unlocked a file locked by another thread.'
- ...
- >>> e2.set()
- >>> t.join()
"""
- pass
+ raise NotImplemented("implement in subclass")
def is_locked(self):
"""
Tell whether or not the file is locked.
- >>> lock = FileLock(_testfile())
- >>> lock.acquire()
- >>> lock.is_locked()
- True
- >>> lock.release()
- >>> lock.is_locked()
- False
"""
- pass
+ raise NotImplemented("implement in subclass")
def i_am_locking(self):
- """Return True if this object is locking the file.
-
- >>> lock1 = FileLock(_testfile(), threaded=False)
- >>> lock1.acquire()
- >>> lock2 = FileLock(_testfile())
- >>> lock1.i_am_locking()
- True
- >>> lock2.i_am_locking()
- False
- >>> try:
- ... lock2.acquire(timeout=2)
- ... except LockTimeout:
- ... lock2.break_lock()
- ... lock2.is_locked()
- ... lock1.is_locked()
- ... lock2.acquire()
- ... else:
- ... print 'expected LockTimeout...'
- ...
- False
- False
- >>> lock1.i_am_locking()
- False
- >>> lock2.i_am_locking()
- True
- >>> lock2.release()
"""
- pass
+ Return True if this object is locking the file.
+ """
+ raise NotImplemented("implement in subclass")
def break_lock(self):
- """Remove a lock. Useful if a locking thread failed to unlock.
-
- >>> lock = FileLock(_testfile())
- >>> lock.acquire()
- >>> lock2 = FileLock(_testfile())
- >>> lock2.is_locked()
- True
- >>> lock2.break_lock()
- >>> lock2.is_locked()
- False
- >>> try:
- ... lock.release()
- ... except NotLocked:
- ... pass
- ... except Exception, e:
- ... print 'unexpected exception', repr(e)
- ... else:
- ... print 'break lock failed'
"""
- pass
+ Remove a lock. Useful if a locking thread failed to unlock.
+ """
+ raise NotImplemented("implement in subclass")
def __enter__(self):
- """Context manager support.
-
- >>> lock = FileLock(_testfile())
- >>> with lock:
- ... lock.is_locked()
- ...
- True
- >>> lock.is_locked()
- False
+ """
+ Context manager support.
"""
self.acquire()
return self
def __exit__(self, *_exc):
- """Context manager support.
-
- >>> 'tested in __enter__'
- 'tested in __enter__'
+ """
+ Context manager support.
"""
self.release()
class LinkFileLock(LockBase):
"""Lock access to a file using atomic property of link(2)."""
def acquire(self, timeout=None):
- """
- >>> d = _testfile()
- >>> os.mkdir(d)
- >>> os.chmod(d, 0444)
- >>> try:
- ... lock = LinkFileLock(os.path.join(d, 'test'))
- ... try:
- ... lock.acquire()
- ... except LockFailed:
- ... pass
- ... else:
- ... lock.release()
- ... print 'erroneously locked', os.path.join(d, 'test')
- ... finally:
- ... os.chmod(d, 0664)
- ... os.rmdir(d)
- """
try:
open(self.unique_name, "wb").close()
except IOError:
@@ -440,9 +289,10 @@ class MkdirFileLock(LockBase):
"""Lock file by creating a directory."""
def __init__(self, path, threaded=True):
"""
- >>> lock = MkdirFileLock(_testfile())
+ >>> lock = MkdirFileLock('somefile')
+ >>> lock = MkdirFileLock('somefile', threaded=False)
"""
- LockBase.__init__(self, path)
+ LockBase.__init__(self, path, threaded)
if threaded:
tname = "%x-" % thread.get_ident()
else:
@@ -467,7 +317,8 @@ def acquire(self, timeout=None):
while True:
try:
os.mkdir(self.lock_file)
- except OSError, err:
+ except OSError:
+ err = sys.exc_info()[1]
if err.errno == errno.EEXIST:
# Already locked.
if os.path.exists(self.unique_name):
@@ -603,8 +454,7 @@ def release(self):
if not self.is_locked():
raise NotLocked
if not self.i_am_locking():
- raise NotMyLock, ("locker:", self._who_is_locking(),
- "me:", self.unique_name)
+ raise NotMyLock((self._who_is_locking(), self.unique_name))
cursor = self.connection.cursor()
cursor.execute("delete from locks"
" where unique_name = ?",
@@ -645,84 +495,3 @@ def break_lock(self):
FileLock = LinkFileLock
else:
FileLock = MkdirFileLock
-
-def _in_thread(func, *args, **kwargs):
- """Execute func(*args, **kwargs) after dt seconds.
-
- Helper for docttests.
- """
- def _f():
- func(*args, **kwargs)
- t = threading.Thread(target=_f, name='/*/*')
- t.start()
- return t
-
-def _testfile():
- """Return platform-appropriate lock file name.
-
- Helper for doctests.
- """
- import tempfile
- return os.path.join(tempfile.gettempdir(), 'trash-%s' % os.getpid())
-
-def _lock_wait_unlock(event1, event2):
- """Lock from another thread.
-
- Helper for doctests.
- """
- lock = FileLock(_testfile())
- with lock:
- event1.set() # we're in,
- event2.wait() # wait for boss's permission to leave
-
-def _test():
- global FileLock
-
- import doctest
- import sys
-
- def test_object(c):
- nfailed = ntests = 0
- for (obj, recurse) in ((c, True),
- (LockBase, True),
- (sys.modules["__main__"], False)):
- tests = doctest.DocTestFinder(recurse=recurse).find(obj)
- runner = doctest.DocTestRunner(verbose="-v" in sys.argv)
- tests.sort(key = lambda test: test.name)
- for test in tests:
- f, t = runner.run(test)
- nfailed += f
- ntests += t
- print FileLock.__name__, "tests:", ntests, "failed:", nfailed
- return nfailed, ntests
-
- nfailed = ntests = 0
-
- if hasattr(os, "link"):
- FileLock = LinkFileLock
- f, t = test_object(FileLock)
- nfailed += f
- ntests += t
-
- if hasattr(os, "mkdir"):
- FileLock = MkdirFileLock
- f, t = test_object(FileLock)
- nfailed += f
- ntests += t
-
- try:
- import sqlite3
- except ImportError:
- print "SQLite3 is unavailable - not testing SQLiteFileLock."
- else:
- print "Testing SQLiteFileLock with sqlite", sqlite3.sqlite_version,
- print "& pysqlite", sqlite3.version
- FileLock = SQLiteFileLock
- f, t = test_object(FileLock)
- nfailed += f
- ntests += t
-
- print "total tests:", ntests, "total failed:", nfailed
-
-if __name__ == "__main__":
- _test()
View
78 notification/management/commands/upgrade_notices.py
@@ -1,78 +0,0 @@
-from django.core.management.base import NoArgsCommand
-from django.db.models import get_model
-from django.utils.translation import ugettext
-
-from notification.models import Notice
-
-# converts pre r70 notices to new approach
-
-def decode_object(ref):
- decoded = ref.split(".")
- if len(decoded) == 4:
- app, name, pk, msgid = decoded
- return get_model(app, name).objects.get(pk=pk), msgid
- app, name, pk = decoded
- return get_model(app, name).objects.get(pk=pk), None
-
-class FormatException(Exception):
- pass
-
-def decode_message(message, decoder):
- out = []
- objects = []
- mapping = {}
- in_field = False
- prev = 0
- for index, ch in enumerate(message):
- if not in_field:
- if ch == '{':
- in_field = True
- if prev != index:
- out.append(message[prev:index])
- prev = index
- elif ch == '}':
- raise FormatException("unmatched }")
- elif in_field:
- if ch == '{':
- raise FormatException("{ inside {}")
- elif ch == '}':
- in_field = False
- obj, msgid = decoder(message[prev+1:index])
- if msgid is None:
- objects.append(obj)
- out.append("%s")
- else:
- mapping[msgid] = obj
- out.append("%("+msgid+")s")
- prev = index + 1
- if in_field:
- raise FormatException("unmatched {")
- if prev <= index:
- out.append(message[prev:index+1])
- result = "".join(out)
- if mapping:
- args = mapping
- else:
- args = tuple(objects)
- return ugettext(result) % args
-
-def message_to_html(message):
- def decoder(ref):
- obj, msgid = decode_object(ref)
- if hasattr(obj, "get_absolute_url"):
- return u"""<a href="%s">%s</a>""" % (obj.get_absolute_url(), unicode(obj)), msgid
- else:
- return unicode(obj), msgid
- return decode_message(message, decoder)
-
-
-class Command(NoArgsCommand):
- help = 'Upgrade notices from old style approach.'
-
- def handle_noargs(self, **options):
- # wrapping in list() is required for sqlite, see http://code.djangoproject.com/ticket/7411
- for notice in list(Notice.objects.all()):
- message = notice.message
- notice.message = message_to_html(message)
- notice.save()
-
View
66 notification/models.py
@@ -27,6 +27,15 @@
from notification import backends
from notification.message import encode_message
+# favour django-mailer but fall back to django.core.mail
+try:
+ mailer = models.get_app("mailer")
+ from mailer import send_mail
+except ImproperlyConfigured:
+ from django.core.mail import send_mail
+
+QUEUE_ALL = getattr(settings, "NOTIFICATION_QUEUE_ALL", False)
+
class LanguageStoreNotAvailable(Exception):
pass
@@ -165,7 +174,7 @@ class NoticeQueueBatch(models.Model):
"""
pickled_data = models.TextField()
-def create_notice_type(label, display, description, default=2):
+def create_notice_type(label, display, description, default=2, verbosity=1):
"""
Creates a new NoticeType.
@@ -185,10 +194,12 @@ def create_notice_type(label, display, description, default=2):
updated = True
if updated:
notice_type.save()
- print "Updated %s NoticeType" % label
+ if verbosity > 1:
+ print "Updated %s NoticeType" % label
except NoticeType.DoesNotExist:
NoticeType(label=label, display=display, description=description, default=default).save()
- print "Created %s NoticeType" % label
+ if verbosity > 1:
+ print "Created %s NoticeType" % label
def get_notification_language(user):
"""
@@ -217,13 +228,12 @@ def get_formatted_messages(formats, label, context):
# conditionally turn off autoescaping for .txt extensions in format
if format.endswith(".txt"):
context.autoescape = False
- name = format.split(".")[0]
- format_templates[name] = render_to_string((
+ format_templates[format] = render_to_string((
'notification/%s/%s' % (label, format),
'notification/%s' % format), context_instance=context)
return format_templates
-def send(users, label, extra_context={}, on_site=True):
+def send_now(users, label, extra_context=None, on_site=True):
"""
Creates a new notice.
@@ -239,6 +249,9 @@ def send(users, label, extra_context={}, on_site=True):
FIXME: this function needs some serious reworking.
"""
+ if extra_context is None:
+ extra_context = {}
+
notice_type = NoticeType.objects.get(label=label)
notice_type = NoticeType.objects.get(label=label)
@@ -254,8 +267,8 @@ def send(users, label, extra_context={}, on_site=True):
formats = (
'short.txt',
- 'plain.txt',
- 'teaser.html',
+ 'full.txt',
+ 'notice.html',
'full.html',
) # TODO make formats configurable
@@ -288,14 +301,14 @@ def send(users, label, extra_context={}, on_site=True):
# Strip newlines from subject
# TODO: this should move to the email backend
subject = ''.join(render_to_string('notification/email_subject.txt', {
- 'message': messages['short'],
+ 'message': messages['short.txt'],
}, context).splitlines())
body = render_to_string('notification/email_body.txt', {
- 'message': messages['plain'],
+ 'message': messages['full.txt'],
}, context)
- notice = Notice.objects.create(user=user, message=messages['teaser'],
+ notice = Notice.objects.create(user=user, message=messages['notice.html'],
notice_type=notice_type, on_site=on_site)
for key, backend in NOTIFICATION_BACKENDS:
recipients = backend_recipients.setdefault(key, [])
@@ -307,15 +320,42 @@ def send(users, label, extra_context={}, on_site=True):
# reset environment to original language
activate(current_language)
-def queue(users, label, extra_context={}, on_site=True):
+def send(*args, **kwargs):
+ """
+ A basic interface around both queue and send_now. This honors a global
+ flag NOTIFICATION_QUEUE_ALL that helps determine whether all calls should
+ be queued or not. A per call ``queue`` or ``now`` keyword argument can be
+ used to always override the default global behavior.
+ """
+ queue_flag = kwargs.pop("queue", False)
+ now_flag = kwargs.pop("now", False)
+ assert not (queue_flag and now_flag), "'queue' and 'now' cannot both be True."
+ if queue_flag:
+ return queue(*args, **kwargs)
+ elif now_flag:
+ return send_now(*args, **kwargs)
+ else:
+ if QUEUE_ALL:
+ return queue(*args, **kwargs)
+ else:
+ return send_now(*args, **kwargs)
+
+def queue(users, label, extra_context=None, on_site=True):
+ """
+ Queue the notification in NoticeQueueBatch. This allows for large amounts
+ of user notifications to be deferred to a seperate process running outside
+ the webserver.
+ """
+ if extra_context is None:
+ extra_context = {}
if isinstance(users, QuerySet):
users = [row["pk"] for row in users.values("pk")]
else:
users = [user.pk for user in users]
notices = []
for user in users:
notices.append((user, label, extra_context, on_site))
- NoticeQueueBatch(pickled_data=pickle.dumps(notices)).save()
+ NoticeQueueBatch(pickled_data=pickle.dumps(notices).encode("base64")).save()
class ObservedItemManager(models.Manager):
View
0 ...fication/templates/notification/plain.txt → notification/templates/notification/full.txt
File renamed without changes.
View
0 ...cation/templates/notification/teaser.html → ...cation/templates/notification/notice.html
File renamed without changes.
View
27 setup.py
@@ -0,0 +1,27 @@
+from distutils.core import setup
+
+setup(
+ name='django-notification',
+ version=__import__('notification').__version__,
+ description='Many sites need to notify users when certain events have occurred and to allow configurable options as to how those notifications are to be received. The project aims to provide a Django app for this sort of functionality.',
+ long_description=open('docs/index.txt').read(),
+ author='James Tauber',
+ author_email='jtauber@jtauber.com',
+ url='http://code.google.com/p/django-notification/',
+ packages=[
+ 'notification',
+ 'notification.management',
+ 'notification.management.commands',
+ 'notification.templatetags',
+ ],
+ package_dir={'notification': 'notification'},
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Framework :: Django',
+ ]
+)

0 comments on commit f4d526c

Please sign in to comment.