~widelands-dev/widelands-website/django_staticfiles

« back to all changes in this revision

Viewing changes to notification/models.py

  • Committer: Holger Rapp
  • Date: 2010-09-26 13:30:30 UTC
  • Revision ID: sirver@gmx.de-20100926133030-ceirjf83vde91tyt
Added a simple events model to display dates on the homepage

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.urls import reverse
12
 
from django.template.loader import render_to_string
13
 
 
14
 
from django.core.exceptions import ImproperlyConfigured
15
 
 
16
 
from django.contrib.sites.models import Site
17
 
from django.contrib.auth.models import User
18
 
from django.contrib.auth.models import AnonymousUser
19
 
 
20
 
from django.contrib.contenttypes.models import ContentType
21
 
from django.contrib.contenttypes.fields import GenericForeignKey
22
 
 
23
 
from django.utils.translation import ugettext_lazy as _
24
 
from django.utils.translation import ugettext, get_language, activate
25
 
 
26
 
# favour django-mailer but fall back to django.core.mail
27
 
if 'mailer' in settings.INSTALLED_APPS:
28
 
    from mailer import send_mail
29
 
else:
30
 
    from django.core.mail import send_mail
31
 
 
32
 
QUEUE_ALL = getattr(settings, 'NOTIFICATION_QUEUE_ALL', False)
33
 
 
34
 
 
35
 
class LanguageStoreNotAvailable(Exception):
36
 
    pass
37
 
 
38
 
 
39
 
class NoticeType(models.Model):
40
 
 
41
 
    label = models.CharField(_('label'), max_length=40)
42
 
    display = models.CharField(_('display'),
43
 
                               max_length=50,
44
 
                               help_text=_('Used as subject when sending emails.'))
45
 
    description = models.CharField(_('description'), max_length=100)
46
 
 
47
 
    # by default only on for media with sensitivity less than or equal to this
48
 
    # number
49
 
    default = models.IntegerField(_('default'))
50
 
 
51
 
    def __unicode__(self):
52
 
        return self.label
53
 
 
54
 
    class Meta:
55
 
        verbose_name = _('notice type')
56
 
        verbose_name_plural = _('notice types')
57
 
 
58
 
 
59
 
# if this gets updated, the create() method below needs to be as well...
60
 
NOTICE_MEDIA = (
61
 
    ('1', _('Email')),
62
 
)
63
 
 
64
 
# how spam-sensitive is the medium
65
 
NOTICE_MEDIA_DEFAULTS = {
66
 
    '1': 2  # email
67
 
}
68
 
 
69
 
 
70
 
class NoticeSetting(models.Model):
71
 
    """Indicates, for a given user, whether to send notifications of a given
72
 
    type to a given medium.
73
 
 
74
 
    Notice types for each user are added if he/she enters the notification page.
75
 
 
76
 
    """
77
 
 
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'))
82
 
 
83
 
    class Meta:
84
 
        verbose_name = _('notice setting')
85
 
        verbose_name_plural = _('notice settings')
86
 
        unique_together = ('user', 'notice_type', 'medium')
87
 
 
88
 
 
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.
92
 
 
93
 
    If a new NoticeSetting is created, the field 'default' of a NoticeType
94
 
    decides whether NoticeSetting.send is True or False as default.
95
 
    """
96
 
    try:
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)
102
 
        setting.save()
103
 
        return setting
104
 
 
105
 
 
106
 
def should_send(user, notice_type, medium):
107
 
    return get_notification_setting(user, notice_type, medium).send
108
 
 
109
 
 
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
112
 
    type of notice."""
113
 
    query = NoticeSetting.objects.filter(
114
 
            notice_type__label=notice_type, send=True)
115
 
 
116
 
    if excl_user:
117
 
        query = query.exclude(user=excl_user)
118
 
 
119
 
    return [notice_setting.user for notice_setting in query]
120
 
 
121
 
 
122
 
class NoticeQueueBatch(models.Model):
123
 
    """A queued notice.
124
 
 
125
 
    Denormalized data for a notice.
126
 
 
127
 
    """
128
 
    pickled_data = models.TextField()
129
 
 
130
 
 
131
 
def create_notice_type(label, display, description, default=2, verbosity=1):
132
 
    """Creates a new NoticeType.
133
 
 
134
 
    This is intended to be used by other apps as a post_migrate
135
 
    manangement step.
136
 
 
137
 
    """
138
 
    try:
139
 
        notice_type = NoticeType.objects.get(label=label)
140
 
        updated = False
141
 
        if display != notice_type.display:
142
 
            notice_type.display = display
143
 
            updated = True
144
 
        if description != notice_type.description:
145
 
            notice_type.description = description
146
 
            updated = True
147
 
        if default != notice_type.default:
148
 
            notice_type.default = default
149
 
            updated = True
150
 
        if updated:
151
 
            notice_type.save()
152
 
            if verbosity > 1:
153
 
                print 'Updated %s NoticeType' % label
154
 
    except NoticeType.DoesNotExist:
155
 
        NoticeType(label=label, display=display,
156
 
                   description=description, default=default).save()
157
 
        if verbosity > 1:
158
 
            print 'Created %s NoticeType' % label
159
 
 
160
 
 
161
 
def get_notification_language(user):
162
 
    """
163
 
    Returns site-specific notification language for this user. Raises
164
 
    LanguageStoreNotAvailable if this site does not use translated
165
 
    notifications.
166
 
    """
167
 
    if getattr(settings, 'NOTIFICATION_LANGUAGE_MODULE', False):
168
 
        try:
169
 
            app_label, model_name = settings.NOTIFICATION_LANGUAGE_MODULE.split(
170
 
                '.')
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
179
 
 
180
 
 
181
 
def get_formatted_messages(formats, label, context):
182
 
    """Returns a dictionary with the format identifier as the key.
183
 
 
184
 
    The values are are fully rendered templates with the given context.
185
 
 
186
 
    """
187
 
    format_templates = {}
188
 
 
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)
195
 
 
196
 
    return format_templates
197
 
 
198
 
 
199
 
def send_now(users, label, extra_context=None, on_site=True):
200
 
    """Creates a new notice.
201
 
 
202
 
    This is intended to be how other apps create new notices.
203
 
 
204
 
    notification.send(user, 'friends_invite_sent', {
205
 
        'spam': 'eggs',
206
 
        'foo': 'bar',
207
 
    )
208
 
 
209
 
    You can pass in on_site=False to prevent the notice emitted from being
210
 
    displayed on the site.
211
 
 
212
 
    """
213
 
    if extra_context is None:
214
 
        extra_context = {}
215
 
 
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'
221
 
    try:
222
 
        notice_type = NoticeType.objects.get(label=label)
223
 
 
224
 
        current_site = Site.objects.get_current()
225
 
        notices_url = u"http://%s%s" % (
226
 
            unicode(current_site),
227
 
            reverse('notification_notices'),
228
 
        )
229
 
 
230
 
        current_language = get_language()
231
 
 
232
 
        formats = (
233
 
            'short.txt', # used for subject
234
 
            'full.txt',  # used for email body
235
 
        )  # TODO make formats configurable
236
 
 
237
 
        for user in users:
238
 
            recipients = []
239
 
            # get user language for user from language store defined in
240
 
            # NOTIFICATION_LANGUAGE_MODULE setting
241
 
            try:
242
 
                language = get_notification_language(user)
243
 
            except LanguageStoreNotAvailable:
244
 
                language = None
245
 
 
246
 
            if language is not None:
247
 
                # activate the user's language
248
 
                activate(language)
249
 
 
250
 
            # update context with user specific translations
251
 
            context = {
252
 
                'user': user,
253
 
                'current_site': current_site,
254
 
                'subject': notice_type.display
255
 
            }
256
 
            context.update(extra_context)
257
 
 
258
 
            # get prerendered format messages and subjects
259
 
            messages = get_formatted_messages(formats, label, context)
260
 
            
261
 
            # Create the subject
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', '')
265
 
            
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,
273
 
            }).lstrip()
274
 
 
275
 
            if should_send(user, notice_type, '1') and user.email:  # Email
276
 
                recipients.append(user.email)
277
 
 
278
 
            send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
279
 
        
280
 
        # reset environment to original language
281
 
        activate(current_language)
282
 
    except NoticeType.DoesNotExist:
283
 
        pass
284
 
 
285
 
def send(*args, **kwargs):
286
 
    """A basic interface around both queue and send_now.
287
 
 
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.
292
 
 
293
 
    """
294
 
    queue_flag = kwargs.pop('queue', False)
295
 
    now_flag = kwargs.pop('now', False)
296
 
    assert not (
297
 
        queue_flag and now_flag), "'queue' and 'now' cannot both be True."
298
 
    if queue_flag:
299
 
        return queue(*args, **kwargs)
300
 
    elif now_flag:
301
 
        return send_now(*args, **kwargs)
302
 
    else:
303
 
        if QUEUE_ALL:
304
 
            return queue(*args, **kwargs)
305
 
        else:
306
 
            return send_now(*args, **kwargs)
307
 
 
308
 
 
309
 
def queue(users, label, extra_context=None, on_site=True):
310
 
    """Queue the notification in NoticeQueueBatch.
311
 
 
312
 
    This allows for large amounts of user notifications to be deferred
313
 
    to a seperate process running outside the webserver.
314
 
 
315
 
    """
316
 
    if extra_context is None:
317
 
        extra_context = {}
318
 
    if isinstance(users, QuerySet):
319
 
        users = [row['pk'] for row in users.values('pk')]
320
 
    else:
321
 
        users = [user.pk for user in users]
322
 
    notices = []
323
 
    for user in users:
324
 
        notices.append((user, label, extra_context, on_site))
325
 
    NoticeQueueBatch(pickled_data=pickle.dumps(
326
 
        notices).encode('base64')).save()
327
 
 
328
 
 
329
 
class ObservedItemManager(models.Manager):
330
 
 
331
 
    def all_for(self, observed, signal):
332
 
        """Returns all ObservedItems for an observed object, to be sent when a
333
 
        signal is emited."""
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
338
 
 
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)
343
 
        return observed_item
344
 
 
345
 
 
346
 
class ObservedItem(models.Model):
347
 
 
348
 
    user = models.ForeignKey(User, verbose_name=_('user'))
349
 
 
350
 
    content_type = models.ForeignKey(ContentType)
351
 
    object_id = models.PositiveIntegerField()
352
 
    observed_object = GenericForeignKey('content_type', 'object_id')
353
 
 
354
 
    notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
355
 
 
356
 
    added = models.DateTimeField(_('added'), default=datetime.datetime.now)
357
 
 
358
 
    # the signal that will be listened to send the notice
359
 
    signal = models.TextField(verbose_name=_('signal'))
360
 
 
361
 
    objects = ObservedItemManager()
362
 
 
363
 
    class Meta:
364
 
        ordering = ['-added']
365
 
        verbose_name = _('observed item')
366
 
        verbose_name_plural = _('observed items')
367
 
 
368
 
    def send_notice(self):
369
 
        send([self.user], self.notice_type.label,
370
 
             {'observed': self.observed_object})
371
 
        
372
 
    def get_content_object(self):
373
 
        """
374
 
        taken from threadedcomments:
375
 
 
376
 
        Wrapper around the GenericForeignKey due to compatibility reasons
377
 
        and due to ``list_display`` limitations.
378
 
        """
379
 
        return self.observed_object
380
 
 
381
 
 
382
 
def observe(observed, observer, notice_type_label, signal='post_save'):
383
 
    """Create a new ObservedItem.
384
 
 
385
 
    To be used by applications to register a user as an observer for
386
 
    some object.
387
 
 
388
 
    """
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)
392
 
    observed_item.save()
393
 
    return observed_item
394
 
 
395
 
 
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()
400
 
 
401
 
 
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
408
 
 
409
 
 
410
 
def is_observing(observed, observer, signal='post_save'):
411
 
    if isinstance(observer, AnonymousUser):
412
 
        return False
413
 
    try:
414
 
        observed_items = ObservedItem.objects.get_for(
415
 
            observed, observer, signal)
416
 
        return True
417
 
    except ObservedItem.DoesNotExist:
418
 
        return False
419
 
    except ObservedItem.MultipleObjectsReturned:
420
 
        return True
421
 
 
422
 
 
423
 
def handle_observations(sender, instance, *args, **kw):
424
 
    send_observation_notices_for(instance)