Ticket #13294: models.py

File models.py, 14.9 KB (added by richard@…, 15 years ago)

Amended models.py that uses HTML Mailer application if it is installed

Line 
1import datetime
2
3try:
4 import cPickle as pickle
5except ImportError:
6 import pickle
7
8from django.db import models
9from django.db.models.query import QuerySet
10from django.conf import settings
11from django.core.urlresolvers import reverse
12from django.template import Context
13from django.template.loader import render_to_string
14
15from django.core.exceptions import ImproperlyConfigured
16
17from django.contrib.sites.models import Site
18from django.contrib.auth.models import User
19from django.contrib.auth.models import AnonymousUser
20
21from django.contrib.contenttypes.models import ContentType
22from django.contrib.contenttypes import generic
23
24from django.utils.translation import ugettext_lazy as _
25from django.utils.translation import ugettext, get_language, activate
26
27# favour django-html_mailer but fall back to django.core.mail
28if 'html_mailer' in settings.INSTALLED_APPS:
29 from html_mailer import send_mail
30else:
31 if 'mailer' in settings.INSTALLED_APPS:
32 from mailer import send_mail
33 else:
34 from django.core.mail import send_mail
35
36QUEUE_ALL = getattr(settings, "NOTIFICATION_QUEUE_ALL", False)
37
38class LanguageStoreNotAvailable(Exception):
39 pass
40
41class NoticeType(models.Model):
42
43 label = models.CharField(_('label'), max_length=40)
44 display = models.CharField(_('display'), max_length=50)
45 description = models.CharField(_('description'), max_length=100)
46
47 # by default only on for media with sensitivity less than or equal to this number
48 default = models.IntegerField(_('default'))
49
50 def __unicode__(self):
51 return self.label
52
53 class Meta:
54 verbose_name = _("notice type")
55 verbose_name_plural = _("notice types")
56
57
58# if this gets updated, the create() method below needs to be as well...
59NOTICE_MEDIA = (
60 ("1", _("Email")),
61)
62
63# how spam-sensitive is the medium
64NOTICE_MEDIA_DEFAULTS = {
65 "1": 2 # email
66}
67
68class NoticeSetting(models.Model):
69 """
70 Indicates, for a given user, whether to send notifications
71 of a given type to a given medium.
72 """
73
74 user = models.ForeignKey(User, verbose_name=_('user'))
75 notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
76 medium = models.CharField(_('medium'), max_length=1, choices=NOTICE_MEDIA)
77 send = models.BooleanField(_('send'))
78
79 class Meta:
80 verbose_name = _("notice setting")
81 verbose_name_plural = _("notice settings")
82 unique_together = ("user", "notice_type", "medium")
83
84def get_notification_setting(user, notice_type, medium):
85 try:
86 return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)
87 except NoticeSetting.DoesNotExist:
88 default = (NOTICE_MEDIA_DEFAULTS[medium] <= notice_type.default)
89 setting = NoticeSetting(user=user, notice_type=notice_type, medium=medium, send=default)
90 setting.save()
91 return setting
92
93def should_send(user, notice_type, medium):
94 return get_notification_setting(user, notice_type, medium).send
95
96
97class NoticeManager(models.Manager):
98
99 def notices_for(self, user, archived=False, unseen=None, on_site=None):
100 """
101 returns Notice objects for the given user.
102
103 If archived=False, it only include notices not archived.
104 If archived=True, it returns all notices for that user.
105
106 If unseen=None, it includes all notices.
107 If unseen=True, return only unseen notices.
108 If unseen=False, return only seen notices.
109 """
110 if archived:
111 qs = self.filter(user=user)
112 else:
113 qs = self.filter(user=user, archived=archived)
114 if unseen is not None:
115 qs = qs.filter(unseen=unseen)
116 if on_site is not None:
117 qs = qs.filter(on_site=on_site)
118 return qs
119
120 def unseen_count_for(self, user, **kwargs):
121 """
122 returns the number of unseen notices for the given user but does not
123 mark them seen
124 """
125 return self.notices_for(user, unseen=True, **kwargs).count()
126
127class Notice(models.Model):
128
129 user = models.ForeignKey(User, verbose_name=_('user'))
130 message = models.TextField(_('message'))
131 notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
132 added = models.DateTimeField(_('added'), default=datetime.datetime.now)
133 unseen = models.BooleanField(_('unseen'), default=True)
134 archived = models.BooleanField(_('archived'), default=False)
135 on_site = models.BooleanField(_('on site'))
136
137 objects = NoticeManager()
138
139 def __unicode__(self):
140 return self.message
141
142 def archive(self):
143 self.archived = True
144 self.save()
145
146 def is_unseen(self):
147 """
148 returns value of self.unseen but also changes it to false.
149
150 Use this in a template to mark an unseen notice differently the first
151 time it is shown.
152 """
153 unseen = self.unseen
154 if unseen:
155 self.unseen = False
156 self.save()
157 return unseen
158
159 class Meta:
160 ordering = ["-added"]
161 verbose_name = _("notice")
162 verbose_name_plural = _("notices")
163
164 def get_absolute_url(self):
165 return ("notification_notice", [str(self.pk)])
166 get_absolute_url = models.permalink(get_absolute_url)
167
168class NoticeQueueBatch(models.Model):
169 """
170 A queued notice.
171 Denormalized data for a notice.
172 """
173 pickled_data = models.TextField()
174
175def create_notice_type(label, display, description, default=2, verbosity=1):
176 """
177 Creates a new NoticeType.
178
179 This is intended to be used by other apps as a post_syncdb manangement step.
180 """
181 try:
182 notice_type = NoticeType.objects.get(label=label)
183 updated = False
184 if display != notice_type.display:
185 notice_type.display = display
186 updated = True
187 if description != notice_type.description:
188 notice_type.description = description
189 updated = True
190 if default != notice_type.default:
191 notice_type.default = default
192 updated = True
193 if updated:
194 notice_type.save()
195 if verbosity > 1:
196 print "Updated %s NoticeType" % label
197 except NoticeType.DoesNotExist:
198 NoticeType(label=label, display=display, description=description, default=default).save()
199 if verbosity > 1:
200 print "Created %s NoticeType" % label
201
202def get_notification_language(user):
203 """
204 Returns site-specific notification language for this user. Raises
205 LanguageStoreNotAvailable if this site does not use translated
206 notifications.
207 """
208 if getattr(settings, 'NOTIFICATION_LANGUAGE_MODULE', False):
209 try:
210 app_label, model_name = settings.NOTIFICATION_LANGUAGE_MODULE.split('.')
211 model = models.get_model(app_label, model_name)
212 language_model = model._default_manager.get(user__id__exact=user.id)
213 if hasattr(language_model, 'language'):
214 return language_model.language
215 except (ImportError, ImproperlyConfigured, model.DoesNotExist):
216 raise LanguageStoreNotAvailable
217 raise LanguageStoreNotAvailable
218
219def get_formatted_messages(formats, label, context):
220 """
221 Returns a dictionary with the format identifier as the key. The values are
222 are fully rendered templates with the given context.
223 """
224 format_templates = {}
225 for format in formats:
226 # conditionally turn off autoescaping for .txt extensions in format
227 if format.endswith(".txt"):
228 context.autoescape = False
229 else:
230 context.autoescape = True
231 format_templates[format] = render_to_string((
232 'notification/%s/%s' % (label, format),
233 'notification/%s' % format), context_instance=context)
234 return format_templates
235
236def send_now(users, label, extra_context=None, on_site=True):
237 """
238 Creates a new notice.
239
240 This is intended to be how other apps create new notices.
241
242 notification.send(user, 'friends_invite_sent', {
243 'spam': 'eggs',
244 'foo': 'bar',
245 )
246
247 You can pass in on_site=False to prevent the notice emitted from being
248 displayed on the site.
249 """
250 if extra_context is None:
251 extra_context = {}
252
253 notice_type = NoticeType.objects.get(label=label)
254
255 current_site = Site.objects.get_current()
256 notices_url = u"http://%s%s" % (
257 unicode(current_site),
258 reverse("notification_notices"),
259 )
260
261 current_language = get_language()
262
263 formats = (
264 'short.txt',
265 'full.txt',
266 'notice.html',
267 'full.html',
268 ) # TODO make formats configurable
269
270 for user in users:
271 recipients = []
272 # get user language for user from language store defined in
273 # NOTIFICATION_LANGUAGE_MODULE setting
274 try:
275 language = get_notification_language(user)
276 except LanguageStoreNotAvailable:
277 language = None
278
279 if language is not None:
280 # activate the user's language
281 activate(language)
282
283 # update context with user specific translations
284 context = Context({
285 "user": user,
286 "notice": ugettext(notice_type.display),
287 "notices_url": notices_url,
288 "current_site": current_site,
289 })
290 context.update(extra_context)
291
292 # get prerendered format messages
293 messages = get_formatted_messages(formats, label, context)
294
295 # Strip newlines from subject
296 subject = ''.join(render_to_string('notification/email_subject.txt', {
297 'message': messages['short.txt'],
298 }, context).splitlines())
299
300 message_text = render_to_string('notification/email_body.txt', {
301 'message': messages['full.txt'],
302 }, context)
303
304 # Only if the html mailer is installed, can the html version be attached to the mail sent. The html_mailer version
305 # of send_mail checks to see if the message parameter is a dictionary with two messages types (for HTML mails)
306 # or a string (for plain text-only mails).
307 if 'html_mailer' in settings.INSTALLED_APPS:
308 message_html = render_to_string('notification/email_body.txt', {
309 'message': messages['full.html'],
310 }, context)
311
312 message = {"text":message_text, "html":message_html}
313 else:
314 message = message_text
315
316 notice = Notice.objects.create(user=user, message=messages['notice.html'],
317 notice_type=notice_type, on_site=on_site)
318 if should_send(user, notice_type, "1") and user.email: # Email
319 recipients.append(user.email)
320 send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipients)
321
322 # reset environment to original language
323 activate(current_language)
324
325def send(*args, **kwargs):
326 """
327 A basic interface around both queue and send_now. This honors a global
328 flag NOTIFICATION_QUEUE_ALL that helps determine whether all calls should
329 be queued or not. A per call ``queue`` or ``now`` keyword argument can be
330 used to always override the default global behavior.
331 """
332 queue_flag = kwargs.pop("queue", False)
333 now_flag = kwargs.pop("now", False)
334 assert not (queue_flag and now_flag), "'queue' and 'now' cannot both be True."
335 if queue_flag:
336 return queue(*args, **kwargs)
337 elif now_flag:
338 return send_now(*args, **kwargs)
339 else:
340 if QUEUE_ALL:
341 return queue(*args, **kwargs)
342 else:
343 return send_now(*args, **kwargs)
344
345def queue(users, label, extra_context=None, on_site=True):
346 """
347 Queue the notification in NoticeQueueBatch. This allows for large amounts
348 of user notifications to be deferred to a seperate process running outside
349 the webserver.
350 """
351 if extra_context is None:
352 extra_context = {}
353 if isinstance(users, QuerySet):
354 users = [row["pk"] for row in users.values("pk")]
355 else:
356 users = [user.pk for user in users]
357 notices = []
358 for user in users:
359 notices.append((user, label, extra_context, on_site))
360 NoticeQueueBatch(pickled_data=pickle.dumps(notices).encode("base64")).save()
361
362class ObservedItemManager(models.Manager):
363
364 def all_for(self, observed, signal):
365 """
366 Returns all ObservedItems for an observed object,
367 to be sent when a signal is emited.
368 """
369 content_type = ContentType.objects.get_for_model(observed)
370 observed_items = self.filter(content_type=content_type, object_id=observed.id, signal=signal)
371 return observed_items
372
373 def get_for(self, observed, observer, signal):
374 content_type = ContentType.objects.get_for_model(observed)
375 observed_item = self.get(content_type=content_type, object_id=observed.id, user=observer, signal=signal)
376 return observed_item
377
378
379class ObservedItem(models.Model):
380
381 user = models.ForeignKey(User, verbose_name=_('user'))
382
383 content_type = models.ForeignKey(ContentType)
384 object_id = models.PositiveIntegerField()
385 observed_object = generic.GenericForeignKey('content_type', 'object_id')
386
387 notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
388
389 added = models.DateTimeField(_('added'), default=datetime.datetime.now)
390
391 # the signal that will be listened to send the notice
392 signal = models.TextField(verbose_name=_('signal'))
393
394 objects = ObservedItemManager()
395
396 class Meta:
397 ordering = ['-added']
398 verbose_name = _('observed item')
399 verbose_name_plural = _('observed items')
400
401 def send_notice(self):
402 send([self.user], self.notice_type.label,
403 {'observed': self.observed_object})
404
405
406def observe(observed, observer, notice_type_label, signal='post_save'):
407 """
408 Create a new ObservedItem.
409
410 To be used by applications to register a user as an observer for some object.
411 """
412 notice_type = NoticeType.objects.get(label=notice_type_label)
413 observed_item = ObservedItem(user=observer, observed_object=observed,
414 notice_type=notice_type, signal=signal)
415 observed_item.save()
416 return observed_item
417
418def stop_observing(observed, observer, signal='post_save'):
419 """
420 Remove an observed item.
421 """
422 observed_item = ObservedItem.objects.get_for(observed, observer, signal)
423 observed_item.delete()
424
425def send_observation_notices_for(observed, signal='post_save'):
426 """
427 Send a notice for each registered user about an observed object.
428 """
429 observed_items = ObservedItem.objects.all_for(observed, signal)
430 for observed_item in observed_items:
431 observed_item.send_notice()
432 return observed_items
433
434def is_observing(observed, observer, signal='post_save'):
435 if isinstance(observer, AnonymousUser):
436 return False
437 try:
438 observed_items = ObservedItem.objects.get_for(observed, observer, signal)
439 return True
440 except ObservedItem.DoesNotExist:
441 return False
442 except ObservedItem.MultipleObjectsReturned:
443 return True
444
445def handle_observations(sender, instance, *args, **kw):
446 send_observation_notices_for(instance)
Back to Top