diff --git a/.env-example b/.env-example index db16df378..2fc8d77f8 100644 --- a/.env-example +++ b/.env-example @@ -4,3 +4,9 @@ MAPIT_URL="https://mapit.mysociety.org/" MAPIT_API_KEY="" GOOGLE_ANALYTICS="" GOOGLE_SITE_VERIFICATION="" +MAILCHIMP_MYSOC_KEY="" +MAILCHIMP_MYSOC_SERVER_PREFIX="" +MAILCHIMP_MYSOC_LIST_ID="" +MAILCHIMP_TCC_KEY="" +MAILCHIMP_TCC_SERVER_PREFIX="" +MAILCHIMP_TCC_LIST_ID="" diff --git a/conf/env-example b/conf/env-example index 9e5cfbad6..e10adfdf9 100644 --- a/conf/env-example +++ b/conf/env-example @@ -1,3 +1,11 @@ SECRET_KEY="secr3t-k3y" GOOGLE_ANALYTICS="" GOOGLE_SITE_VERIFICATION="" +MAILCHIMP_MYSOC_KEY="" +MAILCHIMP_MYSOC_SERVER_PREFIX="" +MAILCHIMP_MYSOC_LIST_ID="" +MAILCHIMP_MYSOC_DATA_UPDATE_TAG="" +MAILCHIMP_MYSOC_CLIMATE_INTEREST="" +MAILCHIMP_TCC_KEY="" +MAILCHIMP_TCC_SERVER_PREFIX="" +MAILCHIMP_TCC_LIST_ID="" diff --git a/hub/forms.py b/hub/forms.py index 4d8a04081..859ed16bd 100644 --- a/hub/forms.py +++ b/hub/forms.py @@ -8,6 +8,7 @@ BooleanField, CharField, EmailField, + Form, ModelForm, modelformset_factory, ) @@ -158,3 +159,20 @@ def confirm_login_allowed(self, user): self.error_messages["inactive"], code="inactive", ) + + +class MailingListSignupForm(Form): + email = EmailField(label="Email") + full_name = CharField() + mysoc_signup = BooleanField( + required=False, + label=mark_safe( + 'mySociety (privacy policy)' + ), + ) + tcc_signup = BooleanField( + required=False, + label=mark_safe( + 'The Climate Coalition (privacy policy)' + ), + ) diff --git a/hub/management/commands/mailchimp_test.py b/hub/management/commands/mailchimp_test.py new file mode 100644 index 000000000..fe7ca91e1 --- /dev/null +++ b/hub/management/commands/mailchimp_test.py @@ -0,0 +1,21 @@ +from django.conf import settings +from django.core.management.base import BaseCommand + +import mailchimp_marketing as MailchimpMarketing +from mailchimp_marketing.api_client import ApiClientError + + +class Command(BaseCommand): + def handle(self, *args, **kwargs): + try: + client = MailchimpMarketing.Client() + client.set_config( + { + "api_key": settings.MAILCHIMP_MYSOC_KEY, + "server": settings.MAILCHIMP_MYSOC_SERVER_PREFIX, + } + ) + response = client.ping.get() + print(response) + except ApiClientError as error: + print(error) diff --git a/hub/static/css/_area.scss b/hub/static/css/_area.scss index 15ccaecb5..add7c17b2 100644 --- a/hub/static/css/_area.scss +++ b/hub/static/css/_area.scss @@ -181,3 +181,17 @@ [data-copy-text][data-copied] { animation: 500ms linear success-ping; } + +// Mailing list signup on area page +// TODO: This is super hacky, maybe we should make a component for this? +.row > * + * > .mailing-list-signup { + margin-top: map-get($spacers, 4); + padding-top: map-get($spacers, 4); + border-top: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color); + + @include media-breakpoint-up('lg') { + margin-top: 0; + padding-top: 0; + border-top: none; + } +} \ No newline at end of file diff --git a/hub/static/css/_utilities.scss b/hub/static/css/_utilities.scss index 215c77386..284fc5dbc 100644 --- a/hub/static/css/_utilities.scss +++ b/hub/static/css/_utilities.scss @@ -59,6 +59,22 @@ $utilities: map-merge( ), ), ), + "max-width": map-merge( + map-get($utilities, "max-width"), + ( + values: ( + 50: 50%, + 100: 100%, + 10rem: 10rem, // 160px, 3-6 words + 20rem: 20rem, // 320px, 6-9 words + 30rem: 30rem, // 480px, 9-12 words + 35rem: 35rem, + 40rem: 40rem, // 640px, 12-15 words + 45rem: 45rem, + 50rem: 50rem, + ), + ), + ), "position": map-merge( map-get($utilities, "position"), ( diff --git a/hub/static/js/home.js b/hub/static/js/home.js index 88b79f6b5..7283bb177 100644 --- a/hub/static/js/home.js +++ b/hub/static/js/home.js @@ -2,6 +2,42 @@ import $ from 'jquery/dist/jquery.slim' import Collapse from 'bootstrap/js/dist/collapse' import trackEvent from './analytics.esm.js' +async function mailingListSignup($form) { + const response = await fetch($form.attr('action'), { + method: $form.attr('method') || 'GET', + mode: 'cors', + credentials: 'same-origin', + body: $form.serialize(), + headers: { + "Content-Type": 'application/x-www-form-urlencoded', + "Accept": 'application/json; charset=utf-8', + }, + }) + return response.json() +} + +var setUpCollapsableMailingListForm = function() { + var $form = $(this); + var selectors = '.js-mailing-list-name, .js-mailing-list-extras'; + var trigger = '.js-mailing-list-email input#email'; + var instances = []; + + var updateUI = function() { + var emailEntered = $(trigger).val() !== ''; + $.each(instances, function(i, instance){ + emailEntered ? instance.show() : instance.hide(); + }); + }; + + $(selectors, $form).addClass('collapse').each(function(){ + instances.push(new Collapse(this, { toggle: false })); + }); + $(trigger, $form).on('keyup change', function(){ + updateUI(); + }); + updateUI(); +}; + $(function(){ if( 'geolocation' in navigator ) { $('.js-geolocate').removeClass('d-none'); @@ -85,4 +121,36 @@ $(function(){ window.location.href = href; }); }) + + $('.js-collapsable-mailing-list-form').each(setUpCollapsableMailingListForm); + + $('.js-mailing-list-signup').on('submit', function(e){ + e.preventDefault(); + var $form = $(this); + $('.invalid-feedback').remove() + mailingListSignup($form).then(function(response){ + if (response['response'] == 'ok') { + $form.hide() + $('.js-mailing-list-success').removeClass('d-none') + } else { + console.log(response) + for (var k in response["errors"]) { + var id = '#' + k + var el = $(id) + el.addClass('is-invalid') + var error_el = $('
') + error_el.addClass('invalid-feedback d-block fs-6 mt-2') + error_el.html( '

' + response["errors"][k].join(", ") + '

' ) + el.after(error_el) + } + + if ("mailchimp" in response["errors"]) { + var error_el = $('
') + error_el.addClass('invalid-feedback d-block fs-6 mt-2') + error_el.html( '

There was a problem signing you up, please try again.

' ) + $form.before(error_el) + } + } + }); + }) }) diff --git a/hub/templates/hub/area.html b/hub/templates/hub/area.html index 5815e4ed1..a1833a251 100644 --- a/hub/templates/hub/area.html +++ b/hub/templates/hub/area.html @@ -675,18 +675,25 @@

{{ dataset.label }}

-{% if not user.is_authenticated %} -
+
- -{% endif %} + {% endblock %} diff --git a/hub/templates/hub/includes/mailing-list-form.html b/hub/templates/hub/includes/mailing-list-form.html new file mode 100644 index 000000000..5ba07b363 --- /dev/null +++ b/hub/templates/hub/includes/mailing-list-form.html @@ -0,0 +1,48 @@ +
+ {% csrf_token %} +

Want to know when we add new data?

+
+ Tell us your email address and we’ll notify you when we add new data to the Local Intelligence Hub. +
+ + + {% if form.errors.email %} +
+ {{ form.errors.email }} +
+ {% endif %} +
+
+ + + {% if form.errors.full_name %} +
+ {{ form.errors.full_name }} +
+ {% endif %} +
+
+
+ You can also, optionally, join our newsletters for more inspiration on data, democracy, and climate action: +
+ + +
+
+ + +
+
+ +
+ +
+

Thanks for signing up!

+

We’ll let you know when there’s new data we think you’ll be interested in, on the Local Intelligence Hub.

+
diff --git a/hub/templates/hub/sign_up.html b/hub/templates/hub/sign_up.html new file mode 100644 index 000000000..785344913 --- /dev/null +++ b/hub/templates/hub/sign_up.html @@ -0,0 +1,13 @@ +{% extends "hub/base.html" %} + +{% block content %} + +
+
+ + {% include 'hub/includes/mailing-list-form.html' %} + +
+
+ +{% endblock %} diff --git a/hub/templates/hub/sign_up_success.html b/hub/templates/hub/sign_up_success.html new file mode 100644 index 000000000..1102f69f4 --- /dev/null +++ b/hub/templates/hub/sign_up_success.html @@ -0,0 +1,18 @@ +{% extends "hub/base.html" %} + +{% block content %} + +
+
+ +
+

Signed up

+ +

Thanks for signing up!

+ +
+ +
+
+ +{% endblock %} diff --git a/hub/templates/hub/sources.html b/hub/templates/hub/sources.html index c3377fc19..2634d070f 100644 --- a/hub/templates/hub/sources.html +++ b/hub/templates/hub/sources.html @@ -3,14 +3,30 @@ {% block content %}
-
+

Datasets and data sources

+

+ The Local Intelligence Hub brings together data from a number of public and private sources, under four categories: MP, Public opinion, Place, and Movement. +

+
-
-

The Local Intelligence Hub brings together data from a number of public and private sources, under four categories: MP, Public opinion, Place, and Movement.

-

We are constantly incorporating new data into the Local Intelligence Hub, for the benefit of the entire climate and nature movement. If your organisation has data to contribute, please get in touch.

+
+
+
+
+ {% include 'hub/includes/mailing-list-form.html' with classes="js-collapsable-mailing-list-form" %} +
+
+

Got new data to contribute?

+

We are constantly incorporating new data into the Local Intelligence Hub, for the benefit of the entire climate and nature movement.

+

If your organisation has data to contribute, please get in touch.

+
+
+
+ +
{% for slug, category in categories.items %}

{{ category.label }}

diff --git a/hub/views/core.py b/hub/views/core.py index a916cee36..564272daa 100644 --- a/hub/views/core.py +++ b/hub/views/core.py @@ -1,8 +1,15 @@ +import re + +from django.conf import settings from django.db import connection from django.db.utils import OperationalError from django.http import JsonResponse -from django.views.generic import TemplateView +from django.views.generic import FormView, TemplateView + +import mailchimp_marketing as MailChimp +from mailchimp_marketing.api_client import ApiClientError +from hub.forms import MailingListSignupForm from hub.mixins import TitleMixin from hub.models import Area, DataSet @@ -98,6 +105,113 @@ class ContactView(TitleMixin, TemplateView): template_name = "hub/contact.html" +class MailChimpSuccessView(TitleMixin, TemplateView): + page_title = "Contact us" + template_name = "hub/sign_up_success.html" + + +class MailChimpSignupView(TitleMixin, FormView): + form_class = MailingListSignupForm + page_title = "Signup to our mailing list" + template_name = "hub/sign_up.html" + success_url = "/mailing-list-success/" + + def form_invalid(self, form): + response = super().form_invalid(form) + if self.request.accepts("text/html"): + return response + else: + return JsonResponse({"errors": form.errors}, status=400) + + def form_valid(self, form): + mysoc_client = MailChimp.Client() + tcc_client = MailChimp.Client() + + mysoc_climate_signup = form.cleaned_data.get("mysoc_signup", False) + tcc_signup = form.cleaned_data.get("tcc_signup", False) + + name = form.cleaned_data.get("full_name") + merge_fields = None + if name is not None: + name = name.strip() + name = re.sub(r"\s+", " ", name) + parts = name.split(" ", maxsplit=1) + + if len(parts) >= 1: + merge_fields = {"FNAME": parts[0]} + if len(parts) == 2: + merge_fields["LNAME"] = parts[1] + + mysoc_client.set_config( + { + "api_key": settings.MAILCHIMP_MYSOC_KEY, + "server": settings.MAILCHIMP_MYSOC_SERVER_PREFIX, + } + ) + + tcc_client.set_config( + { + "api_key": settings.MAILCHIMP_TCC_KEY, + "server": settings.MAILCHIMP_TCC_SERVER_PREFIX, + } + ) + + content = { + "email_address": form.cleaned_data.get("email"), + "status": "subscribed", + } + + if merge_fields is not None: + content["merge_fields"] = merge_fields + + mysoc_content = {**content, "tags": [settings.MAILCHIMP_MYSOC_DATA_UPDATE_TAG]} + + if mysoc_climate_signup: + mysoc_content["interests"] = {} + mysoc_content["interests"][settings.MAILCHIMP_MYSOC_CLIMATE_INTEREST] = True + + if tcc_signup: + tcc_content = content + + http_status = 200 + response_data = {"data": content} + + try: + response = mysoc_client.lists.batch_list_members( + settings.MAILCHIMP_MYSOC_LIST_ID, + {"members": [mysoc_content], "update_existing": True}, + ) + + response_data = {"response": "ok", "data": content} + + if ( + tcc_signup + and hasattr(settings, "MAILCHIMP_TCC_LIST_ID") + and settings.MAILCHIMP_TCC_LIST_ID != "" + ): + response = tcc_client.lists.batch_list_members( + settings.MAILCHIMP_TCC_LIST_ID, + {"members": [tcc_content], "update_existing": True}, + ) + print("posting to TCC API", tcc_content) + + except ApiClientError as error: + http_status = 500 + response_data = { + "errors": { + "mailchmip": [ + error.text, + ] + } + } + + response = super().form_valid(form) + if self.request.accepts("text/html"): + return response + else: + return JsonResponse(response_data, status=http_status) + + class StyleView(TitleMixin, TemplateView): page_title = "Style preview" template_name = "hub/style.html" diff --git a/local_intelligence_hub/settings.py b/local_intelligence_hub/settings.py index 8a25e5747..7f47629f7 100644 --- a/local_intelligence_hub/settings.py +++ b/local_intelligence_hub/settings.py @@ -24,6 +24,14 @@ HIDE_DEBUG_TOOLBAR=(bool, False), GOOGLE_ANALYTICS=(str, ""), GOOGLE_SITE_VERIFICATION=(str, ""), + MAILCHIMP_MYSOC_KEY=(str, ""), + MAILCHIMP_MYSOC_SERVER_PREFIX=(str, ""), + MAILCHIMP_MYSOC_LIST_ID=(str, ""), + MAILCHIMP_MYSOC_DATA_UPDATE_TAG=(str, ""), + MAILCHIMP_MYSOC_CLIMATE_INTEREST=(str, ""), + MAILCHIMP_TCC_KEY=(str, ""), + MAILCHIMP_TCC_SERVER_PREFIX=(str, ""), + MAILCHIMP_TCC_LIST_ID=(str, ""), ) environ.Env.read_env(BASE_DIR / ".env") @@ -40,6 +48,16 @@ GOOGLE_ANALYTICS = env("GOOGLE_ANALYTICS") GOOGLE_SITE_VERIFICATION = env("GOOGLE_SITE_VERIFICATION") +# mailing list signup config +MAILCHIMP_MYSOC_KEY = env("MAILCHIMP_MYSOC_KEY") +MAILCHIMP_MYSOC_SERVER_PREFIX = env("MAILCHIMP_MYSOC_SERVER_PREFIX") +MAILCHIMP_MYSOC_LIST_ID = env("MAILCHIMP_MYSOC_LIST_ID") +MAILCHIMP_MYSOC_DATA_UPDATE_TAG = env("MAILCHIMP_MYSOC_DATA_UPDATE_TAG") +MAILCHIMP_MYSOC_CLIMATE_INTEREST = env("MAILCHIMP_MYSOC_CLIMATE_INTEREST") +MAILCHIMP_TCC_KEY = env("MAILCHIMP_TCC_KEY") +MAILCHIMP_TCC_SERVER_PREFIX = env("MAILCHIMP_TCC_SERVER_PREFIX") +MAILCHIMP_TCC_LIST_ID = env("MAILCHIMP_TCC_LIST_ID") + # make sure CSRF checking still works behind load balancers SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") diff --git a/local_intelligence_hub/urls.py b/local_intelligence_hub/urls.py index 49f01c6d6..58d125ad6 100644 --- a/local_intelligence_hub/urls.py +++ b/local_intelligence_hub/urls.py @@ -69,6 +69,14 @@ path("location/", area.AreaSearchView.as_view(), name="area_search"), path("go/", include(landingpages.urlpatterns)), path("style/", core.StyleView.as_view(), name="style"), + path( + "mailing-list/", core.MailChimpSignupView.as_view(), name="mailing_list_signup" + ), + path( + "mailing-list-success/", + core.MailChimpSuccessView.as_view(), + name="mailing_list_sucess", + ), path("status/", core.StatusView.as_view(), name="status"), path("me/", accounts.MyAccountView.as_view(), name="my_account"), path("signup/", accounts.SignupView.as_view(), name="signup"), diff --git a/poetry.lock b/poetry.lock index 58dc7c17f..0bcbfd24d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -339,6 +339,28 @@ html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (==0.29.37)"] +[[package]] +name = "mailchimp-marketing" +version = "3.0.80" +description = "Mailchimp Marketing API" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.dependencies] +certifi = ">=2017.4.17" +python-dateutil = ">=2.1" +requests = ">=2.23" +six = ">=1.10" +urllib3 = ">=1.23" + +[package.source] +type = "git" +url = "https://github.com/mailchimp/mailchimp-marketing-python.git" +reference = "HEAD" +resolved_reference = "3305fa45b3f436767a539c5fba9cb2b0a083d761" + [[package]] name = "mccabe" version = "0.7.0" @@ -696,7 +718,7 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "4bddcaf30a4c21bac52e52ecf2f9e830520fae0a6e4104feda5c9d40ff989591" +content-hash = "a558e14fb2d6a61678f6acc3ea3de3f1c4dca0904f9732293a84296480303f8a" [metadata.files] appdirs = [ @@ -1051,6 +1073,7 @@ lxml = [ {file = "lxml-4.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f10250bb190fb0742e3e1958dd5c100524c2cc5096c67c8da51233f7448dc137"}, {file = "lxml-4.9.4.tar.gz", hash = "sha256:b1541e50b78e15fa06a2670157a1962ef06591d4c998b998047fff5e3236880e"}, ] +mailchimp-marketing = [] mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, diff --git a/pyproject.toml b/pyproject.toml index 87004c6b2..7e0df7fe7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ mysoc-dataset = "^0.3.0" django-jsonform = "^2.15.0" lxml = "^4.9.2" beautifulsoup4 = "^4.11.1" +mailchimp-marketing = {git = "https://github.com/mailchimp/mailchimp-marketing-python.git"} [tool.poetry.dev-dependencies] django-debug-toolbar = "^3.7.0"