Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'pb'

  • Loading branch information...
commit 29d5ae604099b8406fc3c31084dd6f66dccf2bc8 2 parents 408ede1 + e454b1e
@brosner brosner authored
View
6 docs/usage.txt
@@ -27,14 +27,14 @@ Here is an example::
from django.conf import settings
from django.utils.translation import ugettext_noop as _
-
+
if "notification" in settings.INSTALLED_APPS:
from notification import models as 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)
else:
print "Skipping creation of NoticeTypes as notification app not found"
View
36 notification/backends/__init__.py
@@ -0,0 +1,36 @@
+
+import sys
+
+from django.conf import settings
+from django.core import exceptions
+
+from base import BaseBackend
+
+# mostly for backend compatibility
+default_backends = (
+ ("email", "notification.backends.email.EmailBackend"),
+)
+
+def load_backends():
+ backends = []
+ for medium_id, bits in enumerate(getattr(settings, "NOTIFICATION_BACKENDS", default_backends)):
+ if len(bits) == 2:
+ label, backend_path = bits
+ spam_sensitivity = None
+ elif len(bits) == 3:
+ label, backend_path, spam_sensitivity = bits
+ else:
+ raise exceptions.ImproperlyConfigured, "NOTIFICATION_BACKENDS does not contain enough data."
+ dot = backend_path.rindex(".")
+ backend_mod, backend_class = backend_path[:dot], backend_path[dot+1:]
+ try:
+ # import the module and get the module from sys.modules
+ __import__(backend_mod)
+ mod = sys.modules[backend_mod]
+ except ImportError, e:
+ raise exceptions.ImproperlyConfigured, 'Error importing notification backend %s: "%s"' % (backend_mod, e)
+ # add the backend label and an instantiated backend class to the
+ # backends list.
+ backend_instance = getattr(mod, backend_class)(medium_id, spam_sensitivity)
+ backends.append(((medium_id, label), backend_instance))
+ return dict(backends)
View
42 notification/backends/base.py
@@ -0,0 +1,42 @@
+
+from django.template.loader import render_to_string
+
+class BaseBackend(object):
+ """
+ The base backend.
+ """
+ def __init__(self, medium_id, spam_sensitivity=None):
+ self.medium_id = medium_id
+ if spam_sensitivity is not None:
+ self.spam_sensitivity = spam_sensitivity
+
+ def can_send(self, user, notice_type):
+ """
+ Determines whether this backend is allowed to send a notification to
+ the given user and notice_type.
+ """
+ from notification.models import should_send
+ if should_send(user, notice_type, self.medium_id):
+ return True
+ return False
+
+ def deliver(self, recipient, notice_type, extra_context):
+ """
+ Deliver a notification to the given recipient.
+ """
+ raise NotImplemented()
+
+ def get_formatted_messages(self, formats, label, context):
+ """
+ Returns a dictionary with the format identifier as the key. The values are
+ are fully rendered templates with the given context.
+ """
+ format_templates = {}
+ for format in formats:
+ # conditionally turn off autoescaping for .txt extensions in format
+ if format.endswith(".txt"):
+ context.autoescape = False
+ format_templates[format] = render_to_string((
+ "notification/%s/%s" % (label, format),
+ "notification/%s" % format), context_instance=context)
+ return format_templates
View
56 notification/backends/email.py
@@ -0,0 +1,56 @@
+from django.conf import settings
+from django.core.mail import send_mail
+from django.core.urlresolvers import reverse
+from django.db.models.loading import get_app
+from django.template import Context
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext
+from django.core.exceptions import ImproperlyConfigured
+
+from django.contrib.sites.models import Site
+
+from notification import backends
+from notification.message import message_to_text
+
+
+class EmailBackend(backends.BaseBackend):
+ spam_sensitivity = 2
+
+ def can_send(self, user, notice_type):
+ can_send = super(EmailBackend, self).can_send(user, notice_type)
+ if can_send and user.email:
+ return True
+ return False
+
+ def deliver(self, recipient, sender, notice_type, extra_context):
+ # TODO: require this to be passed in extra_context
+ current_site = Site.objects.get_current()
+ notices_url = u"http://%s%s" % (
+ unicode(Site.objects.get_current()),
+ reverse("notification_notices"),
+ )
+
+ # update context with user specific translations
+ context = Context({
+ "recipient": recipient,
+ "sender": sender,
+ "notice": ugettext(notice_type.display),
+ "notices_url": notices_url,
+ "current_site": current_site,
+ })
+ context.update(extra_context)
+
+ messages = self.get_formatted_messages((
+ "short.txt",
+ "full.txt"
+ ), notice_type.label, context)
+
+ subject = "".join(render_to_string("notification/email_subject.txt", {
+ "message": messages["short.txt"],
+ }, context).splitlines())
+
+ body = render_to_string("notification/email_body.txt", {
+ "message": messages["full.txt"],
+ }, context)
+
+ send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [recipient.email])
View
19 notification/lockfile.py
@@ -1,4 +1,3 @@
-
"""
lockfile.py - Platform-independent advisory file locks.
@@ -53,19 +52,16 @@
import sys
import socket
import os
+import thread
import threading
import time
import errno
+import urllib
# Work with PEP8 and non-PEP8 versions of threading module.
-try:
- threading.current_thread
-except AttributeError:
+if not hasattr(threading, "current_thread"):
threading.current_thread = threading.currentThread
-try:
- # python 2.6 has threading.current_thread so we need to do this separately.
- threading.Thread.get_name
-except AttributeError:
+if not hasattr(threading.Thread, "get_name"):
threading.Thread.get_name = threading.Thread.getName
__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
@@ -167,7 +163,8 @@ def __init__(self, path, threaded=True):
self.hostname = socket.gethostname()
self.pid = os.getpid()
if threaded:
- tname = "%s-" % threading.current_thread().get_name()
+ name = threading.current_thread().get_name()
+ tname = "%s-" % urllib.quote(name, safe="")
else:
tname = ""
dirname = os.path.dirname(self.lock_file)
@@ -238,7 +235,7 @@ def acquire(self, timeout=None):
try:
open(self.unique_name, "wb").close()
except IOError:
- raise LockFailed
+ raise LockFailed("failed to create %s" % self.unique_name)
end_time = time.time()
if timeout is not None and timeout > 0:
@@ -336,7 +333,7 @@ def acquire(self, timeout=None):
time.sleep(wait)
else:
# Couldn't create the lock for some other reason
- raise LockFailed
+ raise LockFailed("failed to create %s" % self.lock_file)
else:
open(self.unique_name, "wb").close()
return
View
107 notification/message.py
@@ -0,0 +1,107 @@
+from django.db.models import get_model
+from django.utils.translation import ugettext
+
+# a notice like "foo and bar are now friends" is stored in the database
+# as "{auth.User.5} and {auth.User.7} are now friends".
+#
+# encode_object takes an object and turns it into "{app.Model.pk}" or
+# "{app.Model.pk.msgid}" if named arguments are used in send()
+# decode_object takes "{app.Model.pk}" and turns it into the object
+#
+# encode_message takes either ("%s and %s are now friends", [foo, bar]) or
+# ("%(foo)s and %(bar)s are now friends", {'foo':foo, 'bar':bar}) and turns
+# it into "{auth.User.5} and {auth.User.7} are now friends".
+#
+# decode_message takes "{auth.User.5} and {auth.User.7}" and converts it
+# into a string using the given decode function to convert the object to
+# string representation
+#
+# message_to_text and message_to_html use decode_message to produce a
+# text and html version of the message respectively.
+
+def encode_object(obj, name=None):
+ encoded = "%s.%s.%s" % (obj._meta.app_label, obj._meta.object_name, obj.pk)
+ if name:
+ encoded = "%s.%s" % (encoded, name)
+ return "{%s}" % encoded
+
+
+def encode_message(message_template, objects):
+ if objects is None:
+ return message_template
+ if isinstance(objects, list) or isinstance(objects, tuple):
+ return message_template % tuple(encode_object(obj) for obj in objects)
+ if type(objects) is dict:
+ return message_template % dict((name, encode_object(obj, name)) for name, obj in objects.iteritems())
+ return ""
+
+
+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_text(message):
+ def decoder(ref):
+ obj, msgid = decode_object(ref)
+ return unicode(obj), msgid
+ return decode_message(message, decoder)
+
+
+def message_to_html(message):
+ def decoder(ref):
+ obj, msgid = decode_object(ref)
+ if hasattr(obj, "get_absolute_url"): # don't fail silenty if get_absolute_url hasn't been defined
+ return u"""<a href="%s">%s</a>""" % (obj.get_absolute_url(), unicode(obj)), msgid
+ else:
+ return unicode(obj), msgid
+ return decode_message(message, decoder)
+
View
51 notification/models.py
@@ -22,12 +22,17 @@
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
+from notification import backends
+from notification.message import encode_message
+
+
QUEUE_ALL = getattr(settings, "NOTIFICATION_QUEUE_ALL", False)
class LanguageStoreNotAvailable(Exception):
pass
+
class NoticeType(models.Model):
label = models.CharField(_("label"), max_length=40)
@@ -45,15 +50,15 @@ class Meta:
verbose_name_plural = _("notice types")
-# if this gets updated, the create() method below needs to be as well...
-NOTICE_MEDIA = (
- ("1", _("Email")),
-)
+NOTIFICATION_BACKENDS = backends.load_backends()
+
+NOTICE_MEDIA = []
+NOTICE_MEDIA_DEFAULTS = {}
+for key, backend in NOTIFICATION_BACKENDS.items():
+ # key is a tuple (medium_id, backend_label)
+ NOTICE_MEDIA.append(key)
+ NOTICE_MEDIA_DEFAULTS[key[0]] = backend.spam_sensitivity
-# how spam-sensitive is the medium
-NOTICE_MEDIA_DEFAULTS = {
- "1": 2 # email
-}
class NoticeSetting(models.Model):
"""
@@ -298,33 +303,9 @@ def send_now(users, label, extra_context=None, on_site=True, sender=None):
# activate the user's language
activate(language)
- # update context with user specific translations
- context = Context({
- "recipient": user,
- "sender": sender,
- "notice": ugettext(notice_type.display),
- "notices_url": notices_url,
- "current_site": current_site,
- })
- context.update(extra_context)
-
- # get prerendered format messages
- messages = get_formatted_messages(formats, label, context)
-
- # Strip newlines from subject
- subject = "".join(render_to_string("notification/email_subject.txt", {
- "message": messages["short.txt"],
- }, context).splitlines())
-
- body = render_to_string("notification/email_body.txt", {
- "message": messages["full.txt"],
- }, context)
-
- notice = Notice.objects.create(recipient=user, message=messages["notice.html"],
- notice_type=notice_type, on_site=on_site, sender=sender)
- if should_send(user, notice_type, "1") and user.email and user.is_active: # Email
- recipients.append(user.email)
- send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
+ for backend in NOTIFICATION_BACKENDS.values():
+ if backend.can_send(user, notice_type):
+ backend.deliver(user, sender, notice_type, extra_context)
# reset environment to original language
activate(current_language)
View
0  notification/templatetags/__init__.py
No changes.
View
23 notification/templatetags/captureas_tag.py
@@ -0,0 +1,23 @@
+from django import template
+
+register = template.Library()
+
+@register.tag(name='captureas')
+def do_captureas(parser, token):
+ try:
+ tag_name, args = token.contents.split(None, 1)
+ except ValueError:
+ raise template.TemplateSyntaxError("'captureas' node requires a variable name.")
+ nodelist = parser.parse(('endcaptureas',))
+ parser.delete_first_token()
+ return CaptureasNode(nodelist, args)
+
+class CaptureasNode(template.Node):
+ def __init__(self, nodelist, varname):
+ self.nodelist = nodelist
+ self.varname = varname
+
+ def render(self, context):
+ output = self.nodelist.render(context)
+ context[self.varname] = output
+ return ''
View
2  notification/urls.py
@@ -9,4 +9,4 @@
url(r"^(\d+)/$", single, name="notification_notice"),
url(r"^feed/$", feed_for_user, name="notification_feed_for_user"),
url(r"^mark_all_seen/$", mark_all_seen, name="notification_mark_all_seen"),
-)
+)
View
2  notification/views.py
@@ -191,4 +191,4 @@ def mark_all_seen(request):
for notice in Notice.objects.notices_for(request.user, unseen=True):
notice.unseen = False
notice.save()
- return HttpResponseRedirect(reverse("notification_notices"))
+ return HttpResponseRedirect(reverse("notification_notices"))
Please sign in to comment.
Something went wrong with that request. Please try again.