Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
a373ced
call it 0.4.0 pre
frog32 Sep 7, 2011
ea80518
Merge commit 'afa63' into develop
frog32 Jun 26, 2012
27eed87
add django-admin-import to subscriber module
frog32 Jun 26, 2012
ad2fd0f
Merge branch 'maint' into develop
frog32 Jun 27, 2012
7d6d834
update a lot of code to be pep8 conform and remove some unneeded imports
frog32 Jul 17, 2012
cf2fec9
update content_image_url template tag to accept an optional identifier
frog32 Jul 18, 2012
7545c74
fix deprecation warnings in django-admin-import part of subscriber mo…
frog32 Sep 3, 2012
1d988ce
fix imagecontent default template
frog32 Sep 13, 2012
0e9e91a
better email sender name solution, but still not perfect
sspross Sep 12, 2012
3414881
initial celery support
frog32 Sep 21, 2012
ae1cc98
some pep8 cleanup
frog32 Sep 22, 2012
e52c604
allow email links with mailto protocol
frog32 Sep 28, 2012
bdbc404
fix email sender name
frog32 Sep 28, 2012
1075338
fix the redirect view to not include google analytics data in links w…
frog32 Sep 28, 2012
52494b9
avoid deprecated feincms module
frog32 Oct 1, 2012
d887d35
add copy action to newsletter admin
frog32 Oct 17, 2012
8f68f9a
create a way to track links which are stored in the template and not …
frog32 Oct 17, 2012
f257aa6
fix a bug where django versions prior to 1.4 couldn't show headers
frog32 Oct 23, 2012
61cfce4
let workflow newsletters take an extra context
frog32 Oct 26, 2012
dd8b109
fix a typo, thanks to @georgemarshall
frog32 Nov 1, 2012
cbe6da7
Merge branch 'feature-extra-context' into develop
frog32 Nov 7, 2012
900eaf1
load pennyblack_tags during validation of a content type
frog32 Nov 8, 2012
b1fc8ba
fix pennyblack.Job to use timezone aware start and end dates
frog32 Nov 27, 2012
a5ebb45
switch example template to utf-8
frog32 Nov 29, 2012
43b5b26
add precedence and list-unsubscribe headers
frog32 Nov 29, 2012
e4bcb4c
new feature email attachments
frog32 Nov 30, 2012
a0dc710
fix unsubscribe header
frog32 Dec 8, 2012
1250da4
stay backwards compatible with python<2.7
frog32 Dec 17, 2012
0fc5e38
allow special attachments on workflow newsletters
frog32 Jan 7, 2013
c31b82f
add user agent tracking and add a view to show every receiver of a ne…
frog32 Feb 6, 2013
3319e87
bugfix for django<1.4
frog32 Feb 11, 2013
e8bf06c
fix a query to work with django 1.3
frog32 Feb 14, 2013
3933431
add contact_type to email_client model to recort the type of a contact
frog32 Feb 14, 2013
690711e
fix mailto redirects for django >= 1.4
frog32 Apr 24, 2013
791b57e
Fix jobunit/change_form translations
nickburlett May 26, 2013
0ea39df
Merge pull request #1 from nickburlett/master
nickburlett May 26, 2013
cd28265
FIx typo in Job help text
nickburlett May 27, 2013
b58a305
add request context for unsubscribe view
nickburlett Jun 8, 2013
4962802
add a public view mechanism for jobs
nickburlett Jun 8, 2013
8791a50
1.5 compatible url tag in admin
frog32 Jun 11, 2013
5371a49
compatible to django 1.5
LeaFin Jun 11, 2013
d5392ea
Merge pull request #39 from LeaFin/patch-1
frog32 Jun 11, 2013
f2cf14d
support Pillow
nickburlett Jun 11, 2013
553d58e
Merge remote-tracking branch 'official/develop' into develop
nickburlett Jun 11, 2013
c69f687
Fix jobunit/change_form translations
nickburlett May 26, 2013
df78ef5
FIx typo in Job help text
nickburlett May 27, 2013
2a80776
add request context for unsubscribe view
nickburlett Jun 8, 2013
1f30e4c
support Pillow
nickburlett Jun 11, 2013
d32cf79
add 2 new authors
frog32 Jul 25, 2013
b3b2a7f
fixed url templatetag in base newsletter template to work with django…
sspross Aug 15, 2013
72247ae
Fix NewsletterSubscriber date_subscribed
nickburlett Aug 18, 2013
7f2a298
Merge branch 'develop' of https://github.com/allink/pennyblack into d…
nickburlett Aug 18, 2013
0e4561f
Fix NewsletterSubscriber date_subscribed
nickburlett Aug 18, 2013
2cdb0df
Update timezone fix to work with Django 1.3
nickburlett Aug 20, 2013
57bb42c
convert thumbnail images to RGB before saving them
nickburlett Mar 31, 2015
3220cda
Fix blank `Job.public_slug`
nickburlett Oct 19, 2015
8315074
Merge branch 'master' of http://github.com/nickburlett/pennyblack
nickburlett Oct 19, 2015
8e751cb
Merge commit '57bb42c0c66fffd8beebb7eb6c33e28ab8e71ff4'
nickburlett Oct 19, 2015
301d032
exception handling in Job.send
nickburlett Feb 5, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ The authors of pennyblack are:

* Marc Egli
* Janick Pfenninger
* Silvan Spross
* Silvan Spross
* Leandra Finger
* Nick Burlett
5 changes: 1 addition & 4 deletions example/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
from django.db import models

from pennyblack.models import Newsletter
from pennyblack.options import NewsletterReceiverMixin, JobUnitMixin
from pennyblack.content.richtext import TextOnlyNewsletterContent, \
TextWithImageNewsletterContent

Expand All @@ -13,6 +10,6 @@
('main', 'Main Region'),
),
})

Newsletter.create_content_type(TextOnlyNewsletterContent)
Newsletter.create_content_type(TextWithImageNewsletterContent)
4 changes: 2 additions & 2 deletions example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': 'db.sqlite', # Or path to database file if using sqlite3.
'USER': '', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
Expand Down Expand Up @@ -132,4 +132,4 @@
'TINYMCE_JS_URL': os.path.join(STATIC_URL, 'javascript/tiny_mce/tiny_mce.js'),
'TINYMCE_CONTENT_CSS_URL': None,
'TINYMCE_LINK_LIST_URL': None
}
}
4 changes: 2 additions & 2 deletions example/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
{'document_root': settings.MEDIA_ROOT}),
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
(r'^admin/', include(admin.site.urls)),
url(r'^newsletter/', include('pennyblack.urls'), name = 'pennyblack'),

url(r'^newsletter/', include('pennyblack.urls'), name='pennyblack'),
)
5 changes: 3 additions & 2 deletions pennyblack/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = (0, 3, 5,)
VERSION = (0, 4, 0, 'pre')
__version__ = '.'.join(map(str, VERSION))

# Do not use Django settings at module level as recommended
Expand All @@ -20,11 +20,12 @@ def __init__(self, settings_module):

settings = LazySettings()


def send_newsletter(newsletter_name, *args, **kwargs):
"""
Gets a newsletter by its name and tries to send it to receiver
"""
from pennyblack.models import Newsletter
newsletter = Newsletter.objects.get_workflow_newsletter_by_name(newsletter_name)
if newsletter:
newsletter.send(*args, **kwargs)
newsletter.send(*args, **kwargs)
6 changes: 3 additions & 3 deletions pennyblack/admin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from django.contrib import admin

from pennyblack import settings
from pennyblack.models.newsletter import Newsletter, NewsletterAdmin
from pennyblack.models.job import Job, JobAdmin, JobStatistic, JobStatisticAdmin
from pennyblack.models.sender import Sender, SenderAdmin

admin.site.register(Newsletter, NewsletterAdmin)
admin.site.register(Job,JobAdmin)

admin.site.register(Job, JobAdmin)
admin.site.register(JobStatistic, JobStatisticAdmin)
admin.site.register(Sender,SenderAdmin)
admin.site.register(Sender, SenderAdmin)
55 changes: 28 additions & 27 deletions pennyblack/content/richtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from django.db import models
from django.forms.util import ErrorList
from django.template import Context, Template, TemplateSyntaxError
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _

from pennyblack import settings
Expand All @@ -13,35 +12,37 @@

import re
import os
import Image
from PIL import Image
import exceptions

HREF_RE = re.compile(r'href\="((\{\{[^}]+\}\}|[^"><])+)"')


class NewsletterSectionAdminForm(RichTextContentAdminForm):
def clean(self):
cleaned_data = super(NewsletterSectionAdminForm, self).clean()
try:
t = Template(cleaned_data['text'])
Template("{%%load pennyblack_tags %%}%s" % cleaned_data['text'])
except TemplateSyntaxError, e:
self._errors["text"] = ErrorList([e])
except exceptions.KeyError:
pass
try:
t = Template(cleaned_data['title'])
Template("{%%load pennyblack_tags %%}%s" % cleaned_data['title'])
except TemplateSyntaxError, e:
self._errors["title"] = ErrorList([e])
except exceptions.KeyError:
pass
return cleaned_data

class Meta:
exclude = ('image_thumb', 'image_width', 'image_height', 'image_url_replaced')

def __init__(self, *args, **kwargs):
super(NewsletterSectionAdminForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'title', self.fields.pop('title'))


class TextOnlyNewsletterContent(RichTextContent):
"""
Has a title and a text wich both can contain template code.
Expand All @@ -51,9 +52,9 @@ class TextOnlyNewsletterContent(RichTextContent):
feincms_item_editor_form = NewsletterSectionAdminForm

feincms_item_editor_includes = {
'head': [ settings.TINYMCE_CONFIG_URL ],
}
'head': [settings.TINYMCE_CONFIG_URL],
}

baselayout = "content/text_only/section.html"

class Meta:
Expand All @@ -74,16 +75,18 @@ def replace_links(self, job):
if u'link_url' in link:
continue
replacelink = job.add_link(link)
self.text = ''.join((self.text[:match.start(1)+offset], replacelink, self.text[match.end(1)+offset:]))
self.text = ''.join((self.text[:match.start(1) + offset],
replacelink,
self.text[match.end(1) + offset:]))
offset += len(replacelink) - len(match.group(1))

def prepare_to_send(self):
"""
insert link_style into all a tags
"""
self.text = re.sub(r"<a ","<a style=\"{% get_newsletterstyle request link_style %}\"", self.text)
self.text = re.sub(r"<a ", "<a style=\"{% get_newsletterstyle request link_style %}\"", self.text)
self.save()

def get_template(self):
"""
Creates a template
Expand All @@ -93,12 +96,12 @@ def get_template(self):
{%% block title %%}%s{%% endblock %%}
{%% block text %%}%s{%% endblock %%}
""" % (self.baselayout, self.title, self.text,))

def render(self, request, **kwargs):
context = request.content_context
context['request'] = request
context.update({'content':self, 'content_width':settings.NEWSLETTER_CONTENT_WIDTH})
if hasattr(self,'get_extra_context'):
context.update({'content': self, 'content_width': settings.NEWSLETTER_CONTENT_WIDTH})
if hasattr(self, 'get_extra_context'):
context.update(self.get_extra_context())
return self.get_template().render(Context(context))

Expand All @@ -109,20 +112,20 @@ class TextWithImageNewsletterContent(TextOnlyNewsletterContent):
"""
image_original = models.ForeignKey(MediaFile)
image_thumb = models.ImageField(upload_to='newsletter/images', blank=True,
width_field='image_width', height_field='image_height')
width_field='image_width', height_field='image_height')
image_width = models.IntegerField(default=0)
image_height = models.IntegerField(default=0)
image_url = models.CharField(max_length=250 ,blank=True)
image_url = models.CharField(max_length=250, blank=True)
image_url_replaced = models.CharField(max_length=250, default='')
position = models.CharField(max_length=10, choices=settings.TEXT_AND_IMAGE_CONTENT_POSITIONS)

baselayout = "content/text_and_image/section.html"

class Meta:
abstract = True
verbose_name = _('text and image content')
verbose_name_plural = _('text and image contents')

def get_extra_context(self):
text_width = settings.NEWSLETTER_CONTENT_WIDTH if self.position == 'top' else (settings.NEWSLETTER_CONTENT_WIDTH - 20 - settings.TEXT_AND_IMAGE_CONTENT_IMAGE_WIDTH_SIDE)
return {
Expand All @@ -140,21 +143,19 @@ def get_image_url(self, context=None):
return self.image_url
template = Template(self.image_url_replaced)
return template.render(context)

def replace_links(self, job):
super(TextWithImageNewsletterContent, self).replace_links(job)
if not is_link(self.image_url, self.image_url_replaced):
self.image_url_replaced = job.add_link(self.image_url)
self.save()

def save(self, *args, **kwargs):
image_width = settings.NEWSLETTER_CONTENT_WIDTH if self.position == 'top' else settings.TEXT_AND_IMAGE_CONTENT_IMAGE_WIDTH_SIDE
im=Image.open(self.image_original.file.path)
im = Image.open(self.image_original.file.path)
im.thumbnail((image_width, 1000), Image.ANTIALIAS)
img_temp = files.temp.NamedTemporaryFile()
im.save(img_temp,'jpeg', quality=settings.JPEG_QUALITY, optimize=True)
im.convert('RGB').save(img_temp, 'jpeg', quality=settings.JPEG_QUALITY, optimize=True)
img_temp.flush()
self.image_thumb.save(os.path.split(self.image_original.file.name)[1], files.File(img_temp), save=False)
super(TextWithImageNewsletterContent, self).save(*args, **kwargs)


20 changes: 13 additions & 7 deletions pennyblack/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,35 @@
LANGUAGES = getattr(settings, 'LANGUAGES')
LANGUAGE_CODE = getattr(settings, 'LANGUAGE_CODE')

NEWSLETTER_TYPE = getattr(settings, 'PENNYPLACK_NEWLETTER_TYPE', ((1, 'Massmail'),(2, 'Workflow')))
NEWSLETTER_TYPE = getattr(settings, 'PENNYPLACK_NEWLETTER_TYPE', ((1, 'Massmail'), (2, 'Workflow')))
NEWSLETTER_TYPE_MASSMAIL = getattr(settings, 'PENNYPLACK_NEWLETTER_TYPE_MASSMAIL', (1,))
NEWSLETTER_TYPE_WORKFLOW = getattr(settings, 'PENNYPLACK_NEWLETTER_TYPE_WORKFLOW', (2,))

JOB_STATUS = getattr(settings, 'PENNYBLACK_JOB_STATUS', ((1,'Draft'),(11,'Pending'),(21,'Sending'),(31,'Finished'),(41,'Error'),(42,'Timeout (will retry)'),(32,'ReadOnly')))
# hide attachments by default
NEWSLETTER_SHOW_ATTACHMENTS = getattr(settings, 'PENNYBLACK_NEWSLETTER_SHOW_ATTACHMENTS', False)

JOB_STATUS_CAN_SEND = getattr(settings, 'PENNYBLACK_JOB_STATUS_CAN_SEND', (1,41))
JOB_STATUS_PENDING = getattr(settings, 'PENNYBLACK_JOB_STATUS_PENDING', (11,42))
JOB_STATUS = getattr(settings, 'PENNYBLACK_JOB_STATUS', ((1, 'Draft'), (11, 'Pending'), (21, 'Sending'), (31, 'Finished'), (41, 'Error'), (42, 'Timeout (will retry)'), (32, 'ReadOnly')))

JOB_STATUS_CAN_SEND = getattr(settings, 'PENNYBLACK_JOB_STATUS_CAN_SEND', (1, 41))
JOB_STATUS_PENDING = getattr(settings, 'PENNYBLACK_JOB_STATUS_PENDING', (11, 42))
JOB_STATUS_CAN_EDIT = getattr(settings, 'PENNYBLACK_JOB_STATUS_CAN_EDIT', (1,))
JOB_STATUS_CAN_VIEW_PUBLIC = getattr(settings, 'PENNYBLACK_JOB_STATUS_CAN_VIEW_PUBLIC', (11, 21, 31, 42, 32))
JOB_MAIL_INLINE_COUNT = getattr(settings, 'PENNYBLACK_JOB_MAIL_INLINE_COUNT', 50)
# bounce detection
BOUNCE_DETECTION_ENABLE = getattr(settings, 'PENNYBLACK_BOUNCE_DETECTION_ENABLE', False)
BOUNCE_DETECTION_DAYS_TO_LOOK_BACK = getattr(settings, 'PENNYBLACK_BOUNCE_DETECTION_DAYS_TO_LOOK_BACK',5)
BOUNCE_DETECTION_DAYS_TO_LOOK_BACK = getattr(settings, 'PENNYBLACK_BOUNCE_DETECTION_DAYS_TO_LOOK_BACK', 5)
BOUNCE_DETECTION_BOUNCE_EMAIL_FOLDER = getattr(settings, 'PENNYBLACK_BOUNCE_DETECTION_BOUNCE_EMAIL_FOLDER', 'INBOX.bounced')
# getmail interval in minutes
BOUNCE_DETECTION_GETMAIL_INTERVAL = getattr(settings, 'PENNYBLACK_BOUNCE_DETECTION_GETMAIL_INTERVAL', 15)

# content
NEWSLETTER_CONTENT_WIDTH = getattr(settings, 'PENNYBLACK_NEWSLETTER_CONTENT_WIDTH', 600)

TEXT_AND_IMAGE_CONTENT_POSITIONS = getattr(settings, 'PENNYBLACK_TEXT_AND_IMAGE_CONTENT_POSITIONS', (('left','Left'),('right','Right'),('top','Top')))
TEXT_AND_IMAGE_CONTENT_POSITIONS = getattr(settings, 'PENNYBLACK_TEXT_AND_IMAGE_CONTENT_POSITIONS', (('left', 'Left'), ('right', 'Right'), ('top', 'Top')))
TEXT_AND_IMAGE_CONTENT_IMAGE_WIDTH_SIDE = getattr(settings, 'PENNYBLACK_TEXT_AND_IMAGE_CONTENT_IMAGE_WIDTH_SIDE', 100)

JPEG_QUALITY = getattr(settings, 'PENNYBLACK_JPEG_QUALITY', 75)

# subscriber module

SUBSCRIBER_BOUNCES_UNTIL_DEACTIVATION = getattr(settings, 'SUBSCRIBER_BOUNCES_UNTIL_DEACTIVATION', 2)
SUBSCRIBER_BOUNCES_UNTIL_DEACTIVATION = getattr(settings, 'SUBSCRIBER_BOUNCES_UNTIL_DEACTIVATION', 2)
12 changes: 7 additions & 5 deletions pennyblack/forms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from django import forms


class CollectionSelectForm(forms.Form):
collections = forms.MultipleChoiceField()

def __init__(self, group_object=None, extra_fields=None, *args, **kwargs):
super(CollectionSelectForm, self).__init__(*args, **kwargs)
choices = tuple((number,c[0]) for number, c in enumerate(group_object.get_newsletter_receiver_collections()))
self.fields['collections'].choices = choices
for key, field in extra_fields.items():
self.fields[key] = field
super(CollectionSelectForm, self).__init__(*args, **kwargs)
choices = tuple((number, c[0]) for number, c in enumerate(group_object.get_newsletter_receiver_collections()))
self.fields['collections'].choices = choices
for key, field in extra_fields.items():
self.fields[key] = field
3 changes: 2 additions & 1 deletion pennyblack/management/commands/getmail.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.core.management.base import BaseCommand, CommandError
from django.core.management.base import BaseCommand
from pennyblack.models import Sender


class Command(BaseCommand):
args = ''
help = 'Gets all Bounce emails'
Expand Down
3 changes: 2 additions & 1 deletion pennyblack/management/commands/sendmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pennyblack.models import Job
from pennyblack import settings


class Command(BaseCommand):
args = ''
help = 'Sends all pending Newsletters'
Expand All @@ -10,4 +11,4 @@ def handle(self, *args, **options):
pending_jobs = Job.objects.filter(status__in=settings.JOB_STATUS_PENDING)
for job in pending_jobs:
job.send()
print str(job)+ ' sent'
print u"%s sent" % job
5 changes: 3 additions & 2 deletions pennyblack/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# coding=utf-8
from pennyblack.models.newsletter import Newsletter
from pennyblack.models.job import Job
from pennyblack.models.job import Job, JobStatistic
from pennyblack.models.link import Link, LinkClick
from pennyblack.models.mail import Mail
from pennyblack.models.sender import Sender
from pennyblack.models.emailclient import EmailClient

__all__ = ('Newsletter','Job', 'Link', 'LinkClick', 'Mail', 'Sender')
__all__ = ('Newsletter', 'Job', 'JobStatistic', 'Link', 'LinkClick', 'Mail', 'Sender', 'EmailClient')
33 changes: 33 additions & 0 deletions pennyblack/models/emailclient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""
http://user-agent-string.info/
"""
from django.db import models
from django.utils.translation import ugettext_lazy as _

import datetime
try:
from django.utils import timezone
except ImportError:
now = datetime.datetime.now
else:
now = timezone.now


class EmailClient(models.Model):
"""
Stores some information about the used email client and about the user
"""
mail = models.ForeignKey('pennyblack.Mail', related_name='clients')
user_agent = models.CharField(max_length=255, db_index=True)
referer = models.CharField(max_length=1023, blank=True)
ip_address = models.IPAddressField()
visited = models.DateTimeField(default=now)
contact_type = models.CharField(max_length=15, default='')

class Meta:
verbose_name = _('email client')
verbose_name_plural = _('email clients')
app_label = 'pennyblack'

def __unicode__(self):
return self.user_agent
Loading