Browse files

reworked how subscribers are defined. django-campaign no longer has i…

…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...
1 parent 2d743f4 commit e276a4fd3c14f35395ce768daf85fe2ccc49db2a @arneb arneb committed Oct 16, 2009
View
8 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/
View
2 campaign/__init__.py
@@ -1,2 +1,2 @@
-VERSION = (0, 1, 1)
+VERSION = (0, 2, 'pre')
__version__ = '.'.join(map(str, VERSION))
View
113 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
@@ -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',)
@@ -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)
View
32 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)
View
12 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()
View
14 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()
View
15 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()
View
44 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})
View
41 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)
+
View
27 campaign/forms.py
@@ -1,27 +0,0 @@
-import csv
-from django import forms
-from django.utils.translation import ugettext as _
-try:
- from django.utils.simplejson import simplejson as json
-except ImportError:
- from django.utils import simplejson as json
-
-class UploadForm(forms.Form):
- """
- validates that the uploaded file contains parseable json data.
- """
- file = forms.FileField()
-
- def clean_file(self):
- try:
- json.loads(self.cleaned_data['file'].read())
- self.cleaned_data['file'].seek(0)
- return self.cleaned_data['file']
- except Exception, e:
- try:
- reader = csv.reader(self.cleaned_data['file'].readlines())
- self.cleaned_data['file'].seek(0)
- return self.cleaned_data['file']
- except Exception, e:
- raise forms.ValidationError(_(u"uploaded file must contain json or csv data"))
-
View
102 campaign/models.py
@@ -2,9 +2,12 @@
from django import template
from django.db import models
from django.utils.translation import ugettext_lazy as _
-from django.contrib.auth.models import User
-from django.core.mail import EmailMessage, SMTPConnection, EmailMultiAlternatives
-from campaign.queue import SMTPQueue, SMTPLoggingConnection
+from django.core.mail import EmailMultiAlternatives
+from django.contrib.contenttypes.models import ContentType
+from campaign.fields import JSONField
+from campaign.context import MailContext
+from campaign.backends import backend
+
class MailTemplate(models.Model):
"""
@@ -23,58 +26,42 @@ class MailTemplate(models.Model):
def __unicode__(self):
return self.name
-
-
-class Recipient(models.Model):
- """
- A recipient of a Mail doesn't have to correspond to a User object, but can.
- If a User object is present the email address will automatically be filled
- in, if none is given.
-
- """
- user = models.ForeignKey(User, blank=True, null=True, verbose_name=_(u"User"))
- email = models.EmailField(_(u"email address"), blank=True, unique=True)
- salutation = models.CharField(_(u"salutation"), blank=True, null=True, max_length=255)
- added = models.DateTimeField(_(u"added"), editable=False)
-
- def __unicode__(self):
- return self.email
-
- def save(self, *args, **kwargs):
- if not self.pk:
- self.added = datetime.now()
- if self.user is not None and not self.email:
- self.email = self.user.email
- return super(Recipient, self).save(*args, **kwargs)
-
- class Meta:
- abstract = True
-
-
-
-class Subscriber(Recipient):
- """
- The actual Recipient of a Mail, see Recipient docstring for more info.
-
- """
class SubscriberList(models.Model):
"""
- A list of Subscriber objects.
+ A pointer to another Django model which holds the subscribers.
"""
name = models.CharField(_(u"Name"), max_length=255)
- subscribers = models.ManyToManyField(Subscriber, null=True, verbose_name=_(u"Subscribers"))
+ content_type = models.ForeignKey(ContentType)
+ filter_condition = JSONField(default="{}", help_text=_(u"Django ORM compatible lookup kwargs which are used to get the list of objects."))
+ email_field_name = models.CharField(_(u"Email-Field name"), max_length=64, help_text=_(u"Name of the model field which stores the recipients email address"))
def __unicode__(self):
return self.name
+
+ def _get_filter(self):
+ # simplejson likes to put unicode objects as dictionary keys
+ # but keyword arguments must be str type
+ fc = {}
+ for k,v in self.filter_condition.iteritems():
+ fc.update({str(k): v})
+ return fc
+
+ def object_list(self):
+ return self.content_type.model_class()._default_manager.filter(**self._get_filter())
+
+ def object_count(self):
+ return self.content_type.model_class()._default_manager.filter(**self._get_filter()).count()
+
+
class Campaign(models.Model):
"""
A Campaign is the central part of this app. Once a Campaign is created,
- has a MailTemplate and a number of Recipients, it can be send out.
+ has a MailTemplate and one or more SubscriberLists, it can be send out.
Most of the time of Campain will have a one-to-one relationship with a
MailTemplate, but templates may be reused in other Campaigns and maybe
Campaigns will have support for multiple templates in the future, therefore
@@ -89,20 +76,19 @@ class Campaign(models.Model):
def __unicode__(self):
return self.name
-
+
def send(self):
"""
Sends the mails to the recipients.
"""
- connection = SMTPLoggingConnection()
- num_sent = self._send(connection)
+ num_sent = self._send()
self.sent = True
self.save()
return num_sent
- def _send(self, connection):
+ def _send(self):
"""
Does the actual work
"""
@@ -114,32 +100,38 @@ def _send(self, connection):
sent = 0
used_addresses = []
for recipient_list in self.recipients.all():
- for recipient in recipient_list.subscribers.all():
+ for recipient in recipient_list.object_list():
# never send mail to blacklisted email addresses
- if not BlacklistEntry.objects.filter(email=recipient.email).count() and not recipient.email in used_addresses:
- msg = EmailMultiAlternatives(subject, connection=connection, to=[recipient.email,])
- msg.body = text_template.render(template.Context({'salutation': recipient.salutation,}))
+ recipient_email = getattr(recipient, recipient_list.email_field_name)
+ if not BlacklistEntry.objects.filter(email=recipient_email).count() and not recipient_email in used_addresses:
+ msg = EmailMultiAlternatives(subject, to=[recipient_email,])
+ context = MailContext(recipient)
+ msg.body = text_template.render(context)
if self.template.html is not None and self.template.html != u"":
- html_content = html_template.render(template.Context({'salutation': recipient.salutation,}))
+ html_content = html_template.render(context)
msg.attach_alternative(html_content, 'text/html')
- sent += msg.send()
- used_addresses.append(recipient.email)
+ sent += backend.send_mail(msg)
+ used_addresses.append(recipient_email)
return sent
-class BlacklistEntry(Recipient):
+class BlacklistEntry(models.Model):
"""
If a user has requested removal from the subscriber-list, he is added
- to the blacklist to prevent accidential adding of the same user again.
+ to the blacklist to prevent accidential adding of the same user again
+ on subsequent imports from a data source.
"""
-
+ email = models.EmailField()
+ added = models.DateTimeField(default=datetime.now, editable=False)
-class BounceEntry(Recipient):
+class BounceEntry(models.Model):
"""
Records bouncing Recipients. To be processed by a human.
"""
+ email = models.CharField(_(u"recipient"), max_length=255, blank=True, null=True)
exception = models.TextField(_(u"exception"), blank=True, null=True)
+
View
98 campaign/queue.py
@@ -1,98 +0,0 @@
-from django.core.mail import SMTPConnection
-
-
-
-class SMTPQueue(SMTPConnection):
- """
- Can be used instead of Django's SMTPConnection class to queue
- a bunch of emails for later sending.
- """
-
- def __init__(self, *args, **kwargs):
- super(SMTPQueue, self).__init__(*args, **kwargs)
- self._email_messages = []
-
-
- def send_messages(self, email_messages):
- """
- Queues one or more EmailMessage objects and returns the number of email
- messages queued.
- """
- if not email_messages:
- return
-
- num_sent = 0
- for message in email_messages:
- sent = self._queue(message)
- if sent:
- num_sent += 1
- return num_sent
-
-
- def _queue(self, email_message):
- self._email_messages.append(email_message)
-
-
- def defer(self):
- """
- Save the state of this Queue for later sending
- """
- try:
- import cPickle as pickle
- except ImportError:
- import pickle
-
- return pickle.dumps(self)
-
-
- def flush(self):
- """
- Will send all queued EmailMessage objects.
- """
- if not self._email_messages:
- return
- new_conn_created = self.open()
- if not self.connection:
- # We failed silently on open(). Trying to send would be pointless.
- return
- num_sent = 0
- for message in self._email_messages:
- sent = self._send(message)
- if sent:
- num_sent += 1
- if new_conn_created:
- self.close()
- #print "sent: %s" % num_sent
- return num_sent
-
-
-class SMTPLoggingConnection(SMTPConnection):
- """
- Logs bounces etc.
-
- """
- def send_messages(self, email_messages):
- """
- Sends one or more EmailMessage objects and returns the number of email
- messages sent.
- """
- from campaign.models import BounceEntry
- if not email_messages:
- return
- new_conn_created = self.open()
- if not self.connection:
- # We failed silently on open(). Trying to send would be pointless.
- return
- num_sent = 0
- for message in email_messages:
- try:
- sent = self._send(message)
- except Exception, e:
- BounceEntry.objects.create(email=message.recipients()[0], exception=str(e))
- sent = False
- if sent:
- num_sent += 1
- if new_conn_created:
- self.close()
- return num_sent
-
View
2 campaign/templates/admin/campaign/campaign/send_object.html
@@ -31,7 +31,7 @@
<h3>{% trans "Subscriber lists" %}</h3>
<ul>
{% for subscriber_list in object.recipients.all %}
- {% with subscriber_list.subscribers.count as subscribercount %}
+ {% with subscriber_list.object_count as subscribercount %}
<li><strong>{{ subscriber_list.name }}</strong> – {% blocktrans with subscribercount as subscriber_count and subscribercount|pluralize as pluralized %}{{ subscriber_count }} recipient{{ pluralized }}{% endblocktrans %}</li>
{% endwith %}
{% endfor %}
View
2 campaign/urls.py
@@ -1,5 +1,5 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('',
- url(r'^(?P<object_id>[\d]+)/$', 'campaign.views.view_online', {}, name="campaign_view_online"),
+ url(r'^view/(?P<object_id>[\d]+)/$', 'campaign.views.view_online', {}, name="campaign_view_online"),
)
View
7 campaign/views.py
@@ -1,6 +1,6 @@
from django import template, http
from django.shortcuts import get_object_or_404
-from campaign.models import Campaign
+from campaign.models import Campaign, BlacklistEntry
def view_online(request, object_id):
campaign = get_object_or_404(Campaign, pk=object_id, online=True)
@@ -12,6 +12,7 @@ def view_online(request, object_id):
tpl = template.Template(campaign.template.plain)
content_type = 'text/plain, charset=utf-8'
- return http.HttpResponse(tpl.render(template.Context({})), content_type=content_type)
+ return http.HttpResponse(tpl.render(template.Context({})),
+ content_type=content_type)
-
+
View
53 docs/backends.txt
@@ -0,0 +1,53 @@
+.. _ref-backends:
+
+=================
+Backends
+=================
+
+To decouple the actual sending of the e-mails from the application logic
+and therefore make django-campaign more scaleable version 0.2 introduces a
+concept of backends which encapsulate the whole process of sending the e-mails.
+
+
+Writing your own Backend
+------------------------
+
+Backends for django-campaign must adhere to the following API. Some of the
+methods are mandatory and some are optional, especially if you inherit from
+the ``base`` backend.
+
+The basic structure of a backend is as follows::
+
+ from campaign.backends.base import BaseBackend
+
+ class ExampleBackend(BaseBackend):
+ def __init__(self):
+ # do some setup here, e.g. processing your own settings
+
+ ...
+
+ backend = ExampleBackend()
+
+If this code is stored in a file ``myproject/myapp/example.py`` then the
+setting to use this backend would be::
+
+ CAMPAIGN_BACKEND = 'myproject.myapp.example'
+
+Each backend must define a ``backend`` variable, which should be an instance
+of the backend.
+
+
+Backend Methods
+---------------
+
+The following methods must be implemented by every backend:
+
+``send_mail(self, email, fail_silently=False)``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This method takes an instance of ``django.core.mail.EmailMessage`` as argument
+and is responsible for whatever is needed to send this email to the recipient.
+
+The ``fail_silently`` argument specifies whether exceptions should bubble up or
+should be hidden from upper layers.
+
View
38 docs/concepts.txt
@@ -0,0 +1,38 @@
+=========================================================
+Philisophy behind some of the concepts of django-campaign
+=========================================================
+
+Why is there no Subscriber-Model?
+---------------------------------
+
+I've tried to use a bunch of different Subscriber-Models bundled with the app
+itself but none of them was usable for more than one use-case so I decided
+to drop the concept of a Subscriber-Model and instead added a mechanism for
+you to hook your own Subscriber (or User or whatever) model into the flow.
+
+By adding a SubscriberList Object with a pointer to the ContentType of your
+Model and by optionally adding lookup kwargs to narrow the selection you can
+specifiy which objects of your model class for a list of subscribers. You
+can even build SubscriberLists for different Models and send a Campaign in
+one step to multiple SubscriberLists.
+
+Adding a SubscriberList for all active Users present in the django.contrib.auth
+module one would simply add a SubscriberList object::
+
+ from django.contrib.auth.models import User
+ from django.contrib.contenttypes.models import ContentType
+ from campaing.models import SubscriberList
+
+ obj = SubscriberList.objects.create(
+ content_type=ContentType.objects.get_for_model(User),
+ filter_condition={'is_active': True}
+ )
+
+Of course this can also be done using Django's built-in admin interface.
+
+Being alble to add any number and combinations of ContentTypes and lookup kwargs
+and assining one or multiple SubscriberLists to a Campaign one should be able
+to map any real-world scenario to the workflow. If a subscriber is present in
+multiple SubscriberLists this is not a problem because the code makes sure
+that every Campaign is only sent once to every given email address.
+
View
194 docs/conf.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+#
+# django-campaign documentation build configuration file, created by
+# sphinx-quickstart on Thu Oct 15 12:48:22 2009.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = []
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.txt'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'django-campaign'
+copyright = u'2009, Arne Brodowski'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.2'
+# The full version, including alpha/beta/rc tags.
+release = '0.2'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'django-campaigndoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'django-campaign.tex', u'django-campaign Documentation',
+ u'Arne Brodowski', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
View
38 docs/index.txt
@@ -0,0 +1,38 @@
+====================================================================
+django-campaign - A basic newsletter app for the Django webframework
+====================================================================
+
+Django-campaign is an application for the Django webframework to make
+sending out newsletters to a rather small group of subscribers easy.
+It is not a solution to drive your multi-thousand-subscriber campaigns, but
+a few hundred - maybe a few thousand - subscribers should be possible.
+
+Some of the core features are:
+
+* Multipart Emails made easy - just add a plain-text *and* a html-template.
+* Full control over the Subscriber-Model and therefore the template context
+ used to render the mails.
+* Add context processors to add whatever you need to a mail template based on
+ the recipient. This makes it easy to personalize messages.
+* View the campaigns online
+* German translation for the app
+
+Planned features:
+
+* easy i18n support for the emails
+* simple and optional subscribe/unsubscribe handling
+* every recipient should be able to see his personalized message online
+
+Contents
+--------
+
+.. toctree::
+ :maxdepth: 2
+
+ overview
+ install
+ settings
+ backends
+ templates
+ concepts
+
View
44 docs/install.txt
@@ -0,0 +1,44 @@
+==========================
+Installing django-campaign
+==========================
+
+This document describes how to install django-campaign.
+
+Quickstart
+----------
+
+1. Install django-campaign with ``easy_install``::
+
+ easy_install django-campaign
+
+2. Add ``campaign`` to your ``INSTALLED_APPS`` setting::
+
+ INSTALLED_APPS = (
+ ...
+ 'campaign',
+ ...
+ )
+
+3. Add an entry to your URL-conf. Using ``campaign`` here is a matter of taste,
+ feel free to mount the app under a different URL::
+
+ urlpatterns += patterns('',
+ (r'^campaign/', include('campaign.urls'))
+ )
+
+4. Then run ``manage.py syncdb`` to create the neccessary database tables.
+
+Manual Install
+--------------
+
+Instead of using ``easy_install`` to install the latest release you can also
+run a svn checkout of the recent development version. Just checkout the code
+and add the folder ``campaign`` to your PYTHONPATH, for example by copying or
+symlinking the folder to your site-packages directory.
+
+Downloading a release, unpacking and adding the folder to the PYTHONPATH is,
+of course, also possible.
+
+After you have added the folder to the PYTHONPATH follow the instructions under
+Quickstart_ above starting at point 2.
+
View
7 docs/overview.txt
@@ -4,11 +4,8 @@ How to send Newsletters with django-campaign
Here is a very brief overview how to use django-campaign:
-* Import (or enter by hand) a number of subscribers into the Subscriber model.
- Importing json formatted data is possible.
-
-* Build one or more SubscriberList objects, which hold a bunch of Subscriber
- objects.
+* Setup one or more SubscriberList objects in the admin interface or
+ programmatically.
* Create a Campaign object and the corresponding MailTemplate object.
View
42 docs/settings.txt
@@ -0,0 +1,42 @@
+.. _ref-settings:
+
+==================
+Available Settings
+==================
+
+Here is a list of all available settings of django-campaign and their
+default values. All settings are prefixed with ``CAMPAIGN_``, although this
+is a bit verbose it helps to make it easy to identify these settings.
+
+
+CAMPAIGN_BACKEND
+----------------
+
+Default: ``'campaign.backends.send_mail'``
+
+The backend used for the actual sending of the emails. The default backend
+``campaign.backends.send_mail`` uses Django's built-in e-mail sending
+capabilities.
+
+Additionally the following backends are available:
+
+ * ``campaign.backends.django_mailer``: Uses the resuseable django_mailer
+ app to queue mails in the database and bulk-send them via cron-job.
+
+ * ``campaign.backends.debug``: Simple backend which prints some information
+ on stdout instead of sending the email. This only exists to demonstrate
+ how to extract different values from the supplied email message instance.
+
+Please see the :ref:`backend docs <ref-backends>` about implementing your
+own backend.
+
+
+CAMPAIGN_CONTEXT_PROCESSORS
+---------------------------
+
+Default: ``()`` (empty tuple)
+
+Similar to Django's Template Context Processors these are callables which take
+a Subscriber object as their argument and return a dictionary with items to
+add to the Template Context which is used to render the Mail.
+
View
10 docs/templates.txt
@@ -0,0 +1,10 @@
+================
+E-Mail Templates
+================
+
+All e-mail templates are pure Django Templates, please see the `Django
+Template Documentation`_ for details. This document only contains some parts
+specific to django-campaign.
+
+
+.. _`Django Template Documentation`: http://docs.djangoproject.com/en/dev/topics/templates/
View
4 setup.py
@@ -4,10 +4,12 @@
name='django-campaign',
version=__import__('campaign').__version__,
description='A basic newsletter app for the Django webframework',
- long_description=open('docs/overview.txt').read(),
+ long_description=open('docs/index.txt').read(),
author='Arne Brodowski',
author_email='arne@rcs4u.de',
+ license="BSD",
url='http://code.google.com/p/django-campaign/',
+ download_url='http://code.google.com/p/django-campaign/downloads/list'
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Web Environment',

0 comments on commit e276a4f

Please sign in to comment.