summaryrefslogtreecommitdiff
path: root/notification/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'notification/models.py')
-rw-r--r--notification/models.py469
1 files changed, 469 insertions, 0 deletions
diff --git a/notification/models.py b/notification/models.py
new file mode 100644
index 0000000..88dc31a
--- /dev/null
+++ b/notification/models.py
@@ -0,0 +1,469 @@
+import datetime
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+from django.db import models
+from django.db.models.query import QuerySet
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.core.mail import send_mail
+from django.core.urlresolvers import reverse
+from django.template import Context
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext, get_language, activate
+
+from django.contrib.sites.models import Site
+from django.contrib.auth.models import User
+from django.contrib.auth.models import AnonymousUser
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+
+QUEUE_ALL = getattr(settings, "NOTIFICATION_QUEUE_ALL", False)
+
+
+class LanguageStoreNotAvailable(Exception):
+ pass
+
+class NoticeType(models.Model):
+
+ label = models.CharField(_("label"), max_length=40)
+ display = models.CharField(_("display"), max_length=50)
+ description = models.CharField(_("description"), max_length=100)
+
+ # by default only on for media with sensitivity less than or equal to this number
+ default = models.IntegerField(_("default"))
+
+ def __unicode__(self):
+ return self.label
+
+ class Meta:
+ verbose_name = _("notice type")
+ verbose_name_plural = _("notice types")
+
+
+# if this gets updated, the create() method below needs to be as well...
+NOTICE_MEDIA = (
+ ("1", _("Email")),
+)
+
+# how spam-sensitive is the medium
+NOTICE_MEDIA_DEFAULTS = {
+ "1": 2 # email
+}
+
+class NoticeSetting(models.Model):
+ """
+ Indicates, for a given user, whether to send notifications
+ of a given type to a given medium.
+ """
+
+ user = models.ForeignKey(User, verbose_name=_("user"))
+ notice_type = models.ForeignKey(NoticeType, verbose_name=_("notice type"))
+ medium = models.CharField(_("medium"), max_length=1, choices=NOTICE_MEDIA)
+ send = models.BooleanField(_("send"))
+
+ class Meta:
+ verbose_name = _("notice setting")
+ verbose_name_plural = _("notice settings")
+ unique_together = ("user", "notice_type", "medium")
+
+
+def get_notification_setting(user, notice_type, medium):
+ try:
+ return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)
+ except NoticeSetting.DoesNotExist:
+ default = (NOTICE_MEDIA_DEFAULTS[medium] <= notice_type.default)
+ setting = NoticeSetting(user=user, notice_type=notice_type, medium=medium, send=default)
+ setting.save()
+ return setting
+
+
+def should_send(user, notice_type, medium):
+ return get_notification_setting(user, notice_type, medium).send
+
+
+class NoticeManager(models.Manager):
+
+ def notices_for(self, user, archived=False, unseen=None, on_site=None, sent=False):
+ """
+ returns Notice objects for the given user.
+
+ If archived=False, it only include notices not archived.
+ If archived=True, it returns all notices for that user.
+
+ If unseen=None, it includes all notices.
+ If unseen=True, return only unseen notices.
+ If unseen=False, return only seen notices.
+ """
+ if sent:
+ lookup_kwargs = {"sender": user}
+ else:
+ lookup_kwargs = {"recipient": user}
+ qs = self.filter(**lookup_kwargs)
+ nqs = []
+ for each in qs:
+ nqs.append(each.message)
+ if not archived:
+ self.filter(archived=archived)
+ if unseen is not None:
+ qs = qs.filter(unseen=unseen)
+ if on_site is not None:
+ qs = qs.filter(on_site=on_site)
+ return nqs
+
+ def unseen_count_for(self, recipient, **kwargs):
+ """
+ returns the number of unseen notices for the given user but does not
+ mark them seen
+ """
+ return self.notices_for(recipient, unseen=True, **kwargs).count()
+
+ def received(self, recipient, **kwargs):
+ """
+ returns notices the given recipient has recieved.
+ """
+ kwargs["sent"] = False
+ return self.notices_for(recipient, **kwargs)
+
+ def sent(self, sender, **kwargs):
+ """
+ returns notices the given sender has sent
+ """
+ kwargs["sent"] = True
+ return self.notices_for(sender, **kwargs)
+
+
+class Notice(models.Model):
+
+ recipient = models.ForeignKey(User, related_name="recieved_notices", verbose_name=_("recipient"))
+ sender = models.ForeignKey(User, null=True, related_name="sent_notices", verbose_name=_("sender"))
+ message = models.TextField(_("message"))
+ notice_type = models.ForeignKey(NoticeType, verbose_name=_("notice type"))
+ added = models.DateTimeField(_("added"), default=datetime.datetime.now)
+ unseen = models.BooleanField(_("unseen"), default=True)
+ archived = models.BooleanField(_("archived"), default=False)
+ on_site = models.BooleanField(_("on site"))
+
+ objects = NoticeManager()
+
+ def __unicode__(self):
+ return self.message
+
+ def archive(self):
+ self.archived = True
+ self.save()
+
+ def is_unseen(self):
+ """
+ returns value of self.unseen but also changes it to false.
+
+ Use this in a template to mark an unseen notice differently the first
+ time it is shown.
+ """
+ unseen = self.unseen
+ if unseen:
+ self.unseen = False
+ self.save()
+ return unseen
+
+ class Meta:
+ ordering = ["-added"]
+ verbose_name = _("notice")
+ verbose_name_plural = _("notices")
+
+ def get_absolute_url(self):
+ return reverse("notification_notice", args=[str(self.pk)])
+
+
+class NoticeQueueBatch(models.Model):
+ """
+ A queued notice.
+ Denormalized data for a notice.
+ """
+ pickled_data = models.TextField()
+
+
+def create_notice_type(label, display, description, default=2, verbosity=1):
+ """
+ Creates a new NoticeType.
+
+ This is intended to be used by other apps as a post_syncdb manangement step.
+ """
+ try:
+ notice_type = NoticeType.objects.get(label=label)
+ updated = False
+ if display != notice_type.display:
+ notice_type.display = display
+ updated = True
+ if description != notice_type.description:
+ notice_type.description = description
+ updated = True
+ if default != notice_type.default:
+ notice_type.default = default
+ updated = True
+ if updated:
+ notice_type.save()
+ if verbosity > 1:
+ print "Updated %s NoticeType" % label
+ except NoticeType.DoesNotExist:
+ NoticeType(label=label, display=display, description=description, default=default).save()
+ if verbosity > 1:
+ print "Created %s NoticeType" % label
+
+
+def get_notification_language(user):
+ """
+ Returns site-specific notification language for this user. Raises
+ LanguageStoreNotAvailable if this site does not use translated
+ notifications.
+ """
+ if getattr(settings, "NOTIFICATION_LANGUAGE_MODULE", False):
+ try:
+ app_label, model_name = settings.NOTIFICATION_LANGUAGE_MODULE.split(".")
+ model = models.get_model(app_label, model_name)
+ language_model = model._default_manager.get(user__id__exact=user.id)
+ if hasattr(language_model, "language"):
+ return language_model.language
+ except (ImportError, ImproperlyConfigured, model.DoesNotExist):
+ raise LanguageStoreNotAvailable
+ raise LanguageStoreNotAvailable
+
+
+def get_formatted_messages(formats, label, context):
+ """
+ Returns a dictionary with the format identifier as the key. The values are
+ are fully rendered templates with the given context.
+ """
+ format_templates = {}
+ for format in formats:
+ # conditionally turn off autoescaping for .txt extensions in format
+ if format.endswith(".txt"):
+ context.autoescape = False
+ else:
+ context.autoescape = True
+ print label,"labels"
+ format_templates[format] = label #("gstudio/notification/%s/%s" % (label, format),
+ #"gstudio/notification/%s" % format)
+ return format_templates
+
+
+def send_now(users, label, extra_context=None, on_site=True, sender=None):
+ """
+ Creates a new notice.
+
+ This is intended to be how other apps create new notices.
+
+ notification.send(user, "friends_invite_sent", {
+ "spam": "eggs",
+ "foo": "bar",
+ )
+
+ You can pass in on_site=False to prevent the notice emitted from being
+ displayed on the site.
+ """
+ if extra_context is None:
+ extra_context = {}
+
+ notice_type = NoticeType.objects.get(label=label)
+
+ protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http")
+ current_site = Site.objects.get_current()
+
+ notices_url = u"%s://%s%s" % (
+ protocol,
+ unicode(current_site),
+ reverse("notification_notices"),
+ )
+
+ current_language = get_language()
+
+ formats = (
+ "short.txt",
+ "full.txt",
+ "notice.html",
+ "full.html",
+ ) # TODO make formats configurable
+
+ for user in users:
+ recipients = []
+ # get user language for user from language store defined in
+ # NOTIFICATION_LANGUAGE_MODULE setting
+ try:
+ language = get_notification_language(user)
+ except LanguageStoreNotAvailable:
+ language = None
+
+ if language is not None:
+ # activate the user's language
+ activate(language)
+
+ # update context with user specific translations
+ context = Context({
+ "recipient": user,
+ "sender": sender,
+ "notice": ugettext(notice_type.display),
+ "notices_url": notices_url,
+ "current_site": current_site,
+ })
+ context.update(extra_context)
+
+ # get prerendered format messages
+ messages = get_formatted_messages(formats, label, context)
+
+ # Strip newlines from subject
+ subject = "".join(render_to_string("gstudio/notification/email_subject.txt", {
+ "message": messages["short.txt"],
+ }, context).splitlines())
+
+ body = render_to_string("gstudio/notification/email_body.txt", {
+ "message": messages["full.txt"],
+ }, context)
+
+ notice = Notice.objects.create(recipient=user, message=messages["notice.html"],
+ notice_type=notice_type, on_site=on_site, sender=sender)
+ if should_send(user, notice_type, "1") and user.email and user.is_active: # Email
+ recipients.append(user.email)
+ send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
+
+ # reset environment to original language
+ activate(current_language)
+
+
+def send(*args, **kwargs):
+ """
+ A basic interface around both queue and send_now. This honors a global
+ flag NOTIFICATION_QUEUE_ALL that helps determine whether all calls should
+ be queued or not. A per call ``queue`` or ``now`` keyword argument can be
+ used to always override the default global behavior.
+ """
+ queue_flag = kwargs.pop("queue", False)
+ now_flag = kwargs.pop("now", False)
+ assert not (queue_flag and now_flag), "'queue' and 'now' cannot both be True."
+ if queue_flag:
+ return queue(*args, **kwargs)
+ elif now_flag:
+ return send_now(*args, **kwargs)
+ else:
+ if QUEUE_ALL:
+ return queue(*args, **kwargs)
+ else:
+ return send_now(*args, **kwargs)
+
+
+def queue(users, label, extra_context=None, on_site=True, sender=None):
+ """
+ Queue the notification in NoticeQueueBatch. This allows for large amounts
+ of user notifications to be deferred to a seperate process running outside
+ the webserver.
+ """
+ if extra_context is None:
+ extra_context = {}
+ if isinstance(users, QuerySet):
+ users = [row["pk"] for row in users.values("pk")]
+ else:
+ users = [user.pk for user in users]
+ notices = []
+ for user in users:
+ notices.append((user, label, extra_context, on_site, sender))
+ NoticeQueueBatch(pickled_data=pickle.dumps(notices).encode("base64")).save()
+
+
+class ObservedItemManager(models.Manager):
+
+ def all_for(self, observed, signal):
+ """
+ Returns all ObservedItems for an observed object,
+ to be sent when a signal is emited.
+ """
+ content_type = ContentType.objects.get_for_model(observed)
+ observed_items = self.filter(content_type=content_type, object_id=observed.id, signal=signal)
+ return observed_items
+
+ def get_for(self, observed, observer, signal):
+ content_type = ContentType.objects.get_for_model(observed)
+ observed_item = self.get(content_type=content_type, object_id=observed.id, user=observer, signal=signal)
+ return observed_item
+
+
+class ObservedItem(models.Model):
+
+ user = models.ForeignKey(User, verbose_name=_("user"))
+
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ observed_object = generic.GenericForeignKey("content_type", "object_id")
+
+ notice_type = models.ForeignKey(NoticeType, verbose_name=_("notice type"))
+
+ added = models.DateTimeField(_("added"), default=datetime.datetime.now)
+
+ # the signal that will be listened to send the notice
+ signal = models.TextField(verbose_name=_("signal"))
+
+ objects = ObservedItemManager()
+
+ class Meta:
+ ordering = ["-added"]
+ verbose_name = _("observed item")
+ verbose_name_plural = _("observed items")
+
+ def send_notice(self, extra_context=None):
+ if extra_context is None:
+ extra_context = {}
+ extra_context.update({"observed": self.observed_object})
+ send([self.user], self.notice_type.label, extra_context)
+
+
+def observe(observed, observer, notice_type_label, signal="post_save"):
+ """
+ Create a new ObservedItem.
+
+ To be used by applications to register a user as an observer for some object.
+ """
+ notice_type = NoticeType.objects.get(label=notice_type_label)
+ observed_item = ObservedItem(
+ user=observer, observed_object=observed,
+ notice_type=notice_type, signal=signal
+ )
+ observed_item.save()
+ return observed_item
+
+
+def stop_observing(observed, observer, signal="post_save"):
+ """
+ Remove an observed item.
+ """
+ observed_item = ObservedItem.objects.get_for(observed, observer, signal)
+ observed_item.delete()
+
+
+def send_observation_notices_for(observed, signal="post_save", extra_context=None):
+ """
+ Send a notice for each registered user about an observed object.
+ """
+ if extra_context is None:
+ extra_context = {}
+ observed_items = ObservedItem.objects.all_for(observed, signal)
+ for observed_item in observed_items:
+ observed_item.send_notice(extra_context)
+ return observed_items
+
+
+def is_observing(observed, observer, signal="post_save"):
+ if isinstance(observer, AnonymousUser):
+ return False
+ try:
+ observed_items = ObservedItem.objects.get_for(observed, observer, signal)
+ return True
+ except ObservedItem.DoesNotExist:
+ return False
+ except ObservedItem.MultipleObjectsReturned:
+ return True
+
+
+def handle_observations(sender, instance, *args, **kw):
+ send_observation_notices_for(instance)