Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

pluggable-backends: Merged from trunk up to r128.

  • Loading branch information...
commit f4d526cb2a61a6830b4782edc4ce991e47e83035 1 parent e3f854e
Brian Rosner brosner authored
13 AUTHORS
... ... @@ -0,0 +1,13 @@
  1 +
  2 +The PRIMARY AUTHORS are:
  3 +
  4 + * James Tauber
  5 + * Brian Rosner
  6 + * Jannis Leidel
  7 +
  8 +ADDITIONAL CONTRIBUTORS include:
  9 +
  10 + * Eduardo Padoan
  11 + * Fabian Neumann
  12 + * Juanjo Conti
  13 + * Michael Trier
22 LICENSE
... ... @@ -0,0 +1,22 @@
  1 +Copyright (c) 2008 James Tauber and contributors
  2 +
  3 +Permission is hereby granted, free of charge, to any person
  4 +obtaining a copy of this software and associated documentation
  5 +files (the "Software"), to deal in the Software without
  6 +restriction, including without limitation the rights to use,
  7 +copy, modify, merge, publish, distribute, sublicense, and/or sell
  8 +copies of the Software, and to permit persons to whom the
  9 +Software is furnished to do so, subject to the following
  10 +conditions:
  11 +
  12 +The above copyright notice and this permission notice shall be
  13 +included in all copies or substantial portions of the Software.
  14 +
  15 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  16 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  17 +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  18 +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  19 +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  20 +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  21 +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  22 +OTHER DEALINGS IN THE SOFTWARE.
4 MANIFEST.in
... ... @@ -0,0 +1,4 @@
  1 +include AUTHORS
  2 +include LICENSE
  3 +recursive-include docs *
  4 +recursive-include notification/templates/notification *
11 README
... ... @@ -0,0 +1,11 @@
  1 +
  2 +Many sites need to notify users when certain events have occurred and to allow
  3 +configurable options as to how those notifications are to be received.
  4 +
  5 +The project aims to provide a Django app for this sort of functionality. This
  6 +includes:
  7 +
  8 + * submission of notification messages by other apps
  9 + * notification messages on signing in
  10 + * notification messages via email (configurable by user)
  11 + * notification messages via feed
21 docs/index.txt
... ... @@ -0,0 +1,21 @@
  1 +
  2 +===================
  3 +django-notification
  4 +===================
  5 +
  6 +Many sites need to notify users when certain events have occurred and to allow
  7 +configurable options as to how those notifications are to be received.
  8 +
  9 +The project aims to provide a Django app for this sort of functionality. This
  10 +includes:
  11 +
  12 + * Submission of notification messages by other apps.
  13 + * Notification messages on signing in.
  14 + * Notification messages via email (configurable by user).
  15 + * Notification messages via feed.
  16 +
  17 +Contents:
  18 +
  19 +.. toctree::
  20 +
  21 + usage
150 docs/usage.txt
... ... @@ -0,0 +1,150 @@
  1 +
  2 +=====
  3 +Usage
  4 +=====
  5 +
  6 +Integrating notification support into your app is a simple three-step process.
  7 +
  8 + * create your notice types
  9 + * create your notice templates
  10 + * send notifications
  11 +
  12 +Creating Notice Types
  13 +=====================
  14 +
  15 +You need to call ``create_notice_type(label, display, description)`` once to
  16 +create the notice types for your application in the database. ``label`` is just
  17 +the internal shortname that will be used for the type, ``display`` is what the
  18 +user will see as the name of the notification type and `description` is a
  19 +short description.
  20 +
  21 +For example::
  22 +
  23 + notification.create_notice_type("friends_invite", "Invitation Received", "you have received an invitation")
  24 +
  25 +One good way to automatically do this notice type creation is in a
  26 +``management.py`` file for your app, attached to the syncdb signal.
  27 +Here is an example::
  28 +
  29 + from django.db.models import signals, get_app
  30 + from django.core.exceptions import ImproperlyConfigured
  31 +
  32 + try:
  33 + notification = get_app("notification")
  34 +
  35 + def create_notice_types(app, created_models, verbosity, **kwargs):
  36 + notification.create_notice_type("friends_invite", "Invitation Received", "you have received an invitation")
  37 + notification.create_notice_type("friends_accept", "Acceptance Received", "an invitation you sent has been accepted")
  38 +
  39 + signals.post_syncdb.connect(create_notice_types, sender=notification)
  40 + except ImproperlyConfigured:
  41 + print "Skipping creation of NoticeTypes as notification app not found"
  42 +
  43 +Notice that the code is wrapped in a try clause so if django-notification is
  44 +not installed, your app will proceed anyway.
  45 +
  46 +
  47 +Notification templates
  48 +======================
  49 +
  50 +There are four different templates that can to be written for the actual content of the notices:
  51 +
  52 + * ``short.txt`` is a very short, text-only version of the notice (suitable for things like email subjects)
  53 + * ``full.txt`` is a longer, text-only version of the notice (suitable for things like email bodies)
  54 + * ``notice.html`` is a short, html version of the notice, displayed in a user's notice list on the website
  55 + * ``full.html`` is a long, html version of the notice (not currently used for anything)
  56 +
  57 +Each of these should be put in a directory on the template path called ``notification/<notice_type_label>/<template_name>``.
  58 +If any of these are missing, a default would be used. In practice, ``notice.html`` and ``full.txt`` should be provided at a minimum.
  59 +
  60 +For example, ``notification/friends_invite/notice.html`` might contain::
  61 +
  62 + {% load i18n %}{% url invitations as invitation_page %}{% url profile_detail username=invitation.from_user.username as user_url %}
  63 + {% 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 %}
  64 +
  65 +and ``notification/friends_full.txt`` might contain::
  66 +
  67 + {% 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:
  68 +
  69 + http://{{ current_site }}{{ invitation_page }}
  70 + {% endblocktrans %}
  71 +
  72 +The context variables are provided when sending the notification.
  73 +
  74 +
  75 +Sending Notification
  76 +====================
  77 +
  78 +There are two different ways of sending out notifications. We have support
  79 +for blocking and non-blocking methods of sending notifications. The most
  80 +simple way to send out a notification, for example::
  81 +
  82 + notification.send([to_user], "friends_invite", {"from_user": from_user})
  83 +
  84 +One thing to note is that ``send`` is a proxy around either ``send_now`` or
  85 +``queue``. They all have the same signature::
  86 +
  87 + send(users, label, extra_context, on_site)
  88 +
  89 +The parameters are:
  90 +
  91 + * ``users`` is an iterable of ``User`` objects to send the notification to.
  92 + * ``label`` is the label you used in the previous step to identify the notice
  93 + type.
  94 + * ``extra_content`` is a dictionary to add custom context entries to the
  95 + template used to render to notification. This is optional.
  96 + * ``on_site`` is a boolean flag to determine whether an ``Notice`` object is
  97 + created in the database.
  98 +
  99 +``send_now`` vs. ``queue`` vs. ``send``
  100 +---------------------------------------
  101 +
  102 +Lets first break down what each does.
  103 +
  104 +``send_now``
  105 +~~~~~~~~~~~~
  106 +
  107 +This is a blocking call that will check each user for elgibility of the
  108 +notice and actually peform the send.
  109 +
  110 +``queue``
  111 +~~~~~~~~~
  112 +
  113 +This is a non-blocking call that will queue the call to ``send_now`` to
  114 +be executed at a later time. To later execute the call you need to use
  115 +the ``emit_notices`` management command.
  116 +
  117 +``send``
  118 +~~~~~~~~
  119 +
  120 +A proxy around ``send_now`` and ``queue``. It gets its behavior from a global
  121 +setting named ``NOTIFICATION_QUEUE_ALL``. By default it is ``False``. This
  122 +setting is meant to help control whether you want to queue any call to
  123 +``send``.
  124 +
  125 +``send`` also accepts ``now`` and ``queue`` keyword arguments. By default
  126 +each option is set to ``False`` to honor the global setting which is ``False``.
  127 +This enables you to override on a per call basis whether it should call
  128 +``send_now`` or ``queue``.
  129 +
  130 +Optional notification support
  131 +-----------------------------
  132 +
  133 +To allow your app to still function without notification, you can wrap your
  134 +import in a try clause and test that the module has been loaded before sending
  135 +a notice.
  136 +
  137 +For example::
  138 +
  139 + from django.db.models import get_app
  140 + from django.core.exceptions import ImproperlyConfigured
  141 +
  142 + try:
  143 + notification = get_app('notification')
  144 + except ImproperlyConfigured:
  145 + notification = None
  146 +
  147 +and then, later::
  148 +
  149 + if notification:
  150 + notification.send([to_user], "friends_invite", {"from_user": from_user})
2  notification/__init__.py
... ... @@ -0,0 +1,2 @@
  1 +VERSION = (0, 1, 'pre')
  2 +__version__ = '.'.join(map(str, VERSION))
18 notification/engine.py
... ... @@ -1,6 +1,8 @@
1 1
  2 +import sys
2 3 import time
3 4 import logging
  5 +import traceback
4 6
5 7 try:
6 8 import cPickle as pickle
@@ -8,7 +10,9 @@
8 10 import pickle
9 11
10 12 from django.conf import settings
  13 +from django.core.mail import mail_admins
11 14 from django.contrib.auth.models import User
  15 +from django.contrib.sites.models import Site
12 16
13 17 from lockfile import FileLock, AlreadyLocked, LockTimeout
14 18
@@ -38,16 +42,26 @@ def send_all():
38 42
39 43 try:
40 44 for queued_batch in NoticeQueueBatch.objects.all():
41   - notices = pickle.loads(str(queued_batch.pickled_data))
  45 + notices = pickle.loads(str(queued_batch.pickled_data).decode("base64"))
42 46 for user, label, extra_context, on_site in notices:
43 47 user = User.objects.get(pk=user)
44 48 logging.info("emitting notice to %s" % user)
45 49 # call this once per user to be atomic and allow for logging to
46 50 # accurately show how long each takes.
47   - notification.send([user], label, extra_context, on_site)
  51 + notification.send_now([user], label, extra_context, on_site)
48 52 sent += 1
49 53 queued_batch.delete()
50 54 batches += 1
  55 + except:
  56 + # get the exception
  57 + exc_class, e, t = sys.exc_info()
  58 + # email people
  59 + current_site = Site.objects.get_current()
  60 + subject = "[%s emit_notices] %r" % (current_site.name, e)
  61 + message = "%s" % ("\n".join(traceback.format_exception(*sys.exc_info())),)
  62 + mail_admins(subject, message, fail_silently=True)
  63 + # log it as critical
  64 + logging.critical("an exception occurred: %r" % e)
51 65 finally:
52 66 logging.debug("releasing lock...")
53 67 lock.release()
311 notification/lockfile.py
@@ -7,13 +7,13 @@
7 7
8 8 Usage:
9 9
10   ->>> lock = FileLock(_testfile())
  10 +>>> lock = FileLock('somefile')
11 11 >>> try:
12 12 ... lock.acquire()
13 13 ... except AlreadyLocked:
14   -... print _testfile(), 'is locked already.'
  14 +... print 'somefile', 'is locked already.'
15 15 ... except LockFailed:
16   -... print _testfile(), 'can\\'t be locked.'
  16 +... print 'somefile', 'can\\'t be locked.'
17 17 ... else:
18 18 ... print 'got lock'
19 19 got lock
@@ -21,7 +21,7 @@
21 21 True
22 22 >>> lock.release()
23 23
24   ->>> lock = FileLock(_testfile())
  24 +>>> lock = FileLock('somefile')
25 25 >>> print lock.is_locked()
26 26 False
27 27 >>> with lock:
@@ -46,21 +46,28 @@
46 46 UnlockError - base class for all unlocking exceptions
47 47 AlreadyUnlocked - File was not locked.
48 48 NotMyLock - File was locked but not by the current thread/process
49   -
50   -To do:
51   - * Write more test cases
52   - - verify that all lines of code are executed
53   - * Describe on-disk file structures in the documentation.
54 49 """
55 50
56   -from __future__ import division, with_statement
  51 +from __future__ import division
57 52
  53 +import sys
58 54 import socket
59 55 import os
  56 +import thread
60 57 import threading
61 58 import time
62 59 import errno
63   -import thread
  60 +
  61 +# Work with PEP8 and non-PEP8 versions of threading module.
  62 +try:
  63 + threading.current_thread
  64 +except AttributeError:
  65 + threading.current_thread = threading.currentThread
  66 + threading.Thread.get_name = threading.Thread.getName
  67 +
  68 +__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
  69 + 'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock',
  70 + 'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock']
64 71
65 72 class Error(Exception):
66 73 """
@@ -149,14 +156,15 @@ class LockBase:
149 156 """Base class for platform-specific lock classes."""
150 157 def __init__(self, path, threaded=True):
151 158 """
152   - >>> lock = LockBase(_testfile())
  159 + >>> lock = LockBase('somefile')
  160 + >>> lock = LockBase('somefile', threaded=False)
153 161 """
154 162 self.path = path
155 163 self.lock_file = os.path.abspath(path) + ".lock"
156 164 self.hostname = socket.gethostname()
157 165 self.pid = os.getpid()
158 166 if threaded:
159   - tname = "%x-" % thread.get_ident()
  167 + tname = "%s-" % threading.current_thread().get_name()
160 168 else:
161 169 tname = ""
162 170 dirname = os.path.dirname(self.lock_file)
@@ -178,187 +186,45 @@ def acquire(self, timeout=None):
178 186
179 187 * If timeout <= 0, raise AlreadyLocked immediately if the file is
180 188 already locked.
181   -
182   - >>> # As simple as it gets.
183   - >>> lock = FileLock(_testfile())
184   - >>> lock.acquire()
185   - >>> lock.release()
186   -
187   - >>> # No timeout test
188   - >>> e1, e2 = threading.Event(), threading.Event()
189   - >>> t = _in_thread(_lock_wait_unlock, e1, e2)
190   - >>> e1.wait() # wait for thread t to acquire lock
191   - >>> lock2 = FileLock(_testfile())
192   - >>> lock2.is_locked()
193   - True
194   - >>> lock2.i_am_locking()
195   - False
196   - >>> try:
197   - ... lock2.acquire(timeout=-1)
198   - ... except AlreadyLocked:
199   - ... pass
200   - ... except Exception, e:
201   - ... print 'unexpected exception', repr(e)
202   - ... else:
203   - ... print 'thread', threading.currentThread().getName(),
204   - ... print 'erroneously locked an already locked file.'
205   - ... lock2.release()
206   - ...
207   - >>> e2.set() # tell thread t to release lock
208   - >>> t.join()
209   -
210   - >>> # Timeout test
211   - >>> e1, e2 = threading.Event(), threading.Event()
212   - >>> t = _in_thread(_lock_wait_unlock, e1, e2)
213   - >>> e1.wait() # wait for thread t to acquire filelock
214   - >>> lock2 = FileLock(_testfile())
215   - >>> lock2.is_locked()
216   - True
217   - >>> try:
218   - ... lock2.acquire(timeout=0.1)
219   - ... except LockTimeout:
220   - ... pass
221   - ... except Exception, e:
222   - ... print 'unexpected exception', repr(e)
223   - ... else:
224   - ... lock2.release()
225   - ... print 'thread', threading.currentThread().getName(),
226   - ... print 'erroneously locked an already locked file.'
227   - ...
228   - >>> e2.set()
229   - >>> t.join()
230 189 """
231   - pass
  190 + raise NotImplemented("implement in subclass")
232 191
233 192 def release(self):
234 193 """
235 194 Release the lock.
236 195
237 196 If the file is not locked, raise NotLocked.
238   - >>> lock = FileLock(_testfile())
239   - >>> lock.acquire()
240   - >>> lock.release()
241   - >>> lock.is_locked()
242   - False
243   - >>> lock.i_am_locking()
244   - False
245   - >>> try:
246   - ... lock.release()
247   - ... except NotLocked:
248   - ... pass
249   - ... except NotMyLock:
250   - ... print 'unexpected exception', NotMyLock
251   - ... except Exception, e:
252   - ... print 'unexpected exception', repr(e)
253   - ... else:
254   - ... print 'erroneously unlocked file'
255   -
256   - >>> e1, e2 = threading.Event(), threading.Event()
257   - >>> t = _in_thread(_lock_wait_unlock, e1, e2)
258   - >>> e1.wait()
259   - >>> lock2 = FileLock(_testfile())
260   - >>> lock2.is_locked()
261   - True
262   - >>> lock2.i_am_locking()
263   - False
264   - >>> try:
265   - ... lock2.release()
266   - ... except NotMyLock:
267   - ... pass
268   - ... except Exception, e:
269   - ... print 'unexpected exception', repr(e)
270   - ... else:
271   - ... print 'erroneously unlocked a file locked by another thread.'
272   - ...
273   - >>> e2.set()
274   - >>> t.join()
275 197 """
276   - pass
  198 + raise NotImplemented("implement in subclass")
277 199
278 200 def is_locked(self):
279 201 """
280 202 Tell whether or not the file is locked.
281   - >>> lock = FileLock(_testfile())
282   - >>> lock.acquire()
283   - >>> lock.is_locked()
284   - True
285   - >>> lock.release()
286   - >>> lock.is_locked()
287   - False
288 203 """
289   - pass
  204 + raise NotImplemented("implement in subclass")
290 205
291 206 def i_am_locking(self):
292   - """Return True if this object is locking the file.
293   -
294   - >>> lock1 = FileLock(_testfile(), threaded=False)
295   - >>> lock1.acquire()
296   - >>> lock2 = FileLock(_testfile())
297   - >>> lock1.i_am_locking()
298   - True
299   - >>> lock2.i_am_locking()
300   - False
301   - >>> try:
302   - ... lock2.acquire(timeout=2)
303   - ... except LockTimeout:
304   - ... lock2.break_lock()
305   - ... lock2.is_locked()
306   - ... lock1.is_locked()
307   - ... lock2.acquire()
308   - ... else:
309   - ... print 'expected LockTimeout...'
310   - ...
311   - False
312   - False
313   - >>> lock1.i_am_locking()
314   - False
315   - >>> lock2.i_am_locking()
316   - True
317   - >>> lock2.release()
318 207 """
319   - pass
  208 + Return True if this object is locking the file.
  209 + """
  210 + raise NotImplemented("implement in subclass")
320 211
321 212 def break_lock(self):
322   - """Remove a lock. Useful if a locking thread failed to unlock.
323   -
324   - >>> lock = FileLock(_testfile())
325   - >>> lock.acquire()
326   - >>> lock2 = FileLock(_testfile())
327   - >>> lock2.is_locked()
328   - True
329   - >>> lock2.break_lock()
330   - >>> lock2.is_locked()
331   - False
332   - >>> try:
333   - ... lock.release()
334   - ... except NotLocked:
335   - ... pass
336   - ... except Exception, e:
337   - ... print 'unexpected exception', repr(e)
338   - ... else:
339   - ... print 'break lock failed'
340 213 """
341   - pass
  214 + Remove a lock. Useful if a locking thread failed to unlock.
  215 + """
  216 + raise NotImplemented("implement in subclass")
342 217
343 218 def __enter__(self):
344   - """Context manager support.
345   -
346   - >>> lock = FileLock(_testfile())
347   - >>> with lock:
348   - ... lock.is_locked()
349   - ...
350   - True
351   - >>> lock.is_locked()
352   - False
  219 + """
  220 + Context manager support.
353 221 """
354 222 self.acquire()
355 223 return self
356 224
357 225 def __exit__(self, *_exc):
358   - """Context manager support.
359   -
360   - >>> 'tested in __enter__'
361   - 'tested in __enter__'
  226 + """
  227 + Context manager support.
362 228 """
363 229 self.release()
364 230
@@ -366,23 +232,6 @@ class LinkFileLock(LockBase):
366 232 """Lock access to a file using atomic property of link(2)."""
367 233
368 234 def acquire(self, timeout=None):
369   - """
370   - >>> d = _testfile()
371   - >>> os.mkdir(d)
372   - >>> os.chmod(d, 0444)
373   - >>> try:
374   - ... lock = LinkFileLock(os.path.join(d, 'test'))
375   - ... try:
376   - ... lock.acquire()
377   - ... except LockFailed:
378   - ... pass
379   - ... else:
380   - ... lock.release()
381   - ... print 'erroneously locked', os.path.join(d, 'test')
382   - ... finally:
383   - ... os.chmod(d, 0664)
384   - ... os.rmdir(d)
385   - """
386 235 try:
387 236 open(self.unique_name, "wb").close()
388 237 except IOError:
@@ -440,9 +289,10 @@ class MkdirFileLock(LockBase):
440 289 """Lock file by creating a directory."""
441 290 def __init__(self, path, threaded=True):
442 291 """
443   - >>> lock = MkdirFileLock(_testfile())
  292 + >>> lock = MkdirFileLock('somefile')
  293 + >>> lock = MkdirFileLock('somefile', threaded=False)
444 294 """
445   - LockBase.__init__(self, path)
  295 + LockBase.__init__(self, path, threaded)
446 296 if threaded:
447 297 tname = "%x-" % thread.get_ident()
448 298 else:
@@ -467,7 +317,8 @@ def acquire(self, timeout=None):
467 317 while True:
468 318 try:
469 319 os.mkdir(self.lock_file)
470   - except OSError, err:
  320 + except OSError:
  321 + err = sys.exc_info()[1]
471 322 if err.errno == errno.EEXIST:
472 323 # Already locked.
473 324 if os.path.exists(self.unique_name):
@@ -603,8 +454,7 @@ def release(self):
603 454 if not self.is_locked():
604 455 raise NotLocked
605 456 if not self.i_am_locking():
606   - raise NotMyLock, ("locker:", self._who_is_locking(),
607   - "me:", self.unique_name)
  457 + raise NotMyLock((self._who_is_locking(), self.unique_name))
608 458 cursor = self.connection.cursor()
609 459 cursor.execute("delete from locks"
610 460 " where unique_name = ?",
@@ -645,84 +495,3 @@ def break_lock(self):
645 495 FileLock = LinkFileLock
646 496 else:
647 497 FileLock = MkdirFileLock
648   -
649   -def _in_thread(func, *args, **kwargs):
650   - """Execute func(*args, **kwargs) after dt seconds.
651   -
652   - Helper for docttests.
653   - """
654   - def _f():
655   - func(*args, **kwargs)
656   - t = threading.Thread(target=_f, name='/*/*')
657   - t.start()
658   - return t
659   -
660   -def _testfile():
661   - """Return platform-appropriate lock file name.
662   -
663   - Helper for doctests.
664   - """
665   - import tempfile
666   - return os.path.join(tempfile.gettempdir(), 'trash-%s' % os.getpid())
667   -
668   -def _lock_wait_unlock(event1, event2):
669   - """Lock from another thread.
670   -
671   - Helper for doctests.
672   - """
673   - lock = FileLock(_testfile())
674   - with lock:
675   - event1.set() # we're in,
676   - event2.wait() # wait for boss's permission to leave
677   -
678   -def _test():
679   - global FileLock
680   -
681   - import doctest
682   - import sys
683   -
684   - def test_object(c):
685   - nfailed = ntests = 0
686   - for (obj, recurse) in ((c, True),
687   - (LockBase, True),
688   - (sys.modules["__main__"], False)):
689   - tests = doctest.DocTestFinder(recurse=recurse).find(obj)
690   - runner = doctest.DocTestRunner(verbose="-v" in sys.argv)
691   - tests.sort(key = lambda test: test.name)
692   - for test in tests:
693   - f, t = runner.run(test)
694   - nfailed += f
695   - ntests += t
696   - print FileLock.__name__, "tests:", ntests, "failed:", nfailed
697   - return nfailed, ntests
698   -
699   - nfailed = ntests = 0
700   -
701   - if hasattr(os, "link"):
702   - FileLock = LinkFileLock
703   - f, t = test_object(FileLock)
704   - nfailed += f
705   - ntests += t
706   -
707   - if hasattr(os, "mkdir"):
708   - FileLock = MkdirFileLock
709   - f, t = test_object(FileLock)
710   - nfailed += f
711   - ntests += t
712   -
713   - try:
714   - import sqlite3
715   - except ImportError:
716   - print "SQLite3 is unavailable - not testing SQLiteFileLock."
717   - else:
718   - print "Testing SQLiteFileLock with sqlite", sqlite3.sqlite_version,
719   - print "& pysqlite", sqlite3.version
720   - FileLock = SQLiteFileLock
721   - f, t = test_object(FileLock)
722   - nfailed += f
723   - ntests += t
724   -
725   - print "total tests:", ntests, "total failed:", nfailed
726   -
727   -if __name__ == "__main__":
728   - _test()
78 notification/management/commands/upgrade_notices.py
... ... @@ -1,78 +0,0 @@
1   -from django.core.management.base import NoArgsCommand
2   -from django.db.models import get_model
3   -from django.utils.translation import ugettext
4   -
5   -from notification.models import Notice
6   -
7   -# converts pre r70 notices to new approach
8   -
9   -def decode_object(ref):
10   - decoded = ref.split(".")
11   - if len(decoded) == 4:
12   - app, name, pk, msgid = decoded
13   - return get_model(app, name).objects.get(pk=pk), msgid
14   - app, name, pk = decoded
15   - return get_model(app, name).objects.get(pk=pk), None
16   -
17   -class FormatException(Exception):
18   - pass
19   -
20   -def decode_message(message, decoder):
21   - out = []
22   - objects = []
23   - mapping = {}
24   - in_field = False
25   - prev = 0
26   - for index, ch in enumerate(message):
27   - if not in_field:
28   - if ch == '{':
29   - in_field = True
30   - if prev != index:
31   - out.append(message[prev:index])
32   - prev = index
33   - elif ch == '}':
34   - raise FormatException("unmatched }")
35   - elif in_field:
36   - if ch == '{':
37   - raise FormatException("{ inside {}")
38   - elif ch == '}':
39   - in_field = False
40   - obj, msgid = decoder(message[prev+1:index])
41   - if msgid is None:
42   - objects.append(obj)
43   - out.append("%s")
44   - else:
45   - mapping[msgid] = obj
46   - out.append("%("+msgid+")s")
47   - prev = index + 1
48   - if in_field:
49   - raise FormatException("unmatched {")
50   - if prev <= index:
51   - out.append(message[prev:index+1])
52   - result = "".join(out)
53   - if mapping:
54   - args = mapping
55   - else:
56   - args = tuple(objects)
57   - return ugettext(result) % args
58   -
59   -def message_to_html(message):
60   - def decoder(ref):
61   - obj, msgid = decode_object(ref)
62   - if hasattr(obj, "get_absolute_url"):
63   - return u"""<a href="%s">%s</a>""" % (obj.get_absolute_url(), unicode(obj)), msgid
64   - else:
65   - return unicode(obj), msgid
66   - return decode_message(message, decoder)
67   -
68   -
69   -class Command(NoArgsCommand):
70   - help = 'Upgrade notices from old style approach.'
71   -
72   - def handle_noargs(self, **options):
73   - # wrapping in list() is required for sqlite, see http://code.djangoproject.com/ticket/7411
74   - for notice in list(Notice.objects.all()):
75   - message = notice.message
76   - notice.message = message_to_html(message)
77   - notice.save()
78   -
66 notification/models.py
@@ -27,6 +27,15 @@
27 27 from notification import backends
28 28 from notification.message import encode_message
29 29
  30 +# favour django-mailer but fall back to django.core.mail
  31 +try:
  32 + mailer = models.get_app("mailer")
  33 + from mailer import send_mail
  34 +except ImproperlyConfigured:
  35 + from django.core.mail import send_mail
  36 +
  37 +QUEUE_ALL = getattr(settings, "NOTIFICATION_QUEUE_ALL", False)
  38 +
30 39 class LanguageStoreNotAvailable(Exception):
31 40 pass
32 41
@@ -165,7 +174,7 @@ class NoticeQueueBatch(models.Model):
165 174 """
166 175 pickled_data = models.TextField()
167 176
168   -def create_notice_type(label, display, description, default=2):
  177 +def create_notice_type(label, display, description, default=2, verbosity=1):
169 178 """
170 179 Creates a new NoticeType.
171 180
@@ -185,10 +194,12 @@ def create_notice_type(label, display, description, default=2):
185 194 updated = True
186 195 if updated:
187 196 notice_type.save()
188   - print "Updated %s NoticeType" % label
  197 + if verbosity > 1:
  198 + print "Updated %s NoticeType" % label
189 199 except NoticeType.DoesNotExist:
190 200 NoticeType(label=label, display=display, description=description, default=default).save()
191   - print "Created %s NoticeType" % label
  201 + if verbosity > 1:
  202 + print "Created %s NoticeType" % label
192 203
193 204 def get_notification_language(user):
194 205 """
@@ -217,13 +228,12 @@ def get_formatted_messages(formats, label, context):
217 228 # conditionally turn off autoescaping for .txt extensions in format
218 229 if format.endswith(".txt"):
219 230 context.autoescape = False
220   - name = format.split(".")[0]
221   - format_templates[name] = render_to_string((
  231 + format_templates[format] = render_to_string((
222 232 'notification/%s/%s' % (label, format),
223 233 'notification/%s' % format), context_instance=context)
224 234 return format_templates
225 235
226   -def send(users, label, extra_context={}, on_site=True):
  236 +def send_now(users, label, extra_context=None, on_site=True):
227 237 """
228 238 Creates a new notice.
229 239
@@ -239,6 +249,9 @@ def send(users, label, extra_context={}, on_site=True):
239 249
240 250 FIXME: this function needs some serious reworking.
241 251 """
  252 + if extra_context is None:
  253 + extra_context = {}
  254 +
242 255 notice_type = NoticeType.objects.get(label=label)
243 256
244 257 notice_type = NoticeType.objects.get(label=label)
@@ -254,8 +267,8 @@ def send(users, label, extra_context={}, on_site=True):
254 267
255 268 formats = (
256 269 'short.txt',
257   - 'plain.txt',
258   - 'teaser.html',
  270 + 'full.txt',
  271 + 'notice.html',
259 272 'full.html',
260 273 ) # TODO make formats configurable
261 274
@@ -288,14 +301,14 @@ def send(users, label, extra_context={}, on_site=True):
288 301 # Strip newlines from subject
289 302 # TODO: this should move to the email backend
290 303 subject = ''.join(render_to_string('notification/email_subject.txt', {
291   - 'message': messages['short'],
  304 + 'message': messages['short.txt'],
292 305 }, context).splitlines())
293 306
294 307 body = render_to_string('notification/email_body.txt', {
295   - 'message': messages['plain'],
  308 + 'message': messages['full.txt'],
296 309 }, context)
297 310
298   - notice = Notice.objects.create(user=user, message=messages['teaser'],
  311 + notice = Notice.objects.create(user=user, message=messages['notice.html'],
299 312 notice_type=notice_type, on_site=on_site)
300 313 for key, backend in NOTIFICATION_BACKENDS:
301 314 recipients = backend_recipients.setdefault(key, [])
@@ -307,7 +320,34 @@ def send(users, label, extra_context={}, on_site=True):
307 320 # reset environment to original language
308 321 activate(current_language)
309 322
310   -def queue(users, label, extra_context={}, on_site=True):
  323 +def send(*args, **kwargs):
  324 + """
  325 + A basic interface around both queue and send_now. This honors a global
  326 + flag NOTIFICATION_QUEUE_ALL that helps determine whether all calls should
  327 + be queued or not. A per call ``queue`` or ``now`` keyword argument can be
  328 + used to always override the default global behavior.
  329 + """
  330 + queue_flag = kwargs.pop("queue", False)
  331 + now_flag = kwargs.pop("now", False)
  332 + assert not (queue_flag and now_flag), "'queue' and 'now' cannot both be True."
  333 + if queue_flag:
  334 + return queue(*args, **kwargs)
  335 + elif now_flag:
  336 + return send_now(*args, **kwargs)
  337 + else:
  338 + if QUEUE_ALL:
  339 + return queue(*args, **kwargs)
  340 + else:
  341 + return send_now(*args, **kwargs)
  342 +
  343 +def queue(users, label, extra_context=None, on_site=True):
  344 + """
  345 + Queue the notification in NoticeQueueBatch. This allows for large amounts
  346 + of user notifications to be deferred to a seperate process running outside
  347 + the webserver.
  348 + """
  349 + if extra_context is None:
  350 + extra_context = {}
311 351 if isinstance(users, QuerySet):
312 352 users = [row["pk"] for row in users.values("pk")]
313 353 else:
@@ -315,7 +355,7 @@ def queue(users, label, extra_context={}, on_site=True):
315 355 notices = []
316 356 for user in users:
317 357 notices.append((user, label, extra_context, on_site))
318   - NoticeQueueBatch(pickled_data=pickle.dumps(notices)).save()
  358 + NoticeQueueBatch(pickled_data=pickle.dumps(notices).encode("base64")).save()
319 359
320 360 class ObservedItemManager(models.Manager):
321 361
0  notification/templates/notification/plain.txt → notification/templates/notification/full.txt
File renamed without changes
0  notification/templates/notification/teaser.html → notification/templates/notification/notice.html
File renamed without changes
27 setup.py
... ... @@ -0,0 +1,27 @@
  1 +from distutils.core import setup
  2 +
  3 +setup(
  4 + name='django-notification',
  5 + version=__import__('notification').__version__,
  6 + 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.',
  7 + long_description=open('docs/index.txt').read(),
  8 + author='James Tauber',
  9 + author_email='jtauber@jtauber.com',
  10 + url='http://code.google.com/p/django-notification/',
  11 + packages=[
  12 + 'notification',
  13 + 'notification.management',
  14 + 'notification.management.commands',
  15 + 'notification.templatetags',
  16 + ],
  17 + package_dir={'notification': 'notification'},
  18 + classifiers=[
  19 + 'Development Status :: 3 - Alpha',
  20 + 'Environment :: Web Environment',
  21 + 'Intended Audience :: Developers',
  22 + 'License :: OSI Approved :: MIT License',
  23 + 'Operating System :: OS Independent',
  24 + 'Programming Language :: Python',
  25 + 'Framework :: Django',
  26 + ]
  27 +)

0 comments on commit f4d526c

Please sign in to comment.
Something went wrong with that request. Please try again.