~widelands-dev/widelands-website/sphinx_fixes

« back to all changes in this revision

Viewing changes to notification/models.py

  • Committer: franku
  • Date: 2016-05-17 19:28:38 UTC
  • mto: This revision was merged to the branch mainline in revision 409.
  • Revision ID: somal@arcor.de-20160517192838-0pyxjlptxhoyrpo2
added old notification app to widelands; deactivated notification feed

Show diffs side-by-side

added added

removed removed

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