Package: wagtail-birdsong
A plugin for wagtail that allows you to create, send, preview, edit and test email campaigns from within Wagtail.
Campaign templates are created usingmjml.Basic usageInstall birdsong:pipinstallwagtail-birdsongAdd the following to yourINSTALLED_APPS:INSTALLED_APPS=[...'mjml','birdsong','wagtail.contrib.modeladmin',...]Make a new app e.g.email, create amodels.pywith a model that extends the includedCampaignmodel. Some compatible mjml streamfield blocks are included in birdsong for convenience.models.pyfrombirdsong.blocksimportDefaultBlocksfrombirdsong.modelsimportCampaignfromdjango.dbimportmodelsfromwagtail.admin.edit_handlersimportStreamFieldPanelfromwagtail.core.fieldsimportStreamFieldclassSaleCampaign(Campaign):body=StreamField(DefaultBlocks())panels=Campaign.panels+[StreamFieldPanel('body'),]Then in the same app, create awagtail_hooks.pyif it doesn’t exist, this is where the admin is created
for content editors to create/edit/send campaigns.NOTE:TheCampaignAdminis just an extension of Wagtail’sModelAdminclass so most of the same options are available for overriding functionality.NOTE:BirdsongAdminGroupcan be disabled withBIRDSONG_ADMIN_GROUPsetting if you want tomodeladmin_registeryourCampaignAdmindirectly.wagtail_hooks.pyfrombirdsong.wagtail_hooksimport(CampaignAdmin,ContactAdmin,BirdsongAdminGroup,modeladmin_re_register)from.modelsimportSaleCampaignclassCampaignAdmin(CampaignAdmin):model=SaleCampaign@modeladmin_re_registerclassBirdsongAdminGroup(BirdsongAdminGroup):items=(CampaignAdmin,ContactAdmin)Create your campaign template in{app_folder}/templates/mail/{model_name}.htmle.g.email/templates/mail/sale_campaign.html,
alternatively override theget_templatemethod on your campaign model.NOTE:Campaign templates use django-mjml for responsive, well designed emails. To read up how to setup django-mjml you can read the docshere. There is a base template included in Birdsong that can be extended.sale_campaign.html{% extends "birdsong/mail/base_email.html" %}

{% block email_body %}<mj-section><mj-column><mj-text>Hello {{ contact.email }}!</mj-text>{% for b in self.body %}
            {{ b }}
        {% endfor %}</mj-column></mj-section>{% endblock email_body %}You’re now ready to go!Custom Contact modelBy default the includedContactmodel is used for every campaign, but you may want to store extra data, like names and preferences.
You can override the defaultContactmodel by setting an option on the admin for your campaign:models.pyfrombirdsong.modelsimportContactfromdjango.dbimportmodelsclassExtendedContact(Contact):first_name=models.CharField(max_length=255)last_name=models.CharField(max_length=255)location=models.CharField(max_length=255)wagtail_hooks.pyfrombirdsong.wagtail_hooksimport(CampaignAdmin,ContactAdmin,BirdsongAdminGroup,modeladmin_re_register)from.modelsimportSaleCampaign,ExtendedContact# NOTE: Import your custom Contact modelclassCampaignAdmin(CampaignAdmin):campaign=SaleCampaigncontact_class=ExtendedContact# NOTE: Teach CampaignAdmin to use your custom Contact modelclassContactAdmin(ContactAdmin):# NOTE: Overload ContactAdmin to list/edit/add your Contactsmodel=ExtendedContactlist_diplay=('email','first_name','last_name','location')@modeladmin_re_registerclassBirdsongAdminGroup(BirdsongAdminGroup):items=(CampaignAdmin,ContactAdmin)base.py# You may want to redefine the test contact (used in previews) with your new ExtendedContact fieldsBIRDSONG_TEST_CONTACT={'first_name':'Wagtail',# new ExtendedContact field'last_name':'Birdsong',# new ExtendedContact field'email':'birdsong@example.com','location':'us',# new ExtendedContact field}Filtering on contact propertiesYou might want to only send a campaign to a subset of yourContactmodels. Creating a filter usingdjango-filterand adding it to theCampaignAdminallows users to filter on any property.filters.pyfromdjango_filtersimportFilterSetfromdjango_filters.filtersimportAllValuesFilterfrom.modelsimportExtendedContactclassContactFilter(FilterSet):location=AllValuesFilter()classMeta:model=ExtendedContactfields=('location',)wagtail_hooks.pyfrombirdsong.wagtail_hooksimport(CampaignAdmin,ContactAdmin,BirdsongAdminGroup,modeladmin_re_register)from.modelsimportSaleCampaign,ExtendedContactfrom.filtersimportContactFilter# NOTE: Import your custom Contact filterclassCampaignAdmin(CampaignAdmin):campaign=SaleCampaigncontact_class=ExtendedContactcontact_filter_class=ContactFilter# NOTE: Use your custom Contact filterclassContactAdmin(ContactAdmin):model=ExtendedContactlist_diplay=('email','first_name','last_name','location')@modeladmin_re_registerclassBirdsongAdminGroup(BirdsongAdminGroup):items=(CampaignAdmin,ContactAdmin)Users will now be able to send campaigns to a subset of contacts based on location.Unsubscribe urlIncluded in birdsong is a basic way for contacts to unsubscribe, just include the url configuration and add the unsubscribe url to your email template.urls.pyfrombirdsongimporturlsasbirdsong_urlsfromdjango.urlsimportinclude,pathurlpatterns=[...path('mail/',include(birdsong_urls)),...]sale_campaign.html{% extends "birdsong/mail/base_email.html" %}

{% block email_body %}<mj-section><mj-column><mj-text>Hello {{ contact.email }}!</mj-text>{% for b in self.body %}
            {{ b }}
        {% endfor %}</mj-column></mj-section><mj-section><mj-column><mj-textalign="center">Click<ahref="{{ site.full_url }}{% url 'birdsong:unsubscribe' contact.id %}">here</a>to unsubscribe.</mj-text></mj-column></mj-section>{% endblock email_body %}Future featuresMore tests!Proper docsBackends other thans SMTP for sending emails so analytics can be gathered (email opened, bounced etc)Reloading the preview on editBroader permissions for campaigns (send, preview, test send)