Ticket #13294: models.py

File models.py, 14.9 KB (added by richard@…, 5 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