4
import cPickle as pickle
8
from django.db import models
9
from django.db.models.query import QuerySet
10
from django.conf import settings
11
from django.core.urlresolvers import reverse
12
from django.template import Context
13
from django.template.loader import render_to_string
15
from django.core.exceptions import ImproperlyConfigured
17
from django.contrib.sites.models import Site
18
from django.contrib.auth.models import User
19
from django.contrib.auth.models import AnonymousUser
21
from django.contrib.contenttypes.models import ContentType
22
from django.contrib.contenttypes.fields import GenericForeignKey
24
from django.utils.translation import ugettext_lazy as _
25
from django.utils.translation import ugettext, get_language, activate
27
# favour django-mailer but fall back to django.core.mail
28
if 'mailer' in settings.INSTALLED_APPS:
29
from mailer import send_mail
31
from django.core.mail import send_mail
33
QUEUE_ALL = getattr(settings, "NOTIFICATION_QUEUE_ALL", False)
35
class LanguageStoreNotAvailable(Exception):
38
class NoticeType(models.Model):
40
label = models.CharField(_('label'), max_length=40)
41
display = models.CharField(_('display'), max_length=50)
42
description = models.CharField(_('description'), max_length=100)
44
# by default only on for media with sensitivity less than or equal to this number
45
default = models.IntegerField(_('default'))
47
def __unicode__(self):
51
verbose_name = _("notice type")
52
verbose_name_plural = _("notice types")
55
# if this gets updated, the create() method below needs to be as well...
60
# how spam-sensitive is the medium
61
NOTICE_MEDIA_DEFAULTS = {
65
class NoticeSetting(models.Model):
67
Indicates, for a given user, whether to send notifications
68
of a given type to a given medium.
71
user = models.ForeignKey(User, verbose_name=_('user'))
72
notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
73
medium = models.CharField(_('medium'), max_length=1, choices=NOTICE_MEDIA)
74
send = models.BooleanField(_('send'))
77
verbose_name = _("notice setting")
78
verbose_name_plural = _("notice settings")
79
unique_together = ("user", "notice_type", "medium")
81
def get_notification_setting(user, notice_type, medium):
83
return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)
84
except NoticeSetting.DoesNotExist:
85
default = (NOTICE_MEDIA_DEFAULTS[medium] <= notice_type.default)
86
setting = NoticeSetting(user=user, notice_type=notice_type, medium=medium, send=default)
90
def should_send(user, notice_type, medium):
91
return get_notification_setting(user, notice_type, medium).send
94
class NoticeManager(models.Manager):
96
def notices_for(self, user, archived=False, unseen=None, on_site=None):
98
returns Notice objects for the given user.
100
If archived=False, it only include notices not archived.
101
If archived=True, it returns all notices for that user.
103
If unseen=None, it includes all notices.
104
If unseen=True, return only unseen notices.
105
If unseen=False, return only seen notices.
108
qs = self.filter(user=user)
110
qs = self.filter(user=user, archived=archived)
111
if unseen is not None:
112
qs = qs.filter(unseen=unseen)
113
if on_site is not None:
114
qs = qs.filter(on_site=on_site)
117
def unseen_count_for(self, user, **kwargs):
119
returns the number of unseen notices for the given user but does not
122
return self.notices_for(user, unseen=True, **kwargs).count()
124
class Notice(models.Model):
126
user = models.ForeignKey(User, verbose_name=_('user'))
127
message = models.TextField(_('message'))
128
notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
129
added = models.DateTimeField(_('added'), default=datetime.datetime.now)
130
unseen = models.BooleanField(_('unseen'), default=True)
131
archived = models.BooleanField(_('archived'), default=False)
132
on_site = models.BooleanField(_('on site'))
134
objects = NoticeManager()
136
def __unicode__(self):
145
returns value of self.unseen but also changes it to false.
147
Use this in a template to mark an unseen notice differently the first
157
ordering = ["-added"]
158
verbose_name = _("notice")
159
verbose_name_plural = _("notices")
161
def get_absolute_url(self):
162
return ("notification_notice", [str(self.pk)])
163
get_absolute_url = models.permalink(get_absolute_url)
165
class NoticeQueueBatch(models.Model):
168
Denormalized data for a notice.
170
pickled_data = models.TextField()
172
def create_notice_type(label, display, description, default=2, verbosity=1):
174
Creates a new NoticeType.
176
This is intended to be used by other apps as a post_syncdb manangement step.
179
notice_type = NoticeType.objects.get(label=label)
181
if display != notice_type.display:
182
notice_type.display = display
184
if description != notice_type.description:
185
notice_type.description = description
187
if default != notice_type.default:
188
notice_type.default = default
193
print "Updated %s NoticeType" % label
194
except NoticeType.DoesNotExist:
195
NoticeType(label=label, display=display, description=description, default=default).save()
197
print "Created %s NoticeType" % label
199
def get_notification_language(user):
201
Returns site-specific notification language for this user. Raises
202
LanguageStoreNotAvailable if this site does not use translated
205
if getattr(settings, 'NOTIFICATION_LANGUAGE_MODULE', False):
207
app_label, model_name = settings.NOTIFICATION_LANGUAGE_MODULE.split('.')
208
model = models.get_model(app_label, model_name)
209
language_model = model._default_manager.get(user__id__exact=user.id)
210
if hasattr(language_model, 'language'):
211
return language_model.language
212
except (ImportError, ImproperlyConfigured, model.DoesNotExist):
213
raise LanguageStoreNotAvailable
214
raise LanguageStoreNotAvailable
216
def get_formatted_messages(formats, label, context):
218
Returns a dictionary with the format identifier as the key. The values are
219
are fully rendered templates with the given context.
221
format_templates = {}
222
for format in formats:
223
# conditionally turn off autoescaping for .txt extensions in format
224
if format.endswith(".txt"):
225
context.autoescape = False
227
context.autoescape = True
228
format_templates[format] = render_to_string((
229
'notification/%s/%s' % (label, format),
230
'notification/%s' % format), context_instance=context)
231
return format_templates
233
def send_now(users, label, extra_context=None, on_site=True):
235
Creates a new notice.
237
This is intended to be how other apps create new notices.
239
notification.send(user, 'friends_invite_sent', {
244
You can pass in on_site=False to prevent the notice emitted from being
245
displayed on the site.
247
if extra_context is None:
250
notice_type = NoticeType.objects.get(label=label)
252
current_site = Site.objects.get_current()
253
notices_url = u"http://%s%s" % (
254
unicode(current_site),
255
reverse("notification_notices"),
258
current_language = get_language()
265
) # TODO make formats configurable
269
# get user language for user from language store defined in
270
# NOTIFICATION_LANGUAGE_MODULE setting
272
language = get_notification_language(user)
273
except LanguageStoreNotAvailable:
276
if language is not None:
277
# activate the user's language
280
# update context with user specific translations
283
"notice": ugettext(notice_type.display),
284
"notices_url": notices_url,
285
"current_site": current_site,
287
context.update(extra_context)
289
# get prerendered format messages
290
messages = get_formatted_messages(formats, label, context)
292
# Strip newlines from subject
293
subject = ''.join(render_to_string('notification/email_subject.txt', {
294
'message': messages['short.txt'],
295
}, context).splitlines())
297
body = render_to_string('notification/email_body.txt', {
298
'message': messages['full.txt'],
301
notice = Notice.objects.create(user=user, message=messages['notice.html'],
302
notice_type=notice_type, on_site=on_site)
303
if should_send(user, notice_type, "1") and user.email: # Email
304
recipients.append(user.email)
305
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
307
# reset environment to original language
308
activate(current_language)
310
def send(*args, **kwargs):
312
A basic interface around both queue and send_now. This honors a global
313
flag NOTIFICATION_QUEUE_ALL that helps determine whether all calls should
314
be queued or not. A per call ``queue`` or ``now`` keyword argument can be
315
used to always override the default global behavior.
317
queue_flag = kwargs.pop("queue", False)
318
now_flag = kwargs.pop("now", False)
319
assert not (queue_flag and now_flag), "'queue' and 'now' cannot both be True."
321
return queue(*args, **kwargs)
323
return send_now(*args, **kwargs)
326
return queue(*args, **kwargs)
328
return send_now(*args, **kwargs)
330
def queue(users, label, extra_context=None, on_site=True):
332
Queue the notification in NoticeQueueBatch. This allows for large amounts
333
of user notifications to be deferred to a seperate process running outside
336
if extra_context is None:
338
if isinstance(users, QuerySet):
339
users = [row["pk"] for row in users.values("pk")]
341
users = [user.pk for user in users]
344
notices.append((user, label, extra_context, on_site))
345
NoticeQueueBatch(pickled_data=pickle.dumps(notices).encode("base64")).save()
347
class ObservedItemManager(models.Manager):
349
def all_for(self, observed, signal):
351
Returns all ObservedItems for an observed object,
352
to be sent when a signal is emited.
354
content_type = ContentType.objects.get_for_model(observed)
355
observed_items = self.filter(content_type=content_type, object_id=observed.id, signal=signal)
356
return observed_items
358
def get_for(self, observed, observer, signal):
359
content_type = ContentType.objects.get_for_model(observed)
360
observed_item = self.get(content_type=content_type, object_id=observed.id, user=observer, signal=signal)
364
class ObservedItem(models.Model):
366
user = models.ForeignKey(User, verbose_name=_('user'))
368
content_type = models.ForeignKey(ContentType)
369
object_id = models.PositiveIntegerField()
370
observed_object = GenericForeignKey('content_type', 'object_id')
372
notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
374
added = models.DateTimeField(_('added'), default=datetime.datetime.now)
376
# the signal that will be listened to send the notice
377
signal = models.TextField(verbose_name=_('signal'))
379
objects = ObservedItemManager()
382
ordering = ['-added']
383
verbose_name = _('observed item')
384
verbose_name_plural = _('observed items')
386
def send_notice(self):
387
send([self.user], self.notice_type.label,
388
{'observed': self.observed_object})
391
def observe(observed, observer, notice_type_label, signal='post_save'):
393
Create a new ObservedItem.
395
To be used by applications to register a user as an observer for some object.
397
notice_type = NoticeType.objects.get(label=notice_type_label)
398
observed_item = ObservedItem(user=observer, observed_object=observed,
399
notice_type=notice_type, signal=signal)
403
def stop_observing(observed, observer, signal='post_save'):
405
Remove an observed item.
407
observed_item = ObservedItem.objects.get_for(observed, observer, signal)
408
observed_item.delete()
410
def send_observation_notices_for(observed, signal='post_save'):
412
Send a notice for each registered user about an observed object.
414
observed_items = ObservedItem.objects.all_for(observed, signal)
415
for observed_item in observed_items:
416
observed_item.send_notice()
417
return observed_items
419
def is_observing(observed, observer, signal='post_save'):
420
if isinstance(observer, AnonymousUser):
423
observed_items = ObservedItem.objects.get_for(observed, observer, signal)
425
except ObservedItem.DoesNotExist:
427
except ObservedItem.MultipleObjectsReturned:
430
def handle_observations(sender, instance, *args, **kw):
431
send_observation_notices_for(instance)