├── .gitignore
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.markdown
├── alert
├── __init__.py
├── admin.py
├── alerts.py
├── backends.py
├── compat.py
├── example_alerts.py
├── exceptions.py
├── forms.py
├── listeners.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── send_alerts.py
├── managers.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── signals.py
├── south_migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── templates
│ └── alerts
│ │ ├── DjangoAdminAlert
│ │ ├── body.html
│ │ └── title.html
│ │ ├── base_email_body.html
│ │ └── email_shards
│ │ └── default
│ │ ├── a.html
│ │ ├── a.txt
│ │ ├── h1.html
│ │ ├── h1.txt
│ │ ├── h2.html
│ │ ├── h2.txt
│ │ ├── p.html
│ │ └── p.txt
├── templatetags
│ ├── __init__.py
│ └── alert_email_tags.py
└── utils.py
├── setup.cfg
├── setup.py
└── test_project
├── __init__.py
├── alert_tests
├── __init__.py
├── models.py
├── tests.py
└── views.py
├── manage.py
├── settings.py
├── templates
├── alerts
│ └── WelcomeAlert
│ │ ├── EmailBackend
│ │ ├── body.txt
│ │ └── title.txt
│ │ ├── body.txt
│ │ └── title.txt
├── basic.email
├── basic.expected.html
└── basic.expected.txt
└── urls.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | *.pyc
4 | /django_alert.egg-info/
5 | /dist/
6 | .settings/
7 | .pydevproject
8 | *.swp
9 |
10 | /.project
11 |
12 | /build/
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | env:
2 | global:
3 | - DJANGO_SETTINGS_MODULE=test_project.settings
4 | matrix:
5 | - DJANGO=1.4
6 | - DJANGO=1.5
7 | - DJANGO=1.6
8 | - DJANGO=1.7
9 | - DJANGO=1.8
10 | install:
11 | - pip install django==${DJANGO}
12 | - pip install .
13 | language: python
14 | python:
15 | - "2.7"
16 | script: python test_project/manage.py test alert_tests
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2011 James Robert
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.markdown
2 | recursive-include alert/templates *
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # These targets are not files
2 | .PHONY: test
3 |
4 | test:
5 | DJANGO_SETTINGS_MODULE=test_project.settings python test_project/manage.py test alert_tests
6 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/jiaaro/django-alert)
2 |
3 | ## Installation ##
4 |
5 | 1. Install lib with pip:
6 |
7 | `pip install django-alert`
8 |
9 | **- OR -**
10 |
11 | Put the "alert" directory somewhere in your python path
12 |
13 | 2. Add "alert" to your installed apps (in the settings.py file)
14 | 3. Also add "django.contrib.sites" in installed apps in case it's not there.
15 | 4. Run ./manage.py migrate
16 |
17 |
18 | ## Making Alerts ##
19 |
20 | Create an "alerts.py" file and import it at the bottom of your
21 | models.py file. This is where you will define your alert class. Every
22 | alert is subclassed from "alert.utils.BaseAlert"
23 |
24 | Here is an example alert that is sent to users when they first sign up:
25 |
26 | from django.contrib.auth.models import User
27 | from django.db.models.signals import post_save
28 | from alert.utils import BaseAlert
29 |
30 | class WelcomeAlert(BaseAlert):
31 | title = 'Welcome new users'
32 | description = 'When a new user signs up, send them a welcome email'
33 |
34 | signal = post_save
35 | sender = User
36 |
37 | default = False
38 |
39 | def before(self, created, **kwargs):
40 | return created
41 |
42 | def get_applicable_users(self, instance, **kwargs):
43 | return [instance]
44 |
45 |
46 | ## Writing Alert Backends ##
47 |
48 | Alert includes an Email Backend by default. But you can write a backend
49 | for *any* messaging medium!
50 |
51 | Alert Backends just need to subclass BaseAlertBackend and implement a
52 | `send()` method that accepts an alert instance
53 |
54 | You can copy and paste the following code to get started:
55 |
56 | from alert.utils import BaseAlertBackend
57 |
58 | class MyAlertBackend(BaseAlertBackend):
59 | def send()
60 |
61 |
62 | ## Signals ##
63 |
64 | When an alert is sent, a signal is fired (found in alert.signals). The
65 | "sender" keyword argument is the Alert you defined (WelcomeAlert in
66 | this case).
67 |
68 | example:
69 |
70 | from alert.signals import alert_sent
71 |
72 | def do_something_after_welcome_alert_is_sent(sender, alert, **kwargs):
73 | pass
74 |
75 | alert_sent.connect(do_something_after_welcome_alert_is_sent,
76 | sender=WelcomeAlert)
77 |
--------------------------------------------------------------------------------
/alert/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jiaaro/django-alert/53f077e66f9fd5562fc4a3b5132e984dabfcebbb/alert/__init__.py
--------------------------------------------------------------------------------
/alert/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.utils import timezone
3 | from alert.models import Alert, AlertPreference, AdminAlert
4 | from alert.signals import admin_alert_saved
5 |
6 |
7 | class AlertAdmin(admin.ModelAdmin):
8 | list_display = ('username', 'title', 'backend', 'alert_type', 'failed', 'is_sent', 'created',)
9 | list_filter = ('alert_type', 'backend', 'is_sent', 'failed')
10 | search_fields = ('=user__username', '=user__email')
11 | actions = ['resend']
12 | raw_id_fields = ("user",)
13 |
14 | def queryset(self, request):
15 | qs = super(AlertAdmin, self).queryset(request)
16 | if hasattr(qs, "prefetch_related"):
17 | qs = qs.prefetch_related("user")
18 | return qs
19 |
20 | def username(self, obj):
21 | return obj.user.username
22 |
23 | def resend(self, request, qs):
24 | for alert in qs:
25 | alert.send()
26 | resend.short_description = "Resend selected alerts"
27 |
28 |
29 |
30 | class AlertPrefAdmin(admin.ModelAdmin):
31 | list_display = ("user", 'alert_type', "backend", 'preference')
32 | list_filter = ('alert_type', 'backend', 'preference')
33 | search_fields = ('=user__username', '=user__email')
34 | actions = ['subscribe', 'unsubscribe']
35 | raw_id_fields = ("user",)
36 |
37 |
38 | def unsubscribe(self, request, qs):
39 | qs.update(preference=False)
40 | unsubscribe.short_description = 'Set selected preferences to "Unsubscribed"'
41 |
42 |
43 | def subscribe(self, request, qs):
44 | qs.update(preference=True)
45 | subscribe.short_description = 'Set selected preferences to "Subscribed"'
46 |
47 |
48 |
49 | class AdminAlertAdmin(admin.ModelAdmin):
50 | list_display = ("title", "status", "send_time",)
51 | search_fields = ("title",)
52 |
53 | fieldsets = (
54 | (None, {
55 | 'fields': ('title', 'body',)
56 | }),
57 | ("Recipients", {
58 | # 'classes': ('collapse',),
59 | 'fields': ('recipients',)
60 | }),
61 | ("Advanced", {
62 | 'classes': ('collapse',),
63 | 'fields': ('send_at', 'draft')
64 | }),
65 | )
66 |
67 |
68 | def get_readonly_fields(self, request, obj=None):
69 | # don't allow editing if it's already sent
70 | if obj and obj.sent:
71 | return ("title", 'body', 'recipients', 'send_at', 'draft')
72 | else:
73 | return ()
74 |
75 |
76 | def save_model(self, request, obj, form, change):
77 | is_draft = obj.draft
78 | if is_draft:
79 | # set the draft property false for next time
80 | obj.draft = False
81 |
82 | # if it's already been sent then that's it
83 | obj.sent = obj.sent or not is_draft
84 |
85 | obj.save()
86 |
87 | # for now, sent to all site users
88 | recipients = obj.recipients.user_set.all()
89 |
90 | if not is_draft:
91 | admin_alert_saved.send(sender=AdminAlert, instance=obj, recipients=recipients)
92 |
93 |
94 | def status(self, obj):
95 | if obj.sent:
96 | return "scheduled" if obj.send_at < timezone.now() else "sent"
97 | else:
98 | return "unsent (saved as draft)"
99 |
100 |
101 | def send_time(self, obj):
102 | return "-" if not obj.sent else obj.send_at
103 |
104 |
105 |
106 |
107 | admin.site.register(Alert, AlertAdmin)
108 | admin.site.register(AlertPreference, AlertPrefAdmin)
109 | admin.site.register(AdminAlert, AdminAlertAdmin)
110 |
--------------------------------------------------------------------------------
/alert/alerts.py:
--------------------------------------------------------------------------------
1 | from alert.utils import BaseAlert
2 | from alert.signals import admin_alert_saved
3 |
4 | class DjangoAdminAlert(BaseAlert):
5 | title = 'Admin Alert (created in Django Admin Interface)'
6 | description = "Send alerts directly to the site's users from the django admin"
7 |
8 | signal = admin_alert_saved
9 | template_filetype = 'html'
10 |
11 | # by default all users will receive this alert
12 | default = True
13 |
14 | def get_applicable_users(self, instance, recipients, **kwargs):
15 | return recipients
16 |
17 | def get_send_time(self, instance, **kwargs):
18 | return instance.send_at
--------------------------------------------------------------------------------
/alert/backends.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from django.core.mail import send_mail
4 | from django.core.mail import EmailMultiAlternatives
5 | from django.conf import settings
6 |
7 | from alert.utils import BaseAlertBackend
8 | from alert.exceptions import CouldNotSendError
9 | from django.template.defaultfilters import striptags
10 |
11 | strip_head = re.compile(r"
", re.DOTALL)
12 | strip_style = re.compile(r"", re.DOTALL)
13 | strip_script = re.compile(r"", re.DOTALL)
14 | gt_2_lines_whitespace = re.compile(r"\s*\n\s*\n\s*", re.DOTALL)
15 | multiple_spaces = re.compile(r"[\f\v\t ]+", re.DOTALL)
16 |
17 | link1 = re.compile(r"()", re.I)
18 | link2 = re.compile(r"()", re.I)
19 | link_replace = lambda m: "%s (%s)" % m.groups()
20 |
21 | def to_text(html_content):
22 | html_content = strip_style.sub("", html_content)
23 | html_content = strip_script.sub("", html_content)
24 | html_content = strip_head.sub("", html_content)
25 |
26 | html_content = link1.sub(link_replace, html_content)
27 | html_content = link2.sub(link_replace, html_content)
28 |
29 | html_content = striptags(html_content)
30 |
31 | # max out at 2 empty lines
32 | html_content = gt_2_lines_whitespace.sub("\n\n", html_content)
33 | html_content = multiple_spaces.sub(" ", html_content)
34 |
35 | return html_content
36 |
37 |
38 | class EmailBackend(BaseAlertBackend):
39 | title = "Email"
40 |
41 | def send(self, alert):
42 | recipient = alert.user.email
43 | if not recipient: raise CouldNotSendError
44 |
45 | subject = alert.title.replace("\n", "").strip()
46 | to = [recipient]
47 | from_email = settings.DEFAULT_FROM_EMAIL
48 |
49 | try:
50 | if alert.alert_type_obj.template_filetype == 'html':
51 | html_content = alert.body
52 | text_content = to_text(html_content)
53 |
54 | msg = EmailMultiAlternatives(subject, text_content, from_email, to)
55 | msg.attach_alternative(html_content, "text/html")
56 | msg.send()
57 |
58 | else:
59 | send_mail(subject, alert.body, from_email, to)
60 | except Exception, e:
61 | print "sending to", recipient, "failed"
62 | print e
63 | print "\n"
64 | raise CouldNotSendError
65 |
66 |
67 |
--------------------------------------------------------------------------------
/alert/compat.py:
--------------------------------------------------------------------------------
1 |
2 | # Compatibility for Django<1.5
3 | try:
4 | from django.contrib.auth import get_user_model
5 | except ImportError:
6 | from django.contrib.auth.models import User
7 | get_user_model = lambda: User
8 |
--------------------------------------------------------------------------------
/alert/example_alerts.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 | from datetime import timedelta
3 | from django.utils import timezone
4 | from django.db.models.signals import post_save
5 | from django.contrib.auth.models import User
6 |
7 | from alert.utils import BaseAlert
8 |
9 | from example_news_app.models import NewsItem
10 |
11 |
12 |
13 | class WelcomeAlert(BaseAlert):
14 | title = 'Welcome Users'
15 | description = 'When a user signs up, send them a nice welcome email.'
16 |
17 | signal = post_save
18 | sender = User
19 |
20 | # by default all users will receive this alert
21 | default = True
22 |
23 | def before(self, created, **kwargs):
24 | # if created is false, no alert will be sent
25 | return created
26 |
27 | def get_applicable_users(self, instance, **kwargs):
28 | return instance
29 |
30 |
31 |
32 | class NewsAlert(BaseAlert):
33 | title = 'News'
34 | description = 'Get notified as soon as news updates go out.'
35 |
36 | signal = post_save
37 | sender = NewsItem
38 |
39 | # by default users will not receive this alert (i.e. it's opt-in)
40 | default = False
41 |
42 | def before(self, created, **kwargs):
43 | return created
44 |
45 | def get_applicable_users(self, **kwargs):
46 | # this alert applies to all users (but this queryset will be
47 | # filtered based on the users' respective alert preferences
48 | return User.objects.all()
49 |
50 |
51 |
52 | # When a user signs up, one way to keep them engaged is to contact them
53 | # periodically over the next month
54 | #
55 | # The easiest way to do this is to schedule all their emails in advance (we'll
56 | # do 3 in this example:
57 | #
58 | # - 3 days after signup
59 | # - 7 days after signup
60 | # - 30 days after signup
61 |
62 | class MarketingDrip1(BaseAlert):
63 | """
64 | first alert in the marketing drip (3 days after signup)
65 |
66 | templates...
67 |
68 | subject line:
69 | "alerts/MarketingDrip1/EmailBackend/title.html"
70 |
71 | email body:
72 | "alerts/MarketingDrip1/EmailBackend/body.html"
73 | """
74 | title = "Trickle"
75 | description = "Send scheduled marketing emails to users once they give their email"
76 |
77 | template_filetype = "html"
78 |
79 | # don't send on any backends except email
80 | default = defaultdict(lambda: False)
81 | default['EmailBackend'] = True
82 |
83 | signal = post_save
84 | sender = User
85 |
86 | def before(self, created, **kwargs):
87 | return created
88 |
89 | def get_applicable_users(self, instance, **kwargs):
90 | return instance
91 |
92 | def get_send_time(self, **kwargs):
93 | return timezone.now + timedelta(days=3)
94 |
95 |
96 |
97 | class MarketingDrip2(MarketingDrip1):
98 | """
99 | second alert in the marketing drip (7 days after signup)
100 |
101 | We subclass MarketingDrip1 to avoid re-writing all that logic
102 |
103 | templates...
104 |
105 | subject line:
106 | "alerts/MarketingDrip2/EmailBackend/title.html"
107 |
108 | email body:
109 | "alerts/MarketingDrip2/EmailBackend/body.html"
110 | """
111 |
112 | def get_send_time(self, **kwargs):
113 | return timezone.now + timedelta(days=7)
114 |
115 |
116 | class MarketingDrip3(MarketingDrip1):
117 | """
118 | third alert in the marketing drip (30 days after signup)
119 |
120 | We could subclass MarketingDrip1 or MarketingDrip2 at this point. I'm
121 | using MarketingDrip1 to keep things consistent.
122 |
123 | templates...
124 |
125 | subject line:
126 | "alerts/MarketingDrip3/EmailBackend/title.html"
127 |
128 | email body:
129 | "alerts/MarketingDrip3/EmailBackend/body.html"
130 | """
131 |
132 | def get_send_time(self, **kwargs):
133 | return timezone.now + timedelta(days=30)
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/alert/exceptions.py:
--------------------------------------------------------------------------------
1 |
2 | class CouldNotSendError(Exception): pass
3 | class AlertIDAlreadyInUse(Exception): pass
4 | class AlertBackendIDAlreadyInUse(Exception): pass
5 | class InvalidApplicableUsers(Exception): pass
--------------------------------------------------------------------------------
/alert/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from alert.models import AlertPreference, Alert
4 | from alert.utils import ALERT_TYPES, ALERT_BACKENDS, BaseAlert, super_accepter
5 |
6 |
7 |
8 | class AlertPreferenceForm(forms.Form):
9 | """
10 | Shows a form with a checkbox for each alert/backend combination for
11 | the user to choose whether or not to receive the alert through that
12 | backend.
13 | """
14 |
15 | def __init__(self, *args, **kwargs):
16 | kwargs = kwargs.copy()
17 | alerts = kwargs.pop('alerts', None)
18 | backends = kwargs.pop('backends', None)
19 |
20 | if 'user' not in kwargs.keys():
21 | raise TypeError("The \"user\" keyword argument is required but no keyword argument \"user\" was passed")
22 |
23 | user = kwargs.pop('user')
24 |
25 | self.user = user
26 | self.alerts = super_accepter(alerts, ALERT_TYPES)
27 | self.backends = super_accepter(backends, ALERT_BACKENDS)
28 | self.prefs = AlertPreference.objects.get_user_prefs(user).items()
29 |
30 | super(AlertPreferenceForm, self).__init__(*args, **kwargs)
31 |
32 | ids = lambda lst: (x.id for x in lst)
33 |
34 | for ((alert_type, backend), pref) in self.prefs:
35 | if (alert_type not in ids(self.alerts) or backend not in ids(self.backends)):
36 | continue
37 |
38 | attr = self._field_id(alert_type, backend)
39 | alert = ALERT_TYPES[alert_type]
40 | self.fields[attr] = forms.BooleanField(
41 | label=alert.title,
42 | help_text=alert.description,
43 | initial=pref,
44 | required=False
45 | )
46 |
47 |
48 | def save(self, *args, **kwargs):
49 | alert_prefs = []
50 | for backend in self.backends:
51 | for alert in self.alerts:
52 | attr = self._field_id(alert.id, backend.id)
53 |
54 | alert_pref, created = AlertPreference.objects.get_or_create(
55 | user=self.user,
56 | alert_type=alert.id,
57 | backend=backend.id
58 | )
59 | alert_pref.preference = self.cleaned_data[attr]
60 | alert_pref.save()
61 |
62 | alert_prefs.append(alert_pref)
63 |
64 | return alert_prefs
65 |
66 |
67 |
68 |
69 | def _field_id(self, alert_type, backend):
70 | return "%s__%s" % (alert_type, backend)
71 |
72 |
73 |
74 | class UnsubscribeForm(AlertPreferenceForm):
75 | """
76 | This form does not show any check boxes, it expects to be placed into
77 | the page with only a submit button and some text explaining that by
78 | clicking the submit button the user will unsubscribe form the Alert
79 | notifications. (the alert that is passed in)
80 | """
81 |
82 | def __init__(self, *args, **kwargs):
83 | super(UnsubscribeForm, self).__init__(*args, **kwargs)
84 |
85 | for backend in self.backends:
86 | for alert in self.alerts:
87 | field_id = self._field_id(alert.id, backend.id)
88 | self.fields[field_id].widget = forms.HiddenInput()
89 | self.fields[field_id].initial = False
90 |
91 |
92 | def save(self, *args, **kwargs):
93 | alert_prefs = super(UnsubscribeForm, self).save(*args, **kwargs)
94 | affected_alerts = Alert.objects.filter(
95 | is_sent=False,
96 | user=self.user,
97 | backend__in=[backend.id for backend in self.backends],
98 | alert_type__in=[alert.id for alert in self.alerts]
99 | )
100 | affected_alerts.delete()
101 | return alert_prefs
102 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/alert/listeners.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_init, post_save
2 | from alert.models import AlertPreference
3 | from alert.signals import preference_updated
4 | from alert.utils import ALERT_TYPE_CHOICES, ALERT_BACKEND_CHOICES, ALERT_TYPES, ALERT_BACKENDS
5 |
6 | def alertpref_post_init(instance, **kwargs):
7 | instance._current_pref = instance.preference
8 |
9 | def alertpref_post_save(instance, **kwargs):
10 | if instance._current_pref != instance.preference:
11 | preference_updated.send(
12 | sender=instance.alert_type_obj,
13 | user=instance.user,
14 | preference=instance.preference,
15 | instance=instance
16 | )
17 |
18 | post_init.connect(alertpref_post_init, sender=AlertPreference)
19 | post_save.connect(alertpref_post_save, sender=AlertPreference)
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/alert/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jiaaro/django-alert/53f077e66f9fd5562fc4a3b5132e984dabfcebbb/alert/management/__init__.py
--------------------------------------------------------------------------------
/alert/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jiaaro/django-alert/53f077e66f9fd5562fc4a3b5132e984dabfcebbb/alert/management/commands/__init__.py
--------------------------------------------------------------------------------
/alert/management/commands/send_alerts.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 | from django.core.management.base import BaseCommand
3 | from django.core.cache import cache
4 | from django.conf import settings
5 | from alert.models import Alert
6 | from django.contrib.sites.models import Site
7 |
8 |
9 | class CacheRequiredError(Exception): pass
10 |
11 |
12 | class Command(BaseCommand):
13 | help = "Send pending alerts"
14 |
15 | _cache_key = 'currently_sending_alerts'
16 |
17 | def handle(self, *args, **kwargs):
18 | cache.set("_dummy_cache_key", True, 60)
19 | if not cache.get("_dummy_cache_key", False):
20 | raise CacheRequiredError
21 |
22 | if cache.get(self._cache_key, False):
23 | return
24 |
25 | one_day = 60*60*24
26 | cache.set(self._cache_key, True, one_day)
27 |
28 | alerts = Alert.pending.filter(site=settings.SITE_ID)
29 | [alert.send() for alert in alerts]
30 |
31 | cache.delete(self._cache_key)
32 |
--------------------------------------------------------------------------------
/alert/managers.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 | from django.db.models import Manager
3 | from django.db.models.query import QuerySet
4 | from django.utils import timezone
5 | from alert.utils import ALERT_TYPES, ALERT_BACKENDS
6 |
7 |
8 | class AlertManager(Manager):
9 | pass
10 |
11 |
12 | class PendingAlertManager(AlertManager):
13 | """
14 | Alerts that are ready to send NOW.
15 |
16 | This is not the same as unsent alerts; alerts scheduled to be sent in the
17 | future will not be affected by this manager.
18 | """
19 | def get_query_set(self, *args, **kwargs):
20 | qs = super(PendingAlertManager, self).get_query_set(*args, **kwargs)
21 | return qs.filter(when__lte=timezone.now(), is_sent=False)
22 |
23 | def get_queryset(self, *args, **kwargs):
24 | qs = super(PendingAlertManager, self).get_queryset(*args, **kwargs)
25 | return qs.filter(when__lte=timezone.now(), is_sent=False)
26 |
27 |
28 | class AlertPrefsManager(Manager):
29 | def get_queryset_compat(self, *args, **kwargs):
30 | getqs = self.get_queryset if hasattr(Manager, "get_queryset") else self.get_query_set
31 | return getqs(*args, **kwargs)
32 |
33 | def get_user_prefs(self, user):
34 | if not user.is_authenticated():
35 | return dict(((notice_type.id, backend.id), False)
36 | for notice_type in ALERT_TYPES.values()
37 | for backend in ALERT_BACKENDS.values()
38 | )
39 |
40 |
41 | alert_prefs = self.get_queryset_compat().filter(user=user)
42 |
43 | prefs = {}
44 | for pref in alert_prefs:
45 | prefs[pref.alert_type, pref.backend] = pref.preference
46 |
47 | for notice_type in ALERT_TYPES.values():
48 | for backend in ALERT_BACKENDS.values():
49 | if (notice_type.id, backend.id) not in prefs:
50 | default_pref = notice_type.get_default(backend.id)
51 | prefs[notice_type.id, backend.id] = default_pref
52 | return prefs
53 |
54 |
55 | def get_recipients_for_notice(self, notice_type, users):
56 | if isinstance(notice_type, basestring):
57 | notice_type = ALERT_TYPES[notice_type]
58 |
59 | if not users: return ()
60 |
61 | # this optimization really shouldn't be necessary, but it makes a huge difference on mysql
62 | if isinstance(users, QuerySet):
63 | user_ids = list(users.values_list("id", flat=True))
64 | else:
65 | user_ids = [u.id for u in users]
66 |
67 | alert_prefs = self.get_queryset_compat().filter(alert_type=notice_type.id).filter(user__in=user_ids)
68 |
69 | prefs = {}
70 | for pref in alert_prefs:
71 | prefs[pref.user_id, pref.backend] = pref.preference
72 |
73 | user_cache = {}
74 | for user in users:
75 | user_cache[user.id] = user
76 | for backend in ALERT_BACKENDS.values():
77 | if (user.id, backend.id) not in prefs:
78 | prefs[user.id, backend.id] = notice_type.get_default(backend.id)
79 |
80 | return ((user_cache[user_id], ALERT_BACKENDS[backend_id]) for ((user_id, backend_id), pref) in prefs.items() if pref)
81 |
82 |
--------------------------------------------------------------------------------
/alert/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import django
5 | from django.db import models, migrations
6 | from django.conf import settings
7 | import django.utils.timezone
8 |
9 | import alert.models
10 |
11 |
12 | def sites_patch_django17(apps, schema_editor):
13 | if django.VERSION[:2] == (1,7):
14 | from django.contrib.sites.management import create_default_site
15 | from django.apps import apps
16 | create_default_site(apps.get_app_configs()[0])
17 |
18 | class Migration(migrations.Migration):
19 |
20 | dependencies = [
21 | ('auth', '0001_initial'),
22 | ('sites', '0001_initial'),
23 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
24 | ]
25 |
26 | operations = [
27 | migrations.RunPython(
28 | sites_patch_django17,
29 | ),
30 | migrations.CreateModel(
31 | name='AdminAlert',
32 | fields=[
33 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
34 | ('title', models.CharField(max_length=250)),
35 | ('body', models.TextField()),
36 | ('send_at', models.DateTimeField(default=django.utils.timezone.now, help_text=b'schedule the sending of this message in the future')),
37 | ('draft', models.BooleanField(default=False, verbose_name=b"Save as draft (don't send/queue now)")),
38 | ('sent', models.BooleanField(default=False)),
39 | ('recipients', models.ForeignKey(to='auth.Group', help_text=b'who should receive this message?', null=True)),
40 | ],
41 | options={
42 | },
43 | bases=(models.Model,),
44 | ),
45 | migrations.CreateModel(
46 | name='Alert',
47 | fields=[
48 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
49 | ('backend', models.CharField(default=b'EmailBackend', max_length=20)),
50 | ('alert_type', models.CharField(max_length=25)),
51 | ('title', models.CharField(default=alert.models.get_alert_default_title, max_length=250)),
52 | ('body', models.TextField()),
53 | ('when', models.DateTimeField(default=django.utils.timezone.now)),
54 | ('created', models.DateTimeField(default=django.utils.timezone.now)),
55 | ('last_attempt', models.DateTimeField(null=True, blank=True)),
56 | ('is_sent', models.BooleanField(default=False)),
57 | ('failed', models.BooleanField(default=False)),
58 | ('site', models.ForeignKey(default=alert.models.get_alert_default_site, to='sites.Site')),
59 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
60 | ],
61 | options={
62 | },
63 | bases=(models.Model,),
64 | ),
65 | migrations.CreateModel(
66 | name='AlertPreference',
67 | fields=[
68 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
69 | ('alert_type', models.CharField(max_length=25)),
70 | ('backend', models.CharField(max_length=25)),
71 | ('preference', models.BooleanField(default=False)),
72 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
73 | ],
74 | options={
75 | },
76 | bases=(models.Model,),
77 | ),
78 | migrations.AlterUniqueTogether(
79 | name='alertpreference',
80 | unique_together=set([('user', 'alert_type', 'backend')]),
81 | ),
82 | ]
83 |
--------------------------------------------------------------------------------
/alert/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jiaaro/django-alert/53f077e66f9fd5562fc4a3b5132e984dabfcebbb/alert/migrations/__init__.py
--------------------------------------------------------------------------------
/alert/models.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.utils import timezone
3 | from django.contrib.auth.models import Group
4 | from django.contrib.auth.models import User as OriginalUser
5 | from django.contrib.sites.models import Site
6 | from django.db import models
7 |
8 | from alert.utils import ALERT_TYPE_CHOICES, ALERT_BACKEND_CHOICES, ALERT_TYPES, ALERT_BACKENDS
9 | from alert.managers import AlertManager, PendingAlertManager, AlertPrefsManager
10 | from alert.exceptions import CouldNotSendError
11 | from alert.signals import alert_sent
12 |
13 |
14 | def get_alert_default_title():
15 | return "%s alert" % Site.objects.get_current().name
16 |
17 | def get_alert_default_site():
18 | return Site.objects.get_current().id
19 |
20 |
21 | class Alert(models.Model):
22 | user = models.ForeignKey(getattr(settings, 'AUTH_USER_MODEL', OriginalUser))
23 | backend = models.CharField(max_length=20, default='EmailBackend', choices=ALERT_BACKEND_CHOICES)
24 | alert_type = models.CharField(max_length=25, choices=ALERT_TYPE_CHOICES)
25 |
26 | title = models.CharField(max_length=250, default=get_alert_default_title)
27 | body = models.TextField()
28 |
29 | when = models.DateTimeField(default=timezone.now)
30 | created = models.DateTimeField(default=timezone.now)
31 | last_attempt = models.DateTimeField(blank=True, null=True)
32 |
33 | is_sent = models.BooleanField(default=False)
34 | failed = models.BooleanField(default=False)
35 |
36 | site = models.ForeignKey(Site, default=get_alert_default_site)
37 |
38 | objects = AlertManager()
39 | pending = PendingAlertManager()
40 |
41 |
42 | def send(self, commit=True):
43 | backend = self.backend_obj
44 | try:
45 | backend.send(self)
46 | self.is_sent = True
47 | self.failed = False
48 | alert_sent.send(sender=self.alert_type_obj, alert=self)
49 |
50 | except CouldNotSendError:
51 | self.failed = True
52 |
53 | self.last_attempt = timezone.now()
54 | if commit:
55 | self.save()
56 |
57 | @property
58 | def alert_type_obj(self):
59 | return ALERT_TYPES[self.alert_type]
60 |
61 | @property
62 | def backend_obj(self):
63 | return ALERT_BACKENDS[self.backend]
64 |
65 |
66 |
67 | class AlertPreference(models.Model):
68 | user = models.ForeignKey(getattr(settings, 'AUTH_USER_MODEL', OriginalUser))
69 | alert_type = models.CharField(max_length=25, choices=ALERT_TYPE_CHOICES)
70 | backend = models.CharField(max_length=25, choices=ALERT_BACKEND_CHOICES)
71 |
72 | preference = models.BooleanField(default=False)
73 |
74 | objects = AlertPrefsManager()
75 |
76 | class Meta:
77 | unique_together = ('user', 'alert_type', 'backend')
78 |
79 | @property
80 | def alert_type_obj(self):
81 | return ALERT_TYPES[self.alert_type]
82 |
83 | @property
84 | def backend_obj(self):
85 | return ALERT_BACKENDS[self.backend]
86 |
87 |
88 |
89 | class AdminAlert(models.Model):
90 | title = models.CharField(max_length=250)
91 | body = models.TextField()
92 |
93 | recipients = models.ForeignKey(Group, null=True, help_text="who should receive this message?")
94 |
95 | send_at = models.DateTimeField(default=timezone.now, help_text="schedule the sending of this message in the future")
96 | draft = models.BooleanField(default=False, verbose_name="Save as draft (don't send/queue now)")
97 | sent = models.BooleanField(default=False)
98 |
99 |
100 |
101 | import alert.listeners #@UnusedImport
102 | import alert.backends #@UnusedImport
103 | import alert.alerts #@UnusedImport
--------------------------------------------------------------------------------
/alert/signals.py:
--------------------------------------------------------------------------------
1 | import django.dispatch
2 |
3 | alert_sent = django.dispatch.Signal(providing_args=['alert'])
4 | preference_updated = django.dispatch.Signal(providing_args=['user', 'preference', 'instance'])
5 | admin_alert_saved = django.dispatch.Signal(providing_args=['instance', 'recipients'])
--------------------------------------------------------------------------------
/alert/south_migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from south.utils import datetime_utils as datetime
3 | from south.db import db
4 | from south.v2 import SchemaMigration
5 | from django.db import models
6 |
7 |
8 | class Migration(SchemaMigration):
9 |
10 | def forwards(self, orm):
11 | # Adding model 'Alert'
12 | db.create_table(u'alert_alert', (
13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
15 | ('backend', self.gf('django.db.models.fields.CharField')(default='EmailBackend', max_length=20)),
16 | ('alert_type', self.gf('django.db.models.fields.CharField')(max_length=25)),
17 | ('title', self.gf('django.db.models.fields.CharField')(default=u'Test alert', max_length=250)),
18 | ('body', self.gf('django.db.models.fields.TextField')()),
19 | ('when', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
20 | ('created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
21 | ('last_attempt', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
22 | ('is_sent', self.gf('django.db.models.fields.BooleanField')(default=False)),
23 | ('failed', self.gf('django.db.models.fields.BooleanField')(default=False)),
24 | ('site', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['sites.Site'])),
25 | ))
26 | db.send_create_signal(u'alert', ['Alert'])
27 |
28 | # Adding model 'AlertPreference'
29 | db.create_table(u'alert_alertpreference', (
30 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
31 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
32 | ('alert_type', self.gf('django.db.models.fields.CharField')(max_length=25)),
33 | ('backend', self.gf('django.db.models.fields.CharField')(max_length=25)),
34 | ('preference', self.gf('django.db.models.fields.BooleanField')(default=False)),
35 | ))
36 | db.send_create_signal(u'alert', ['AlertPreference'])
37 |
38 | # Adding unique constraint on 'AlertPreference', fields ['user', 'alert_type', 'backend']
39 | db.create_unique(u'alert_alertpreference', ['user_id', 'alert_type', 'backend'])
40 |
41 | # Adding model 'AdminAlert'
42 | db.create_table(u'alert_adminalert', (
43 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
44 | ('title', self.gf('django.db.models.fields.CharField')(max_length=250)),
45 | ('body', self.gf('django.db.models.fields.TextField')()),
46 | ('recipients', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'], null=True)),
47 | ('send_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
48 | ('draft', self.gf('django.db.models.fields.BooleanField')(default=False)),
49 | ('sent', self.gf('django.db.models.fields.BooleanField')(default=False)),
50 | ))
51 | db.send_create_signal(u'alert', ['AdminAlert'])
52 |
53 |
54 | def backwards(self, orm):
55 | # Removing unique constraint on 'AlertPreference', fields ['user', 'alert_type', 'backend']
56 | db.delete_unique(u'alert_alertpreference', ['user_id', 'alert_type', 'backend'])
57 |
58 | # Deleting model 'Alert'
59 | db.delete_table(u'alert_alert')
60 |
61 | # Deleting model 'AlertPreference'
62 | db.delete_table(u'alert_alertpreference')
63 |
64 | # Deleting model 'AdminAlert'
65 | db.delete_table(u'alert_adminalert')
66 |
67 |
68 | models = {
69 | u'alert.adminalert': {
70 | 'Meta': {'object_name': 'AdminAlert'},
71 | 'body': ('django.db.models.fields.TextField', [], {}),
72 | 'draft': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
73 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
74 | 'recipients': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']", 'null': 'True'}),
75 | 'send_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
76 | 'sent': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
77 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '250'})
78 | },
79 | u'alert.alert': {
80 | 'Meta': {'object_name': 'Alert'},
81 | 'alert_type': ('django.db.models.fields.CharField', [], {'max_length': '25'}),
82 | 'backend': ('django.db.models.fields.CharField', [], {'default': "'EmailBackend'", 'max_length': '20'}),
83 | 'body': ('django.db.models.fields.TextField', [], {}),
84 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
85 | 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
86 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
87 | 'is_sent': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
88 | 'last_attempt': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
89 | 'site': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': u"orm['sites.Site']"}),
90 | 'title': ('django.db.models.fields.CharField', [], {'default': "u'Test alert'", 'max_length': '250'}),
91 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
92 | 'when': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'})
93 | },
94 | u'alert.alertpreference': {
95 | 'Meta': {'unique_together': "(('user', 'alert_type', 'backend'),)", 'object_name': 'AlertPreference'},
96 | 'alert_type': ('django.db.models.fields.CharField', [], {'max_length': '25'}),
97 | 'backend': ('django.db.models.fields.CharField', [], {'max_length': '25'}),
98 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
99 | 'preference': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
100 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
101 | },
102 | u'auth.group': {
103 | 'Meta': {'object_name': 'Group'},
104 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
105 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
106 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
107 | },
108 | u'auth.permission': {
109 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
110 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
111 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
112 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
113 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
114 | },
115 | u'auth.user': {
116 | 'Meta': {'object_name': 'User'},
117 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
118 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
119 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
120 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
121 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
122 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
123 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
124 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
125 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
126 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
127 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
128 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
129 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
130 | },
131 | u'contenttypes.contenttype': {
132 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
133 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
134 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
135 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
136 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
137 | },
138 | u'sites.site': {
139 | 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"},
140 | 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
141 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
142 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
143 | }
144 | }
145 |
146 | complete_apps = ['alert']
--------------------------------------------------------------------------------
/alert/south_migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jiaaro/django-alert/53f077e66f9fd5562fc4a3b5132e984dabfcebbb/alert/south_migrations/__init__.py
--------------------------------------------------------------------------------
/alert/templates/alerts/DjangoAdminAlert/body.html:
--------------------------------------------------------------------------------
1 | {% extends "alerts/base_email_body.html" %}
2 |
3 | {% block extra_css %}
4 | #backgroundTable {
5 | background: #f0f0f0;
6 | }
7 | #main-content {
8 | background: #ffffff;
9 | border-style: solid;
10 | border-width: 1px;
11 | border-color: #cccccc;
12 | width: 520px;
13 | margin: 40px;
14 | padding: 30px;
15 | }
16 | {% endblock %}
17 |
18 | {% block content %}
19 |
20 | {{ instance.body|safe }}
21 | |
22 | {% endblock %}
--------------------------------------------------------------------------------
/alert/templates/alerts/DjangoAdminAlert/title.html:
--------------------------------------------------------------------------------
1 | {{ instance.title }}
--------------------------------------------------------------------------------
/alert/templates/alerts/base_email_body.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 | {% block title %}{{ SITE.name }}{% endblock %}
8 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {% block content %}{% endblock %}
61 |
62 | |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/alert/templates/alerts/email_shards/default/a.html:
--------------------------------------------------------------------------------
1 | {{ content }}
--------------------------------------------------------------------------------
/alert/templates/alerts/email_shards/default/a.txt:
--------------------------------------------------------------------------------
1 | {{ content }}{% if href != content %} ({{ href }}){% endif %}
--------------------------------------------------------------------------------
/alert/templates/alerts/email_shards/default/h1.html:
--------------------------------------------------------------------------------
1 | {{ content }}
--------------------------------------------------------------------------------
/alert/templates/alerts/email_shards/default/h1.txt:
--------------------------------------------------------------------------------
1 | {{ content }}
2 | {% for char in content %}={% endfor %}
--------------------------------------------------------------------------------
/alert/templates/alerts/email_shards/default/h2.html:
--------------------------------------------------------------------------------
1 | {{ content }}
--------------------------------------------------------------------------------
/alert/templates/alerts/email_shards/default/h2.txt:
--------------------------------------------------------------------------------
1 | {{ content }}
2 | {% for char in content %}-{% endfor %}
--------------------------------------------------------------------------------
/alert/templates/alerts/email_shards/default/p.html:
--------------------------------------------------------------------------------
1 | {{ content }}
--------------------------------------------------------------------------------
/alert/templates/alerts/email_shards/default/p.txt:
--------------------------------------------------------------------------------
1 | {{ content }}
--------------------------------------------------------------------------------
/alert/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jiaaro/django-alert/53f077e66f9fd5562fc4a3b5132e984dabfcebbb/alert/templatetags/__init__.py
--------------------------------------------------------------------------------
/alert/templatetags/alert_email_tags.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 | from django import template
3 | from django.template.base import TagHelperNode, token_kwargs
4 | from django.template.defaultfilters import safe
5 |
6 | register = template.Library()
7 |
8 |
9 | class EmailContentTransform(TagHelperNode):
10 | def __init__(self, tag_name, parser, token):
11 | self.tag_name = tag_name
12 |
13 | bits = token.split_contents()[1:]
14 | self.kwargs = {k: v.var for k,v in token_kwargs(bits, parser).items()}
15 |
16 | nodelist = parser.parse(('end{0}'.format(tag_name),))
17 | parser.delete_first_token()
18 | self.nodelist = nodelist
19 |
20 | def render(self, context):
21 | email_template_namespace = context.get("alert_shardtype", "default")
22 | shard_ext = context.get("alert_shard_ext", "txt")
23 |
24 | template_file = "alerts/email_shards/{0}/{1}.{2}".format(
25 | email_template_namespace,
26 | self.tag_name,
27 | shard_ext
28 | )
29 |
30 | t = context.template.engine.get_template(template_file)
31 |
32 | content = self.nodelist.render(context)
33 |
34 | with context.push(content=content, **self.kwargs):
35 | rendered = t.render(context)
36 |
37 | if shard_ext == "html":
38 | rendered = safe(rendered.replace("\n", "
"))
39 |
40 | return rendered
41 |
42 |
43 | email_tags = [
44 | "a", "p", "h1", "h2"
45 | ]
46 | for tag_name in email_tags:
47 | fn = partial(EmailContentTransform, tag_name)
48 | register.tag(name=tag_name)(fn)
49 |
50 |
51 | class EmailShardTypeNode(template.Node):
52 | def __init__(self, shard_type, nodelist):
53 | self.shard_type = shard_type
54 | self.nodelist = nodelist
55 |
56 | def render(self, context):
57 | with context.push(alert_shardtype=self.shard_type):
58 | return self.nodelist.render(context)
59 |
60 |
61 | @register.tag
62 | def shardtype(parser, token):
63 | try:
64 | # split_contents() knows not to split quoted strings.
65 | tag_name, shard_type = token.split_contents()
66 | except ValueError:
67 | raise template.TemplateSyntaxError(
68 | "%r tag requires a single argument" % token.contents.split()[0]
69 | )
70 | if not (shard_type[0] == shard_type[-1] and shard_type[0] in ('"', "'")):
71 | raise template.TemplateSyntaxError(
72 | "%r tag's argument should be in quotes" % tag_name
73 | )
74 |
75 | shard_type = shard_type[1:-1]
76 | nodelist = parser.parse(('end{0}'.format(tag_name),))
77 | parser.delete_first_token()
78 |
79 | return EmailShardTypeNode(shard_type, nodelist)
--------------------------------------------------------------------------------
/alert/utils.py:
--------------------------------------------------------------------------------
1 | from alert.exceptions import AlertIDAlreadyInUse, AlertBackendIDAlreadyInUse,\
2 | InvalidApplicableUsers
3 | import django
4 | from django.conf import settings
5 | from django.utils import timezone
6 | from django.template.loader import render_to_string, get_template
7 | from django.contrib.sites.models import Site
8 | from django.template import TemplateDoesNotExist
9 | from django.db import models
10 | from itertools import islice
11 |
12 | from alert.compat import get_user_model
13 |
14 | ALERT_TYPES = {}
15 | ALERT_BACKENDS = {}
16 |
17 | ALERT_TYPE_CHOICES = []
18 | ALERT_BACKEND_CHOICES = []
19 |
20 | def grouper(n, iterable):
21 | iterable = iter(iterable)
22 | while True:
23 | chunk = tuple(islice(iterable, n))
24 | if not chunk: return
25 | yield chunk
26 |
27 | def render_email_to_string(tmpl, cx, alert_type="txt"):
28 | cx['alert_shard_ext'] = alert_type
29 | rendered = render_to_string(tmpl, cx)
30 | return rendered.strip()
31 |
32 | class AlertMeta(type):
33 |
34 | def __new__(cls, name, bases, attrs):
35 | new_alert = super(AlertMeta, cls).__new__(cls, name, bases, attrs)
36 |
37 | # If this isn't a subclass of BaseAlert, don't do anything special.
38 | parents = [b for b in bases if isinstance(b, AlertMeta)]
39 | if not parents:
40 | return new_alert
41 |
42 | # allow subclasses to use the auto id feature
43 | id = getattr(new_alert, 'id', name)
44 | for parent in parents:
45 | if getattr(parent, 'id', None) == id:
46 | id = name
47 | break
48 |
49 | new_alert.id = id
50 |
51 | if new_alert.id in ALERT_TYPES.keys():
52 | raise AlertIDAlreadyInUse("The alert ID, \"%s\" was delared more than once" % new_alert.id)
53 |
54 | ALERT_TYPES[new_alert.id] = new_alert()
55 | ALERT_TYPE_CHOICES.append((new_alert.id, new_alert.title))
56 |
57 | return new_alert
58 |
59 |
60 |
61 | class BaseAlert(object):
62 | __metaclass__ = AlertMeta
63 |
64 | default = False
65 | sender = None
66 | template_filetype = "txt"
67 |
68 |
69 |
70 | def __init__(self):
71 | kwargs = {}
72 | if self.sender:
73 | kwargs['sender'] = self.sender
74 |
75 | self.signal.connect(self.signal_handler, **kwargs)
76 |
77 | def __repr__(self):
78 | return "" % self.id
79 |
80 | def __str__(self):
81 | return str(self.id)
82 |
83 | def signal_handler(self, **kwargs):
84 |
85 | if self.before(**kwargs) is False:
86 | return
87 |
88 | from alert.models import AlertPreference
89 | from alert.models import Alert
90 |
91 | users = self.get_applicable_users(**kwargs)
92 | if isinstance(users, models.Model):
93 | users = [users]
94 |
95 | try:
96 | user_count = users.count()
97 | except:
98 | user_count = len(users)
99 |
100 | User = get_user_model()
101 | if user_count and not isinstance(users[0], User):
102 | raise InvalidApplicableUsers("%s.get_applicable_users() returned an invalid value. Acceptable values are a django.contrib.auth.models.User instance OR an iterable containing 0 or more User instances" % (self.id))
103 |
104 | site = Site.objects.get_current()
105 |
106 | def mk_alert(user, backend):
107 | context = self.get_template_context(BACKEND=backend, USER=user, SITE=site, ALERT=self, **kwargs)
108 | template_kwargs = {'backend': backend, 'context': context }
109 | return Alert(
110 | user=user,
111 | backend=backend.id,
112 | alert_type=self.id,
113 | when=self.get_send_time(**kwargs),
114 | title=self.get_title(**template_kwargs),
115 | body=self.get_body(**template_kwargs)
116 | )
117 | alerts = (mk_alert(user, backend) for (user, backend) in AlertPreference.objects.get_recipients_for_notice(self.id, users))
118 |
119 | # bulk create is much faster so use it when available
120 | if django.VERSION >= (1, 4) and getattr(settings, 'ALERT_USE_BULK_CREATE', True):
121 | created = 0
122 | for alerts_group in grouper(100, alerts):
123 | # break bulk create into groups of 100 to avoid the dreaded
124 | # OperationalError: (2006, 'MySQL server has gone away')
125 | Alert.objects.bulk_create(alerts_group)
126 | created += 100
127 | else:
128 | for alert in alerts: alert.save()
129 |
130 |
131 | def before(self, **kwargs):
132 | pass
133 |
134 |
135 | def get_send_time(self, **kwargs):
136 | return timezone.now()
137 |
138 |
139 | def get_applicable_users(self, instance, **kwargs):
140 | return [instance.user]
141 |
142 |
143 | def get_template_context(self, **kwargs):
144 | return kwargs
145 |
146 |
147 | def _get_template(self, backend, part, filetype='txt'):
148 | template = "alerts/%s/%s/%s.%s" % (self.id, backend.id, part, filetype)
149 | try:
150 | get_template(template)
151 | return template
152 | except TemplateDoesNotExist:
153 | pass
154 |
155 | template = "alerts/%s/%s.%s" % (self.id, part, filetype)
156 | get_template(template)
157 |
158 | return template
159 |
160 |
161 | def get_title_template(self, backend, context):
162 | return self._get_template(backend, 'title', self.template_filetype)
163 |
164 |
165 | def get_body_template(self, backend, context):
166 | return self._get_template(backend, 'body', self.template_filetype)
167 |
168 |
169 | def get_title(self, backend, context):
170 | template = self.get_title_template(backend, context)
171 | return render_to_string(template, context)
172 |
173 |
174 | def get_body(self, backend, context):
175 | template = self.get_body_template(backend, context)
176 | return render_to_string(template, context)
177 |
178 |
179 | def get_default(self, backend):
180 | if isinstance(self.default, bool):
181 | return self.default
182 | return self.default[backend]
183 |
184 |
185 |
186 | class AlertBackendMeta(type):
187 |
188 | def __new__(cls, name, bases, attrs):
189 | new_alert_backend = super(AlertBackendMeta, cls).__new__(cls, name, bases, attrs)
190 |
191 | # If this isn't a subclass of BaseAlert, don't do anything special.
192 | parents = [b for b in bases if isinstance(b, AlertBackendMeta)]
193 | if not parents:
194 | return new_alert_backend
195 |
196 | new_alert_backend.id = getattr(new_alert_backend, 'id', name)
197 |
198 | if new_alert_backend.id in ALERT_BACKENDS.keys():
199 | raise AlertBackendIDAlreadyInUse("The alert ID, \"%s\" was delared more than once" % new_alert_backend.id)
200 |
201 | ALERT_BACKENDS[new_alert_backend.id] = new_alert_backend()
202 | ALERT_BACKEND_CHOICES.append((new_alert_backend.id, new_alert_backend.title))
203 |
204 | return new_alert_backend
205 |
206 |
207 |
208 | class BaseAlertBackend(object):
209 | __metaclass__ = AlertBackendMeta
210 |
211 | def __repr__(self):
212 | return "" % self.id
213 |
214 | def __str__(self):
215 | return str(self.id)
216 |
217 | def mass_send(self, alerts):
218 | from .models import Alert
219 | if isinstance(alerts, Alert):
220 | self.send(alerts)
221 | else:
222 | [self.send(alert) for alert in alerts]
223 |
224 |
225 | def super_accepter(arg, lookup_dict):
226 | """
227 | for the alerts and backends keyword arguments...
228 | - provides resonable defaults
229 | - accept a single alert/backend or a list of them
230 | - accept alert/backend class or the a string containing the alert/backend id
231 | """
232 | # reasonable default
233 | if arg is None: return lookup_dict.values()
234 |
235 | # single item or a list
236 | if not isinstance(arg, (tuple, list)):
237 | arg = [arg]
238 |
239 | # normalize the arguments
240 | ids = ((a if isinstance(a, basestring) else a.id) for a in arg)
241 |
242 | # remove duplicates
243 | _set = {}
244 | ids = (_set.setdefault(id,id) for id in ids if id not in _set)
245 |
246 | # lookup the objects
247 | return [lookup_dict[id] for id in ids]
248 |
249 |
250 | def unsubscribe_user(user, alerts=None, backends=None):
251 | from .forms import UnsubscribeForm
252 | form = UnsubscribeForm(user=user, alerts=alerts, backends=backends)
253 |
254 | data = dict((field, False) for field in form.fields.keys())
255 |
256 | form = UnsubscribeForm(data, user=user, alerts=alerts, backends=backends)
257 | assert(form.is_valid())
258 | form.save()
259 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | universal = 1
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='django-alert',
5 | version='0.8.1',
6 |
7 | author='James Robert',
8 | author_email='jiaaro@gmail.com',
9 |
10 | description=('Send alerts, notifications, and messages based '
11 | 'on events in your django application'),
12 | long_description=open('README.markdown').read(),
13 |
14 | license='MIT',
15 | keywords='django alerts notifications social',
16 |
17 | url='https://djangoalert.com',
18 |
19 | install_requires=[
20 | "django >= 1.4",
21 | ],
22 |
23 | packages=[
24 | 'alert',
25 | 'alert.management',
26 | 'alert.management.commands',
27 | 'alert.south_migrations',
28 | 'alert.migrations',
29 | ],
30 |
31 | include_package_data=True,
32 |
33 | classifiers=[
34 | 'Development Status :: 5 - Production/Stable',
35 | 'License :: OSI Approved :: MIT License',
36 | 'Programming Language :: Python',
37 | 'Framework :: Django',
38 | 'Environment :: Web Environment',
39 | 'Intended Audience :: Developers',
40 | 'Operating System :: OS Independent',
41 | 'Topic :: Internet :: WWW/HTTP',
42 | 'Topic :: Utilities'
43 | ]
44 | )
45 |
--------------------------------------------------------------------------------
/test_project/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jiaaro/django-alert/53f077e66f9fd5562fc4a3b5132e984dabfcebbb/test_project/__init__.py
--------------------------------------------------------------------------------
/test_project/alert_tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jiaaro/django-alert/53f077e66f9fd5562fc4a3b5132e984dabfcebbb/test_project/alert_tests/__init__.py
--------------------------------------------------------------------------------
/test_project/alert_tests/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/test_project/alert_tests/tests.py:
--------------------------------------------------------------------------------
1 | import django
2 | import time
3 | from uuid import uuid1
4 | from datetime import timedelta
5 |
6 | from threading import Thread
7 | from django.template import Template
8 |
9 | from django.test import TestCase, TransactionTestCase
10 | from django.contrib.auth.models import User, Group
11 | from django.utils import timezone
12 | from django.core import management, mail
13 | from django.core.mail import send_mail
14 | from django.conf import settings
15 | from django.db.models.signals import post_save
16 |
17 | from alert.utils import BaseAlert, ALERT_TYPES, BaseAlertBackend, ALERT_BACKENDS,\
18 | super_accepter, unsubscribe_user
19 | from alert.exceptions import AlertIDAlreadyInUse, AlertBackendIDAlreadyInUse, CouldNotSendError
20 | from alert.models import Alert, AlertPreference, AdminAlert
21 | from alert.forms import AlertPreferenceForm, UnsubscribeForm
22 | from alert.admin import AdminAlertAdmin
23 |
24 |
25 | class SubclassTestingAlert(BaseAlert):
26 | """
27 | This will never send any alerts - it's just a check to make sure that
28 | subclassing alerts doesn't explode
29 | """
30 | title = 'Welcome new users'
31 | description = 'When a new user signs up, send them a welcome email'
32 |
33 | signal = post_save
34 | sender = User
35 |
36 | default = True
37 |
38 | def before(self, **kwargs):
39 | return False
40 |
41 | def get_applicable_users(self, instance, **kwargs):
42 | return [instance]
43 |
44 |
45 | class WelcomeAlert(SubclassTestingAlert):
46 | """
47 | everything is inherited from SubclassTestingAlert
48 |
49 | only change is that alerts will actually be sent
50 | """
51 |
52 | def before(self, created, **kwargs):
53 | return created
54 |
55 |
56 | class DummyBackend(BaseAlertBackend):
57 | title = "Dummy"
58 |
59 | def send(self, alert):
60 | pass
61 |
62 |
63 |
64 | class EpicFailBackend(BaseAlertBackend):
65 | """
66 | Backend that fails to send on the first try for every alert
67 | """
68 | id = "EpicFail"
69 | title = "Epic Fail"
70 |
71 | def send(self, alert):
72 | if not alert.failed:
73 | raise CouldNotSendError
74 |
75 |
76 | class SlowBackend(BaseAlertBackend):
77 | """
78 | Backend that takes a full second to send an alert
79 | """
80 | title = "Slow backend"
81 |
82 | def send(self, alert):
83 | time.sleep(1)
84 | send_mail("asdf", 'woot', 'fake@gmail.com', ['superfake@gmail.com'])
85 |
86 |
87 |
88 |
89 | #################################################
90 | ### Tests ###
91 | #################################################
92 |
93 | class AlertTests(TestCase):
94 |
95 | def setUp(self):
96 | pass
97 |
98 |
99 | def test_alert_creation(self):
100 | username = str(uuid1().hex)[:16]
101 | email = "%s@example.com" % username
102 |
103 | user = User.objects.create(username=username, email=email)
104 |
105 | alerts = Alert.objects.filter(user=user)
106 | self.assertEqual(len(alerts), len(ALERT_BACKENDS))
107 | for alert in alerts:
108 | self.assertEqual(alert.alert_type, "WelcomeAlert")
109 | if alert.backend == 'EmailBackend':
110 | self.assertEqual(alert.title, "email subject")
111 | self.assertEqual(alert.body, "email body")
112 | else:
113 | self.assertEqual(alert.title, "default title")
114 | self.assertEqual(alert.body, "default body")
115 |
116 |
117 | def test_alert_registration_only_happens_once(self):
118 | self.assertTrue(isinstance(ALERT_TYPES["WelcomeAlert"], WelcomeAlert))
119 | self.assertEquals(len(ALERT_TYPES), 3)
120 |
121 | def define_again():
122 | class WelcomeAlert(BaseAlert):
123 | title = 'Welcome new users'
124 | signal = post_save
125 |
126 | self.assertRaises(AlertIDAlreadyInUse, define_again)
127 |
128 | def test_alert_id_is_key_in_ALERT_TYPES(self):
129 | for key, alert in ALERT_TYPES.items():
130 | self.assertEqual(key, alert.id)
131 |
132 |
133 |
134 | class AlertBackendTests(TestCase):
135 |
136 | def setUp(self):
137 | username = str(uuid1().hex)[:16]
138 | email = "%s@example.com" % username
139 |
140 | self.user = User.objects.create(username=username, email=email)
141 |
142 |
143 | def test_backend_creation(self):
144 | self.assertTrue(isinstance(ALERT_BACKENDS["DummyBackend"], DummyBackend))
145 |
146 |
147 | def test_backends_use_supplied_id(self):
148 | self.assertTrue(isinstance(ALERT_BACKENDS["EpicFail"], EpicFailBackend))
149 |
150 | def test_pending_manager(self):
151 | self.assertEqual(Alert.pending.all().count(), len(ALERT_BACKENDS))
152 | management.call_command("send_alerts")
153 | self.assertEqual(Alert.pending.all().count(), 1)
154 |
155 |
156 | def test_backend_registration_only_happens_once(self):
157 | self.assertEquals(len(ALERT_BACKENDS), 4)
158 |
159 | def define_again():
160 | class DummyBackend(BaseAlertBackend):
161 | title = 'dummy'
162 |
163 | self.assertRaises(AlertBackendIDAlreadyInUse, define_again)
164 |
165 |
166 | def test_backend_fails_to_send(self):
167 | alert_that_should_fail = Alert.objects.filter(backend='EpicFail')[0]
168 |
169 | before_send = timezone.now()
170 | alert_that_should_fail.send()
171 | after_send = timezone.now()
172 |
173 | self.assertTrue(alert_that_should_fail.failed)
174 | self.assertFalse(alert_that_should_fail.is_sent)
175 | self.assertTrue(alert_that_should_fail.last_attempt is not None)
176 |
177 | self.assertTrue(alert_that_should_fail.last_attempt > before_send)
178 | self.assertTrue(alert_that_should_fail.last_attempt < after_send)
179 |
180 | # and now retry
181 | before_send = timezone.now()
182 | alert_that_should_fail.send()
183 | after_send = timezone.now()
184 |
185 | self.assertFalse(alert_that_should_fail.failed)
186 | self.assertTrue(alert_that_should_fail.is_sent)
187 | self.assertTrue(alert_that_should_fail.last_attempt is not None)
188 |
189 | self.assertTrue(alert_that_should_fail.last_attempt > before_send)
190 | self.assertTrue(alert_that_should_fail.last_attempt < after_send)
191 |
192 |
193 |
194 | class ConcurrencyTests(TransactionTestCase):
195 |
196 | def setUp(self):
197 | username = str(uuid1().hex)[:16]
198 | email = "%s@example.com" % username
199 |
200 | self.user = User.objects.create(username=username, email=email)
201 |
202 |
203 | def testMultipleSimultaneousSendScripts(self):
204 | # Sqlite uses an in-memory database, which does not work with the concurrency tests.
205 | if "sqlite" in settings.DATABASES['default']['ENGINE']:
206 | # Note that the alert django app will work fine with Sqlite. It's only the
207 | # concurrency *tests* that do not work with sqlite.""")
208 | return
209 |
210 | self.assertEqual(len(mail.outbox), 0)
211 |
212 | threads = [Thread(target=management.call_command, args=('send_alerts',)) for i in range(100)]
213 |
214 | for t in threads:
215 | t.start()
216 |
217 | # space them out a little tiny bit
218 | time.sleep(0.001)
219 |
220 | [t.join() for t in threads]
221 |
222 | self.assertEqual(len(mail.outbox), 2)
223 |
224 |
225 |
226 | class EmailBackendTests(TestCase):
227 |
228 | def setUp(self):
229 | pass
230 |
231 |
232 |
233 | class FormTests(TestCase):
234 |
235 | def setUp(self):
236 | self.user = User.objects.create(username='wootz', email='wootz@woot.com')
237 |
238 |
239 | def testNoArgs(self):
240 | pref_form = self.assertRaises(TypeError, AlertPreferenceForm)
241 | unsubscribe_form = self.assertRaises(TypeError, UnsubscribeForm)
242 |
243 |
244 | def testSimpleCase(self):
245 | pref_form = AlertPreferenceForm(user=self.user)
246 | unsubscribe_form = UnsubscribeForm(user=self.user)
247 |
248 | self.assertEqual(len(pref_form.fields), len(ALERT_TYPES) * len(ALERT_BACKENDS))
249 | self.assertEqual(len(unsubscribe_form.fields), len(ALERT_TYPES) * len(ALERT_BACKENDS))
250 |
251 |
252 | def testUnsubscribeFormHasNoVisibleFields(self):
253 | from django.forms import HiddenInput
254 | unsubscribe_form = UnsubscribeForm(user=self.user)
255 |
256 | for field in unsubscribe_form.fields.values():
257 | self.assertTrue(isinstance(field.widget, HiddenInput))
258 |
259 |
260 | def testSuperAccepterNone(self):
261 | types = super_accepter(None, ALERT_TYPES)
262 | backends = super_accepter(None, ALERT_BACKENDS)
263 |
264 | self.assertEqual(len(types), len(ALERT_TYPES))
265 | self.assertEqual(len(backends), len(ALERT_BACKENDS))
266 |
267 |
268 | def testSuperAccepterSingle(self):
269 | backends_by_class = super_accepter(EpicFailBackend, ALERT_BACKENDS)
270 | backends_by_id = super_accepter("EpicFail", ALERT_BACKENDS)
271 |
272 | self.assertEqual(len(backends_by_class), 1)
273 | self.assertEqual(len(backends_by_id), 1)
274 | self.assertEqual(backends_by_class, backends_by_id)
275 |
276 |
277 | def testSuperAccepterList(self):
278 | backends_by_class = super_accepter([EpicFailBackend, DummyBackend], ALERT_BACKENDS)
279 | backends_by_id = super_accepter(["EpicFail", "DummyBackend"], ALERT_BACKENDS)
280 | backends_by_mixed = super_accepter(["EpicFail", DummyBackend], ALERT_BACKENDS)
281 |
282 | self.assertEqual(len(backends_by_class), 2)
283 | self.assertEqual(len(backends_by_id), 2)
284 | self.assertEqual(len(backends_by_mixed), 2)
285 |
286 | self.assertEqual(backends_by_class, backends_by_id)
287 | self.assertEqual(backends_by_class, backends_by_mixed)
288 | self.assertEqual(backends_by_mixed, backends_by_id)
289 |
290 |
291 | def testSuperAccepterDuplicates(self):
292 | backends = super_accepter([EpicFailBackend, DummyBackend, "EpicFail"], ALERT_BACKENDS)
293 | self.assertEqual(len(backends), 2)
294 |
295 |
296 | def testUnsubscribe(self):
297 | details = {
298 | "alert_type": WelcomeAlert.id,
299 | "backend": EpicFailBackend.id,
300 | "user": self.user,
301 | }
302 | AlertPreference.objects.create(preference=True, **details)
303 | self.assertEqual(AlertPreference.objects.get(**details).preference, True)
304 |
305 | unsubscribe_user(self.user, alerts=WelcomeAlert, backends=EpicFailBackend)
306 | self.assertEqual(AlertPreference.objects.get(**details).preference, False)
307 |
308 |
309 | class AdminAlertTests(TestCase):
310 |
311 | def setUp(self):
312 | group = Group.objects.create(name='test_group')
313 | self.admin_alert = AdminAlert(
314 | title="Hello users!",
315 | body="woooord!",
316 | recipients=group
317 | )
318 |
319 | def send_it(self):
320 | AdminAlertAdmin.save_model(AdminAlertAdmin(AdminAlert, None), None, self.admin_alert, None, None)
321 |
322 |
323 | def testDraftMode(self):
324 | self.admin_alert.draft = True
325 | self.send_it()
326 |
327 | self.assertEqual(Alert.objects.count(), 0)
328 |
329 | self.send_it()
330 | self.assertEqual(Alert.objects.count(), User.objects.count())
331 |
332 |
333 | def testScheduling(self):
334 | send_at = timezone.now() + timedelta(days=1)
335 |
336 | self.admin_alert.send_at = send_at
337 | self.send_it()
338 |
339 | for alert in Alert.objects.all():
340 | self.assertEqual(alert.when, send_at)
341 |
342 |
343 | def testOnlySendOnce(self):
344 | self.assertFalse(self.admin_alert.sent)
345 | self.send_it()
346 | self.assertTrue(self.admin_alert.sent)
347 |
348 | alert_count = Alert.objects.count()
349 |
350 | self.send_it()
351 |
352 | self.assertEqual(alert_count, Alert.objects.count())
353 |
354 |
355 | # Email Templates aren't supported before django 1.8
356 | if django.VERSION[:2] >= (1, 8):
357 | from django.template import engines
358 | from alert.utils import render_email_to_string
359 |
360 | def get_template_contents(tmpl):
361 | fs_loader = engines['django'].engine.template_loaders[0]
362 | source, origin = fs_loader.load_template_source(tmpl)
363 | return source
364 |
365 | class EmailTemplateTests(TestCase):
366 | def check_template(self, name, cx):
367 | template_file = "{0}.email".format(name)
368 | expected_txt = get_template_contents("{0}.expected.txt".format(name))
369 | expected_html = get_template_contents("{0}.expected.html".format(name))
370 |
371 | rendered_default = render_email_to_string(template_file, cx)
372 | rendered_txt = render_email_to_string(template_file, cx, alert_type="txt")
373 | rendered_html = render_email_to_string(template_file, cx, alert_type="html")
374 |
375 | # Default shard ext is "txt"
376 | self.assertEqual(rendered_default, rendered_txt)
377 |
378 | self.assertEqual(rendered_txt, expected_txt)
379 | self.assertEqual(rendered_html, expected_html)
380 |
381 |
382 |
383 |
384 | def test_basic_use(self):
385 | self.check_template("basic", {
386 | "username": "Alex"
387 | })
--------------------------------------------------------------------------------
/test_project/alert_tests/views.py:
--------------------------------------------------------------------------------
1 | # Create your views here.
2 |
--------------------------------------------------------------------------------
/test_project/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | try:
6 | import settings # Assumed to be in the same directory.
7 | except ImportError:
8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
9 | sys.exit(1)
10 |
11 | if __name__ == "__main__":
12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
13 |
14 | from django.core.management import execute_from_command_line
15 |
16 | execute_from_command_line(sys.argv)
--------------------------------------------------------------------------------
/test_project/settings.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import sys
3 |
4 | PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
5 | sys.path.insert(0, os.path.join(PROJECT_ROOT, '..'))
6 |
7 | DEBUG = True
8 | TEMPLATE_DEBUG = DEBUG
9 |
10 | SECRET_KEY = "thisIsTotallySecret"
11 |
12 | DATABASES = {
13 | 'default': {
14 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
15 | 'NAME': 'testing.db', # Or path to database file if using sqlite3.
16 | 'USER': '', # Not used with sqlite3.
17 | 'PASSWORD': '', # Not used with sqlite3.
18 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
19 | 'PORT': '', # Set to empty string for default. Not used with sqlite3.
20 | }
21 | }
22 |
23 | TEMPLATE_LOADERS = (
24 | 'django.template.loaders.filesystem.Loader',
25 | 'django.template.loaders.app_directories.Loader',
26 | )
27 |
28 | TEMPLATE_DIRS = (
29 | os.path.join(PROJECT_ROOT, 'templates'),
30 | )
31 |
32 | MIDDLEWARE_CLASSES = (
33 | 'django.middleware.common.CommonMiddleware',
34 | 'django.contrib.sessions.middleware.SessionMiddleware',
35 | 'django.middleware.csrf.CsrfViewMiddleware',
36 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
37 | 'django.contrib.messages.middleware.MessageMiddleware',
38 | )
39 |
40 | ROOT_URLCONF = 'test_project.urls'
41 |
42 | SITE_ID = 1
43 |
44 | INSTALLED_APPS = (
45 | 'django.contrib.auth',
46 | 'django.contrib.contenttypes',
47 | 'django.contrib.sessions',
48 | 'django.contrib.sites',
49 | 'django.contrib.admin',
50 |
51 | 'alert',
52 | 'alert_tests',
53 | )
54 |
--------------------------------------------------------------------------------
/test_project/templates/alerts/WelcomeAlert/EmailBackend/body.txt:
--------------------------------------------------------------------------------
1 | email body
--------------------------------------------------------------------------------
/test_project/templates/alerts/WelcomeAlert/EmailBackend/title.txt:
--------------------------------------------------------------------------------
1 | email subject
--------------------------------------------------------------------------------
/test_project/templates/alerts/WelcomeAlert/body.txt:
--------------------------------------------------------------------------------
1 | default body
--------------------------------------------------------------------------------
/test_project/templates/alerts/WelcomeAlert/title.txt:
--------------------------------------------------------------------------------
1 | default title
--------------------------------------------------------------------------------
/test_project/templates/basic.email:
--------------------------------------------------------------------------------
1 | {% load alert_email_tags %}
2 |
3 | {% p %}Hello {{ username }},{% endp %}
4 |
5 | {% p %}I'm writing you to say hello.{% endp %}
6 |
7 | {% h1 %}Main Points{% endh1 %}
8 |
9 | {% p %}There is only {% a href='http://google.com' %}one{% enda %} point, actually.{% endp %}
10 |
11 | {% p %}Best,
12 | James{% endp %}
--------------------------------------------------------------------------------
/test_project/templates/basic.expected.html:
--------------------------------------------------------------------------------
1 | Hello Alex,
2 |
3 | I'm writing you to say hello.
4 |
5 | Main Points
6 |
7 | There is only one point, actually.
8 |
9 | Best,
James
--------------------------------------------------------------------------------
/test_project/templates/basic.expected.txt:
--------------------------------------------------------------------------------
1 | Hello Alex,
2 |
3 | I'm writing you to say hello.
4 |
5 | Main Points
6 | ===========
7 |
8 | There is only one (http://google.com) point, actually.
9 |
10 | Best,
11 | James
--------------------------------------------------------------------------------
/test_project/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 |
3 | # Uncomment the next two lines to enable the admin:
4 | # from django.contrib import admin
5 | # admin.autodiscover()
6 |
7 | urlpatterns = patterns('',
8 | # Example:
9 | # (r'^test_project/', include('test_project.foo.urls')),
10 |
11 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
12 | # to INSTALLED_APPS to enable admin documentation:
13 | (r'^admin/doc/', include('django.contrib.admindocs.urls')),
14 |
15 | # Uncomment the next line to enable the admin:
16 | (r'^admin/', include(admin.site.urls)),
17 | )
18 |
--------------------------------------------------------------------------------