Skip to content

Commit

Permalink
reworked how subscribers are defined. django-campaign no longer has i…
Browse files Browse the repository at this point in the history
…t's own subscriber model, you now have to supply your own django model. abstracted the sending of mails with a backend-based approach. adding a MailContext which works like Django's RequestContext but based on the current Subscriber instead of the request. adding a bunch of docs. this change is definately backwards incompatible but is required for the next release.
  • Loading branch information
arneb committed Oct 16, 2009
1 parent 2d743f4 commit e276a4f
Show file tree
Hide file tree
Showing 24 changed files with 645 additions and 304 deletions.
8 changes: 4 additions & 4 deletions README
@@ -1,9 +1,9 @@
A basic newsletter app for the Django webframework
--------------------------------------------------

See docs/overview.txt for a brief overview.
* Please see the documentation in the docs folder.

You can reach the author of django-campaign at arne _at_ rcs4u.de.
* You can reach the author of django-campaign at arne _at_ rcs4u.de.

Bug-reports can be filed at the google code project page:
http://code.google.com/p/django-campaign/
* Bug-reports can be filed at the google code project page:
http://code.google.com/p/django-campaign/
2 changes: 1 addition & 1 deletion campaign/__init__.py
@@ -1,2 +1,2 @@
VERSION = (0, 1, 1)
VERSION = (0, 2, 'pre')
__version__ = '.'.join(map(str, VERSION))
113 changes: 5 additions & 108 deletions campaign/admin.py
@@ -1,7 +1,6 @@
import csv
from django import forms
from django.shortcuts import render_to_response
from django import template
from django import forms
from django.utils.functional import update_wrapper
from django.contrib import admin
from django.http import HttpResponseRedirect
Expand All @@ -10,110 +9,9 @@
from django.utils.translation import ugettext as _
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
try:
from django.utils.simplejson import simplejson as json
except ImportError:
from django.utils import simplejson as json

from campaign.models import MailTemplate, Subscriber, Campaign, BlacklistEntry, BounceEntry, SubscriberList
from campaign.forms import UploadForm


class SubscriberAdmin(admin.ModelAdmin):

import_template=None
list_display = ('email', 'salutation')

def has_import_permission(self, request):
"""
TODO: integrate with django's permission system
"""
return request.user.is_superuser

def import_view(self, request, extra_context=None):
"""
Import email addresses and salutation from a uploaded text file.
"""
model = self.model
opts = model._meta

if not self.has_import_permission(request):
raise PermissionDenied

if request.method == 'POST':
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
num_import = 0

try: # try json
data = json.loads(form.cleaned_data['file'].read())

for entry in data:
try:
Subscriber.objects.create(email=entry['email'], salutation=force_unicode(entry['name']))
num_import += 1
except Exception, e:
pass
except: # may be csv data
try:
reader = csv.reader(form.cleaned_data['file'].readlines(), delimiter=',')
for entry in reader:
try:
Subscriber.objects.create(email=entry[0], salutation=force_unicode(entry[1]))
num_import += 1
except Exception, e:
pass
except:
raise

request.user.message_set.create(message=_(u'Successfully imported %(num_import)s %(name)s.' % {'name': force_unicode(opts.verbose_name_plural), 'num_import': num_import,}))
return HttpResponseRedirect('../')

else:
form = UploadForm()

def form_media():
from django.conf import settings
css = ['css/forms.css',]
return forms.Media(css={'screen': ['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in css]})

media = self.media + form_media()
context = {
'title': _('Import %s') % force_unicode(opts.verbose_name_plural),
'is_popup': request.REQUEST.has_key('_popup'),
'media': mark_safe(media),
'root_path': self.admin_site.root_path,
'app_label': opts.app_label,
'opts': opts,
'form': form,
}

context.update(extra_context or {})
return render_to_response(self.import_template or
['admin/%s/%s/import.html' % (opts.app_label, opts.object_name.lower()),
'admin/%s/import.html' % opts.app_label,
'admin/import.html'], context, context_instance=template.RequestContext(request))


def get_urls(self):
from django.conf.urls.defaults import patterns, url

def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
return update_wrapper(wrapper, view)

info = self.admin_site.name, self.model._meta.app_label, self.model._meta.module_name

super_urlpatterns = super(SubscriberAdmin, self).get_urls()
urlpatterns = patterns('',
url(r'^import/$',
wrap(self.import_view),
name='%sadmin_%s_%s_import' % info),
)
urlpatterns += super_urlpatterns

return urlpatterns
from campaign.models import MailTemplate, Campaign, BlacklistEntry, \
BounceEntry, SubscriberList


class CampaignAdmin(admin.ModelAdmin):
filter_horizontal=('recipients',)
Expand Down Expand Up @@ -206,7 +104,6 @@ def wrapper(*args, **kwargs):

admin.site.register(Campaign, CampaignAdmin)
admin.site.register(MailTemplate)
admin.site.register(Subscriber, SubscriberAdmin)
admin.site.register(BlacklistEntry)
admin.site.register(BounceEntry)
admin.site.register(SubscriberList, filter_horizontal=('subscribers',))
admin.site.register(SubscriberList)
32 changes: 32 additions & 0 deletions campaign/backends/__init__.py
@@ -0,0 +1,32 @@
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

__all__ = ('backend')

CAMPAIGN_BACKEND = getattr(settings, 'CAMPAIGN_BACKEND', 'send_mail')

def get_backend(import_path):
if not '.' in import_path:
import_path = "campaign.backends.%s" % import_path
try:
mod = __import__(import_path, {}, {}, [''])
except ImportError, e_user:
# No backend found, display an error message and a list of all
# bundled backends.
backend_dir = __path__[0]
available_backends = [f.split('.py')[0] for f in os.listdir(backend_dir) if not f.startswith('_') and not f.startswith('.') and not f.endswith('.pyc')]
available_backends.sort()
if CAMPAIGN_BACKEND not in available_backends:
raise ImproperlyConfigured("%s isn't an available campaign backend. Available options are: %s" % \
(CAMPAIGN_BACKEND, ', '.join(map(repr, available_backends))))
# if the CAMPAIGN_BACKEND is available in the backend directory
# and an ImportError is raised, don't suppress it
else:
raise
try:
return getattr(mod, 'backend')
except AttributeError:
raise ImproperlyConfigured('Backend "%s" does not define a "backend" instance.' % import_path)

backend = get_backend(CAMPAIGN_BACKEND)
12 changes: 12 additions & 0 deletions campaign/backends/debug.py
@@ -0,0 +1,12 @@
# an example backend which just prints out the email instead of sending it

class DebugBackend(object):
def send_mail(self, email, fail_silently=False):
print "Subject: %s" % email.subject
print "To: %s" % email.recipients()
print "======"
print email.message().as_string() # the actual email message
print "======"
return 0

backend = DebugBackend()
14 changes: 14 additions & 0 deletions campaign/backends/django_mailer.py
@@ -0,0 +1,14 @@
# simple backend using django-mailer to queue and send the mails
from django.conf import settings
from mailer import send_mail

class DjangoMailerBackend(object):
def send_mail(self, email, fail_silently=False):
subject = email.subject
body = email.body
# django_mailer does not support multi-part messages so we loose the
# html content
return send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, email.recipients(),
fail_silently=fail_silently)

backend = DjangoMailerBackend()
15 changes: 15 additions & 0 deletions campaign/backends/send_mail.py
@@ -0,0 +1,15 @@
# simple backend which uses Django's built-in mail sending mechanisms

class SendMailBackend(object):
def send_mail(self, email, fail_silently=False):
"""
Parameters:
``email``: an instance of django.core.mail.EmailMessage
``fail_silently``: a boolean indicating if exceptions should
bubble up
"""
return email.send(fail_silently=fail_silently)

backend = SendMailBackend()
44 changes: 44 additions & 0 deletions campaign/context.py
@@ -0,0 +1,44 @@
# heavily based on Django's RequestContext
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.template import Context

_mail_context_processors = None

# This is a function rather than module-level procedural code because we only
# want it to execute if somebody uses MailContext.
def get_mail_processors():
global _mail_context_processors
if _mail_context_processors is None:
processors = []
for path in getattr(settings, 'CAMPAIGN_CONTEXT_PROCESSORS', ()):
i = path.rfind('.')
module, attr = path[:i], path[i+1:]
try:
mod = __import__(module, {}, {}, [attr])
except ImportError, e:
raise ImproperlyConfigured('Error importing campaign processor module %s: "%s"' % (module, e))
try:
func = getattr(mod, attr)
except AttributeError:
raise ImproperlyConfigured('Module "%s" does not define a "%s" callable campaign processor' % (module, attr))
processors.append(func)
_mail_context_processors = tuple(processors)
return _mail_context_processors

class MailContext(Context):
"""
This subclass of template.Context automatically populates itself using
the processors defined in CAMPAIGN_CONTEXT_PROCESSORS.
Additional processors can be specified as a list of callables
using the "processors" keyword argument.
"""
def __init__(self, subscriber, dict=None, processors=None):
Context.__init__(self, dict)
if processors is None:
processors = ()
else:
processors = tuple(processors)
for processor in get_mail_processors() + processors:
self.update(processor(subscriber))
self.update({'recipient': subscriber})
41 changes: 41 additions & 0 deletions campaign/fields.py
@@ -0,0 +1,41 @@
from django.db import models
from django import forms
from django.utils import simplejson

class JSONWidget(forms.Textarea):
def render(self, name, value, attrs=None):
if not isinstance(value, basestring):
value = simplejson.dumps(value, indent=2)
return super(JSONWidget, self).render(name, value, attrs)

class JSONFormField(forms.CharField):
def __init__(self, *args, **kwargs):
kwargs['widget'] = JSONWidget
super(JSONFormField, self).__init__(*args, **kwargs)

def clean(self, value):
if not value: return
try:
return simplejson.loads(value)
except Exception, exc:
raise forms.ValidationError(u'JSON decode error: %s' % (unicode(exc),))

class JSONField(models.TextField):
__metaclass__ = models.SubfieldBase

def formfield(self, **kwargs):
return super(JSONField, self).formfield(form_class=JSONFormField, **kwargs)

def to_python(self, value):
if isinstance(value, basestring):
value = simplejson.loads(value)
return value

def get_db_prep_save(self, value):
if value is None: return
return simplejson.dumps(value)

def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value)

27 changes: 0 additions & 27 deletions campaign/forms.py

This file was deleted.

0 comments on commit e276a4f

Please sign in to comment.