diff --git a/pinax/notifications/admin.py b/pinax/notifications/admin.py index 1fe028b3..f756a609 100644 --- a/pinax/notifications/admin.py +++ b/pinax/notifications/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import NoticeType, NoticeQueueBatch, NoticeSetting +from .models import NoticeType, NoticeQueueBatch, NoticeSetting, Notice class NoticeTypeAdmin(admin.ModelAdmin): @@ -9,8 +9,16 @@ class NoticeTypeAdmin(admin.ModelAdmin): class NoticeSettingAdmin(admin.ModelAdmin): list_display = ["id", "user", "notice_type", "medium", "scoping", "send"] + raw_id_fields = ["user"] +class NoticeAdmin(admin.ModelAdmin): + list_display = [ + "message", "recipient", "sender", "notice_type", "added", "unseen", + "archived"] + raw_id_fields = ["recipient", "sender"] + admin.site.register(NoticeQueueBatch) admin.site.register(NoticeType, NoticeTypeAdmin) +admin.site.register(Notice, NoticeAdmin) admin.site.register(NoticeSetting, NoticeSettingAdmin) diff --git a/pinax/notifications/backends/email.py b/pinax/notifications/backends/email.py index fc7cd03b..9ed69eef 100644 --- a/pinax/notifications/backends/email.py +++ b/pinax/notifications/backends/email.py @@ -4,6 +4,7 @@ from django.utils.translation import ugettext from .base import BaseBackend +from ..utils import get_class_from_path class EmailBackend(BaseBackend): @@ -42,3 +43,12 @@ def deliver(self, recipient, sender, notice_type, extra_context): body = render_to_string("pinax/notifications/email_body.txt", context) send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [recipient.email]) + + Notice = get_class_from_path(path='pinax.notifications.models.Notice') + + # Based on http://stackoverflow.com/a/7390947 + # This is mostly a log for sent notifications. + Notice.objects.create( + recipient=recipient, message=messages["full.txt"], + notice_type=notice_type, sender=sender + ) diff --git a/pinax/notifications/backends/push_notifications.py b/pinax/notifications/backends/push_notifications.py new file mode 100644 index 00000000..fc7cd03b --- /dev/null +++ b/pinax/notifications/backends/push_notifications.py @@ -0,0 +1,44 @@ +from django.conf import settings +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.utils.translation import ugettext + +from .base import BaseBackend + + +class EmailBackend(BaseBackend): + spam_sensitivity = 2 + + def can_send(self, user, notice_type, scoping): + can_send = super(EmailBackend, self).can_send(user, notice_type, scoping) + if can_send and user.email: + return True + return False + + def deliver(self, recipient, sender, notice_type, extra_context): + # TODO: require this to be passed in extra_context + + context = self.default_context() + context.update({ + "recipient": recipient, + "sender": sender, + "notice": ugettext(notice_type.display), + }) + context.update(extra_context) + + messages = self.get_formatted_messages(( + "short.txt", + "full.txt" + ), notice_type.label, context) + + context.update({ + "message": messages["short.txt"], + }) + subject = "".join(render_to_string("pinax/notifications/email_subject.txt", context).splitlines()) + + context.update({ + "message": messages["full.txt"] + }) + body = render_to_string("pinax/notifications/email_body.txt", context) + + send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [recipient.email]) diff --git a/pinax/notifications/backends/sms.py b/pinax/notifications/backends/sms.py new file mode 100644 index 00000000..fc7cd03b --- /dev/null +++ b/pinax/notifications/backends/sms.py @@ -0,0 +1,44 @@ +from django.conf import settings +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.utils.translation import ugettext + +from .base import BaseBackend + + +class EmailBackend(BaseBackend): + spam_sensitivity = 2 + + def can_send(self, user, notice_type, scoping): + can_send = super(EmailBackend, self).can_send(user, notice_type, scoping) + if can_send and user.email: + return True + return False + + def deliver(self, recipient, sender, notice_type, extra_context): + # TODO: require this to be passed in extra_context + + context = self.default_context() + context.update({ + "recipient": recipient, + "sender": sender, + "notice": ugettext(notice_type.display), + }) + context.update(extra_context) + + messages = self.get_formatted_messages(( + "short.txt", + "full.txt" + ), notice_type.label, context) + + context.update({ + "message": messages["short.txt"], + }) + subject = "".join(render_to_string("pinax/notifications/email_subject.txt", context).splitlines()) + + context.update({ + "message": messages["full.txt"] + }) + body = render_to_string("pinax/notifications/email_body.txt", context) + + send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [recipient.email]) diff --git a/pinax/notifications/conf.py b/pinax/notifications/conf.py index 66e1586b..825ac5be 100644 --- a/pinax/notifications/conf.py +++ b/pinax/notifications/conf.py @@ -65,8 +65,8 @@ def configure_backends(self, value): raise ImproperlyConfigured( "NOTIFICATION_BACKENDS does not contain enough data." ) - backend_instance = load_path_attr(backend_path)(medium_id, spam_sensitivity) - backends.append(((medium_id, label), backend_instance)) + backend_instance = load_path_attr(backend_path)(label, spam_sensitivity) + backends.append(((label, label), backend_instance)) return dict(backends) def configure_get_language_model(self, value): diff --git a/pinax/notifications/migrations/0002_auto_20161129_1812.py b/pinax/notifications/migrations/0002_auto_20161129_1812.py new file mode 100644 index 00000000..707d7320 --- /dev/null +++ b/pinax/notifications/migrations/0002_auto_20161129_1812.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pinax_notifications', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Notice', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('message', models.TextField(verbose_name='message')), + ('added', models.DateTimeField(default=django.utils.timezone.now, verbose_name='added', db_index=True)), + ('unseen', models.BooleanField(default=True, db_index=True, verbose_name='unseen')), + ('archived', models.BooleanField(default=False, verbose_name='archived')), + ('on_site', models.BooleanField(default=False, verbose_name='on site')), + ('notice_type', models.ForeignKey(verbose_name='notice type', to='pinax_notifications.NoticeType')), + ('recipient', models.ForeignKey(related_name='received_notices', verbose_name='recipient', to=settings.AUTH_USER_MODEL)), + ('sender', models.ForeignKey(related_name='sent_notices', verbose_name='sender', to=settings.AUTH_USER_MODEL, null=True)), + ], + options={ + 'verbose_name': 'notice', + 'verbose_name_plural': 'notices', + }, + ), + migrations.AlterField( + model_name='noticesetting', + name='medium', + field=models.CharField(max_length=100, verbose_name='medium', choices=[(0, 'email')]), + ), + ] diff --git a/pinax/notifications/models.py b/pinax/notifications/models.py index ef8df463..35c5b33a 100644 --- a/pinax/notifications/models.py +++ b/pinax/notifications/models.py @@ -3,6 +3,8 @@ import base64 + +from django.utils import timezone from django.db import models from django.db.models.query import QuerySet from django.core.exceptions import ImproperlyConfigured @@ -10,6 +12,7 @@ from django.utils.translation import get_language, activate from django.utils.encoding import python_2_unicode_compatible from django.utils.six.moves import cPickle as pickle # pylint: disable-msg=F +from django.core.urlresolvers import reverse from django.contrib.contenttypes.models import ContentType @@ -79,7 +82,7 @@ class NoticeSetting(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("user")) notice_type = models.ForeignKey(NoticeType, verbose_name=_("notice type")) - medium = models.CharField(_("medium"), max_length=1, choices=NOTICE_MEDIA) + medium = models.CharField(_("medium"), max_length=100, choices=NOTICE_MEDIA) send = models.BooleanField(_("send"), default=False) scoping_content_type = models.ForeignKey(ContentType, null=True, blank=True) scoping_object_id = models.PositiveIntegerField(null=True, blank=True) @@ -100,6 +103,98 @@ class Meta: unique_together = ("user", "notice_type", "medium", "scoping_content_type", "scoping_object_id") +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) + 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 qs + + 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( + settings.AUTH_USER_MODEL, related_name="received_notices", + verbose_name=_("recipient")) + sender = models.ForeignKey( + settings.AUTH_USER_MODEL, 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"), db_index=True, default=timezone.now) + unseen = models.BooleanField(_("unseen"), db_index=True, default=True) + archived = models.BooleanField(_("archived"), default=False) + on_site = models.BooleanField(_("on site"), default=False) + + 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: + 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. diff --git a/pinax/notifications/tests/__init__.py b/pinax/notifications/tests/__init__.py index 125565a0..e0a4a1c0 100644 --- a/pinax/notifications/tests/__init__.py +++ b/pinax/notifications/tests/__init__.py @@ -2,5 +2,5 @@ def get_backend_id(backend_name): from ..models import NOTICE_MEDIA for bid, bname in NOTICE_MEDIA: if bname == backend_name: - return bid + return bname return None diff --git a/pinax/notifications/utils.py b/pinax/notifications/utils.py index a974c0f7..d68aa3a1 100644 --- a/pinax/notifications/utils.py +++ b/pinax/notifications/utils.py @@ -1,6 +1,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.contrib.contenttypes.models import ContentType +from django.utils import importlib from .conf import settings @@ -48,3 +49,10 @@ def notice_setting_for_user(user, notice_type, medium, scoping=None): kwargs.update({"send": default}) setting = user.noticesetting_set.create(**kwargs) return setting + + +def get_class_from_path(path): + # This function is helpful to avoid circular imports. + module_name, class_name = path.rsplit(".", 1) + class_ = getattr(importlib.import_module(module_name), class_name) + return class_