Author: brosner
Date: Sun Jan 4 00:35:29 2009
New Revision: 129
Added:
branches/pluggable-backends/AUTHORS
- copied unchanged from r128, /trunk/AUTHORS
branches/pluggable-backends/LICENSE
- copied unchanged from r128, /trunk/LICENSE
branches/pluggable-backends/MANIFEST.in
- copied unchanged from r128, /trunk/MANIFEST.in
branches/pluggable-backends/README
- copied unchanged from r128, /trunk/README
branches/pluggable-backends/docs/
- copied from r128, /trunk/docs/
branches/pluggable-backends/docs/index.txt
- copied unchanged from r128, /trunk/docs/index.txt
branches/pluggable-backends/docs/usage.txt
- copied unchanged from r128, /trunk/docs/usage.txt
branches/pluggable-backends/notification/templates/notification/full.txt
- copied unchanged from r128,
/trunk/notification/templates/notification/full.txt
branches/pluggable-backends/notification/templates/notification/notice.html
- copied unchanged from r128,
/trunk/notification/templates/notification/notice.html
branches/pluggable-backends/setup.py
- copied unchanged from r128, /trunk/setup.py
Removed:
branches/pluggable-backends/notification/management/commands/upgrade_notices.py
branches/pluggable-backends/notification/templates/notification/plain.txt
branches/pluggable-backends/notification/templates/notification/teaser.html
Modified:
branches/pluggable-backends/ (props changed)
branches/pluggable-backends/notification/__init__.py
branches/pluggable-backends/notification/engine.py
branches/pluggable-backends/notification/lockfile.py
branches/pluggable-backends/notification/models.py
Log:
pluggable-backends: Merged from trunk up to r128.
Modified: branches/pluggable-backends/notification/__init__.py
==============================================================================
--- branches/pluggable-backends/notification/__init__.py (original)
+++ branches/pluggable-backends/notification/__init__.py Sun Jan 4
00:35:29 2009
@@ -0,0 +1,2 @@
+VERSION = (0, 1, 'pre')
+__version__ = '.'.join(map(str, VERSION))
\ No newline at end of file
Modified: branches/pluggable-backends/notification/engine.py
==============================================================================
--- branches/pluggable-backends/notification/engine.py (original)
+++ branches/pluggable-backends/notification/engine.py Sun Jan 4 00:35:29
2009
@@ -1,6 +1,8 @@
+import sys
import time
import logging
+import traceback
try:
import cPickle as pickle
@@ -8,7 +10,9 @@
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 @@
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()
Modified: branches/pluggable-backends/notification/lockfile.py
==============================================================================
--- branches/pluggable-backends/notification/lockfile.py (original)
+++ branches/pluggable-backends/notification/lockfile.py Sun Jan 4
00:35:29 2009
@@ -7,13 +7,13 @@
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
@@ -21,7 +21,7 @@
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 @@
"""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,187 +186,45 @@
* 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()
@@ -366,23 +232,6 @@
"""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 @@
"""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 @@
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 @@
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 @@
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()
Modified: branches/pluggable-backends/notification/models.py
==============================================================================
--- branches/pluggable-backends/notification/models.py (original)
+++ branches/pluggable-backends/notification/models.py Sun Jan 4 00:35:29
2009
@@ -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 @@
"""
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 @@
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 @@
# 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 @@
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 @@
formats = (
'short.txt',
- 'plain.txt',
- 'teaser.html',
+ 'full.txt',
+ 'notice.html',
'full.html',
) # TODO make formats configurable
@@ -288,14 +301,14 @@
# 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,7 +320,34 @@
# 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:
@@ -315,7 +355,7 @@
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):
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"pinax-updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/pinax-updates?hl=en
-~----------~----~----~----~------~----~------~--~---