├── .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 | [![Build Status](https://travis-ci.org/jiaaro/django-alert.png?branch=master)](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 | 63 | 64 |
59 | 60 | {% block content %}{% endblock %} 61 | 62 |
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 | --------------------------------------------------------------------------------