├── mailchimp ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── mcwebhooks.py │ │ ├── mcsitegroups.py │ │ ├── mcmakemerge.py │ │ └── mcdequeue.py ├── migrations │ ├── __init__.py │ ├── 0001_initial.py │ ├── 0003_fixed_template_id.py │ ├── 0004_fixed_template_id_max.py │ ├── 0006_added_locks.py │ ├── 0007_extra_info.py │ ├── 0005_added_link_to_object.py │ └── 0002_added_queue.py ├── templatetags │ ├── __init__.py │ ├── mailchimp_admin_tags.py │ └── mailchimp_tags.py ├── chimpy │ ├── __init__.py │ ├── README.txt │ ├── utils.py │ ├── test_chimpy.py │ └── chimpy.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── django.po │ └── it │ │ └── LC_MESSAGES │ │ └── django.po ├── __init__.py ├── constants.py ├── exceptions.py ├── cron.py ├── signals.py ├── admin.py ├── urls.py ├── templates │ └── mailchimp │ │ ├── send_test.html │ │ ├── campaign_information.html │ │ ├── send_button.html │ │ └── overview.html ├── settings.py ├── views.py ├── models.py ├── utils.py └── chimp.py ├── .gitignore ├── MANIFEST.in ├── setup.py ├── LICENSE └── README.rst /mailchimp/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mailchimp/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mailchimp/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mailchimp/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mailchimp/chimpy/__init__.py: -------------------------------------------------------------------------------- 1 | from chimpy import Connection 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .pydevproject 4 | .project 5 | django_mailchimp.egg-info 6 | dist 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README 2 | include LICENSE 3 | recursive-include mailchimp/templates * 4 | recursive-include mailchimp/locale * -------------------------------------------------------------------------------- /mailchimp/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojii/django-mailchimp/HEAD/mailchimp/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /mailchimp/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojii/django-mailchimp/HEAD/mailchimp/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /mailchimp/templatetags/mailchimp_admin_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | @register.filter 6 | def can_dequeue(user, obj): 7 | return obj.can_dequeue(user) -------------------------------------------------------------------------------- /mailchimp/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Note about version numbers: 3 | 4 | All versions with an uneven minor version number are unstable! Versions with 5 | even minor version numbers are considered stable. 6 | """ 7 | __version__ = '0.1.27' 8 | -------------------------------------------------------------------------------- /mailchimp/constants.py: -------------------------------------------------------------------------------- 1 | STATUS_OK = "Everything's Chimpy!" 2 | REGULAR_CAMPAIGN = 'regular' 3 | PLAINTEXT_CAMPAIGN = 'plaintext' 4 | ABSPLIT_CAMPAIGN = 'absplit' 5 | RSS_CAMPAIGN = 'rss' 6 | TRANS_CAMPAIGN = 'trans' 7 | AUTO_CAMPAIGN = 'auto' 8 | -------------------------------------------------------------------------------- /mailchimp/exceptions.py: -------------------------------------------------------------------------------- 1 | class ChimpException(Exception): pass 2 | 3 | class MCCampaignDoesNotExist(ChimpException): pass 4 | class MCListDoesNotExist(ChimpException): pass 5 | class MCConnectionFailed(ChimpException): pass 6 | class MCTemplateDoesNotExist(ChimpException): pass 7 | 8 | class MailchimpWarning(Warning): pass -------------------------------------------------------------------------------- /mailchimp/cron.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example cronjob: 3 | 4 | from cronjobs.base import Cron 5 | from cronjobs.constants import MINUTES 6 | from mailchimp.utils import dequeue 7 | 8 | class DequeueCron(Cron): 9 | run_every = 1 10 | interval_unit = MINUTES 11 | 12 | def job(self): 13 | try: 14 | dequeue() 15 | return True 16 | except: 17 | return False 18 | """ -------------------------------------------------------------------------------- /mailchimp/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | args = ["list", "fired_at", "email", "interests", "fname", "lname", "merges"] 4 | 5 | mc_subscribe = Signal(providing_args=args) 6 | mc_unsubscribe = Signal(providing_args=args) 7 | mc_profile = Signal(providing_args=args) 8 | mc_upemail = Signal(providing_args=["list", "old_email", "new_email", "fired_at"]) 9 | mc_cleaned = Signal(providing_args=["fired_at", "list", "reason", "email"]) 10 | 11 | 12 | def get_signal(name): 13 | return globals()['mc_%s' % name] -------------------------------------------------------------------------------- /mailchimp/chimpy/README.txt: -------------------------------------------------------------------------------- 1 | chimpy 2 | ====== 3 | 4 | MailChimp_ is a commercial mailing lists online service. This project provides 5 | a python wrapper around the API. 6 | 7 | Status: the initial code is a thin wrapper around the list management methods and 8 | most of the campaign methods. This functionality is working. 9 | 10 | 11 | .. _MailChimp: http://www.mailchimp.com 12 | 13 | 14 | Usage 15 | ----- 16 | 17 | You need an MailChimp account and api key to get started. 18 | 19 | See the tests for examples. 20 | 21 | 22 | License 23 | ------- 24 | 25 | New BSD License 26 | 27 | -------------------------------------------------------------------------------- /mailchimp/management/commands/mcwebhooks.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from mailchimp.utils import get_connection 3 | 4 | 5 | class Command(BaseCommand): 6 | def handle(self, *args, **options): 7 | print 'Installing webhooks for all lists' 8 | c = get_connection() 9 | for list in c.lists.values(): 10 | print 'Checking list %s' % list.name 11 | # def add_webhook_if_not_exists(self, url, actions, sources): 12 | if list.install_webhook(): 13 | print ' ok' 14 | else: 15 | print ' ERROR!' 16 | print 'Done' -------------------------------------------------------------------------------- /mailchimp/management/commands/mcsitegroups.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from django.contrib.sites.models import Site 3 | from mailchimp.utils import get_connection 4 | 5 | 6 | class Command(BaseCommand): 7 | def handle(self, *args, **options): 8 | print 'Installing site segment groups for all lists and all sites' 9 | c = get_connection() 10 | interests = [] 11 | for site in Site.objects.all(): 12 | interests.append(site.domain) 13 | for list in c.lists.values(): 14 | print 'Checking list %s' % list.name 15 | list.add_interests_if_not_exist(*interests) 16 | print ' ok' 17 | print 'Done' -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = __import__('mailchimp').__version__ 4 | 5 | setup( 6 | name = 'django-mailchimp', 7 | version = version, 8 | description = 'Mailchimp wrapper for Django', 9 | author = 'Jonas Obrist', 10 | url = 'http://github.com/ojii/django-mailchimp', 11 | packages = find_packages(), 12 | zip_safe=False, 13 | package_data={ 14 | 'mailchimp': [ 15 | 'templates/mailchimp/*.html', 16 | 'locale/en/LC_MESSAGES/django.po', 17 | 'locale/en/LC_MESSAGES/django.mo', 18 | 'locale/de/LC_MESSAGES/django.po', 19 | 'locale/de/LC_MESSAGES/django.mo', 20 | ], 21 | }, 22 | ) -------------------------------------------------------------------------------- /mailchimp/management/commands/mcmakemerge.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from django.contrib.sites.models import Site 3 | from mailchimp.utils import get_connection 4 | 5 | 6 | class Command(BaseCommand): 7 | def handle(self, *args, **options): 8 | if len(args) != 1: 9 | print 'You have to specify exactly one argument to this command' 10 | return 11 | merge = args[0] 12 | print 'Adding the merge var `%s` to all lists' % merge 13 | c = get_connection() 14 | for list in c.lists.values(): 15 | print 'Checking list %s' % list.name 16 | list.add_merges_if_not_exists(merge) 17 | print ' ok' 18 | print 'Done' -------------------------------------------------------------------------------- /mailchimp/management/commands/mcdequeue.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from mailchimp.utils import dequeue 3 | from optparse import make_option 4 | 5 | 6 | class Command(BaseCommand): 7 | 8 | def handle(self, *args, **options): 9 | if len(args) and args[0].isdigit(): 10 | limit = int(args[0]) 11 | else: 12 | limit = None 13 | print 'Dequeueing Campaigns' 14 | done = False 15 | for camp in dequeue(limit): 16 | done = True 17 | if camp: 18 | print '- Dequeued campaign %s (%s)' % (camp.name, camp.campaign_id) 19 | else: 20 | print 'ERROR' 21 | if not done: 22 | print 'Nothing to dequeue' 23 | print 'Done' -------------------------------------------------------------------------------- /mailchimp/chimpy/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | def transform_datetime(dt): 4 | """ converts datetime parameter""" 5 | 6 | if dt is None: 7 | dt = '' 8 | else: 9 | assert isinstance(dt, datetime) 10 | dt = dt.strftime('%Y-%m-%d %H:%M:%S') 11 | 12 | return dt 13 | 14 | 15 | def flatten(params, key=None): 16 | """ flatten nested dictionaries and lists """ 17 | flat = {} 18 | for name, val in params.items(): 19 | if key is not None and not isinstance(key, int): 20 | name = "%s[%s]" % (key, name) 21 | if isinstance(val, dict): 22 | flat.update(flatten(val, name)) 23 | elif isinstance(val, list): 24 | flat.update(flatten(dict(enumerate(val)), name)) 25 | elif val is not None: 26 | flat[name] = val 27 | return flat 28 | 29 | -------------------------------------------------------------------------------- /mailchimp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from mailchimp.models import Campaign 3 | from mailchimp.settings import VIEWS_OVERVIEW 4 | 5 | 6 | class MailchimpAdmin(admin.ModelAdmin): 7 | def get_urls(self): 8 | from django.conf.urls.defaults import patterns, url 9 | urlpatterns = patterns('', 10 | url(r'^$', 11 | VIEWS_OVERVIEW, 12 | name='mailchimp_campaign_changelist', 13 | kwargs={'page':'1'}), 14 | ) 15 | return urlpatterns 16 | 17 | def has_add_permission(self, request): 18 | # disable the 'add' button 19 | return False 20 | 21 | def has_change_permission(self, request, obj=None): 22 | return request.user.has_perm('mailchimp.can_view') 23 | 24 | def has_delete_permission(self, request, obj=None): 25 | return False 26 | 27 | 28 | admin.site.register(Campaign, MailchimpAdmin) -------------------------------------------------------------------------------- /mailchimp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from mailchimp.settings import VIEWS_INFO, VIEWS_OVERVIEW, VIEWS_SCHEDULE_OBJECT, VIEWS_TEST_OBJECT 3 | from mailchimp.views import webhook, dequeue, cancel, test_real 4 | 5 | urlpatterns = patterns('', 6 | url(r'^$', VIEWS_OVERVIEW, name='mailchimp_overview', kwargs={'page':'1'}), 7 | url(r'^(?P\d+)/$', VIEWS_OVERVIEW, name='mailchimp_overview'), 8 | url(r'^send/(?P\d+)/(?P\d+)/$', VIEWS_SCHEDULE_OBJECT, name='mailchimp_schedule_for_object'), 9 | url(r'^test/(?P\d+)/(?P\d+)/$', VIEWS_TEST_OBJECT, name='mailchimp_test_for_object'), 10 | url(r'^test/(?P\d+)/(?P\d+)/real/$', test_real, name='mailchimp_real_test_for_object'), 11 | url(r'^info/(?P\w+)/$', VIEWS_INFO, name='mailchimp_campaign_info'), 12 | url(r'^dequeue/(?P\d+)/', dequeue, name='mailchimp_dequeue'), 13 | url(r'^cancel/(?P\d+)/', cancel, name='mailchimp_cancel'), 14 | url(r'^webhook/(?P\w+)/', webhook, name='mailchimp_webhook'), 15 | ) 16 | -------------------------------------------------------------------------------- /mailchimp/templates/mailchimp/send_test.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_modify adminmedia %} 3 | 4 | {% block extrahead %}{{ block.super }} 5 | 6 | {% endblock %} 7 | 8 | {% block bodyclass %}change-form{% endblock %} 9 | 10 | {% block breadcrumbs %}{% endblock %} 11 | 12 | {% block content %} 13 | 22 |
23 |
24 |

{% trans "Test Sending in Progress" %}

25 |

{% blocktrans %}Please do not leave the page while the test mail is being sent. 26 | This process might take several minutes.{% endblocktrans %}

27 |
28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /mailchimp/templatetags/mailchimp_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.utils.translation import ugettext_lazy as _ 4 | from django.conf import settings 5 | from mailchimp.utils import is_queued_or_sent 6 | 7 | register = template.Library() 8 | 9 | 10 | def mailchimp_send_for_object(context, object): 11 | is_sent = is_queued_or_sent(object) 12 | sent_date = None 13 | campaign_id = None 14 | if is_sent and hasattr(is_sent, 'sent_date'): 15 | sent_date = is_sent.sent_date 16 | campaign_id = is_sent.campaign_id 17 | if hasattr(object, 'mailchimp_allow_send'): 18 | objchck = object.mailchimp_allow_send 19 | else: 20 | objchck = lambda r: True 21 | request = context['request'] 22 | return { 23 | 'content_type': ContentType.objects.get_for_model(object).pk, 24 | 'primary_key': object.pk, 25 | 'allow': request.user.has_perm('mailchimp.can_send') and objchck(request), 26 | 'is_sent': is_sent, 27 | 'sent_date': sent_date, 28 | 'campaign_id': campaign_id, 29 | 'can_view': sent_date and request.user.has_perm('mailchimp.can_view'), 30 | 'admin_prefix': settings.ADMIN_MEDIA_PREFIX, 31 | 'can_test': bool(request.user.email), 32 | } 33 | register.inclusion_tag('mailchimp/send_button.html', takes_context=True)(mailchimp_send_for_object) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Jonas Obrist 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Jonas Obrist nor the names of its contributors may be 12 | used to endorse or promote products derived from this software without 13 | specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /mailchimp/templates/mailchimp/campaign_information.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_modify adminmedia %} 3 | 4 | {% block bodyclass %}change-form{% endblock %} 5 | 6 | {% block breadcrumbs %}{% if not is_popup %} 7 | 12 | {% endif %}{% endblock %} 13 | 14 | {% block content %} 15 |
16 | 27 |
28 |

{% trans "Campaign Information:" %} {{ campaign.mc.subject }}

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% if campaign.object %} 39 | 40 | 41 | 42 | 43 | {% endif %} 44 |

{% trans "Sent Date" %}

{{ campaign.sent_date }}

{% trans "View on Mailchimp" %}

{% trans "Click here" %}

{% trans "object" %}

{{ campaign.object }}

45 |
46 |
47 | {% endblock %} -------------------------------------------------------------------------------- /mailchimp/templates/mailchimp/send_button.html: -------------------------------------------------------------------------------- 1 | {% block extrastyle %}{{ block.super }} 2 | {% endblock %} 16 | 17 | {% if allow %} 18 | {% load i18n %} 19 |
  • 20 | {% if is_sent %} 21 | {% if sent_date %} 22 | {% if can_view %} 23 | {% trans "Sent via mailchimp:" %}{{ sent_date }} 24 | {% else %} 25 | {% trans "Sent via mailchimp:" %}{{ sent_date }} 26 | {% endif %} 27 | {% else %} 28 | {% trans "scheduled for sending via mailchimp" %} 29 | {% endif %} 30 | {% else %} 31 | {% trans "Send via Mailchimp to ALL subscribers of this list." %} 32 |
  • 33 |
  • 34 | {% if can_test %} 35 | {% trans "Send Test Mail" %} 36 | {% else %} 37 | {% trans "your account requires a valid email to send test mails" %} 38 | {% endif %} 39 | {% endif %} 40 |
  • 41 | {% if not is_sent %} 42 | 54 | {% endif %} 55 | {% endif %} -------------------------------------------------------------------------------- /mailchimp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Campaign' 12 | db.create_table('mailchimp_campaign', ( 13 | ('content', self.gf('django.db.models.fields.TextField')()), 14 | ('sent_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), 15 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 16 | ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), 17 | ('campaign_id', self.gf('django.db.models.fields.CharField')(max_length=50)), 18 | )) 19 | db.send_create_signal('mailchimp', ['Campaign']) 20 | 21 | # Adding model 'Reciever' 22 | db.create_table('mailchimp_reciever', ( 23 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 24 | ('campaign', self.gf('django.db.models.fields.related.ForeignKey')(related_name='recievers', to=orm['mailchimp.Campaign'])), 25 | ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)), 26 | )) 27 | db.send_create_signal('mailchimp', ['Reciever']) 28 | 29 | 30 | def backwards(self, orm): 31 | 32 | # Deleting model 'Campaign' 33 | db.delete_table('mailchimp_campaign') 34 | 35 | # Deleting model 'Reciever' 36 | db.delete_table('mailchimp_reciever') 37 | 38 | 39 | models = { 40 | 'mailchimp.campaign': { 41 | 'Meta': {'object_name': 'Campaign'}, 42 | 'campaign_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 43 | 'content': ('django.db.models.fields.TextField', [], {}), 44 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 45 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 46 | 'sent_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) 47 | }, 48 | 'mailchimp.reciever': { 49 | 'Meta': {'object_name': 'Reciever'}, 50 | 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recievers'", 'to': "orm['mailchimp.Campaign']"}), 51 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 52 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 53 | } 54 | } 55 | 56 | complete_apps = ['mailchimp'] 57 | -------------------------------------------------------------------------------- /mailchimp/templates/mailchimp/overview.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load i18n admin_modify adminmedia mailchimp_admin_tags %} 3 | 4 | {% block bodyclass %}change-list{% endblock %} 5 | 6 | {% block breadcrumbs %}{% if not is_popup %} 7 | 11 | {% endif %}{% endblock %} 12 | 13 | {% block coltype %}flex{% endblock %} 14 | 15 | {% block content %} 16 |
    17 | {% block object-tools %} 18 | {% endblock %} 19 |

    {% trans "Log" %}

    20 |
    21 | 22 | 23 | 24 | {% trans "Subject" %} 25 | {% trans "Sent Date" %} 26 | 27 | 28 | 29 | {% for camp in paginator.objects %} 30 | 31 | 32 | 33 | 34 | {% endfor %} 35 | 36 |
    {{ camp.mc.subject }}{{ camp.sent_date }}
    37 | {% if paginator.has_pages %} 38 |

    39 | {% if not paginator.is_first %} 40 | {% trans "First" %} 41 | {% endif %} 42 | {% for bullet in paginator.bullets %} 43 | {% if bullet.active %} 44 | {{ bullet.number }} 45 | {% else %} 46 | {{ bullet.number }} 47 | {% endif %} 48 | {% endfor %} 49 | {% if not paginator.is_last %} 50 | {% trans "Last" %} 51 | {% endif %} 52 |

    53 | {% endif %} 54 |
    55 |

    {% trans "Queue" %}

    56 |
    57 | 58 | 59 | 60 | {% trans "Subject" %} 61 | {% trans "Actions" %} 62 | 63 | 64 | 65 | {% for obj in queue %} 66 | 67 | 68 | 69 | 70 | {% empty %} 71 | 72 | {% endfor %} 73 | 74 |
    {{ obj.subject }} {% trans "via" %} {{ obj.get_list.name }} ({{ obj.get_list.id }}){% if request.user|can_dequeue:obj %}{% trans "Send Now!" %} | {% trans "Cancel!" %}{% else %} {% endif %}
    {% trans "No Campaigns in the Queue" %}
    75 |
    76 |
    77 | 85 | {% endblock %} 86 | -------------------------------------------------------------------------------- /mailchimp/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2010-03-18 10:26+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: models.py:171 20 | msgid "Mailchimp Log" 21 | msgstr "" 22 | 23 | #: models.py:172 24 | msgid "Mailchimp Logs" 25 | msgstr "" 26 | 27 | #: templates/mailchimp/campaign_information.html:8 28 | #: templates/mailchimp/overview.html:8 29 | msgid "Home" 30 | msgstr "" 31 | 32 | #: templates/mailchimp/campaign_information.html:9 33 | #: templates/mailchimp/overview.html:9 34 | msgid "mailchimp overview" 35 | msgstr "" 36 | 37 | #: templates/mailchimp/campaign_information.html:28 38 | msgid "Campaign Subject" 39 | msgstr "" 40 | 41 | #: templates/mailchimp/campaign_information.html:31 42 | #: templates/mailchimp/overview.html:25 43 | msgid "Sent Date" 44 | msgstr "" 45 | 46 | #: templates/mailchimp/campaign_information.html:35 47 | msgid "Recipients" 48 | msgstr "" 49 | 50 | #: templates/mailchimp/campaign_information.html:40 51 | msgid "Object" 52 | msgstr "" 53 | 54 | #: templates/mailchimp/overview.html:19 55 | msgid "Log" 56 | msgstr "" 57 | 58 | #: templates/mailchimp/overview.html:24 templates/mailchimp/overview.html:60 59 | msgid "Subject" 60 | msgstr "" 61 | 62 | #: templates/mailchimp/overview.html:40 63 | msgid "First" 64 | msgstr "" 65 | 66 | #: templates/mailchimp/overview.html:50 67 | msgid "Last" 68 | msgstr "" 69 | 70 | #: templates/mailchimp/overview.html:55 71 | msgid "Queue" 72 | msgstr "" 73 | 74 | #: templates/mailchimp/overview.html:61 75 | msgid "Actions" 76 | msgstr "" 77 | 78 | #: templates/mailchimp/overview.html:68 79 | msgid "Send Now!" 80 | msgstr "" 81 | 82 | #: templates/mailchimp/overview.html:68 83 | msgid "Cancel!" 84 | msgstr "" 85 | 86 | #: templates/mailchimp/overview.html:71 87 | msgid "No Campaigns in the Queue" 88 | msgstr "" 89 | 90 | #: templates/mailchimp/send_button.html:23 91 | #: templates/mailchimp/send_button.html:25 92 | msgid "Sent via mailchimp:" 93 | msgstr "" 94 | 95 | #: templates/mailchimp/send_button.html:28 96 | msgid "scheduled for sending via mailchimp" 97 | msgstr "" 98 | 99 | #: templates/mailchimp/send_button.html:31 100 | msgid "Send via Mailchimp" 101 | msgstr "" 102 | 103 | #: templates/mailchimp/send_button.html:35 104 | msgid "Send Test Mail" 105 | msgstr "" 106 | 107 | #: templates/mailchimp/send_button.html:37 108 | msgid "your account requires a valid email to send test mails" 109 | msgstr "" 110 | 111 | #: templates/mailchimp/send_test.html:22 112 | msgid "Test Sending in Progress" 113 | msgstr "" 114 | 115 | #: templates/mailchimp/send_test.html:23 116 | msgid "" 117 | "Please do not leave the page while the test mail is being sent.\n" 118 | "\t\tThis process might take several minutes." 119 | msgstr "" 120 | -------------------------------------------------------------------------------- /mailchimp/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2010-03-18 10:26+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: models.py:171 20 | msgid "Mailchimp Log" 21 | msgstr "" 22 | 23 | #: models.py:172 24 | msgid "Mailchimp Logs" 25 | msgstr "" 26 | 27 | #: templates/mailchimp/campaign_information.html:8 28 | #: templates/mailchimp/overview.html:8 29 | msgid "Home" 30 | msgstr "" 31 | 32 | #: templates/mailchimp/campaign_information.html:9 33 | #: templates/mailchimp/overview.html:9 34 | msgid "mailchimp overview" 35 | msgstr "" 36 | 37 | #: templates/mailchimp/campaign_information.html:28 38 | msgid "Campaign Subject" 39 | msgstr "" 40 | 41 | #: templates/mailchimp/campaign_information.html:31 42 | #: templates/mailchimp/overview.html:25 43 | msgid "Sent Date" 44 | msgstr "" 45 | 46 | #: templates/mailchimp/campaign_information.html:35 47 | msgid "Recipients" 48 | msgstr "" 49 | 50 | #: templates/mailchimp/campaign_information.html:40 51 | msgid "Object" 52 | msgstr "" 53 | 54 | #: templates/mailchimp/overview.html:19 55 | msgid "Log" 56 | msgstr "" 57 | 58 | #: templates/mailchimp/overview.html:24 templates/mailchimp/overview.html:60 59 | msgid "Subject" 60 | msgstr "" 61 | 62 | #: templates/mailchimp/overview.html:40 63 | msgid "First" 64 | msgstr "" 65 | 66 | #: templates/mailchimp/overview.html:50 67 | msgid "Last" 68 | msgstr "" 69 | 70 | #: templates/mailchimp/overview.html:55 71 | msgid "Queue" 72 | msgstr "" 73 | 74 | #: templates/mailchimp/overview.html:61 75 | msgid "Actions" 76 | msgstr "" 77 | 78 | #: templates/mailchimp/overview.html:68 79 | msgid "Send Now!" 80 | msgstr "" 81 | 82 | #: templates/mailchimp/overview.html:68 83 | msgid "Cancel!" 84 | msgstr "" 85 | 86 | #: templates/mailchimp/overview.html:71 87 | msgid "No Campaigns in the Queue" 88 | msgstr "" 89 | 90 | #: templates/mailchimp/send_button.html:23 91 | #: templates/mailchimp/send_button.html:25 92 | msgid "Sent via mailchimp:" 93 | msgstr "" 94 | 95 | #: templates/mailchimp/send_button.html:28 96 | msgid "scheduled for sending via mailchimp" 97 | msgstr "" 98 | 99 | #: templates/mailchimp/send_button.html:31 100 | msgid "Send via Mailchimp" 101 | msgstr "" 102 | 103 | #: templates/mailchimp/send_button.html:35 104 | msgid "Send Test Mail" 105 | msgstr "" 106 | 107 | #: templates/mailchimp/send_button.html:37 108 | msgid "your account requires a valid email to send test mails" 109 | msgstr "" 110 | 111 | #: templates/mailchimp/send_test.html:22 112 | msgid "Test Sending in Progress" 113 | msgstr "" 114 | 115 | #: templates/mailchimp/send_test.html:23 116 | msgid "" 117 | "Please do not leave the page while the test mail is being sent.\n" 118 | "\t\tThis process might take several minutes." 119 | msgstr "" 120 | -------------------------------------------------------------------------------- /mailchimp/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2010-03-18 10:26+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: models.py:171 20 | msgid "Mailchimp Log" 21 | msgstr "" 22 | 23 | #: models.py:172 24 | msgid "Mailchimp Logs" 25 | msgstr "" 26 | 27 | #: templates/mailchimp/campaign_information.html:8 28 | #: templates/mailchimp/overview.html:8 29 | msgid "Home" 30 | msgstr "" 31 | 32 | #: templates/mailchimp/campaign_information.html:9 33 | #: templates/mailchimp/overview.html:9 34 | msgid "mailchimp overview" 35 | msgstr "" 36 | 37 | #: templates/mailchimp/campaign_information.html:28 38 | msgid "Campaign Subject" 39 | msgstr "" 40 | 41 | #: templates/mailchimp/campaign_information.html:31 42 | #: templates/mailchimp/overview.html:25 43 | msgid "Sent Date" 44 | msgstr "" 45 | 46 | #: templates/mailchimp/campaign_information.html:35 47 | msgid "Recipients" 48 | msgstr "" 49 | 50 | #: templates/mailchimp/campaign_information.html:40 51 | msgid "Object" 52 | msgstr "" 53 | 54 | #: templates/mailchimp/overview.html:19 55 | msgid "Log" 56 | msgstr "" 57 | 58 | #: templates/mailchimp/overview.html:24 templates/mailchimp/overview.html:60 59 | msgid "Subject" 60 | msgstr "" 61 | 62 | #: templates/mailchimp/overview.html:40 63 | msgid "First" 64 | msgstr "" 65 | 66 | #: templates/mailchimp/overview.html:50 67 | msgid "Last" 68 | msgstr "" 69 | 70 | #: templates/mailchimp/overview.html:55 71 | msgid "Queue" 72 | msgstr "" 73 | 74 | #: templates/mailchimp/overview.html:61 75 | msgid "Actions" 76 | msgstr "" 77 | 78 | #: templates/mailchimp/overview.html:68 79 | msgid "Send Now!" 80 | msgstr "" 81 | 82 | #: templates/mailchimp/overview.html:68 83 | msgid "Cancel!" 84 | msgstr "" 85 | 86 | #: templates/mailchimp/overview.html:71 87 | msgid "No Campaigns in the Queue" 88 | msgstr "" 89 | 90 | #: templates/mailchimp/send_button.html:23 91 | #: templates/mailchimp/send_button.html:25 92 | msgid "Sent via mailchimp:" 93 | msgstr "" 94 | 95 | #: templates/mailchimp/send_button.html:28 96 | msgid "scheduled for sending via mailchimp" 97 | msgstr "" 98 | 99 | #: templates/mailchimp/send_button.html:31 100 | msgid "Send via Mailchimp" 101 | msgstr "" 102 | 103 | #: templates/mailchimp/send_button.html:35 104 | msgid "Send Test Mail" 105 | msgstr "" 106 | 107 | #: templates/mailchimp/send_button.html:37 108 | msgid "your account requires a valid email to send test mails" 109 | msgstr "" 110 | 111 | #: templates/mailchimp/send_test.html:22 112 | msgid "Test Sending in Progress" 113 | msgstr "" 114 | 115 | #: templates/mailchimp/send_test.html:23 116 | msgid "" 117 | "Please do not leave the page while the test mail is being sent.\n" 118 | "\t\tThis process might take several minutes." 119 | msgstr "" 120 | -------------------------------------------------------------------------------- /mailchimp/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2010-03-18 10:26+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: models.py:171 20 | msgid "Mailchimp Log" 21 | msgstr "" 22 | 23 | #: models.py:172 24 | msgid "Mailchimp Logs" 25 | msgstr "" 26 | 27 | #: templates/mailchimp/campaign_information.html:8 28 | #: templates/mailchimp/overview.html:8 29 | msgid "Home" 30 | msgstr "" 31 | 32 | #: templates/mailchimp/campaign_information.html:9 33 | #: templates/mailchimp/overview.html:9 34 | msgid "mailchimp overview" 35 | msgstr "" 36 | 37 | #: templates/mailchimp/campaign_information.html:28 38 | msgid "Campaign Subject" 39 | msgstr "" 40 | 41 | #: templates/mailchimp/campaign_information.html:31 42 | #: templates/mailchimp/overview.html:25 43 | msgid "Sent Date" 44 | msgstr "" 45 | 46 | #: templates/mailchimp/campaign_information.html:35 47 | msgid "Recipients" 48 | msgstr "" 49 | 50 | #: templates/mailchimp/campaign_information.html:40 51 | msgid "Object" 52 | msgstr "" 53 | 54 | #: templates/mailchimp/overview.html:19 55 | msgid "Log" 56 | msgstr "" 57 | 58 | #: templates/mailchimp/overview.html:24 templates/mailchimp/overview.html:60 59 | msgid "Subject" 60 | msgstr "" 61 | 62 | #: templates/mailchimp/overview.html:40 63 | msgid "First" 64 | msgstr "" 65 | 66 | #: templates/mailchimp/overview.html:50 67 | msgid "Last" 68 | msgstr "" 69 | 70 | #: templates/mailchimp/overview.html:55 71 | msgid "Queue" 72 | msgstr "" 73 | 74 | #: templates/mailchimp/overview.html:61 75 | msgid "Actions" 76 | msgstr "" 77 | 78 | #: templates/mailchimp/overview.html:68 79 | msgid "Send Now!" 80 | msgstr "" 81 | 82 | #: templates/mailchimp/overview.html:68 83 | msgid "Cancel!" 84 | msgstr "" 85 | 86 | #: templates/mailchimp/overview.html:71 87 | msgid "No Campaigns in the Queue" 88 | msgstr "" 89 | 90 | #: templates/mailchimp/send_button.html:23 91 | #: templates/mailchimp/send_button.html:25 92 | msgid "Sent via mailchimp:" 93 | msgstr "" 94 | 95 | #: templates/mailchimp/send_button.html:28 96 | msgid "scheduled for sending via mailchimp" 97 | msgstr "" 98 | 99 | #: templates/mailchimp/send_button.html:31 100 | msgid "Send via Mailchimp" 101 | msgstr "" 102 | 103 | #: templates/mailchimp/send_button.html:35 104 | msgid "Send Test Mail" 105 | msgstr "" 106 | 107 | #: templates/mailchimp/send_button.html:37 108 | msgid "your account requires a valid email to send test mails" 109 | msgstr "" 110 | 111 | #: templates/mailchimp/send_test.html:22 112 | msgid "Test Sending in Progress" 113 | msgstr "" 114 | 115 | #: templates/mailchimp/send_test.html:23 116 | msgid "" 117 | "Please do not leave the page while the test mail is being sent.\n" 118 | "\t\tThis process might take several minutes." 119 | msgstr "" 120 | -------------------------------------------------------------------------------- /mailchimp/migrations/0003_fixed_template_id.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Changing field 'Queue.template_id' 12 | db.alter_column('mailchimp_queue', 'template_id', self.gf('django.db.models.fields.PositiveSmallIntegerField')()) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Changing field 'Queue.template_id' 18 | db.alter_column('mailchimp_queue', 'template_id', self.gf('django.db.models.fields.CharField')(max_length=50)) 19 | 20 | 21 | models = { 22 | 'mailchimp.campaign': { 23 | 'Meta': {'object_name': 'Campaign'}, 24 | 'campaign_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 25 | 'content': ('django.db.models.fields.TextField', [], {}), 26 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 28 | 'sent_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) 29 | }, 30 | 'mailchimp.queue': { 31 | 'Meta': {'object_name': 'Queue'}, 32 | 'authenticate': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 33 | 'auto_footer': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 34 | 'auto_tweet': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 35 | 'campaign_type': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 36 | 'contents': ('django.db.models.fields.TextField', [], {}), 37 | 'folder_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), 38 | 'from_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 39 | 'from_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 40 | 'generate_text': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 41 | 'google_analytics': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'list_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 44 | 'segment_options': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 45 | 'segment_options_all': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 46 | 'segment_options_conditions': ('django.db.models.fields.TextField', [], {}), 47 | 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 48 | 'template_id': ('django.db.models.fields.PositiveSmallIntegerField', [], {}), 49 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 50 | 'to_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 51 | 'tracking_html_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 52 | 'tracking_opens': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 53 | 'tracking_text_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 54 | 'type_opts': ('django.db.models.fields.TextField', [], {}) 55 | }, 56 | 'mailchimp.reciever': { 57 | 'Meta': {'object_name': 'Reciever'}, 58 | 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recievers'", 'to': "orm['mailchimp.Campaign']"}), 59 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 61 | } 62 | } 63 | 64 | complete_apps = ['mailchimp'] 65 | -------------------------------------------------------------------------------- /mailchimp/migrations/0004_fixed_template_id_max.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Changing field 'Queue.template_id' 12 | db.alter_column('mailchimp_queue', 'template_id', self.gf('django.db.models.fields.PositiveIntegerField')()) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Changing field 'Queue.template_id' 18 | db.alter_column('mailchimp_queue', 'template_id', self.gf('django.db.models.fields.PositiveSmallIntegerField')()) 19 | 20 | 21 | models = { 22 | 'mailchimp.campaign': { 23 | 'Meta': {'object_name': 'Campaign'}, 24 | 'campaign_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 25 | 'content': ('django.db.models.fields.TextField', [], {}), 26 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 28 | 'sent_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) 29 | }, 30 | 'mailchimp.queue': { 31 | 'Meta': {'object_name': 'Queue'}, 32 | 'authenticate': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 33 | 'auto_footer': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 34 | 'auto_tweet': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 35 | 'campaign_type': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 36 | 'contents': ('django.db.models.fields.TextField', [], {}), 37 | 'folder_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), 38 | 'from_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 39 | 'from_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 40 | 'generate_text': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 41 | 'google_analytics': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'list_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 44 | 'segment_options': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 45 | 'segment_options_all': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 46 | 'segment_options_conditions': ('django.db.models.fields.TextField', [], {}), 47 | 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 48 | 'template_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 49 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 50 | 'to_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 51 | 'tracking_html_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 52 | 'tracking_opens': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 53 | 'tracking_text_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 54 | 'type_opts': ('django.db.models.fields.TextField', [], {}) 55 | }, 56 | 'mailchimp.reciever': { 57 | 'Meta': {'object_name': 'Reciever'}, 58 | 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recievers'", 'to': "orm['mailchimp.Campaign']"}), 59 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 61 | } 62 | } 63 | 64 | complete_apps = ['mailchimp'] 65 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Django Mailchimp 3 | ================ 4 | 5 | This is an integrated app for Django dealing with the Mailchimp mailing list system. 6 | 7 | Quick start guide: 8 | ------------------ 9 | 10 | Installation: 11 | ************* 12 | 13 | 1. Install django_mailchimp:: 14 | 15 | pip install django_mailchimp 16 | 17 | 2. Add an ``MAILCHIMP_API_KEY`` to your settings.py with your mailchimp API key as the value (obviously) 18 | 19 | 3. Add ``mailchimp`` to your project's list of INSTALLED_APPS 20 | 21 | 4. To start using the API, you should start by using utils.get_connection(). This will use the API_KEY you 22 | just defined in settings.py 23 | 24 | 25 | Subscribing a user to a list: 26 | ***************************** 27 | 28 | 1. To get the list:: 29 | 30 | list = mailchimp.utils.get_connection().get_list_by_id() 31 | 32 | 2. Now add a member to the mailing list:: 33 | 34 | list.subscribe('example@example.com',{'EMAIL':'example@example.com'}) 35 | 36 | 37 | Those pesky merge vars: 38 | ----------------------- 39 | 40 | General info: 41 | ************* 42 | 43 | Mailchimp is a quite generic service. As such, it needs to store information on people who subscribe to a list, 44 | and that information is specific to this very list! 45 | 46 | So to help you build dynamic forms (presumabely), mailchimp added the merge_vars. They are, basically, a 47 | dictionnary showing infromation and meta-information defined for each piece of information. 48 | Here's what the default set of merge vars look like (ona brand new list with default options):: 49 | 50 | [ 51 | { 52 | 'field_type': 'email', 53 | 'name': 'Email Address', 54 | 'show': True, 55 | 'default': None, 56 | 'req': True, 57 | 'public': True, 58 | 'tag': 'EMAIL', 59 | 'helptext': None, 60 | 'order': '1', 61 | 'size': '25' 62 | },{ 63 | 'field_type': 'text', 64 | 'name': 'First Name', 65 | 'show': True, 66 | 'default': '', 67 | 'req': False, 68 | 'public': True, 69 | 'tag': 'FNAME', 70 | 'helptext': '', 71 | 'order': '2', 72 | 'size': '25' 73 | },{ 74 | 'field_type': 'text', 75 | 'name': 'Last Name', 76 | 'show': True, 77 | 'default': '', 78 | 'req': False, 79 | 'public': True, 80 | 'tag': 'LNAME', 81 | 'helptext': '', 82 | 'order': '3', 83 | 'size': '25' 84 | } 85 | ] 86 | 87 | As you can see, it's a list of 3 dictionnaries, each containing several fields that you should use to build your 88 | user interface with (since you're using this app, that means your Django form). 89 | 90 | Obtaining them: 91 | *************** 92 | 93 | You can recreate this list using the following API call:: 94 | 95 | list = mailchimp.utils.get_connection().get_list_by_id() 96 | print list.merges 97 | 98 | 99 | Using them: 100 | *********** 101 | 102 | When you make a post to mailchimp, you need to pass merge_vars. For example, in a new list created with the default 103 | settings on the mailchimp website, the following call adds a member to a list (with a little more info than our bare minimum example up there):: 104 | 105 | list = mailchimp.utils.get_connection().get_list_by_id() 106 | list.subscribe('example@example.com',{'EMAIL':'example@example.com', 'FNAME': 'Monthy', 'LNAME':'Pyhtons'}) 107 | 108 | Note the use of the 'tag' field as the key for fields (why they didn't call it 'key' or 'id' is beyond comprehension). 109 | 110 | 111 | 112 | Create a view: 113 | -------------- 114 | 115 | We'll now try to move up the stack and create the necessary elements to make a useable mailchimp interface 116 | 117 | Fire up your favorite editor and open your views.py. Put in the following snippet of code:: 118 | 119 | MAILCHIMP_LIST_ID = 'spamspamspamspameggsspamspam' # DRY :) 120 | REDIRECT_URL_NAME = '/mailing_list_success/' 121 | def add_email_to_mailing_list(request): 122 | if request.POST['email']: 123 | email_address = requst.POST['email'] 124 | list = mailchimp.utils.get_connection().get_list_by_id(MAILCHIMP_LIST_ID) 125 | list.subscribe(email_address,{'EMAIL':email_address}) 126 | return HttpResponseRedirect('/mailing_list_success/') 127 | else: 128 | return HttpResponseRedirect('/mailing_list_failure/') 129 | 130 | Of course, if you feel redirecting the user is not the right approach (handling a form might be a good idea), feel 131 | free to adapt this simple example to your needs :p 132 | 133 | 134 | -------------------------------------------------------------------------------- /mailchimp/migrations/0006_added_locks.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Queue.locked' 12 | db.add_column('mailchimp_queue', 'locked', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'Queue.locked' 18 | db.delete_column('mailchimp_queue', 'locked') 19 | 20 | 21 | models = { 22 | 'contenttypes.contenttype': { 23 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 24 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 25 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 26 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 28 | }, 29 | 'mailchimp.campaign': { 30 | 'Meta': {'object_name': 'Campaign'}, 31 | 'campaign_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 32 | 'content': ('django.db.models.fields.TextField', [], {}), 33 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), 34 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 35 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 36 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 37 | 'sent_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) 38 | }, 39 | 'mailchimp.queue': { 40 | 'Meta': {'object_name': 'Queue'}, 41 | 'authenticate': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 42 | 'auto_footer': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 43 | 'auto_tweet': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 44 | 'campaign_type': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 45 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), 46 | 'contents': ('django.db.models.fields.TextField', [], {}), 47 | 'folder_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), 48 | 'from_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 49 | 'from_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 50 | 'generate_text': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 51 | 'google_analytics': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 52 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 53 | 'list_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 54 | 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 55 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 56 | 'segment_options': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 57 | 'segment_options_all': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 58 | 'segment_options_conditions': ('django.db.models.fields.TextField', [], {}), 59 | 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 60 | 'template_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 61 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 62 | 'to_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 63 | 'tracking_html_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 64 | 'tracking_opens': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 65 | 'tracking_text_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 66 | 'type_opts': ('django.db.models.fields.TextField', [], {}) 67 | }, 68 | 'mailchimp.reciever': { 69 | 'Meta': {'object_name': 'Reciever'}, 70 | 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recievers'", 'to': "orm['mailchimp.Campaign']"}), 71 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 72 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 73 | } 74 | } 75 | 76 | complete_apps = ['mailchimp'] 77 | -------------------------------------------------------------------------------- /mailchimp/migrations/0007_extra_info.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Campaign.extra_info' 12 | db.add_column('mailchimp_campaign', 'extra_info', self.gf('django.db.models.fields.TextField')(null=True)) 13 | 14 | # Adding field 'Queue.extra_info' 15 | db.add_column('mailchimp_queue', 'extra_info', self.gf('django.db.models.fields.TextField')(null=True)) 16 | 17 | 18 | def backwards(self, orm): 19 | 20 | # Deleting field 'Campaign.extra_info' 21 | db.delete_column('mailchimp_campaign', 'extra_info') 22 | 23 | # Deleting field 'Queue.extra_info' 24 | db.delete_column('mailchimp_queue', 'extra_info') 25 | 26 | 27 | models = { 28 | 'contenttypes.contenttype': { 29 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 30 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 32 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 34 | }, 35 | 'mailchimp.campaign': { 36 | 'Meta': {'object_name': 'Campaign'}, 37 | 'campaign_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 38 | 'content': ('django.db.models.fields.TextField', [], {}), 39 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), 40 | 'extra_info': ('django.db.models.fields.TextField', [], {'null': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 43 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 44 | 'sent_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) 45 | }, 46 | 'mailchimp.queue': { 47 | 'Meta': {'object_name': 'Queue'}, 48 | 'authenticate': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 49 | 'auto_footer': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 50 | 'auto_tweet': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 51 | 'campaign_type': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 52 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), 53 | 'contents': ('django.db.models.fields.TextField', [], {}), 54 | 'extra_info': ('django.db.models.fields.TextField', [], {'null': 'True'}), 55 | 'folder_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), 56 | 'from_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 57 | 'from_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 58 | 'generate_text': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 59 | 'google_analytics': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'list_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 62 | 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 63 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 64 | 'segment_options': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 65 | 'segment_options_all': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 66 | 'segment_options_conditions': ('django.db.models.fields.TextField', [], {}), 67 | 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 68 | 'template_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 69 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 70 | 'to_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 71 | 'tracking_html_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 72 | 'tracking_opens': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 73 | 'tracking_text_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 74 | 'type_opts': ('django.db.models.fields.TextField', [], {}) 75 | }, 76 | 'mailchimp.reciever': { 77 | 'Meta': {'object_name': 'Reciever'}, 78 | 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recievers'", 'to': "orm['mailchimp.Campaign']"}), 79 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 80 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 81 | } 82 | } 83 | 84 | complete_apps = ['mailchimp'] 85 | -------------------------------------------------------------------------------- /mailchimp/migrations/0005_added_link_to_object.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Campaign.object_id' 12 | db.add_column('mailchimp_campaign', 'object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)) 13 | 14 | # Adding field 'Campaign.content_type' 15 | db.add_column('mailchimp_campaign', 'content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'], null=True, blank=True)) 16 | 17 | # Adding field 'Queue.object_id' 18 | db.add_column('mailchimp_queue', 'object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)) 19 | 20 | # Adding field 'Queue.content_type' 21 | db.add_column('mailchimp_queue', 'content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'], null=True, blank=True)) 22 | 23 | 24 | def backwards(self, orm): 25 | 26 | # Deleting field 'Campaign.object_id' 27 | db.delete_column('mailchimp_campaign', 'object_id') 28 | 29 | # Deleting field 'Campaign.content_type' 30 | db.delete_column('mailchimp_campaign', 'content_type_id') 31 | 32 | # Deleting field 'Queue.object_id' 33 | db.delete_column('mailchimp_queue', 'object_id') 34 | 35 | # Deleting field 'Queue.content_type' 36 | db.delete_column('mailchimp_queue', 'content_type_id') 37 | 38 | 39 | models = { 40 | 'contenttypes.contenttype': { 41 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 42 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 43 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 44 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 45 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 46 | }, 47 | 'mailchimp.campaign': { 48 | 'Meta': {'object_name': 'Campaign'}, 49 | 'campaign_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 50 | 'content': ('django.db.models.fields.TextField', [], {}), 51 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), 52 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 53 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 54 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 55 | 'sent_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) 56 | }, 57 | 'mailchimp.queue': { 58 | 'Meta': {'object_name': 'Queue'}, 59 | 'authenticate': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 60 | 'auto_footer': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 61 | 'auto_tweet': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 62 | 'campaign_type': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 63 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), 64 | 'contents': ('django.db.models.fields.TextField', [], {}), 65 | 'folder_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), 66 | 'from_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 67 | 'from_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 68 | 'generate_text': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 69 | 'google_analytics': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 70 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 71 | 'list_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 72 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 73 | 'segment_options': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 74 | 'segment_options_all': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 75 | 'segment_options_conditions': ('django.db.models.fields.TextField', [], {}), 76 | 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 77 | 'template_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 78 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 79 | 'to_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 80 | 'tracking_html_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 81 | 'tracking_opens': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 82 | 'tracking_text_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 83 | 'type_opts': ('django.db.models.fields.TextField', [], {}) 84 | }, 85 | 'mailchimp.reciever': { 86 | 'Meta': {'object_name': 'Reciever'}, 87 | 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recievers'", 'to': "orm['mailchimp.Campaign']"}), 88 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 89 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 90 | } 91 | } 92 | 93 | complete_apps = ['mailchimp'] 94 | -------------------------------------------------------------------------------- /mailchimp/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | from mailchimp.exceptions import MailchimpWarning 4 | import warnings 5 | 6 | API_KEY = getattr(settings, 'MAILCHIMP_API_KEY', None) 7 | if API_KEY is None: 8 | raise ImproperlyConfigured('django-mailchimp requires the MAILCHIMP_API_KEY setting') 9 | 10 | SECURE = getattr(settings, 'MAILCHIMP_SECURE', True) 11 | 12 | # THIS DOES NOT WORK: 13 | #REAL_CACHE = bool(getattr(settings, 'MAILCHIMP_USE_REAL_CACHE', False)) 14 | """ 15 | In [1]: from mailchimp.utils import get_connection 16 | 17 | In [2]: c = get_connection () 18 | 19 | In [3]: c.campaigns 20 | key lists 21 | value {'f48ceea763': , 'a41f00cba2': } 22 | key lists 23 | value {'f48ceea763': , 'a41f00cba2': } 24 | --------------------------------------------------------------------------- 25 | TypeError Traceback (most recent call last) 26 | 27 | /home/jonas/workspace/affichage/ in () 28 | 29 | /home/jonas/workspace/django-mailchimp/mailchimp/chimp.pyc in campaigns(self) 30 | 354 @property 31 | 355 def campaigns(self): 32 | --> 356 return self.get_campaigns() 33 | 357 34 | 358 def get_campaigns(self): 35 | 36 | /home/jonas/workspace/django-mailchimp/mailchimp/chimp.pyc in get_campaigns(self) 37 | 357 38 | 358 def get_campaigns(self): 39 | --> 359 return self.cache.get('campaigns', self._get_categories) 40 | 360 41 | 361 @property 42 | 43 | /home/jonas/workspace/django-mailchimp/mailchimp/utils.py in get(self, key, obj, *args, **kwargs) 44 | 37 value = self._get(key) 45 | 38 if value is None: 46 | ---> 39 value = obj(*args, **kwargs) if callable(obj) else obj 47 | 40 self._set(key, value) 48 | 41 return value 49 | 50 | /home/jonas/workspace/django-mailchimp/mailchimp/chimp.pyc in _get_categories(self) 51 | 374 52 | 375 def _get_categories(self): 53 | --> 376 return build_dict(self, Campaign, self.con.campaigns()) 54 | 377 55 | 378 def _get_lists(self): 56 | 57 | /home/jonas/workspace/django-mailchimp/mailchimp/utils.py in build_dict(master, klass, data, key) 58 | 87 59 | 88 def build_dict(master, klass, data, key='id'): 60 | ---> 89 return dict([(info[key], klass(master, info)) for info in data]) 61 | 90 62 | 91 def _convert(name): 63 | 64 | /home/jonas/workspace/django-mailchimp/mailchimp/chimp.pyc in __init__(self, master, info) 65 | 92 def __init__(self, master, info): 66 | 93 super(Campaign, self).__init__(master, info) 67 | ---> 94 self.list = self.master.get_list_by_id(self.list_id) 68 | 95 self._content = None 69 | 96 self.frozen_info = info 70 | 71 | /home/jonas/workspace/django-mailchimp/mailchimp/chimp.pyc in get_list_by_id(self, id) 72 | 383 73 | 384 def get_list_by_id(self, id): 74 | --> 385 return self._get_by_id('lists', id) 75 | 386 76 | 387 def get_campaign_by_id(self, id): 77 | 78 | /home/jonas/workspace/django-mailchimp/mailchimp/chimp.pyc in _get_by_id(self, thing, id) 79 | 396 def _get_by_id(self, thing, id): 80 | 397 try: 81 | --> 398 return getattr(self, thing)[id] 82 | 399 except KeyError: 83 | 400 self.cache.flush(thing) 84 | 85 | /home/jonas/workspace/django-mailchimp/mailchimp/chimp.pyc in lists(self) 86 | 361 @property 87 | 362 def lists(self): 88 | --> 363 return self.get_lists() 89 | 364 90 | 365 def get_lists(self): 91 | 92 | /home/jonas/workspace/django-mailchimp/mailchimp/chimp.pyc in get_lists(self) 93 | 364 94 | 365 def get_lists(self): 95 | --> 366 return self.cache.get('lists', self._get_lists) 96 | 367 97 | 368 @property 98 | 99 | /home/jonas/workspace/django-mailchimp/mailchimp/utils.py in get(self, key, obj, *args, **kwargs) 100 | 38 if value is None: 101 | 39 value = obj(*args, **kwargs) if callable(obj) else obj 102 | ---> 40 self._set(key, value) 103 | 41 return value 104 | 42 105 | 106 | /home/jonas/workspace/django-mailchimp/mailchimp/utils.py in _real_set(self, key, value) 107 | 44 print 'key', key, type(key) 108 | 45 print 'value', value, type(value) 109 | ---> 46 cache.set(key, value, CACHE_TIMEOUT) 110 | 47 111 | 48 def _real_get(self, key): 112 | 113 | /home/jonas/workspace/affichage/parts/django/django/core/cache/backends/locmem.pyc in set(self, key, value, timeout) 114 | 81 try: 115 | 82 try: 116 | ---> 83 self._set(key, pickle.dumps(value), timeout) 117 | 84 except pickle.PickleError: 118 | 85 pass 119 | 120 | /usr/lib/python2.6/copy_reg.pyc in _reduce_ex(self, proto) 121 | 68 else: 122 | 69 if base is self.__class__: 123 | ---> 70 raise TypeError, "can't pickle %s objects" % base.__name__ 124 | 71 state = base(self) 125 | 72 args = (self.__class__, base, state) 126 | 127 | TypeError: can't pickle instancemethod objects 128 | 129 | """ 130 | REAL_CACHE = False 131 | CACHE_TIMEOUT = getattr(settings, 'MAILCHIMP_CACHE_TIMEOUT', 300) 132 | 133 | WEBHOOK_KEY = getattr(settings, 'MAILCHIMP_WEBHOOK_KEY', '') 134 | if not WEBHOOK_KEY: 135 | warnings.warn("you did not define a MAILCHIMP_WEBHOOK_KEY setting. " 136 | "django-mailchimp will create a random one by itself", MailchimpWarning) 137 | import string 138 | import random 139 | alphanum = string.ascii_letters + string.digits 140 | for x in range(50): 141 | WEBHOOK_KEY += random.choice(alphanum) 142 | 143 | VIEWS_OVERVIEW = getattr(settings, 'MAILCHIMP_VIEWS_OVERVIEW', 'mailchimp.views.overview') 144 | VIEWS_INFO = getattr(settings, 'MAILCHIMP_VIEWS_INFO', 'mailchimp.views.campaign_information') 145 | VIEWS_SCHEDULE_OBJECT = getattr(settings, 'MAILCHIMP_VIEWS_SEND_OBJECT', 'mailchimp.views.schedule_campaign_for_object') 146 | VIEWS_TEST_OBJECT = getattr(settings, 'MAILCHIMP_VIEWS_TEST_OBJECT', 'mailchimp.views.test_campaign_for_object') -------------------------------------------------------------------------------- /mailchimp/migrations/0002_added_queue.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Queue' 12 | db.create_table('mailchimp_queue', ( 13 | ('type_opts', self.gf('django.db.models.fields.TextField')()), 14 | ('segment_options_all', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), 15 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 16 | ('contents', self.gf('django.db.models.fields.TextField')()), 17 | ('subject', self.gf('django.db.models.fields.CharField')(max_length=255)), 18 | ('campaign_type', self.gf('django.db.models.fields.CharField')(max_length=50)), 19 | ('authenticate', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), 20 | ('title', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), 21 | ('from_email', self.gf('django.db.models.fields.EmailField')(max_length=75)), 22 | ('segment_options', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), 23 | ('list_id', self.gf('django.db.models.fields.CharField')(max_length=50)), 24 | ('auto_tweet', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), 25 | ('from_name', self.gf('django.db.models.fields.CharField')(max_length=255)), 26 | ('folder_id', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), 27 | ('generate_text', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), 28 | ('to_email', self.gf('django.db.models.fields.EmailField')(max_length=75)), 29 | ('tracking_text_clicks', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), 30 | ('auto_footer', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), 31 | ('tracking_html_clicks', self.gf('django.db.models.fields.BooleanField')(default=True, blank=True)), 32 | ('google_analytics', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), 33 | ('segment_options_conditions', self.gf('django.db.models.fields.TextField')()), 34 | ('template_id', self.gf('django.db.models.fields.CharField')(max_length=50)), 35 | ('tracking_opens', self.gf('django.db.models.fields.BooleanField')(default=True, blank=True)), 36 | )) 37 | db.send_create_signal('mailchimp', ['Queue']) 38 | 39 | 40 | def backwards(self, orm): 41 | 42 | # Deleting model 'Queue' 43 | db.delete_table('mailchimp_queue') 44 | 45 | 46 | models = { 47 | 'mailchimp.campaign': { 48 | 'Meta': {'object_name': 'Campaign'}, 49 | 'campaign_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 50 | 'content': ('django.db.models.fields.TextField', [], {}), 51 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 52 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 53 | 'sent_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) 54 | }, 55 | 'mailchimp.queue': { 56 | 'Meta': {'object_name': 'Queue'}, 57 | 'authenticate': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 58 | 'auto_footer': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 59 | 'auto_tweet': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 60 | 'campaign_type': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 61 | 'contents': ('django.db.models.fields.TextField', [], {}), 62 | 'folder_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), 63 | 'from_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 64 | 'from_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 65 | 'generate_text': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 66 | 'google_analytics': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 67 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 68 | 'list_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 69 | 'segment_options': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 70 | 'segment_options_all': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 71 | 'segment_options_conditions': ('django.db.models.fields.TextField', [], {}), 72 | 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 73 | 'template_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 74 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 75 | 'to_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 76 | 'tracking_html_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 77 | 'tracking_opens': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 78 | 'tracking_text_clicks': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 79 | 'type_opts': ('django.db.models.fields.TextField', [], {}) 80 | }, 81 | 'mailchimp.reciever': { 82 | 'Meta': {'object_name': 'Reciever'}, 83 | 'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recievers'", 'to': "orm['mailchimp.Campaign']"}), 84 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), 85 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 86 | } 87 | } 88 | 89 | complete_apps = ['mailchimp'] 90 | -------------------------------------------------------------------------------- /mailchimp/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.models import ContentType 2 | from django.core.urlresolvers import reverse 3 | from django.views.decorators.csrf import csrf_exempt 4 | from django.http import Http404 5 | from mailchimp.models import Campaign, Queue 6 | from mailchimp.settings import WEBHOOK_KEY 7 | from mailchimp.signals import get_signal 8 | from mailchimp.utils import BaseView, Lazy, get_connection 9 | import datetime 10 | import re 11 | 12 | class MailchimpBaseView(BaseView): 13 | @property 14 | def connection(self): 15 | return get_connection() 16 | 17 | 18 | class MailchimpView(MailchimpBaseView): 19 | required_permissions = ['mailchimp.can_view'] 20 | 21 | 22 | class Overview(MailchimpView): 23 | template = 'mailchimp/overview.html' 24 | def handle_post(self): 25 | return self.not_allowed() 26 | 27 | def handle_get(self): 28 | data = { 29 | 'paginator': self.paginate(Campaign.objects.all(), int(self.kwargs.get('page', 1))), 30 | 'queue': Queue.objects.all() 31 | } 32 | return self.render_to_response(data) 33 | 34 | def get_page_link(self, page): 35 | return self.reverse('mailchimp_overview', page=page) 36 | 37 | 38 | class ScheduleCampaignForObject(MailchimpView): 39 | def auth_check(self): 40 | basic = super(ScheduleCampaignForObject, self).auth_check() 41 | if not basic: 42 | return basic 43 | return self.request.user.has_perm('mailchimp.can_send') 44 | 45 | def handle_post(self): 46 | return self.not_allowed() 47 | 48 | def back(self): 49 | return self.redirect(self.request.META['HTTP_REFERER']) 50 | 51 | def handle_get(self): 52 | ct = ContentType.objects.get(pk=self.kwargs['content_type']) 53 | obj = ct.model_class().objects.get(pk=self.kwargs['pk']) 54 | if obj.mailchimp_schedule(self.connection): 55 | self.message_success("The Campaign has been scheduled for sending.") 56 | else: 57 | self.message_error("An error has occured while trying to send, please try again later.") 58 | return self.back() 59 | 60 | 61 | class TestCampaignForObjectReal(ScheduleCampaignForObject): 62 | def handle_get(self): 63 | ct = ContentType.objects.get(pk=self.kwargs['content_type']) 64 | obj = ct.model_class().objects.get(pk=self.kwargs['pk']) 65 | self.connection.warnings.reset() 66 | if obj.mailchimp_test(self.connection, self.request): 67 | self.message_success("A Test Campaign has been sent to your email address (%s)." % self.request.user.email) 68 | for message, category, filename, lineno in self.connection.warnings.get(): 69 | self.message_warning("%s: %s" % (category.__name__, message)) 70 | else: 71 | self.message_error("And error has occured while trying to send the test mail to you, please try again later") 72 | return self.simplejson(True) 73 | 74 | 75 | class TestCampaignForObject(ScheduleCampaignForObject): 76 | template = 'mailchimp/send_test.html' 77 | 78 | def handle_get(self): 79 | data = { 80 | 'ajaxurl': reverse('mailchimp_real_test_for_object', kwargs=self.kwargs), 81 | 'redirecturl': self.request.META['HTTP_REFERER'] 82 | } 83 | return self.render_to_response(data) 84 | 85 | 86 | class CampaignInformation(MailchimpView): 87 | template = 'mailchimp/campaign_information.html' 88 | def handle_post(self): 89 | return self.not_allowed() 90 | 91 | def handle_get(self): 92 | camp = Campaign.objects.get_or_404(campaign_id=self.kwargs['campaign_id']) 93 | data = {'campaign': camp} 94 | extra_info = camp.get_extra_info() 95 | if camp.object and hasattr(camp.object, 'mailchimp_get_extra_info'): 96 | extra_info = camp.object.mailchimp_get_extra_info() 97 | data['extra_info'] = extra_info 98 | return self.render_to_response(data) 99 | 100 | 101 | class WebHook(MailchimpBaseView): 102 | def handle_get(self): 103 | return self.response("hello chimp") 104 | 105 | def handle_post(self): 106 | if self.kwargs.get('key', '') != WEBHOOK_KEY: 107 | return self.not_found() 108 | data = self.request.POST 109 | signal = get_signal(data['type']) 110 | ts = data["fired_at"] 111 | fired_at = datetime.datetime.strptime(ts, "%Y-%m-%d %H:%M:%S") 112 | kwargs = { 113 | 'list': self.connection.get_list_by_id(data["data[list_id]"]), 114 | 'fired_at': fired_at, 115 | 'type': data['type'], 116 | } 117 | if data['type'] == 'cleaned': 118 | kwargs.update({ 119 | 'reason': data['data[reason]'], 120 | 'email': data['data[email]'], 121 | }) 122 | elif data['type'] == 'upemail': 123 | kwargs.update({ 124 | 'old_email': data['data[old_email]'], 125 | 'new_email': data['data[new_email]'], 126 | }) 127 | else: 128 | merge_re = re.compile('data\[merges\]\[(?Pw+)\]') 129 | merges = {} 130 | for key, value in data.items(): 131 | match = merge_re.match(key) 132 | if match: 133 | name = match.group('name').lower() 134 | if name in ('interests', 'fname', 'lname'): 135 | continue 136 | merges[name] = value 137 | kwargs.update({ 138 | 'email': data['data[email]'], 139 | 'fname': data['data[merges][FNAME]'], 140 | 'lname': data['data[merges][LNAME]'], 141 | 'merges': merges, 142 | }) 143 | if 'data[merges][INTERESTS]' in data: 144 | kwargs['interests'] = [i.strip() for i in data['data[merges][INTERESTS]'].split(',')] 145 | signal.send(sender=self.connection, **kwargs) 146 | return self.response("ok") 147 | 148 | 149 | class Dequeue(ScheduleCampaignForObject): 150 | def handle_get(self): 151 | q = Queue.objects.get_or_404(pk=self.kwargs['id']) 152 | if q.send(): 153 | self.message_success("The Campaign has successfully been dequeued.") 154 | else: 155 | self.message_error("An error has occured while trying to dequeue this campaign, please try again later.") 156 | return self.back() 157 | 158 | 159 | class Cancel(ScheduleCampaignForObject): 160 | def handle_get(self): 161 | q = Queue.objects.get_or_404(pk=self.kwargs['id']) 162 | q.delete() 163 | self.message_success("The Campaign has been canceled.") 164 | return self.back() 165 | 166 | 167 | webhook = csrf_exempt(WebHook()) 168 | dequeue = Dequeue() 169 | cancel = Cancel() 170 | campaign_information = CampaignInformation() 171 | overview = Overview() 172 | schedule_campaign_for_object = ScheduleCampaignForObject() 173 | test_campaign_for_object = TestCampaignForObject() 174 | test_real = TestCampaignForObjectReal() -------------------------------------------------------------------------------- /mailchimp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import simplejson 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.contrib.contenttypes import generic 5 | from django.core.urlresolvers import reverse 6 | from django.shortcuts import get_object_or_404 7 | from django.utils.translation import ugettext_lazy as _ 8 | from mailchimp.utils import get_connection 9 | 10 | 11 | class QueueManager(models.Manager): 12 | def queue(self, campaign_type, contents, list_id, template_id, subject, 13 | from_email, from_name, to_email, folder_id=None, tracking_opens=True, 14 | tracking_html_clicks=True, tracking_text_clicks=False, title=None, 15 | authenticate=False, google_analytics=None, auto_footer=False, 16 | auto_tweet=False, segment_options=False, segment_options_all=True, 17 | segment_options_conditions=[], type_opts={}, obj=None, extra_info=[]): 18 | """ 19 | Queue a campaign 20 | """ 21 | kwargs = locals().copy() 22 | kwargs['segment_options_conditions'] = simplejson.dumps(segment_options_conditions) 23 | kwargs['type_opts'] = simplejson.dumps(type_opts) 24 | kwargs['contents'] = simplejson.dumps(contents) 25 | kwargs['extra_info'] = simplejson.dumps(extra_info) 26 | for thing in ('template_id', 'list_id'): 27 | thingy = kwargs[thing] 28 | if hasattr(thingy, 'id'): 29 | kwargs[thing] = thingy.id 30 | del kwargs['self'] 31 | del kwargs['obj'] 32 | if obj: 33 | kwargs['object_id'] = obj.pk 34 | kwargs['content_type'] = ContentType.objects.get_for_model(obj) 35 | return self.create(**kwargs) 36 | 37 | def dequeue(self, limit=None): 38 | if limit: 39 | qs = self.filter(locked=False)[:limit] 40 | else: 41 | qs = self.filter(locked=False) 42 | for obj in qs: 43 | yield obj.send() 44 | 45 | def get_or_404(self, *args, **kwargs): 46 | return get_object_or_404(self.model, *args, **kwargs) 47 | 48 | 49 | 50 | class Queue(models.Model): 51 | """ 52 | A FIFO queue for async sending of campaigns 53 | """ 54 | campaign_type = models.CharField(max_length=50) 55 | contents = models.TextField() 56 | list_id = models.CharField(max_length=50) 57 | template_id = models.PositiveIntegerField() 58 | subject = models.CharField(max_length=255) 59 | from_email = models.EmailField() 60 | from_name = models.CharField(max_length=255) 61 | to_email = models.EmailField() 62 | folder_id = models.CharField(max_length=50, null=True, blank=True) 63 | tracking_opens = models.BooleanField(default=True) 64 | tracking_html_clicks = models.BooleanField(default=True) 65 | tracking_text_clicks = models.BooleanField(default=False) 66 | title = models.CharField(max_length=255, null=True, blank=True) 67 | authenticate = models.BooleanField(default=False) 68 | google_analytics = models.CharField(max_length=100, blank=True, null=True) 69 | auto_footer = models.BooleanField(default=False) 70 | generate_text = models.BooleanField(default=False) 71 | auto_tweet = models.BooleanField(default=False) 72 | segment_options = models.BooleanField(default=False) 73 | segment_options_all = models.BooleanField() 74 | segment_options_conditions = models.TextField() 75 | type_opts = models.TextField() 76 | content_type = models.ForeignKey(ContentType, null=True, blank=True) 77 | object_id = models.PositiveIntegerField(null=True, blank=True) 78 | content_object = generic.GenericForeignKey('content_type', 'object_id') 79 | extra_info = models.TextField(null=True) 80 | locked = models.BooleanField(default=False) 81 | 82 | objects = QueueManager() 83 | 84 | def send(self): 85 | """ 86 | send (schedule) this queued object 87 | """ 88 | # check lock 89 | if self.locked: 90 | return False 91 | # aquire lock 92 | self.locked = True 93 | self.save() 94 | # get connection and send the mails 95 | c = get_connection() 96 | tpl = c.get_template_by_id(self.template_id) 97 | content_data = dict([(str(k), v) for k,v in simplejson.loads(self.contents).items()]) 98 | built_template = tpl.build(**content_data) 99 | tracking = {'opens': self.tracking_opens, 100 | 'html_clicks': self.tracking_html_clicks, 101 | 'text_clicks': self.tracking_text_clicks} 102 | if self.google_analytics: 103 | analytics = {'google': self.google_analytics} 104 | else: 105 | analytics = {} 106 | segment_opts = {'match': 'all' if self.segment_options_all else 'any', 107 | 'conditions': simplejson.loads(self.segment_options_conditions)} 108 | type_opts = simplejson.loads(self.type_opts) 109 | title = self.title or self.subject 110 | camp = c.create_campaign(self.campaign_type, c.get_list_by_id(self.list_id), 111 | built_template, self.subject, self.from_email, self.from_name, 112 | self.to_email, self.folder_id, tracking, title, self.authenticate, 113 | analytics, self.auto_footer, self.generate_text, self.auto_tweet, 114 | segment_opts, type_opts) 115 | if camp.send_now_async(): 116 | self.delete() 117 | kwargs = {} 118 | if self.content_type and self.object_id: 119 | kwargs['content_type'] = self.content_type 120 | kwargs['object_id'] = self.object_id 121 | if self.extra_info: 122 | kwargs['extra_info'] = simplejson.loads(self.extra_info) 123 | return Campaign.objects.create(camp.id, segment_opts, **kwargs) 124 | # release lock if failed 125 | self.locked = False 126 | self.save() 127 | return False 128 | 129 | def get_dequeue_url(self): 130 | return reverse('mailchimp_dequeue', kwargs={'id': self.id}) 131 | 132 | def get_cancel_url(self): 133 | return reverse('mailchimp_cancel', kwargs={'id': self.id}) 134 | 135 | def get_list(self): 136 | return get_connection().lists[self.list_id] 137 | 138 | @property 139 | def object(self): 140 | """ 141 | The object might have vanished until now, so triple check that it's there! 142 | """ 143 | if self.object_id: 144 | model = self.content_type.model_class() 145 | try: 146 | return model.objects.get(id=self.object_id) 147 | except model.DoesNotExist: 148 | return None 149 | return None 150 | 151 | def get_object_admin_url(self): 152 | if not self.object: 153 | return '' 154 | name = 'admin:%s_%s_change' % (self.object._meta.app_label, 155 | self.object._meta.module_name) 156 | return reverse(name, args=(self.object.pk,)) 157 | 158 | def can_dequeue(self, user): 159 | if user.is_superuser: 160 | return True 161 | if not user.is_staff: 162 | return False 163 | if callable(getattr(self.object, 'mailchimp_can_dequeue', None)): 164 | return self.object.mailchimp_can_dequeue(user) 165 | return user.has_perm('mailchimp.can_send') and user.has_perm('mailchimp.can_dequeue') 166 | 167 | 168 | class CampaignManager(models.Manager): 169 | def create(self, campaign_id, segment_opts, content_type=None, object_id=None, 170 | extra_info=[]): 171 | con = get_connection() 172 | camp = con.get_campaign_by_id(campaign_id) 173 | extra_info = simplejson.dumps(extra_info) 174 | obj = self.model(content=camp.content, campaign_id=campaign_id, 175 | name=camp.title, content_type=content_type, object_id=object_id, 176 | extra_info=extra_info) 177 | obj.save() 178 | segment_opts = dict([(str(k), v) for k,v in segment_opts.items()]) 179 | for email in camp.list.filter_members(segment_opts): 180 | Reciever.objects.create(campaign=obj, email=email) 181 | return obj 182 | 183 | def get_or_404(self, *args, **kwargs): 184 | return get_object_or_404(self.model, *args, **kwargs) 185 | 186 | 187 | class DeletedCampaign(object): 188 | subject = u'' 189 | 190 | 191 | class Campaign(models.Model): 192 | sent_date = models.DateTimeField(auto_now_add=True) 193 | campaign_id = models.CharField(max_length=50) 194 | content = models.TextField() 195 | name = models.CharField(max_length=255) 196 | content_type = models.ForeignKey(ContentType, null=True, blank=True) 197 | object_id = models.PositiveIntegerField(null=True, blank=True) 198 | content_object = generic.GenericForeignKey('content_type', 'object_id') 199 | extra_info = models.TextField(null=True) 200 | 201 | objects = CampaignManager() 202 | 203 | class Meta: 204 | ordering = ['-sent_date'] 205 | permissions = [('can_view', 'Can view Mailchimp information'), 206 | ('can_send', 'Can send Mailchimp newsletters')] 207 | verbose_name = _('Mailchimp Log') 208 | verbose_name_plural = _('Mailchimp Logs') 209 | 210 | def get_absolute_url(self): 211 | return reverse('mailchimp_campaign_info', kwargs={'campaign_id': self.campaign_id}) 212 | 213 | def get_object_admin_url(self): 214 | if not self.object: 215 | return '' 216 | name = 'admin:%s_%s_change' % (self.object._meta.app_label, 217 | self.object._meta.module_name) 218 | return reverse(name, args=(self.object.pk,)) 219 | 220 | def get_extra_info(self): 221 | if self.extra_info: 222 | return simplejson.loads(self.extra_info) 223 | return [] 224 | 225 | @property 226 | def object(self): 227 | """ 228 | The object might have vanished until now, so triple check that it's there! 229 | """ 230 | if self.object_id: 231 | model = self.content_type.model_class() 232 | try: 233 | return model.objects.get(id=self.object_id) 234 | except model.DoesNotExist: 235 | return None 236 | return None 237 | 238 | @property 239 | def mc(self): 240 | try: 241 | if not hasattr(self, '_mc'): 242 | self._mc = get_connection().get_campaign_by_id(self.campaign_id) 243 | return self._mc 244 | except: 245 | return DeletedCampaign() 246 | 247 | 248 | class Reciever(models.Model): 249 | campaign = models.ForeignKey(Campaign, related_name='recievers') 250 | email = models.EmailField() -------------------------------------------------------------------------------- /mailchimp/chimpy/test_chimpy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for chimpy. Run them with noserunner 3 | 4 | You need to activate groups in the Mailchimp web UI before running tests: 5 | 6 | * Browse to http://admin.mailchimp.com 7 | * List setting -> Groups for segmentation 8 | * Check "add groups to my list" 9 | 10 | """ 11 | 12 | import os 13 | import pprint 14 | import operator 15 | import random 16 | import md5 17 | import datetime 18 | 19 | import chimpy 20 | 21 | chimp = None 22 | 23 | 24 | EMAIL_ADDRESS = 'casualbear@googlemail.com' 25 | EMAIL_ADDRESS2 = 'dummy@dummy.com' 26 | LIST_NAME = 'unittests' 27 | LIST_ID = None 28 | 29 | 30 | def setup_module(): 31 | assert 'MAILCHIMP_APIKEY' in os.environ, \ 32 | "please set the MAILCHIMP_APIKEY environment variable\n" \ 33 | "you can get a new api key by calling:\n" \ 34 | " wget 'http://api.mailchimp.com/1.1/?output=json&method=login" \ 35 | "&password=xxxxxx&username=yyyyyyyy' -O apikey" 36 | 37 | 38 | global chimp 39 | chimp = chimpy.Connection(os.environ['MAILCHIMP_APIKEY']) 40 | 41 | 42 | def test_ping(): 43 | assert chimp.ping() == "Everything's Chimpy!" 44 | 45 | 46 | def test_lists(): 47 | lists = chimp.lists() 48 | pprint.pprint(lists) 49 | list_names = map(lambda x: x['name'], lists) 50 | assert LIST_NAME in list_names 51 | 52 | 53 | def list_id(): 54 | global LIST_ID 55 | if LIST_ID is None: 56 | test_list = [x for x in chimp.lists() if x['name'] == LIST_NAME].pop() 57 | LIST_ID = test_list['id'] 58 | return LIST_ID 59 | 60 | # use double_optin=False to prevent manual intervention 61 | def test_list_subscribe_and_unsubscribe(): 62 | result = chimp.list_subscribe(list_id(), EMAIL_ADDRESS, 63 | {'FIRST': 'unit', 'LAST': 'tests'}, 64 | double_optin=False) 65 | pprint.pprint(result) 66 | assert result == True 67 | 68 | members = chimp.list_members(list_id()) 69 | emails = map(lambda x: x['email'], members) 70 | print members 71 | assert EMAIL_ADDRESS in emails 72 | 73 | result = chimp.list_unsubscribe(list_id(), 74 | EMAIL_ADDRESS, 75 | delete_member=True, 76 | send_goodbye=False, 77 | send_notify=False) 78 | pprint.pprint(result) 79 | assert result == True 80 | 81 | def test_list_batch_subscribe_and_batch_unsubscribe(): 82 | batch = [{'EMAIL':EMAIL_ADDRESS,'EMAIL_TYPE':'html'}, 83 | {'EMAIL':EMAIL_ADDRESS2,'EMAIL_TYPE':'text'}] 84 | 85 | result = chimp.list_batch_subscribe(list_id(), 86 | batch, 87 | double_optin=False, 88 | update_existing=False, 89 | replace_interests=False) 90 | 91 | assert result['success_count'] == 2 92 | 93 | members = chimp.list_members(list_id()) 94 | emails = map(lambda x: x['email'], members) 95 | assert EMAIL_ADDRESS in emails 96 | assert EMAIL_ADDRESS2 in emails 97 | 98 | result = chimp.list_batch_unsubscribe(list_id(), 99 | [EMAIL_ADDRESS,EMAIL_ADDRESS2], 100 | delete_member=True, 101 | send_goodbye=False, 102 | send_notify=False) 103 | 104 | assert result['success_count'] == 2 105 | 106 | def test_list_interest_groups_add_and_delete(): 107 | # check no lists exists 108 | pprint.pprint(chimp.list_interest_groups(list_id())) 109 | assert len(chimp.list_interest_groups(list_id())['groups']) == 0 110 | 111 | # add list 112 | assert chimp.list_interest_group_add(list_id(), 'test') 113 | assert len(chimp.list_interest_groups(list_id())['groups']) == 1 114 | 115 | # delete list 116 | assert chimp.list_interest_group_del(list_id(), 'test') 117 | assert len(chimp.list_interest_groups(list_id())['groups']) == 0 118 | 119 | def test_list_merge_vars_add_and_delete(): 120 | pprint.pprint(chimp.list_merge_vars(list_id())) 121 | assert len(chimp.list_merge_vars(list_id())) == 3 122 | 123 | # add list 124 | assert chimp.list_merge_var_add(list_id(), 'test', 'some text') 125 | assert len(chimp.list_merge_vars(list_id())) == 4 126 | 127 | # delete list 128 | assert chimp.list_merge_var_del(list_id(), 'test') 129 | assert len(chimp.list_merge_vars(list_id())) == 3 130 | 131 | def test_list_update_member_and_member_info(): 132 | # set up 133 | assert chimp.list_subscribe(list_id(), EMAIL_ADDRESS, 134 | {'FIRST': 'unit', 'LAST': 'tests'}, 135 | double_optin=False) 136 | assert chimp.list_merge_var_add(list_id(), 'TEST', 'test merge var') 137 | assert chimp.list_interest_group_add(list_id(), 'tlist') 138 | 139 | 140 | # update member and get the info back 141 | assert chimp.list_update_member(list_id(), EMAIL_ADDRESS, 142 | {'TEST': 'abc', 143 | 'INTERESTS': 'tlist'}, replace_interests=False) 144 | info = chimp.list_member_info(list_id(), EMAIL_ADDRESS) 145 | pprint.pprint(info) 146 | 147 | # tear down 148 | assert chimp.list_merge_var_del(list_id(), 'TEST') 149 | assert chimp.list_interest_group_del(list_id(), 'tlist') 150 | assert chimp.list_unsubscribe(list_id(), EMAIL_ADDRESS, 151 | delete_member=True, 152 | send_goodbye=False, 153 | send_notify=False) 154 | 155 | # check the info matches the set up 156 | assert 'TEST' in info['merges'] 157 | assert info['merges']['TEST'] == 'abc' 158 | assert 'tlist' in info['merges']['INTERESTS'] 159 | 160 | 161 | def test_create_delete_campaign(): 162 | uid = md5.new(str(random.random())).hexdigest() 163 | subject = 'chimpy campaign test %s' % uid 164 | options = {'list_id': list_id(), 165 | 'subject': subject, 166 | 'from_email': EMAIL_ADDRESS, 167 | 'from_name': 'chimpy', 168 | 'generate_text': True 169 | } 170 | 171 | #this just to be sure flatten utility is working 172 | segment_opts = {'match': 'any', 173 | 'conditions':[{'field': 'date', 'op': 'gt', 'value': '2000-01-01'}, 174 | {'field': 'email', 'op': 'like', 'value': '@'}]} 175 | 176 | html = """

    My test newsletter

    Just testing

    177 | Unsubscribe*|REWARDS|*""" 178 | 179 | 180 | content = {'html': html} 181 | cid = chimp.campaign_create('regular', options, content, segment_opts=segment_opts) 182 | 183 | assert isinstance(cid, str) 184 | 185 | # check if the new campaign really is there 186 | campaigns = chimp.campaigns(filter_subject=subject) 187 | assert len(campaigns)==1 188 | assert campaigns[0]['id'] == cid 189 | 190 | # our content properly addd? 191 | final_content = chimp.campaign_content(cid) 192 | assert '

    My test newsletter

    ' in final_content['html'] 193 | assert 'My test newsletter' in final_content['text'] 194 | 195 | # clean up 196 | chimp.campaign_delete(cid) 197 | 198 | def test_replicate_update_campaign(): 199 | """ replicates and updates a campaign """ 200 | 201 | uid = md5.new(str(random.random())).hexdigest() 202 | subject = 'chimpy campaign test %s' % uid 203 | options = {'list_id': list_id(), 204 | 'subject': subject, 205 | 'from_email': EMAIL_ADDRESS, 206 | 'from_name': 'chimpy', 207 | 'generate_text': True 208 | } 209 | 210 | html = """

    My test newsletter

    Just testing

    211 | Unsubscribe*|REWARDS|*""" 212 | 213 | 214 | content = {'html': html} 215 | cid = chimp.campaign_create('regular', options, content) 216 | 217 | newcid = chimp.campaign_replicate(cid=cid) 218 | assert isinstance(newcid, str) 219 | 220 | newsubject = 'Fresh subject ' + uid 221 | newtitle = 'Custom title ' + uid 222 | 223 | res = chimp.campaign_update(newcid, 'subject', newsubject) 224 | assert res is True 225 | res = chimp.campaign_update(newcid, 'title', newtitle) 226 | assert res is True 227 | 228 | campaigns = chimp.campaigns(filter_subject=newsubject) 229 | assert len(campaigns)==1 230 | campaigns = chimp.campaigns(filter_title=newtitle) 231 | assert len(campaigns)==1 232 | 233 | #clean up 234 | chimp.campaign_delete(newcid) 235 | chimp.campaign_delete(cid) 236 | 237 | def test_schedule_campaign(): 238 | """ schedules and unschedules a campaign """ 239 | 240 | uid = md5.new(str(random.random())).hexdigest() 241 | subject = 'chimpy campaign schedule test %s' % uid 242 | options = {'list_id': list_id(), 243 | 'subject': subject, 244 | 'from_email': EMAIL_ADDRESS, 245 | 'from_name': 'chimpy', 246 | 'generate_text': True 247 | } 248 | 249 | html = """

    My test newsletter

    Just testing

    250 | Unsubscribe*|REWARDS|*""" 251 | 252 | 253 | content = {'html': html} 254 | cid = chimp.campaign_create('regular', options, content) 255 | 256 | schedule_time = datetime.datetime(2012, 12, 20, 19, 0, 0) 257 | chimp.campaign_schedule(cid, schedule_time) 258 | 259 | campaign = chimp.campaigns(filter_subject=subject)[0] 260 | assert campaign['status'] == 'schedule' 261 | assert campaign['send_time'] == 'Dec 20, 2012 07:00 pm' 262 | 263 | chimp.campaign_unschedule(cid) 264 | campaign = chimp.campaigns(filter_subject=subject)[0] 265 | assert campaign['status'] == 'save' 266 | 267 | #clean up 268 | chimp.campaign_delete(cid) 269 | 270 | def test_rss_campaign(): 271 | """ add, pause, resume rss campaign """ 272 | 273 | uid = md5.new(str(random.random())).hexdigest() 274 | subject = 'chimpy campaign rss test %s' % uid 275 | options = {'list_id': list_id(), 276 | 'subject': subject, 277 | 'from_email': EMAIL_ADDRESS, 278 | 'from_name': 'chimpy', 279 | 'generate_text': True 280 | } 281 | 282 | html = """

    My test RSS newsletter

    Just testing

    283 | Unsubscribe*|REWARDS|*""" 284 | 285 | 286 | content = {'html': html} 287 | type_opts = {'url': 'http://mailchimp.com/blog/rss'} 288 | 289 | cid = chimp.campaign_create('rss', options, content, type_opts=type_opts) 290 | campaign = chimp.campaigns(filter_subject=subject)[0] 291 | assert campaign['type'] == 'rss' 292 | 293 | # Todo: Could not find a way to activate the RSS from the API. You need to 294 | # activate before being able to test pause and resume. send_now and schedule 295 | # didn't do the trick. 296 | 297 | #chimp.campaign_pause(cid) 298 | #chimp.campaign_resume(cid) 299 | 300 | #clean up 301 | chimp.campaign_delete(cid) 302 | 303 | -------------------------------------------------------------------------------- /mailchimp/chimpy/chimpy.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import urllib2 3 | import pprint 4 | from utils import transform_datetime 5 | from utils import flatten 6 | from warnings import warn 7 | from django.utils import simplejson 8 | _debug = 1 9 | 10 | 11 | class ChimpyException(Exception): 12 | pass 13 | 14 | class ChimpyWarning(Warning): 15 | pass 16 | 17 | 18 | class Connection(object): 19 | """mailchimp api connection""" 20 | 21 | output = "json" 22 | version = '1.2' 23 | 24 | def __init__(self, apikey=None, secure=False): 25 | self._apikey = apikey 26 | proto = 'http' 27 | if secure: 28 | proto = 'https' 29 | api_host = 'api.mailchimp.com' 30 | if '-' in apikey: 31 | key, dc = apikey.split('-') 32 | else: 33 | dc = 'us1' 34 | api_host = dc + '.' + api_host 35 | 36 | self.url = '%s://%s/%s/' % (proto, api_host, self.version) 37 | self.opener = urllib2.build_opener() 38 | self.opener.addheaders = [('Content-Type', 'application/x-www-form-urlencoded')] 39 | 40 | def _rpc(self, method, **params): 41 | """make an rpc call to the server""" 42 | 43 | params = urllib.urlencode(params, doseq=True) 44 | 45 | if _debug > 1: 46 | print __name__, "making request with parameters" 47 | pprint.pprint(params) 48 | print __name__, "encoded parameters:", params 49 | 50 | response = self.opener.open("%s?method=%s" %(self.url, method), params) 51 | data = response.read() 52 | response.close() 53 | 54 | if _debug > 1: 55 | print __name__, "rpc call received", data 56 | 57 | result = simplejson.loads(data) 58 | 59 | try: 60 | if 'error' in result: 61 | raise ChimpyException("%s:\n%s" % (result['error'], params)) 62 | except TypeError: 63 | # thrown when results is not iterable (eg bool) 64 | pass 65 | 66 | return result 67 | 68 | def _api_call(self, method, **params): 69 | """make an api call""" 70 | 71 | 72 | # flatten dict variables 73 | params = dict([(str(k), v.encode('utf-8') if isinstance(v, unicode) else v) for k,v in flatten(params).items()]) 74 | params['output'] = self.output 75 | params['apikey'] = self._apikey 76 | 77 | return self._rpc(method=method, **params) 78 | 79 | def ping(self): 80 | return self._api_call(method='ping') 81 | 82 | def lists(self): 83 | return self._api_call(method='lists') 84 | 85 | def list_batch_subscribe(self, 86 | id, 87 | batch, 88 | double_optin=True, 89 | update_existing=False, 90 | replace_interests=False): 91 | 92 | return self._api_call(method='listBatchSubscribe', 93 | id=id, 94 | batch=batch, 95 | double_optin=double_optin, 96 | update_existing=update_existing, 97 | replace_interests=replace_interests) 98 | 99 | def list_batch_unsubscribe(self, 100 | id, 101 | emails, 102 | delete_member=False, 103 | send_goodbye=True, 104 | send_notify=False): 105 | 106 | return self._api_call(method='listBatchUnsubscribe', 107 | id=id, 108 | emails=emails, 109 | delete_member=delete_member, 110 | send_goodbye=send_goodbye, 111 | send_notify=send_notify) 112 | 113 | def list_subscribe(self, 114 | id, 115 | email_address, 116 | merge_vars, 117 | email_type='text', 118 | double_optin=True): 119 | return self._api_call(method='listSubscribe', 120 | id=id, 121 | email_address=email_address, 122 | merge_vars=merge_vars, 123 | email_type=email_type, 124 | double_optin=double_optin) 125 | 126 | def list_unsubscribe(self, 127 | id, 128 | email_address, 129 | delete_member=False, 130 | send_goodbye=True, 131 | send_notify=True): 132 | return self._api_call(method='listUnsubscribe', 133 | id=id, 134 | email_address=email_address, 135 | delete_member=delete_member, 136 | send_goodbye=send_goodbye, 137 | send_notify=send_notify) 138 | 139 | def list_update_member(self, 140 | id, 141 | email_address, 142 | merge_vars, 143 | email_type='', 144 | replace_interests=True): 145 | return self._api_call(method='listUpdateMember', 146 | id=id, 147 | email_address=email_address, 148 | merge_vars=merge_vars, 149 | email_type=email_type, 150 | replace_interests=replace_interests) 151 | 152 | def list_member_info(self, id, email_address): 153 | return self._api_call(method='listMemberInfo', 154 | id=id, 155 | email_address=email_address) 156 | 157 | def list_members(self, id, status='subscribed', since=None, start=0, limit=100): 158 | return self._api_call(method='listMembers', id=id, status=status, since=since, start=start, limit=limit) 159 | 160 | def list_interest_groups(self, id): 161 | return self._api_call(method='listInterestGroups', id=id) 162 | 163 | def list_interest_group_add(self, id, name): 164 | return self._api_call(method='listInterestGroupAdd', id=id, group_name=name) 165 | 166 | def list_interest_group_del(self, id, name): 167 | return self._api_call(method='listInterestGroupDel', id=id, group_name=name) 168 | 169 | def list_merge_vars(self, id): 170 | return self._api_call(method='listMergeVars', id=id) 171 | 172 | def list_merge_var_add(self, id, tag, name, req=False): 173 | return self._api_call(method='listMergeVarAdd', id=id, tag=tag, name=name, req=req) 174 | 175 | def list_merge_var_del(self, id, tag): 176 | return self._api_call(method='listMergeVarDel', id=id, tag=tag) 177 | 178 | def list_webhooks(self, id): 179 | return self._api_call(method='listWebhooks', id=id) 180 | 181 | # public static listWebhookAdd(string apikey, string id, string url, array actions, array sources) 182 | def list_webhook_add(self, id, url, actions, sources): 183 | return self._api_call(method='listWebhookAdd', id=id, url=url, actions=actions, sources=sources) 184 | 185 | def list_webhook_del(self, id, url): 186 | return self._api_call(method='listWebhookDel', id=id, url=url) 187 | 188 | def campaign_content(self, cid): 189 | """Get the content (both html and text) for a campaign, exactly as it would appear in the campaign archive 190 | http://www.mailchimp.com/api/1.1/campaigncontent.func.php 191 | """ 192 | 193 | return self._api_call(method='campaignContent', cid=cid) 194 | 195 | def campaign_create(self, campaign_type, options, content, **kwargs): 196 | """Create a new draft campaign to send. 197 | http://www.mailchimp.com/api/1.1/campaigncreate.func.php 198 | 199 | Optional parameters: segment_opts, type_opts 200 | """ 201 | # enforce the 100 char limit (urlencoded!!!) 202 | title = options.get('title', options['subject']) 203 | if isinstance(title, unicode): 204 | title = title.encode('utf-8') 205 | titlelen = len(urllib.quote_plus(title)) 206 | if titlelen > 99: 207 | title = title[:-(titlelen - 96)] + '...' 208 | warn("cropped campaign title to fit the 100 character limit, new title: '%s'" % title, ChimpyWarning) 209 | subject = options['subject'] 210 | if isinstance(subject, unicode): 211 | subject = subject.encode('utf-8') 212 | subjlen = len(urllib.quote_plus(subject)) 213 | if subjlen > 99: 214 | subject = subject[:-(subjlen - 96)] + '...' 215 | warn("cropped campaign subject to fit the 100 character limit, new subject: '%s'" % subject, ChimpyWarning) 216 | options['title'] = title 217 | options['subject'] = subject 218 | return self._api_call(method='campaignCreate', type=campaign_type, options=options, content=content, **kwargs) 219 | 220 | def campaign_delete(self, cid): 221 | """Delete a campaign. 222 | http://www.mailchimp.com/api/1.1/campaigndelete.func.php 223 | """ 224 | 225 | return self._api_call(method='campaignDelete', cid=cid) 226 | 227 | def campaign_folders(self): 228 | """List all the folders for a user account. 229 | http://www.mailchimp.com/api/1.1/campaignfolders.func.php 230 | """ 231 | 232 | return self._api_call(method='campaignFolders') 233 | 234 | def campaign_pause(self, cid): 235 | """Pause a RSS campaign from sending. 236 | http://www.mailchimp.com/api/1.1/campaignpause.func.php 237 | """ 238 | 239 | return self._api_call(method='campaignPause', cid=cid) 240 | 241 | def campaign_replicate(self, cid): 242 | """Replicate a campaign. 243 | http://www.mailchimp.com/api/1.1/campaignreplicate.func.php 244 | """ 245 | 246 | return self._api_call(method='campaignReplicate', cid=cid) 247 | 248 | def campaign_resume(self, cid): 249 | """Resume sending a RSS campaign. 250 | http://www.mailchimp.com/api/1.1/campaignresume.func.php 251 | """ 252 | 253 | return self._api_call(method='campaignResume', cid=cid) 254 | 255 | def campaign_schedule(self, cid, schedule_time, schedule_time_b=None): 256 | """Schedule a campaign to be sent in the future. 257 | http://www.mailchimp.com/api/1.1/campaignschedule.func.php 258 | """ 259 | 260 | schedule_time = transform_datetime(schedule_time) 261 | 262 | if schedule_time_b: 263 | schedule_time_b = transform_datetime(schedule_time_b) 264 | 265 | return self._api_call(method='campaignSchedule', cid=cid, schedule_time=schedule_time, schedule_time_b=schedule_time_b) 266 | 267 | def campaign_send_now(self, cid): 268 | """Send a given campaign immediately. 269 | http://www.mailchimp.com/api/1.1/campaignsendnow.func.php 270 | """ 271 | 272 | return self._api_call(method='campaignSendNow', cid=cid) 273 | 274 | def campaign_send_test(self, cid, test_emails, **kwargs): 275 | """Send a test of this campaign to the provided email address. 276 | Optional parameter: send_type 277 | http://www.mailchimp.com/api/1.1/campaignsendtest.func.php 278 | """ 279 | 280 | if isinstance(test_emails, str): 281 | test_emails = [test_emails] 282 | 283 | return self._api_call(method='campaignSendTest', cid=cid, test_emails=test_emails, **kwargs) 284 | 285 | def campaign_templates(self): 286 | """ Retrieve all templates defined for your user account """ 287 | 288 | return self._api_call(method='campaignTemplates') 289 | 290 | def campaign_unschedule(self, cid): 291 | """Unschedule a campaign that is scheduled to be sent in the future """ 292 | 293 | return self._api_call(method='campaignUnschedule', cid=cid) 294 | 295 | def campaign_update(self, cid, name, value): 296 | """Update just about any setting for a campaign that has not been sent. 297 | http://www.mailchimp.com/api/1.1/campaignupdate.func.php 298 | """ 299 | 300 | return self._api_call(method='campaignUpdate', cid=cid, name=name, value=value) 301 | 302 | def campaigns(self, filter_id='', filter_folder=None, filter_fromname='', filter_fromemail='', 303 | filter_title='', filter_subject='', filter_sendtimestart=None, filter_sendtimeend=None, 304 | filter_exact=False, start=0, limit=50): 305 | """Get the list of campaigns and their details matching the specified filters. 306 | Timestamps should be passed as datatime objects. 307 | 308 | http://www.mailchimp.com/api/1.1/campaigns.func.php 309 | """ 310 | 311 | filter_sendtimestart = transform_datetime(filter_sendtimestart) 312 | filter_sendtimeend = transform_datetime(filter_sendtimeend) 313 | 314 | 315 | return self._api_call(method='campaigns', 316 | filter_id=filter_id, filter_folder=filter_folder, filter_fromname=filter_fromname, 317 | filter_fromemail=filter_fromemail, filter_title=filter_title, filter_subject=filter_subject, 318 | filter_sendtimestart=filter_sendtimestart, filter_sendtimeend=filter_sendtimeend, 319 | filter_exact=filter_exact, start=start, limit=limit) 320 | 321 | def campaign_segment_test(self, list_id, options): 322 | return self._api_call(method='campaignSegmentTest', list_id=list_id, options=options) -------------------------------------------------------------------------------- /mailchimp/utils.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render_to_response 2 | from django.template import RequestContext 3 | from django.contrib import messages 4 | from django.core.urlresolvers import reverse 5 | from django.core.cache import cache 6 | from django.contrib.contenttypes.models import ContentType 7 | from django.utils import simplejson 8 | from django.contrib.auth import logout 9 | from django.contrib.messages import debug, info, success, warning, error, add_message 10 | from django.http import ( 11 | HttpResponse, HttpResponseForbidden, Http404, HttpResponseNotAllowed, 12 | HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseNotModified, 13 | HttpResponseBadRequest, HttpResponseNotFound, HttpResponseGone, 14 | HttpResponseServerError 15 | ) 16 | from mailchimp.settings import API_KEY, SECURE, REAL_CACHE, CACHE_TIMEOUT 17 | import re 18 | import warnings 19 | 20 | class KeywordArguments(dict): 21 | def __getattr__(self, attr): 22 | return self[attr] 23 | 24 | 25 | class Cache(object): 26 | def __init__(self, prefix=''): 27 | self._data = {} 28 | self._clear_lock = False 29 | self._prefix = prefix 30 | if REAL_CACHE: 31 | self._set = getattr(self, '_real_set') 32 | self._get = getattr(self, '_real_get') 33 | self._del = getattr(self, '_real_del') 34 | else: 35 | self._set = getattr(self, '_fake_set') 36 | self._get = getattr(self, '_fake_get') 37 | self._del = getattr(self, '_fake_del') 38 | 39 | 40 | def get(self, key, obj, *args, **kwargs): 41 | if self._clear_lock: 42 | self.flush(key) 43 | self._clear_lock = False 44 | value = self._get(key) 45 | if value is None: 46 | value = obj(*args, **kwargs) if callable(obj) else obj 47 | self._set(key, value) 48 | return value 49 | 50 | def _real_set(self, key, value): 51 | cache.set(key, value, CACHE_TIMEOUT) 52 | 53 | def _real_get(self, key): 54 | return cache.get(key, None) 55 | 56 | def _real_del(self, key): 57 | cache.delete(key) 58 | 59 | def _fake_set(self, key, value): 60 | self._data[key] = value 61 | 62 | def _fake_get(self, key): 63 | return self._data.get(key, None) 64 | 65 | def _fake_del(self, key): 66 | if key in self._data: 67 | del self._data[key] 68 | 69 | def get_child_cache(self, key): 70 | return Cache('%s_%s_' % (self._prefix, key)) 71 | 72 | def flush(self, *keys): 73 | for key in keys: 74 | if key in self._data: 75 | self._del(key) 76 | 77 | def lock(self): 78 | self._clear_lock = True 79 | 80 | def clear(self, call): 81 | self.lock() 82 | return call() 83 | 84 | 85 | def wrap(base, parent, name, *baseargs, **basekwargs): 86 | def _wrapped(*args, **kwargs): 87 | fullargs = baseargs + args 88 | kwargs.update(basekwargs) 89 | return getattr(parent, '%s_%s' % (base, name))(*fullargs, **kwargs) 90 | return _wrapped 91 | 92 | 93 | def build_dict(master, klass, data, key='id'): 94 | return dict([(info[key], klass(master, info)) for info in data]) 95 | 96 | def _convert(name): 97 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 98 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 99 | 100 | 101 | class Bullet(object): 102 | def __init__(self, number, link, active): 103 | self.number = number 104 | self.link = link 105 | self.active = active 106 | 107 | 108 | class Paginator(object): 109 | def __init__(self, objects, page, get_link, per_page=20, bullets=5): 110 | page = int(page) 111 | self.page = page 112 | self.get_link = get_link 113 | self.all_objects = objects 114 | self.objects_count = objects.count() 115 | per_page = per_page() if callable(per_page) else per_page 116 | self.pages_count = int(float(self.objects_count) / float(per_page)) + 1 117 | self.bullets_count = 5 118 | self.per_page = per_page 119 | self.start = (page - 1) * per_page 120 | self.end = page * per_page 121 | self.is_first = page == 1 122 | self.first_bullet = Bullet(1, self.get_link(1), False) 123 | self.is_last = page == self.pages_count 124 | self.last_bullet = Bullet(self.pages_count, self.get_link(self.pages_count), False) 125 | self.has_pages = self.pages_count != 1 126 | self._objects = None 127 | self._bullets = None 128 | 129 | @property 130 | def bullets(self): 131 | if self._bullets is None: 132 | pre = int(float(self.bullets_count) / 2) 133 | bullets = [Bullet(self.page, self.get_link(self.page), True)] 134 | diff = 0 135 | for i in range(1, pre + 1): 136 | this = self.page - i 137 | if this: 138 | bullets.insert(0, Bullet(this, self.get_link(this), False)) 139 | else: 140 | diff = pre - this 141 | break 142 | for i in range(1, pre + 1 + diff): 143 | this = self.page + i 144 | if this <= self.pages_count: 145 | bullets.append(Bullet(this, self.get_link(this), False)) 146 | else: 147 | break 148 | self._bullets = bullets 149 | return self._bullets 150 | 151 | @property 152 | def objects(self): 153 | if self._objects is None: 154 | self._objects = self.all_objects[self.start:self.end] 155 | return self._objects 156 | 157 | 158 | class InternalRequest(object): 159 | def __init__(self, request, args, kwargs): 160 | self.request = request 161 | self.args = args 162 | self.kwargs = kwargs 163 | 164 | def contribute_to_class(self, cls): 165 | cls.request = self.request 166 | cls.args = self.args 167 | cls.kwargs = self.kwargs 168 | 169 | 170 | class BaseView(object): 171 | """ 172 | A base class to create class based views. 173 | 174 | It will automatically check allowed methods if a list of allowed methods are 175 | given. It also automatically tries to route to 'handle_`method`' methods if 176 | they're available. So if for example you define a 'handle_post' method and 177 | the request method is 'POST', this one will be called instead of 'handle'. 178 | 179 | For each request a new instance of this class will be created and it will get 180 | three attributes set: request, args and kwargs. 181 | """ 182 | # A list of allowed methods (if empty any method will be allowed) 183 | allowed_methods = [] 184 | # The template to use in the render_to_response helper 185 | template = 'base.html' 186 | # Only allow access to logged in users 187 | login_required = False 188 | # Only allow access to users with certain permissions 189 | required_permissions = [] 190 | # Only allow access to superusers 191 | superuser_required = False 192 | # Response to send when request is automatically declined 193 | auto_decline_response = 'not_found' 194 | 195 | #=========================================================================== 196 | # Dummy Attributes (DO NOT OVERWRITE) 197 | #=========================================================================== 198 | request = None 199 | args = tuple() 200 | kwargs = {} 201 | 202 | #=========================================================================== 203 | # Internal Methods 204 | #=========================================================================== 205 | 206 | def __init__(self, *args, **kwargs): 207 | # Preserve args and kwargs 208 | self._initial_args = args 209 | self._initial_kwargs = kwargs 210 | 211 | @property 212 | def __name__(self): 213 | """ 214 | INTERNAL: required by django 215 | """ 216 | return self.get_view_name() 217 | 218 | def __call__(self, request, *args, **kwargs): 219 | """ 220 | INTERNAL: Called by django when a request should be handled by this view. 221 | Creates a new instance of this class to sandbox 222 | """ 223 | if self.allowed_methods and request.method not in self.allowed_methods: 224 | return getattr(self, self.auto_decline_response)() 225 | if self.login_required and not request.user.is_authenticated(): 226 | return getattr(self, self.auto_decline_response)() 227 | if self.superuser_required and not request.user.is_superuser: 228 | return getattr(self, self.auto_decline_response)() 229 | if self.required_permissions and not request.user.has_perms(self.required_permissions): 230 | return getattr(self, self.auto_decline_response)() 231 | handle_func_name = 'handle_%s' % request.method.lower() 232 | if not hasattr(self, handle_func_name): 233 | handle_func_name = 'handle' 234 | # Create a sandbox instance of this class to safely set the request, args and kwargs attributes 235 | sandbox = self.__class__(*self._initial_args, **self._initial_kwargs) 236 | sandbox.args = args 237 | sandbox.kwargs = kwargs 238 | sandbox.request = request 239 | return getattr(sandbox, handle_func_name)() 240 | 241 | #=========================================================================== 242 | # Misc Helpers 243 | #=========================================================================== 244 | 245 | def get_view_name(self): 246 | """ 247 | Returns the name of this view 248 | """ 249 | return self.__class__.__name__ 250 | 251 | def get_template(self): 252 | return self.template 253 | 254 | def logout(self): 255 | logout(self.request) 256 | 257 | 258 | def get_page_link(self, page): 259 | return '%s?page=%s' % (self.request.path, page) 260 | 261 | def paginate(self, objects, page): 262 | return Paginator(objects, page, self.get_page_link, 20, 5) 263 | 264 | def reverse(self, view_name, *args, **kwargs): 265 | return reverse(view_name, args=args or (), kwargs=kwargs or {}) 266 | 267 | #=========================================================================== 268 | # Handlers 269 | #=========================================================================== 270 | 271 | def handle(self): 272 | """ 273 | Write your view logic here 274 | """ 275 | pass 276 | 277 | #=========================================================================== 278 | # Response Helpers 279 | #=========================================================================== 280 | 281 | def not_allowed(self, data=''): 282 | return HttpResponseNotAllowed(data) 283 | 284 | def forbidden(self, data=''): 285 | return HttpResponseForbidden(data) 286 | 287 | def redirect(self, url): 288 | return HttpResponseRedirect(url) 289 | 290 | def named_redirect(self, viewname, urlconf=None, args=None, kwargs=None, 291 | prefix=None, current_app=None): 292 | return self.redirect(reverse(view, urlconf, args, kwargs, prefix, current_app)) 293 | 294 | def permanent_redirect(self, url): 295 | return HttpResponsePermanentRedirect(url) 296 | 297 | def named_permanent_redirect(self, viewname, urlconf=None, args=None, 298 | kwargs=None, prefix=None, current_app=None): 299 | return self.permanent_redirect(reverse(view, urlconf, args, kwargs, prefix, current_app)) 300 | 301 | def not_modified(self, data=''): 302 | return HttpResponseNotModified(data) 303 | 304 | def bad_request(self, data=''): 305 | return HttpResponseBadRequest(data) 306 | 307 | def not_found(self, data=''): 308 | return HttpResponseNotFound(data) 309 | 310 | def gone(self, data=''): 311 | return HttpResponseGone(data) 312 | 313 | def server_error(self, data=''): 314 | return HttpResponseServerError(data) 315 | 316 | def simplejson(self, data): 317 | return HttpResponse(simplejson.dumps(data), content_type='application/json') 318 | 319 | def response(self, data): 320 | return HttpResponse(data) 321 | 322 | def render_to_response(self, data, request_context=True): 323 | if request_context: 324 | return render_to_response(self.get_template(), data, RequestContext(self.request)) 325 | return render_to_response(self.get_template(), data) 326 | 327 | #=========================================================================== 328 | # Message Helpers 329 | #=========================================================================== 330 | 331 | def message_debug(self, message): 332 | debug(self.request, message) 333 | 334 | def message_info(self, message): 335 | info(self.request, message) 336 | 337 | def message_success(self, message): 338 | success(self.request, message) 339 | 340 | def message_warning(self, message): 341 | warning(self.request, message) 342 | 343 | def message_error(self, message): 344 | error(self.request, message) 345 | 346 | def add_message(self, msgtype, message): 347 | add_message(self.request, msgtype, message) 348 | 349 | 350 | class WarningProxy(object): 351 | __stuff = {} 352 | def __init__(self, logger, obj): 353 | WarningProxy.__stuff[self] = {} 354 | WarningProxy.__stuff[self]['logger'] = logger 355 | WarningProxy.__stuff[self]['obj'] = obj 356 | 357 | def __getattr__(self, attr): 358 | WarningProxy.__stuff[self]['logger'].lock() 359 | val = getattr(WarningProxy.__stuff[self]['obj'], attr) 360 | WarningProxy.__stuff[self]['logger'].release() 361 | return WarningProxy(WarningProxy.__stuff[self]['logger'], val) 362 | 363 | def __setattr__(self, attr, value): 364 | WarningProxy.__stuff[self]['logger'].lock() 365 | setattr(WarningProxy.__stuff[self]['obj'], attr) 366 | WarningProxy.__stuff[self]['logger'].release() 367 | 368 | def __call__(self, *args, **kwargs): 369 | WarningProxy.__stuff[self]['logger'].lock() 370 | val = WarningProxy.__stuff[self]['obj'](*args, **kwargs) 371 | WarningProxy.__stuff[self]['logger'].release() 372 | return val 373 | 374 | 375 | class WarningLogger(object): 376 | def __init__(self): 377 | self.proxies = [] 378 | self.queue = [] 379 | self._old = warnings.showwarning 380 | 381 | def proxy(self, obj): 382 | return WarningProxy(self, obj) 383 | 384 | def lock(self): 385 | warnings.showwarning = self._showwarning 386 | 387 | def _showwarning(self, message, category, filename, lineno, fileobj=None): 388 | self.queue.append((message, category, filename, lineno)) 389 | self._old(message, category, filename, lineno, fileobj) 390 | 391 | def release(self): 392 | warnings.showwarning = self._old 393 | 394 | def get(self): 395 | queue = list(self.queue) 396 | self.queue = [] 397 | return queue 398 | 399 | def reset(self): 400 | self.queue = [] 401 | 402 | 403 | class Lazy(object): 404 | def __init__(self, real): 405 | self.__real = real 406 | self.__cache = {} 407 | 408 | def __getattr__(self, attr): 409 | if attr not in self.__cache: 410 | self.__cache[attr] = getattr(self.__real, attr) 411 | return self.__cache[attr] 412 | 413 | 414 | def dequeue(limit=None): 415 | from mailchimp.models import Queue 416 | for camp in Queue.objects.dequeue(limit): 417 | yield camp 418 | 419 | def is_queued_or_sent(object): 420 | from mailchimp.models import Queue, Campaign 421 | object_id = object.pk 422 | content_type = ContentType.objects.get_for_model(object) 423 | q = Queue.objects.filter(content_type=content_type, object_id=object_id) 424 | if q.count(): 425 | return q[0] 426 | c = Campaign.objects.filter(content_type=content_type, object_id=object_id) 427 | if c.count(): 428 | return c[0] 429 | return False 430 | 431 | # this has to be down here to prevent circular imports 432 | from mailchimp.chimp import Connection 433 | # open a non-connected connection (lazily connect on first get_connection call) 434 | CONNECTION = Connection(secure=SECURE) 435 | 436 | def get_connection(): 437 | if not CONNECTION.is_connected: 438 | CONNECTION.connect(API_KEY) 439 | return CONNECTION -------------------------------------------------------------------------------- /mailchimp/chimp.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.contrib.sites.models import Site 3 | from mailchimp.chimpy.chimpy import Connection as BaseConnection, ChimpyException 4 | from mailchimp.utils import wrap, build_dict, Cache, WarningLogger 5 | from mailchimp.exceptions import (MCCampaignDoesNotExist, MCListDoesNotExist, 6 | MCConnectionFailed, MCTemplateDoesNotExist) 7 | from mailchimp.constants import * 8 | from mailchimp.settings import WEBHOOK_KEY 9 | import datetime 10 | 11 | 12 | class SegmentCondition(object): 13 | OPERATORS = { 14 | 'eq': lambda a,b: a == b, 15 | 'ne': lambda a,b: a != b, 16 | 'gt': lambda a,b: a > b, 17 | 'lt': lambda a,b: a < b, 18 | 'like': lambda a,b: a in b, 19 | 'nlike': lambda a,b: a not in b, 20 | 'starts': lambda a,b: str(a).startswith(str(b)), 21 | 'ends': lambda a,b: str(a).endswith(str(b)) 22 | } 23 | 24 | def __init__(self, field, op, value): 25 | self.field = field 26 | self.op = op 27 | self.value = value 28 | check_function_name = 'check_%s' % self.field 29 | if not hasattr(self, check_function_name): 30 | check_function_name = 'merge_check' 31 | self.checker = getattr(self, check_function_name) 32 | 33 | def check(self, member): 34 | return self.checker(member) 35 | 36 | def check_interests(self, member): 37 | interests = self.value.split(',') 38 | if self.op == 'all': 39 | for interest in interests: 40 | if interest not in member.interests: 41 | return False 42 | return True 43 | elif self.op == 'one': 44 | for interest in interests: 45 | if interest in member.interests: 46 | return True 47 | return False 48 | else: 49 | for interest in interests: 50 | if interest in member.interests: 51 | return False 52 | return True 53 | 54 | def merge_check(self, member): 55 | return self.OPERATORS[self.op](member.merges[self.field.upper()], self.value) 56 | 57 | 58 | class BaseChimpObject(object): 59 | _attrs = () 60 | _methods = () 61 | 62 | verbose_attr = 'id' 63 | cache_key = 'id' 64 | 65 | def __init__(self, master, info): 66 | self.master = master 67 | for attr in self._attrs: 68 | setattr(self, attr, info[attr]) 69 | 70 | base = self.__class__.__name__.lower() 71 | self.cache = master.cache.get_child_cache(getattr(self, self.cache_key)) 72 | self.con = master.con 73 | 74 | for method in self._methods: 75 | setattr(self, method, wrap(base, self.master.con, method, self.id)) 76 | 77 | def __repr__(self): 78 | return '<%s object: %s>' % (self.__class__.__name__, getattr(self, self.verbose_attr)) 79 | 80 | 81 | class Campaign(BaseChimpObject): 82 | _attrs = ('archive_url', 'create_time', 'emails_sent', 'folder_id', 83 | 'from_email', 'from_name', 'id', 'inline_css', 'list_id', 84 | 'send_time', 'status', 'subject', 'title', 'to_email', 'type', 85 | 'web_id') 86 | 87 | _methods = ('delete', 'pause', 'replicate', 'resume', 'schedule', 88 | 'send_now', 'send_test', 'unschedule') 89 | 90 | verbose_attr = 'subject' 91 | 92 | def __init__(self, master, info): 93 | super(Campaign, self).__init__(master, info) 94 | try: 95 | self.list = self.master.get_list_by_id(self.list_id) 96 | except MCListDoesNotExist: 97 | self.list = None 98 | self._content = None 99 | self.frozen_info = info 100 | 101 | def __unicode__(self): 102 | return self.subject 103 | __str__ = __unicode__ 104 | 105 | @property 106 | def content(self): 107 | return self.get_content() 108 | 109 | def get_content(self): 110 | if self._content is None: 111 | self._content = self.con.campaign_content(self.id) 112 | return self._content 113 | 114 | def send_now_async(self): 115 | now = datetime.datetime.utcnow() 116 | soon = now + datetime.timedelta(minutes=1) 117 | return self.schedule(soon) 118 | 119 | def delete(self): 120 | return self.con.campaign_delete(self.id) 121 | 122 | def pause(self): 123 | return self.con.campaign_pause(self.id) 124 | 125 | def update(self): 126 | status = [] 127 | for key, value in self._get_diff(): 128 | status.append(self.con.campaign_update(self.id, key, value)) 129 | return all(status) 130 | 131 | def _get_diff(self): 132 | diff = [] 133 | new_frozen = {} 134 | for key in self._attrs: 135 | current = getattr(self, key) 136 | if self.frozen_info[key] != current: 137 | diff.append((key, current)) 138 | new_frozen[key] = current 139 | self.frozen_info = new_frozen 140 | return diff 141 | 142 | @property 143 | def is_sent(self): 144 | return self.status == 'sent' 145 | 146 | 147 | class Member(BaseChimpObject): 148 | _attrs = ('email', 'timestamp') 149 | 150 | _extended_attrs = ('id', 'ip_opt', 'ip_signup', 'merges', 'status') 151 | 152 | verbose_attr = 'email' 153 | cache_key = 'email' 154 | 155 | def __init__(self, master, info): 156 | super(Member, self).__init__(master, info) 157 | 158 | def __unicode__(self): 159 | return self.email 160 | __str__ = __unicode__ 161 | 162 | def __getattr__(self, attr): 163 | if attr in self._extended_attrs: 164 | return self.info[attr] 165 | raise AttributeError, attr 166 | 167 | @property 168 | def interests(self): 169 | return [i.strip() for i in self.merges['INTERESTS'].split(',')] 170 | 171 | @property 172 | def info(self): 173 | return self.get_info() 174 | 175 | def get_info(self): 176 | return self.cache.get('list_member_info', self.con.list_member_info, self.master.id, self.email) 177 | 178 | def update(self): 179 | return self.con.list_update_member(self.master.id, self.email, self.merges) 180 | 181 | 182 | class LazyMemberDict(dict): 183 | def __init__(self, master): 184 | super(LazyMemberDict, self).__init__() 185 | self._list = master 186 | 187 | def __getitem__(self, key): 188 | if key in self: 189 | return super(LazyMemberDict, self).__getitem__(key) 190 | value = self._list.get_member(key) 191 | self[key] = value 192 | return value 193 | 194 | 195 | class List(BaseChimpObject): 196 | ''' 197 | This represents a mailing list. Most of the methods (defined in _methods) are wrappers of the flat 198 | API found in chimpy.chimpy. As such, signatures are the same. 199 | ''' 200 | _methods = ('batch_subscribe', 201 | 'batch_unsubscribe', 202 | 'subscribe', # Sig: (email_address,merge_vars{},email_type='text',double_optin=True) 203 | 'unsubscribe') 204 | 205 | _attrs = ('id', 'member_count', 'date_created', 'name', 'web_id') 206 | 207 | verbose_attr = 'name' 208 | 209 | def __init__(self, *args, **kwargs): 210 | super(List, self).__init__(*args, **kwargs) 211 | self.members = LazyMemberDict(self) 212 | 213 | def segment_test(self, match, conditions): 214 | return self.master.con.campaign_segment_test(self.id, {'match': match, 'conditions': conditions}) 215 | 216 | def add_interest_group(self, groupname): 217 | return self.master.con.list_interest_group_add(self.id, groupname) 218 | 219 | def remove_interest_group(self, groupname): 220 | return self.master.con.list_interest_group_del(self.id, groupname) 221 | 222 | def update_interest_group(self, oldname, newname): 223 | return self.master.con.list_interest_group_update(self.id, oldname, newname) 224 | 225 | def add_interests_if_not_exist(self, *interests): 226 | self.cache.flush('interest_groups') 227 | interest_groups = self.interest_groups['groups'] 228 | for interest in set(interests): 229 | if interest not in interest_groups: 230 | self.add_interest_group(interest) 231 | interest_groups.append(interest) 232 | 233 | @property 234 | def webhooks(self): 235 | return self.get_webhooks() 236 | 237 | def get_webhooks(self): 238 | return self.cache.get('webhooks', self.master.con.list_webhooks, self.id) 239 | 240 | def add_webhook(self, url, actions, sources): 241 | return self.master.con.list_webhook_add(self.id, url, actions, sources) 242 | 243 | def remove_webhook(self, url): 244 | return self.master.con.list_webhook_del(self.id, url) 245 | 246 | def add_webhook_if_not_exists(self, url, actions, sources): 247 | for webhook in self.webhooks: 248 | if webhook['url'] == url: 249 | return True 250 | return self.add_webhook(url, actions, sources) 251 | 252 | def install_webhook(self): 253 | domain = Site.objects.get_current().domain 254 | if not (domain.startswith('http://') or domain.startswith('https://')): 255 | domain = 'http://%s' % domain 256 | if domain.endswith('/'): 257 | domain = domain[:-1] 258 | url = domain + reverse('mailchimp_webhook', kwargs={'key': WEBHOOK_KEY}) 259 | actions = {'subscribe': True, 260 | 'unsubscribe': True, 261 | 'profile': True, 262 | 'cleaned': True, 263 | 'upemail': True,} 264 | sources = {'user': True, 265 | 'admin': True, 266 | 'api': False} 267 | return self.add_webhook_if_not_exists(url, actions, sources) 268 | 269 | @property 270 | def interest_groups(self): 271 | return self.get_interest_groups() 272 | 273 | def get_interest_groups(self): 274 | return self.cache.get('interest_groups', self.master.con.list_interest_groups, self.id) 275 | 276 | def add_merge(self, key, desc, req={}): 277 | return self.master.con.list_merge_var_add(self.id, key, desc, req if req else False) 278 | 279 | def remove_merge(self, key): 280 | return self.master.con.list_merge_var_del(self.id, key) 281 | 282 | def add_merges_if_not_exists(self, *new_merges): 283 | self.cache.flush('merges') 284 | merges = [m['tag'].upper() for m in self.merges] 285 | for merge in set(new_merges): 286 | if merge.upper() not in merges: 287 | self.add_merge(merge, merge, False) 288 | merges.append(merge.upper()) 289 | 290 | @property 291 | def merges(self): 292 | return self.get_merges() 293 | 294 | def get_merges(self): 295 | return self.cache.get('merges', self.master.con.list_merge_vars, self.id) 296 | 297 | def __unicode__(self): 298 | return self.name 299 | __str__ = __unicode__ 300 | 301 | def get_member(self, email): 302 | try: 303 | data = self.master.con.list_member_info(self.id, email) 304 | except ChimpyException: 305 | return None 306 | # actually it would make more sense giving the member everything 307 | memberdata = {} 308 | memberdata['timestamp'] = data['timestamp'] 309 | memberdata['email'] = data['email'] 310 | return Member(self, memberdata) 311 | 312 | def filter_members(self, segment_opts): 313 | """ 314 | segment_opts = {'match': 'all' if self.segment_options_all else 'any', 315 | 'conditions': simplejson.loads(self.segment_options_conditions)} 316 | """ 317 | mode = all if segment_opts['match'] == 'all' else any 318 | conditions = [SegmentCondition(**dict((str(k), v) for k,v in c.items())) for c in segment_opts['conditions']] 319 | for email, member in self.members.items(): 320 | if mode([condition.check(member) for condition in conditions]): 321 | yield member 322 | 323 | 324 | class Template(BaseChimpObject): 325 | _attrs = ('id', 'layout', 'name', 'preview_image', 'sections') 326 | 327 | verbose_attr = 'name' 328 | 329 | def build(self, **kwargs): 330 | class BuiltTemplate(object): 331 | def __init__(self, template, data): 332 | self.template = template 333 | self.data = data 334 | self.id = self.template.id 335 | 336 | def __iter__(self): 337 | return iter(self.data.items()) 338 | data = {} 339 | for key, value in kwargs.items(): 340 | if key in self.sections: 341 | data['html_%s' % key] = value 342 | return BuiltTemplate(self, data) 343 | 344 | 345 | class Connection(object): 346 | REGULAR = REGULAR_CAMPAIGN 347 | PLAINTEXT = PLAINTEXT_CAMPAIGN 348 | ABSPLIT = ABSPLIT_CAMPAIGN 349 | RSS = RSS_CAMPAIGN 350 | TRANS = TRANS_CAMPAIGN 351 | AUTO = AUTO_CAMPAIGN 352 | DOES_NOT_EXIST = { 353 | 'templates': MCTemplateDoesNotExist, 354 | 'campaigns': MCCampaignDoesNotExist, 355 | 'lists': MCListDoesNotExist, 356 | } 357 | 358 | def __init__(self, api_key=None, secure=False, check=True): 359 | self._secure = secure 360 | self._check = check 361 | self._api_key = None 362 | self.con = None 363 | self.is_connected = False 364 | if api_key is not None: 365 | self.connect(api_key) 366 | 367 | def connect(self, api_key): 368 | self._api_key = api_key 369 | self.cache = Cache(api_key) 370 | self.warnings = WarningLogger() 371 | self.con = self.warnings.proxy(BaseConnection(self._api_key, self._secure)) 372 | if self._check: 373 | status = self.ping() 374 | if status != STATUS_OK: 375 | raise MCConnectionFailed(status) 376 | self.is_connected = True 377 | 378 | def ping(self): 379 | return self.con.ping() 380 | 381 | @property 382 | def campaigns(self): 383 | return self.get_campaigns() 384 | 385 | def get_campaigns(self): 386 | return self.cache.get('campaigns', self._get_categories) 387 | 388 | @property 389 | def lists(self): 390 | return self.get_lists() 391 | 392 | def get_lists(self): 393 | return self.cache.get('lists', self._get_lists) 394 | 395 | @property 396 | def templates(self): 397 | return self.get_templates() 398 | 399 | def get_templates(self): 400 | return self.cache.get('templates', self._get_templates) 401 | 402 | def _get_categories(self): 403 | return build_dict(self, Campaign, self.con.campaigns()) 404 | 405 | def _get_lists(self): 406 | return build_dict(self, List, self.con.lists()) 407 | 408 | def _get_templates(self): 409 | return build_dict(self, Template, self.con.campaign_templates()) 410 | 411 | def get_list_by_id(self, id): 412 | return self._get_by_id('lists', id) 413 | 414 | def get_campaign_by_id(self, id): 415 | return self._get_by_id('campaigns', id) 416 | 417 | def get_template_by_id(self, id): 418 | return self._get_by_id('templates', id) 419 | 420 | def get_template_by_name(self, name): 421 | return self._get_by_key('templates', 'name', name) 422 | 423 | def _get_by_id(self, thing, id): 424 | try: 425 | return getattr(self, thing)[id] 426 | except KeyError: 427 | self.cache.flush(thing) 428 | try: 429 | return getattr(self, thing)[id] 430 | except KeyError: 431 | raise self.DOES_NOT_EXIST[thing](id) 432 | 433 | def _get_by_key(self, thing, name, key): 434 | for id, obj in getattr(self, thing).items(): 435 | if getattr(obj, name) == key: 436 | return obj 437 | raise self.DOES_NOT_EXIST[thing]('%s=%s' % (name, key)) 438 | 439 | def create_campaign(self, campaign_type, campaign_list, template, subject, 440 | from_email, from_name, to_email, folder_id=None, 441 | tracking={'opens':True, 'html_clicks': True}, title='', 442 | authenticate=False, analytics={}, auto_footer=False, 443 | generate_text=False, auto_tweet=False, segment_opts={}, 444 | type_opts={}): 445 | """ 446 | Creates a new campaign and returns it for the arguments given. 447 | """ 448 | options = {} 449 | if title: 450 | options['title'] = title 451 | else: 452 | options['title'] = subject 453 | options['list_id'] = campaign_list.id 454 | options['template_id'] = template.id 455 | options['subject'] = subject 456 | options['from_email'] = from_email 457 | options['from_name'] = from_name 458 | options['to_email'] = to_email 459 | if folder_id: 460 | options['folder_id'] = folder_id 461 | options['tracking'] = tracking 462 | options['authenticate'] = bool(authenticate) 463 | if analytics: 464 | options['analytics'] = analytics 465 | options['auto_footer'] = bool(auto_footer) 466 | options['generate_text'] = bool(generate_text) 467 | options['auto_tweet'] = bool(auto_tweet) 468 | content = dict(template) 469 | kwargs = {} 470 | if segment_opts.get('conditions', None): 471 | kwargs['segment_opts'] = segment_opts 472 | if type_opts: 473 | kwargs['type_opts'] = type_opts 474 | cid = self.con.campaign_create(campaign_type, options, content, 475 | **kwargs) 476 | camp = self.get_campaign_by_id(cid) 477 | camp.template_object = template 478 | return camp 479 | 480 | def queue(self, campaign_type, contents, list_id, template_id, subject, 481 | from_email, from_name, to_email, folder_id=None, tracking_opens=True, 482 | tracking_html_clicks=True, tracking_text_clicks=False, title=None, 483 | authenticate=False, google_analytics=None, auto_footer=False, 484 | auto_tweet=False, segment_options=False, segment_options_all=True, 485 | segment_options_conditions=[], type_opts={}, obj=None): 486 | from mailchimp.models import Queue 487 | kwargs = locals().copy() 488 | del kwargs['Queue'] 489 | del kwargs['self'] 490 | return Queue.objects.queue(**kwargs) 491 | --------------------------------------------------------------------------------