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.urls import reverse
12
from django.template.loader import render_to_string
14
from django.core.exceptions import ImproperlyConfigured
16
from django.contrib.sites.models import Site
17
from django.contrib.auth.models import User
18
from django.contrib.auth.models import AnonymousUser
20
from django.contrib.contenttypes.models import ContentType
21
from django.contrib.contenttypes.fields import GenericForeignKey
23
from django.utils.translation import ugettext_lazy as _
24
from django.utils.translation import ugettext, get_language, activate
26
# favour django-mailer but fall back to django.core.mail
27
if 'mailer' in settings.INSTALLED_APPS:
28
from mailer import send_mail
30
from django.core.mail import send_mail
32
QUEUE_ALL = getattr(settings, 'NOTIFICATION_QUEUE_ALL', False)
35
class LanguageStoreNotAvailable(Exception):
39
class NoticeType(models.Model):
41
label = models.CharField(_('label'), max_length=40)
42
display = models.CharField(_('display'),
44
help_text=_('Used as subject when sending emails.'))
45
description = models.CharField(_('description'), max_length=100)
47
# by default only on for media with sensitivity less than or equal to this
49
default = models.IntegerField(_('default'))
51
def __unicode__(self):
55
verbose_name = _('notice type')
56
verbose_name_plural = _('notice types')
59
# if this gets updated, the create() method below needs to be as well...
64
# how spam-sensitive is the medium
65
NOTICE_MEDIA_DEFAULTS = {
70
class NoticeSetting(models.Model):
71
"""Indicates, for a given user, whether to send notifications of a given
72
type to a given medium.
74
Notice types for each user are added if he/she enters the notification page.
78
user = models.ForeignKey(User, verbose_name=_('user'))
79
notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
80
medium = models.CharField(_('medium'), max_length=1, choices=NOTICE_MEDIA)
81
send = models.BooleanField(_('send'))
84
verbose_name = _('notice setting')
85
verbose_name_plural = _('notice settings')
86
unique_together = ('user', 'notice_type', 'medium')
89
def get_notification_setting(user, notice_type, medium):
90
"""Return NotceSetting for a specific user. If a NoticeSetting of
91
given NoticeType didn't exist for given user, a NoticeSetting is created.
93
If a new NoticeSetting is created, the field 'default' of a NoticeType
94
decides whether NoticeSetting.send is True or False as default.
97
return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)
98
except NoticeSetting.DoesNotExist:
99
default = (NOTICE_MEDIA_DEFAULTS[medium] <= notice_type.default)
100
setting = NoticeSetting(
101
user=user, notice_type=notice_type, medium=medium, send=default)
106
def should_send(user, notice_type, medium):
107
return get_notification_setting(user, notice_type, medium).send
110
def get_observers_for(notice_type, excl_user=None):
111
"""Returns the list of users which wants to get a message (email) for this
113
query = NoticeSetting.objects.filter(
114
notice_type__label=notice_type, send=True)
117
query = query.exclude(user=excl_user)
119
return [notice_setting.user for notice_setting in query]
122
class NoticeQueueBatch(models.Model):
125
Denormalized data for a notice.
128
pickled_data = models.TextField()
131
def create_notice_type(label, display, description, default=2, verbosity=1):
132
"""Creates a new NoticeType.
134
This is intended to be used by other apps as a post_migrate
139
notice_type = NoticeType.objects.get(label=label)
141
if display != notice_type.display:
142
notice_type.display = display
144
if description != notice_type.description:
145
notice_type.description = description
147
if default != notice_type.default:
148
notice_type.default = default
153
print 'Updated %s NoticeType' % label
154
except NoticeType.DoesNotExist:
155
NoticeType(label=label, display=display,
156
description=description, default=default).save()
158
print 'Created %s NoticeType' % label
161
def get_notification_language(user):
163
Returns site-specific notification language for this user. Raises
164
LanguageStoreNotAvailable if this site does not use translated
167
if getattr(settings, 'NOTIFICATION_LANGUAGE_MODULE', False):
169
app_label, model_name = settings.NOTIFICATION_LANGUAGE_MODULE.split(
171
model = models.get_model(app_label, model_name)
172
language_model = model._default_manager.get(
173
user__id__exact=user.id)
174
if hasattr(language_model, 'language'):
175
return language_model.language
176
except (ImportError, ImproperlyConfigured, model.DoesNotExist):
177
raise LanguageStoreNotAvailable
178
raise LanguageStoreNotAvailable
181
def get_formatted_messages(formats, label, context):
182
"""Returns a dictionary with the format identifier as the key.
184
The values are are fully rendered templates with the given context.
187
format_templates = {}
189
for format in formats:
190
# Switch off escaping for .txt templates was done here, but now it
191
# resides in the templates
192
format_templates[format] = render_to_string((
193
'notification/%s/%s' % (label, format),
194
'notification/%s' % format), context)
196
return format_templates
199
def send_now(users, label, extra_context=None, on_site=True):
200
"""Creates a new notice.
202
This is intended to be how other apps create new notices.
204
notification.send(user, 'friends_invite_sent', {
209
You can pass in on_site=False to prevent the notice emitted from being
210
displayed on the site.
213
if extra_context is None:
216
# FrankU: This try statement is added to pass notice types
217
# which are deleted but used by third party apps to create a notice
218
# e.g. django-messages installed some notice-types which are superfluous
219
# because they just create a notice (which is not used anymore), but not
220
# used for sending email, like: 'message deleted' or 'message recovered'
222
notice_type = NoticeType.objects.get(label=label)
224
current_site = Site.objects.get_current()
225
notices_url = u"http://%s%s" % (
226
unicode(current_site),
227
reverse('notification_notices'),
230
current_language = get_language()
233
'short.txt', # used for subject
234
'full.txt', # used for email body
235
) # TODO make formats configurable
239
# get user language for user from language store defined in
240
# NOTIFICATION_LANGUAGE_MODULE setting
242
language = get_notification_language(user)
243
except LanguageStoreNotAvailable:
246
if language is not None:
247
# activate the user's language
250
# update context with user specific translations
253
'current_site': current_site,
254
'subject': notice_type.display
256
context.update(extra_context)
258
# get prerendered format messages and subjects
259
messages = get_formatted_messages(formats, label, context)
262
# Use 'email_subject.txt' to add Strings in every emails subject
263
subject = render_to_string('notification/email_subject.txt',
264
{'message': messages['short.txt'],}).replace('\n', '')
266
# Strip leading newlines. Make writing the email templates easier:
267
# Each linebreak in the templates results in a linebreak in the emails
268
# If the first line in a template contains only template tags the
269
# email will contain an empty line at the top.
270
body = render_to_string('notification/email_body.txt', {
271
'message': messages['full.txt'],
272
'notices_url': notices_url,
275
if should_send(user, notice_type, '1') and user.email: # Email
276
recipients.append(user.email)
278
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
280
# reset environment to original language
281
activate(current_language)
282
except NoticeType.DoesNotExist:
285
def send(*args, **kwargs):
286
"""A basic interface around both queue and send_now.
288
This honors a global flag NOTIFICATION_QUEUE_ALL that helps
289
determine whether all calls should be queued or not. A per call
290
``queue`` or ``now`` keyword argument can be used to always override
291
the default global behavior.
294
queue_flag = kwargs.pop('queue', False)
295
now_flag = kwargs.pop('now', False)
297
queue_flag and now_flag), "'queue' and 'now' cannot both be True."
299
return queue(*args, **kwargs)
301
return send_now(*args, **kwargs)
304
return queue(*args, **kwargs)
306
return send_now(*args, **kwargs)
309
def queue(users, label, extra_context=None, on_site=True):
310
"""Queue the notification in NoticeQueueBatch.
312
This allows for large amounts of user notifications to be deferred
313
to a seperate process running outside the webserver.
316
if extra_context is None:
318
if isinstance(users, QuerySet):
319
users = [row['pk'] for row in users.values('pk')]
321
users = [user.pk for user in users]
324
notices.append((user, label, extra_context, on_site))
325
NoticeQueueBatch(pickled_data=pickle.dumps(
326
notices).encode('base64')).save()
329
class ObservedItemManager(models.Manager):
331
def all_for(self, observed, signal):
332
"""Returns all ObservedItems for an observed object, to be sent when a
334
content_type = ContentType.objects.get_for_model(observed)
335
observed_items = self.filter(
336
content_type=content_type, object_id=observed.id, signal=signal)
337
return observed_items
339
def get_for(self, observed, observer, signal):
340
content_type = ContentType.objects.get_for_model(observed)
341
observed_item = self.get(
342
content_type=content_type, object_id=observed.id, user=observer, signal=signal)
346
class ObservedItem(models.Model):
348
user = models.ForeignKey(User, verbose_name=_('user'))
350
content_type = models.ForeignKey(ContentType)
351
object_id = models.PositiveIntegerField()
352
observed_object = GenericForeignKey('content_type', 'object_id')
354
notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
356
added = models.DateTimeField(_('added'), default=datetime.datetime.now)
358
# the signal that will be listened to send the notice
359
signal = models.TextField(verbose_name=_('signal'))
361
objects = ObservedItemManager()
364
ordering = ['-added']
365
verbose_name = _('observed item')
366
verbose_name_plural = _('observed items')
368
def send_notice(self):
369
send([self.user], self.notice_type.label,
370
{'observed': self.observed_object})
372
def get_content_object(self):
374
taken from threadedcomments:
376
Wrapper around the GenericForeignKey due to compatibility reasons
377
and due to ``list_display`` limitations.
379
return self.observed_object
382
def observe(observed, observer, notice_type_label, signal='post_save'):
383
"""Create a new ObservedItem.
385
To be used by applications to register a user as an observer for
389
notice_type = NoticeType.objects.get(label=notice_type_label)
390
observed_item = ObservedItem(user=observer, observed_object=observed,
391
notice_type=notice_type, signal=signal)
396
def stop_observing(observed, observer, signal='post_save'):
397
"""Remove an observed item."""
398
observed_item = ObservedItem.objects.get_for(observed, observer, signal)
399
observed_item.delete()
402
def send_observation_notices_for(observed, signal='post_save'):
403
"""Send a notice for each registered user about an observed object."""
404
observed_items = ObservedItem.objects.all_for(observed, signal)
405
for observed_item in observed_items:
406
observed_item.send_notice()
407
return observed_items
410
def is_observing(observed, observer, signal='post_save'):
411
if isinstance(observer, AnonymousUser):
414
observed_items = ObservedItem.objects.get_for(
415
observed, observer, signal)
417
except ObservedItem.DoesNotExist:
419
except ObservedItem.MultipleObjectsReturned:
423
def handle_observations(sender, instance, *args, **kw):
424
send_observation_notices_for(instance)