Skip to content

Commit

Permalink
Merge branch 'feat/user_preferences' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
jproffitt committed Aug 30, 2017
2 parents 60cd81d + 5a2ec73 commit f0553e4
Show file tree
Hide file tree
Showing 14 changed files with 310 additions and 17 deletions.
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ Django library for separating the message content from transmission method
return [User.objects.order_by('?')[0]]

registry.register(WelcomeEmail) # finally, register your notification class

# Alternatively, a class decorator can be used to register the notification:

@registry.register_decorator()
class WelcomeEmail(EmailNotification):
...


2. Create templates for rendering the email using this file structure:

Expand Down Expand Up @@ -95,23 +95,44 @@ Second, use the `HeraldPasswordResetForm` in place of django's built in `Passwor

# you may simply just need to override the password reset url like so:
url(r'^password_reset/$', password_reset, name='password_reset', {'password_reset_form': HeraldPasswordResetForm}),

# of if you are using something like django-authtools:
url(r'^password_reset/$', PasswordResetView.as_view(form_class=HeraldPasswordResetForm), name='password_reset'),

# or you may have a customized version of the password reset view:
class MyPasswordResetView(FormView):
form_class = HeraldPasswordResetForm # change the form class here

# or, you may have a custom password reset form already. In that case, you will want to extend from the HeraldPasswordResetForm:
class MyPasswordResetForm(HeraldPasswordResetForm):
...

# alternatively, you could even just send the notification wherever you wish, seperate from the form:
PasswordResetEmail(some_user).send()

Third, you may want to customize the templates for the email. By default, herald will use the `registration/password_reset_email.html` that is provided by django for both the html and text versions of the email. But you can simply override `herald/html/password_reset.html` and/or `herald/text/password_reset.txt` to suit your needs.

## User Disabled Notifications

If you want to disable certain notifications per user, add a record to the UserNotification table and
add notifications to the disabled_notifications many to many table.

For example:

user = User.objects.get(id=user.id)

notification = Notification.objects.get(notification_class='MyNotification')

# disable the notification
user.usernotification.disabled_notifications.add(notification)

By default, notifications can be disabled. You can put can_disable = False in your notification class and the system will
populate the database with this default. Your Notification class can also override the verbose_name by setting it in your
inherited Notification class. Like this:

class MyNotification(EmailNotification):
can_disable = False
verbose_name = "My Required Notification"

## Email Attachments

Expand Down
12 changes: 11 additions & 1 deletion herald/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# django <= 1.9
from django.core.urlresolvers import reverse

from .models import SentNotification
from .models import SentNotification, Notification


@admin.register(SentNotification)
Expand Down Expand Up @@ -83,3 +83,13 @@ def resend_view(self, request, object_id, extra_context=None): # pylint: disabl
self.message_user(request, 'The notification failed to resend.', messages.ERROR)

return self.response_post_save_change(request, obj)


@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
"""
Admin for viewing/managing notifications in the system
"""
list_display = ('notification_class', 'verbose_name', 'can_disable')
search_fields = ('notification_class', 'verbose_name')

31 changes: 31 additions & 0 deletions herald/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
Django app config for herald. Using this to call autodiscover
"""

import re

from django.apps import AppConfig
from django.db.utils import OperationalError, ProgrammingError


class HeraldConfig(AppConfig):
Expand All @@ -13,4 +16,32 @@ class HeraldConfig(AppConfig):
name = 'herald'

def ready(self):
from .models import Notification
from herald import registry

self.module.autodiscover()

try:
# add any new notifications to database.
for index, klass in enumerate(registry._registry):
if klass.verbose_name:
verbose_name = klass.verbose_name
else:
verbose_name = re.sub(
r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))',
r' \1',
klass.__name__
)

notification, created = Notification.objects.get_or_create(notification_class=klass.__name__)
if created:
notification.verbose_name = verbose_name
notification.can_disable = klass.can_disable
notification.save()

except OperationalError:
# if the table is not created yet, just keep going.
pass
except ProgrammingError:
# if the database is not created yet, keep going (ie: during testing)
pass
17 changes: 15 additions & 2 deletions herald/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class NotificationBase(object):
render_types = []
template_name = None
context = None
user = None
can_disable = True
verbose_name = None

def get_context_data(self):
"""
Expand All @@ -44,7 +47,7 @@ def get_context_data(self):

return context

def send(self, raise_exception=False):
def send(self, raise_exception=False, user=None):
"""
Handles the preparing the notification for sending. Called to trigger the send from code.
If raise_exception is True, it will raise any exceptions rather than simply logging them.
Expand Down Expand Up @@ -76,7 +79,8 @@ def send(self, raise_exception=False):
subject=subject,
extra_data=json.dumps(extra_data) if extra_data else None,
notification_class='{}.{}'.format(self.__class__.__module__, self.__class__.__name__),
attachments=self._get_encoded_attachments()
attachments=self._get_encoded_attachments(),
user=user,
)

return self.resend(sent_notification, raise_exception=raise_exception)
Expand Down Expand Up @@ -174,6 +178,15 @@ def resend(cls, sent_notification, raise_exception=False):
returns boolean whether or not the notification was sent successfully
"""

# handle skipping a notification based on user preference
if hasattr(sent_notification.user, 'usernotification'):
notifications = sent_notification.user.usernotification
if notifications.disabled_notifications.filter(notification_class=cls.__name__).exists():
sent_notification.date_sent = timezone.now()
sent_notification.status = sent_notification.STATUS_USER_DISABLED
sent_notification.save()
return True

try:
cls._send(
sent_notification.get_recipients(),
Expand Down
41 changes: 41 additions & 0 deletions herald/migrations/0002_auto_20161017_1201.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models
from django.conf import settings


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('herald', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('notification_class', models.CharField(max_length=80)),
('can_disable', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='UserNotification',
fields=[
('user', models.OneToOneField(primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
('disabled_notifications', models.ManyToManyField(to='herald.Notification')),
],
),
migrations.AddField(
model_name='sentnotification',
name='user',
field=models.ForeignKey(default=None, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AlterField(
model_name='sentnotification',
name='status',
field=models.PositiveSmallIntegerField(default=0, choices=[(0, b'Pending'), (1, b'Success'), (2, b'Failed'), (3, b'User Disabled')]),
),
]
25 changes: 25 additions & 0 deletions herald/migrations/0003_auto_20161021_1448.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-10-21 18:48
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('herald', '0002_auto_20161017_1201'),
]

operations = [
migrations.AlterField(
model_name='notification',
name='notification_class',
field=models.CharField(max_length=80, unique=True),
),
migrations.AlterField(
model_name='sentnotification',
name='status',
field=models.PositiveSmallIntegerField(choices=[(0, 'Pending'), (1, 'Success'), (2, 'Failed'), (3, 'User Disabled')], default=0),
),
]
33 changes: 33 additions & 0 deletions herald/migrations/0004_notification_verbose_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.3 on 2016-11-29 17:19
from __future__ import unicode_literals

import re

from django.db import migrations, models


def populate_verbose_name(apps, schema_editor):
Notification = apps.get_model('herald', 'Notification')
for notification in Notification.objects.all():
notification.verbose_name = re.sub(
r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))',
r' \1',
notification.notification_class
)
notification.save()


class Migration(migrations.Migration):
dependencies = [
('herald', '0003_auto_20161021_1448'),
]

operations = [
migrations.AddField(
model_name='notification',
name='verbose_name',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.RunPython(populate_verbose_name),
]
16 changes: 16 additions & 0 deletions herald/migrations/0005_merge_20170407_1316.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-04-07 17:16
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('herald', '0002_sentnotification_attachments'),
('herald', '0004_notification_verbose_name'),
]

operations = [
]
22 changes: 22 additions & 0 deletions herald/migrations/0006_auto_20170825_1813.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2017-08-25 23:13
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('herald', '0005_merge_20170407_1316'),
]

operations = [
migrations.AlterField(
model_name='sentnotification',
name='user',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]
31 changes: 30 additions & 1 deletion herald/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jsonpickle
import six

from django.conf import settings
from django.db import models
from django.utils.module_loading import import_string

Expand All @@ -19,11 +20,13 @@ class SentNotification(models.Model):
STATUS_PENDING = 0
STATUS_SUCCESS = 1
STATUS_FAILED = 2
STATUS_USER_DISABLED = 3

STATUSES = (
(0, 'Pending'),
(1, 'Success'),
(2, 'Failed'),
(3, 'User Disabled')
)

text_content = models.TextField(null=True, blank=True)
Expand All @@ -36,6 +39,7 @@ class SentNotification(models.Model):
status = models.PositiveSmallIntegerField(choices=STATUSES, default=STATUS_PENDING)
notification_class = models.CharField(max_length=255)
error_message = models.CharField(max_length=255, null=True, blank=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=None, null=True, on_delete=models.SET_NULL)
attachments = models.TextField(null=True, blank=True)

def __str__(self):
Expand Down Expand Up @@ -65,9 +69,34 @@ def get_extra_data(self):
return {}
else:
return json.loads(self.extra_data)

def get_attachments(self):
if self.attachments:
return jsonpickle.loads(self.attachments)
else:
return None

@six.python_2_unicode_compatible
class Notification(models.Model):
"""
NotificationClasses are created on app init.
"""
notification_class = models.CharField(max_length=80, unique=True)
verbose_name = models.CharField(max_length=100, blank=True, null=True)
can_disable = models.BooleanField(default=True)

def __str__(self):
return self.verbose_name if self.verbose_name else self.notification_class


class UserNotification(models.Model):
"""
Add a User Notification record, then add disabled notifications to disable records.
On your user Admin, add the field user_notification
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
primary_key=True
)
disabled_notifications = models.ManyToManyField(Notification)
Loading

0 comments on commit f0553e4

Please sign in to comment.