diff --git a/opentech/public/mailchimp/__init__.py b/opentech/public/mailchimp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/public/mailchimp/apps.py b/opentech/public/mailchimp/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..6825eca974a4a4ece3a2fb3acbc7ab9ae8799f9d --- /dev/null +++ b/opentech/public/mailchimp/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MailchimpConfig(AppConfig): + name = 'mailchimp' diff --git a/opentech/public/mailchimp/forms.py b/opentech/public/mailchimp/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..afda5b9e3f0c0d79da9646a23b3f7c7de40ce82f --- /dev/null +++ b/opentech/public/mailchimp/forms.py @@ -0,0 +1,15 @@ +from django import forms + + +class NewsletterForm(forms.Form): + email = forms.EmailField(label='Email Address') + fname = forms.CharField(label='First Name', required=False) + lname = forms.CharField(label='Last Name', required=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + class_name = 'input--secondary' + if field.required: + class_name += ' input__secondary--required' + field.widget.attrs = {'class': class_name} diff --git a/opentech/public/mailchimp/migrations/__init__.py b/opentech/public/mailchimp/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/public/mailchimp/models.py b/opentech/public/mailchimp/models.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/public/mailchimp/templates/mailchimp/newsletter_signup.html b/opentech/public/mailchimp/templates/mailchimp/newsletter_signup.html new file mode 100644 index 0000000000000000000000000000000000000000..4c70ae9abe8f9e980d69af7188e9d800df553056 --- /dev/null +++ b/opentech/public/mailchimp/templates/mailchimp/newsletter_signup.html @@ -0,0 +1,16 @@ +<h4>Get the latest internet freedom news</h4> +<form class="form" action="{% url "newsletter:subscribe" %}" method="post"> + <div> + {% for field in newsletter_form %} + <label for="{{ field.id_for_label }}"{% if field.field.required %} required{% endif %}> + <span>{{ field.label }}</span> + {% if field.field.required %} + <span class="form__required">*</span> + {% endif %} + {{ field }} + {% endfor %} + <div class="form-actions form-wrapper"> + <input type="submit" value="Sign up" class="form-submit link link--button-transparent link--footer-signup"> + </div> + </div> +</form> diff --git a/opentech/public/mailchimp/tests.py b/opentech/public/mailchimp/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..4e838712116aa9e1e3ef5ebae892e6706f7b1d64 --- /dev/null +++ b/opentech/public/mailchimp/tests.py @@ -0,0 +1,69 @@ +from unittest import mock +import re + +from django.test import override_settings, TestCase +from django.urls import reverse + +import responses + + +any_url = re.compile(".") + + +class TestNewsletterView(TestCase): + url = reverse('newsletter:subscribe') + + def setUp(self): + self.origin = 'https://testserver/' + self.client.defaults = {'HTTP_ORIGIN': self.origin} + + def test_redirected_home_if_get(self): + response = self.client.get(self.url, secure=True, follow=True) + request = response.request + self.assertRedirects(response, '{}://{}/'.format(request['wsgi.url_scheme'], request['SERVER_NAME'])) + + @override_settings( + MAILCHIMP_API_KEY='a' * 32, + MAILCHIMP_LIST_ID='12345' + ) + @responses.activate + def test_can_subscribe(self): + responses.add(responses.POST, any_url, json={'id': '1234'}, status=200) + + response = self.client.post(self.url, data={'email': 'email@email.com'}, secure=True, follow=True) + self.assertRedirects(response, self.origin) + + messages = list(response.context['messages']) + self.assertEqual(len(messages), 1) + self.assertIn('Thank you', str(messages[0])) + + def test_error_in_form(self): + response = self.client.post(self.url, data={'email': 'email_is_bad.com'}, secure=True, follow=True) + self.assertRedirects(response, self.origin) + + messages = list(response.context['messages']) + self.assertEqual(len(messages), 1) + self.assertIn('errors with', str(messages[0])) + + @override_settings( + MAILCHIMP_API_KEY='a' * 32, + MAILCHIMP_LIST_ID='12345' + ) + @responses.activate + @mock.patch('opentech.public.mailchimp.views.logging') + def test_error_with_mailchimp(self, logging): + # Copied from the mailchimp playground + response_data = { + "title": "Invalid Resource", + "status": 400, + "detail": "Please provide a valid email address.", + } + responses.add(responses.POST, any_url, json=response_data, status=400) + response = self.client.post(self.url, data={'email': 'email@email.com'}, secure=True, follow=True) + + self.assertRedirects(response, self.origin) + + messages = list(response.context['messages']) + self.assertEqual(len(messages), 1) + self.assertIn('problem', str(messages[0])) + logging.info.assert_called_once_with(response_data) diff --git a/opentech/public/mailchimp/urls.py b/opentech/public/mailchimp/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..4fe8fd700905dce757a61635859439915c5eb90f --- /dev/null +++ b/opentech/public/mailchimp/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from .views import MailchimpSubscribeView + + +app_name = 'newsletter' + + +urlpatterns = [ + path('subscribe/', MailchimpSubscribeView.as_view(), name='subscribe') +] diff --git a/opentech/public/mailchimp/views.py b/opentech/public/mailchimp/views.py new file mode 100644 index 0000000000000000000000000000000000000000..19a7f6795851dc2bbf029d4035a9e7772b9ee436 --- /dev/null +++ b/opentech/public/mailchimp/views.py @@ -0,0 +1,84 @@ +import logging + +from django.conf import settings +from django.contrib import messages +from django.http import HttpResponseRedirect +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import RedirectView +from django.views.generic.edit import FormMixin + +from mailchimp3 import MailChimp + +from .forms import NewsletterForm + +logging.getLogger('opentech') + + +@method_decorator(csrf_exempt, name='dispatch') +class MailchimpSubscribeView(FormMixin, RedirectView): + form_class = NewsletterForm + + def post(self, request, *args, **kwargs): + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_invalid(self, form): + self.error(form) + return HttpResponseRedirect(self.get_success_url()) + + def form_valid(self, form): + mailchimp_enabled = settings.MAILCHIMP_API_KEY and settings.MAILCHIMP_LIST_ID + + dummy_key = 'a' * 32 + + client = MailChimp(mc_api=settings.MAILCHIMP_API_KEY or dummy_key, timeout=5.0, enabled=mailchimp_enabled) + + data = form.cleaned_data.copy() + email = data.pop('email') + data = { + k.upper(): v + for k, v in data.items() + } + try: + client.lists.members.create(settings.MAILCHIMP_LIST_ID, { + 'email_address': email, + 'status': 'pending', + 'merge_fields': data, + }) + except Exception as e: + self.warning(e) + else: + if mailchimp_enabled: + self.success() + else: + self.warning(Exception( + 'Incorrect Mailchimp configuration: API_KEY: {}, LIST_ID: {}'.format( + str(settings.MAILCHIMP_API_KEY), + str(settings.MAILCHIMP_LIST_ID), + ) + )) + + return super().form_valid(form) + + def error(self, form): + messages.error(self.request, _('Sorry, there were errors with your form.') + str(form.errors)) + + def warning(self, e): + messages.warning(self.request, _('Sorry, there has been an problem. Please try again later.')) + logging.info(e.args[0]) + + def success(self): + messages.success(self.request, _('Thank you for subscribing')) + + def get_success_url(self): + # Go back to where you came from + return self.request.META['HTTP_ORIGIN'] + + def get_redirect_url(self): + # We don't know where you came from, go home + return '/' diff --git a/opentech/public/navigation/migrations/0002_remove_unused_navigation_elements.py b/opentech/public/navigation/migrations/0002_remove_unused_navigation_elements.py new file mode 100644 index 0000000000000000000000000000000000000000..c6ad36c26d5142d3f9c8906b12f8c3543a62f14a --- /dev/null +++ b/opentech/public/navigation/migrations/0002_remove_unused_navigation_elements.py @@ -0,0 +1,25 @@ +# Generated by Django 2.0.2 on 2018-08-08 15:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('navigation', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='navigationsettings', + name='footer_links', + ), + migrations.RemoveField( + model_name='navigationsettings', + name='footer_navigation', + ), + migrations.RemoveField( + model_name='navigationsettings', + name='secondary_navigation', + ), + ] diff --git a/opentech/public/navigation/models.py b/opentech/public/navigation/models.py index b4eb9fccb6279560d660ed0a30c3bebb0069725b..35adefb7bc8d1fa792584d090ab003b13183de96 100644 --- a/opentech/public/navigation/models.py +++ b/opentech/public/navigation/models.py @@ -30,27 +30,9 @@ class NavigationSettings(BaseSetting, ClusterableModel): blank=True, help_text="Main site navigation" ) - secondary_navigation = StreamField( - [('link', LinkBlock()), ], - blank=True, - help_text="Alternative navigation" - ) - footer_navigation = StreamField( - [('column', LinkColumnWithHeader()), ], - blank=True, - help_text="Multiple columns of footer links with optional header." - ) - footer_links = StreamField( - [('link', LinkBlock()), ], - blank=True, - help_text="Single list of elements at the base of the page." - ) panels = [ StreamFieldPanel('primary_navigation'), - StreamFieldPanel('secondary_navigation'), - StreamFieldPanel('footer_navigation'), - StreamFieldPanel('footer_links'), ] def save(self, *args, **kwargs): diff --git a/opentech/public/navigation/templates/navigation/footerlinks.html b/opentech/public/navigation/templates/navigation/footerlinks.html deleted file mode 100644 index 098dd8df5b83a3baa07ec85ca9d69df1e2a7fc7d..0000000000000000000000000000000000000000 --- a/opentech/public/navigation/templates/navigation/footerlinks.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load wagtailcore_tags %} -<nav role="navigation" aria-label="Tertiary"> - <ul class="nav nav--tertiary"> - {% for link in footerlinks %} - {% include_block link with class="footer" %} - {% endfor %} - </ul> -</nav> diff --git a/opentech/public/navigation/templates/navigation/footernav.html b/opentech/public/navigation/templates/navigation/footernav.html deleted file mode 100644 index 576feaf688b5dd281bece692256ac135a9c7f8bf..0000000000000000000000000000000000000000 --- a/opentech/public/navigation/templates/navigation/footernav.html +++ /dev/null @@ -1,6 +0,0 @@ -{% load wagtailcore_tags %} -<nav class="grid grid--narrow" role="navigation" aria-label="Tertiary"> - {% for footer in footernav %} - {% include_block footer %} - {% endfor %} -</nav> diff --git a/opentech/public/navigation/templates/navigation/secondarynav.html b/opentech/public/navigation/templates/navigation/secondarynav.html deleted file mode 100644 index 3341960e6a5be2e1b89e4176b4b9309ec62f7cfb..0000000000000000000000000000000000000000 --- a/opentech/public/navigation/templates/navigation/secondarynav.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load wagtailcore_tags %} -<nav role="navigation" aria-label="Secondary"> - <ul class="nav nav--secondary" role="menubar"> - {% for link in secondarynav %} - {% include_block link %} - {% endfor %} - </ul> -</nav> diff --git a/opentech/public/navigation/templates/navigation/sidebar.html b/opentech/public/navigation/templates/navigation/sidebar.html deleted file mode 100644 index b322e2c7f2e50a53f3192c9127bd577aa72d2d8a..0000000000000000000000000000000000000000 --- a/opentech/public/navigation/templates/navigation/sidebar.html +++ /dev/null @@ -1,14 +0,0 @@ -{% load wagtailcore_tags %} -{% if children.exists %} - <aside class="sidebar"> - <div class="sidebar__inner"> - <h5>In this section</h5> - <ul> - {% for child in children %} - <li><a href="{% pageurl child %}">{{ child.title }}</a></li> - {% endfor %} - </ul> - </div> - </aside> -{% endif %} - diff --git a/opentech/public/navigation/templatetags/navigation_tags.py b/opentech/public/navigation/templatetags/navigation_tags.py index 2ef6d1828690d477ee5403e85301cf6493aac27b..8ae04b03aec4391ab61627dc6752fa6271b0f963 100644 --- a/opentech/public/navigation/templatetags/navigation_tags.py +++ b/opentech/public/navigation/templatetags/navigation_tags.py @@ -20,45 +20,3 @@ def primarynav(context): 'request': request, 'APPLY_SITE': apply_site, } - - -# Secondary nav snippets -@esi_inclusion_tag('navigation/secondarynav.html') -def secondarynav(context): - request = context['request'] - site = context.get('PUBLIC_SITE', request.site) - return { - 'secondarynav': NavigationSettings.for_site(site).secondary_navigation, - 'request': request, - } - - -# Footer nav snippets -@esi_inclusion_tag('navigation/footernav.html') -def footernav(context): - request = context['request'] - site = context.get('PUBLIC_SITE', request.site) - return { - 'footernav': NavigationSettings.for_site(site).footer_navigation, - 'request': request, - } - - -# Footer nav snippets -@esi_inclusion_tag('navigation/sidebar.html') -def sidebar(context): - return { - 'children': context['page'].get_children().live().public().in_menu(), - 'request': context['request'], - } - - -# Footer nav snippets -@esi_inclusion_tag('navigation/footerlinks.html') -def footerlinks(context): - request = context['request'] - site = context.get('PUBLIC_SITE', request.site) - return { - 'footerlinks': NavigationSettings.for_site(site).footer_links, - 'request': request, - } diff --git a/opentech/public/urls.py b/opentech/public/urls.py index 10ac60a78e38cb9fbbff60875a7513e9f4916078..44aef2a3ec606d73d586e65d4e3feb361ea0ba67 100644 --- a/opentech/public/urls.py +++ b/opentech/public/urls.py @@ -1,9 +1,12 @@ -from django.urls import path +from django.urls import include, path from .esi import views as esi_views from .search import views as search_views +from .mailchimp import urls as newsletter_urls + urlpatterns = [ path('esi/<slug>/', esi_views.esi, name='esi'), path('search/', search_views.search, name='search'), + path('newsletter/', include(newsletter_urls)) ] diff --git a/opentech/public/utils/context_processors.py b/opentech/public/utils/context_processors.py index 2444fdb1b890c218c1189a74039e68a935ecdbd2..149a40181ae1a97390338f32baa92c4c174ed096 100644 --- a/opentech/public/utils/context_processors.py +++ b/opentech/public/utils/context_processors.py @@ -1,9 +1,12 @@ from opentech.apply.home.models import ApplyHomePage from opentech.public.home.models import HomePage +from opentech.public.mailchimp.forms import NewsletterForm + def global_vars(request): return { 'APPLY_SITE': ApplyHomePage.objects.first().get_site(), 'PUBLIC_SITE': HomePage.objects.first().get_site(), + 'newsletter_form': NewsletterForm() } diff --git a/opentech/settings/base.py b/opentech/settings/base.py index ee5936c07b073ef82777fcf99cf1cdeaa6f0cc33..163b9fabe504d2b451102d1276dd6da4bae6c7e2 100644 --- a/opentech/settings/base.py +++ b/opentech/settings/base.py @@ -29,6 +29,7 @@ INSTALLED_APPS = [ 'opentech.public.esi', 'opentech.public.funds', 'opentech.public.home', + 'opentech.public.mailchimp', 'opentech.public.navigation', 'opentech.public.news', 'opentech.public.people', @@ -374,3 +375,7 @@ if 'REDIS_URL' in env: CELERY_BROKER_URL = env.get('REDIS_URL') else: CELERY_TASK_ALWAYS_EAGER = True + + +MAILCHIMP_API_KEY = env.get('MAILCHIMP_API_KEY') +MAILCHIMP_LIST_ID = env.get('MAILCHIMP_LIST_ID') diff --git a/opentech/static_src/src/sass/public/components/_form.scss b/opentech/static_src/src/sass/public/components/_form.scss index 1d816674003ee53ff23e93da1afc5777c4b4d9d9..d8d2e59d39c59787e54339a453f01abfa86a1a89 100644 --- a/opentech/static_src/src/sass/public/components/_form.scss +++ b/opentech/static_src/src/sass/public/components/_form.scss @@ -155,7 +155,7 @@ input[type='text']:not(.input--secondary), input[type='date'], input[type='time'], - input[type='email'], + input[type='email']:not(.input--secondary), input[type='number'], input[type='password'], input[type='datetime-local'] { diff --git a/opentech/static_src/src/sass/public/components/_messages.scss b/opentech/static_src/src/sass/public/components/_messages.scss new file mode 100644 index 0000000000000000000000000000000000000000..a732b3c02c88324a2409d84b2ed4c7136d6bd1db --- /dev/null +++ b/opentech/static_src/src/sass/public/components/_messages.scss @@ -0,0 +1,44 @@ +.messages { + position: relative; + right: 50%; + left: 50%; + width: 100vw; + margin-right: -50vw; + margin-left: -50vw; + + &__text { + position: relative; + max-height: 1000px; + padding: 15px; + padding-right: 35px; + border: solid 1px; + + &--info , &--success { + background: $color--mint; + border-color: darken($color--mint, 20%); + } + + &--warning, &--error { + font-weight: bold; + color: $color--white; + background: $color--error; + border-color: darken($color--error, 20%); + } + + &--hide { + max-height: 0; + padding-top: 0; + padding-bottom: 0; + border: 0 none; + transition: all $transition; // sass-lint:disable-line no-transition-all + transform-origin: top; + } + } + + &__close { + position: absolute; + top: 15px; + right: 15px; + + } +} diff --git a/opentech/static_src/src/sass/public/layout/_footer.scss b/opentech/static_src/src/sass/public/layout/_footer.scss index a1f5d97e3d93670304c1c4c8bd4994d426d81eac..ac71e029cd5a8da5db2e8bf03f0fd2fd95203b55 100644 --- a/opentech/static_src/src/sass/public/layout/_footer.scss +++ b/opentech/static_src/src/sass/public/layout/_footer.scss @@ -45,16 +45,18 @@ } } - input[type='text'] { - width: 100%; - max-width: 390px; - margin-bottom: 1rem; - color: $color--white; - background: transparent; - border-top: 0; - border-right: 0; - border-bottom: 4px solid $color--light-blue; - border-left: 0; + input{ + &[type='text'], &[type='email'] { + width: 100%; + max-width: 390px; + margin-bottom: 1rem; + color: $color--white; + background: transparent; + border-top: 0; + border-right: 0; + border-bottom: 4px solid $color--light-blue; + border-left: 0; + } } label { diff --git a/opentech/static_src/src/sass/public/main.scss b/opentech/static_src/src/sass/public/main.scss index de867a4a611b89cc7c5a13b4bdca4800c758e580..8928471a2984f625853c0b1af4699162ca4ef311 100755 --- a/opentech/static_src/src/sass/public/main.scss +++ b/opentech/static_src/src/sass/public/main.scss @@ -28,6 +28,7 @@ @import 'components/list'; @import 'components/listing'; @import 'components/media-box'; +@import 'components/messages'; @import 'components/nav'; @import 'components/responsive-object'; @import 'components/rich-text'; @@ -40,4 +41,3 @@ @import 'layout/sidebar'; // Pages - diff --git a/opentech/templates/base.html b/opentech/templates/base.html index 50ef09ea09990fc8c79075f026046d543c3e4330..0730652a903cbaaa9820a9c56e44515b8dab15bb 100644 --- a/opentech/templates/base.html +++ b/opentech/templates/base.html @@ -150,34 +150,7 @@ <footer class="footer"> <div class="grid grid--two wrapper wrapper--large"> <div class="footer__inner"> - <h4>Get the latest internet freedom news</h4> - <form class="form"> - <div> - <div class="mailchimp-signup-subscribe-form-description"></div> - <div id="mailchimp-newsletter-32632431e3-mergefields" class="mailchimp-newsletter-mergefields"> - <div class="form-item form-type-textfield form-item-mergevars-EMAIL"> - <label for="edit-mergevars-email">Email Address <span class="form-required" title="This field is required.">*</span></label> - <input type="text" id="edit-mergevars-email" name="mergevars[EMAIL]" value="" size="25" maxlength="128" class="form-text required input--secondary"> - </div> - - <div class="form-item form-type-textfield form-item-mergevars-FNAME"> - <label for="edit-mergevars-fname">First Name </label> - <input type="text" id="edit-mergevars-fname" name="mergevars[FNAME]" value="" size="25" maxlength="128" class="form-text input--secondary"> - </div> - - <div class="form-item form-type-textfield form-item-mergevars-LNAME"> - <label for="edit-mergevars-lname">Last Name </label> - <input type="text" id="edit-mergevars-lname" name="mergevars[LNAME]" value="" size="25" maxlength="128" class="form-text input--secondary"> - </div> - - </div> - <input type="hidden" name="form_build_id" value="form-2Dy9x5istHLUmufjcHabtyuZ_niL-RlfSoHBIq39hpI"> - <input type="hidden" name="form_id" value="mailchimp_signup_subscribe_block_otf_newsletter_form"> - <div class="form-actions form-wrapper" id="edit-actions--3"> - <input type="submit" id="edit-submit--3" name="op" value="Sign up" class="form-submit link link--button-transparent link--footer-signup"> - </div> - </div> - </form> + {% include "mailchimp/newsletter_signup.html" %} </div> <div class="footer__inner"> @@ -196,12 +169,10 @@ <p> <a href="mailto:hello@opentech.fund">hello@opentech.fund</a></br> <a href="mailto:press@opentech.fund">press@opentech.fund</a></br> - <span>PGP: 67AC DDCF B909 4685 36DD BC03 F766 3861 965A 90D2</span> + <span>PGP: <a href="https://keybase.io/opentechfund/pgp_keys.asc?fingerprint=67acddcfb909468536ddbc03f7663861965a90d2">67AC DDCF B909 4685 36DD BC03 F766 3861 965A 90D2</a></span> </p> - <p><a href="/rss.xml">RSS Feed</a></p> - - <p><a class="link link--underlined" href="#">Terms of Use</a></p> + <p><a class="link link--underlined" href="/tos">Terms of Use</a></p> <p>Test the OTF website for censorship</p> @@ -216,14 +187,6 @@ <img src="{% static 'images/radio-free-asia-logo.svg' %}" alt="logo fo radio free asia"> </a> </div> - - <section> - {% footernav %} - </section> - - <section> - {% footerlinks %} - </section> </div> </div> diff --git a/requirements.txt b/requirements.txt index 0ea9b0b33fa436dc7538579fffaa11c5336c4588..4a5561150c9302752af377776340b269fe8ce696 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ celery==4.2.1 factory_boy==2.9.2 # wagtail_factories - waiting on merge and release form master branch git+git://github.com/mvantellingen/wagtail-factories.git#egg=wagtail_factories -responses == 0.9.0 +responses==0.9.0 flake8 @@ -33,3 +33,4 @@ whitenoise==2.0.4 uwsgi==2.0.15 ConcurrentLogHandler==0.9.1 raven==6.9.0 +mailchimp3==3.0.4