recaptchaInstance = e}
diff --git a/newamericadotorg/assets/react/program-page/components/Subscribe.js b/newamericadotorg/assets/react/program-page/components/Subscribe.js
index 1654f1304..cb4aea6c9 100644
--- a/newamericadotorg/assets/react/program-page/components/Subscribe.js
+++ b/newamericadotorg/assets/react/program-page/components/Subscribe.js
@@ -68,8 +68,6 @@ export default class Subscribe extends Component {
params: {
email: email || '',
name: '',
- organization: '',
- job_title: '',
zipcode: '',
},
subscriptions
@@ -210,8 +208,6 @@ export default class Subscribe extends Component {
1 && 'col-md-6'}`}>
-
-
recaptchaInstance = e}
diff --git a/newamericadotorg/settings/base.py b/newamericadotorg/settings/base.py
index 6c9d88f09..fd336ef86 100644
--- a/newamericadotorg/settings/base.py
+++ b/newamericadotorg/settings/base.py
@@ -408,8 +408,8 @@
CSP_REPORT_URI = os.environ.get('CSP_REPORT_URI')
CSP_REPORT_PERCENTAGE = 0.05
-CREATESEND_API_KEY = os.getenv("CREATESEND_API_KEY")
-CREATESEND_CLIENTID = os.getenv("CREATESEND_CLIENTID")
-CREATESEND_LISTID = os.getenv("CREATESEND_LISTID")
+MAILCHIMP_HOST = os.getenv("MAILCHIMP_HOST")
+MAILCHIMP_API_KEY = os.getenv("MAILCHIMP_API_KEY")
+MAILCHIMP_LIST_ID = os.getenv("MAILCHIMP_LIST_ID")
RECAPTCHA_SECRET_KEY = os.getenv("NEW_RECAPTCHA_SECRET_KEY")
diff --git a/pyproject.toml b/pyproject.toml
index 030251cac..3c358104e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,7 +25,6 @@ dependencies = [
# Other packages
'Wand >= 0.6.11',
- 'createsend >= 7.0, <8',
'psycopg2 >=2.8, <3',
'WeasyPrint==51',
'python-docx >= 0.8.11',
diff --git a/requirements.txt b/requirements.txt
index 7cfce015b..f41b29807 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -32,8 +32,6 @@ cffi==1.15.1
# weasyprint
charset-normalizer==3.1.0
# via requests
-createsend==7.0.0
- # via newamerica-cms (pyproject.toml)
cssselect2==0.7.0
# via
# cairosvg
@@ -167,7 +165,6 @@ sentry-sdk==1.19.1
# via newamerica-cms (pyproject.toml)
six==1.16.0
# via
- # createsend
# html5lib
# l18n
# python-dateutil
diff --git a/subscribe/campaign_monitor.py b/subscribe/campaign_monitor.py
deleted file mode 100644
index d550ae985..000000000
--- a/subscribe/campaign_monitor.py
+++ /dev/null
@@ -1,74 +0,0 @@
-import traceback
-
-import createsend
-from django.conf import settings
-
-from subscribe.models import MailingListSegment
-
-auth = {"api_key": settings.CREATESEND_API_KEY}
-
-
-def update_segments():
- newamerica_list = createsend.List(auth, settings.CREATESEND_LISTID)
-
- fields = newamerica_list.custom_fields()
- segments = None
- for f in fields:
- if f.FieldName == "Subscriptions":
- segments = set(f.FieldOptions)
-
- existing_segments = set(
- MailingListSegment.objects.values_list("title", flat=True)
- )
- created = 0
- for segment in segments.difference(existing_segments):
- MailingListSegment.objects.create(title=segment)
- created += 1
- return created
-
-
-def update_subscriber(email, name, custom_fields):
- subscriber = createsend.Subscriber(auth, settings.CREATESEND_LISTID, email)
- try:
- s = subscriber.get()
- # prevent overwriting Subscriptions by merging existing data
- for cf in s.CustomFields:
- exists = False
- for new_cf in custom_fields:
- if cf.Key == "Subscriptions" and cf.Value == new_cf["value"]:
- exists = True
- if not exists:
- custom_fields.append({"key": cf.Key, "value": cf.Value})
-
- subscriber.update(email, name, custom_fields, True, "Unchanged")
- except createsend.BadRequest:
- try:
- subscriber.add(
- settings.CREATESEND_LISTID,
- email,
- name,
- custom_fields,
- True,
- "Unchanged",
- )
- except Exception:
- return "BAD_REQUEST"
-
- return "OK"
-
-
-def get_error(exc_info):
- exc_type, exc_val, exc_tb = exc_info
- tb = traceback.extract_tb(exc_tb)
- cause = tb[len(tb) - 1]
-
- filename = cause[0]
- line = cause[1]
- msg = traceback.format_exception_only(exc_type, exc_val)[0].replace(
- "\n", "; "
- )
-
- error = 'file=%s line=%s msg="%s"' % (filename, line, msg)
- context = {"traceback": traceback.format_tb(exc_tb)}
-
- return {"msg": error, "context": context}
diff --git a/subscribe/mailchimp.py b/subscribe/mailchimp.py
new file mode 100644
index 000000000..5b0f53a67
--- /dev/null
+++ b/subscribe/mailchimp.py
@@ -0,0 +1,143 @@
+import traceback
+import urllib.parse
+import requests
+import hashlib
+from enum import Enum
+from django.conf import settings
+from urllib.parse import urljoin
+import logging
+from subscribe.models import MailingListSegment
+
+logger = logging.getLogger(__name__)
+
+
+class StatusEnum(Enum):
+ OK = "OK"
+ BAD_REQUEST = "BAD_REQUEST"
+
+
+class MailchimpError(Exception):
+ pass
+
+
+class MailchimpClient:
+ def __init__(self, *, host, api_key):
+ self.api_key = api_key
+ self.host = host
+
+ def _build_url(self, path):
+ return urljoin(f"https://{self.host}", path)
+
+ def _do_request(self, method, path, **kwargs):
+ try:
+ response = requests.request(
+ method,
+ self._build_url(path),
+ headers={"Authorization": f"Bearer {self.api_key}"},
+ timeout=10,
+ **kwargs,
+ )
+ response.raise_for_status()
+ return response.json()
+ except requests.exceptions.RequestException as e:
+ raise MailchimpError(f"Request to Mailchimp failed") from e
+
+ def get(self, path):
+ return self._do_request("GET", path)
+
+ def post(self, path, data):
+ return self._do_request("POST", path, json=data)
+
+ def put(self, path, data):
+ return self._do_request("PUT", path, json=data)
+
+ def _get_subscriber_hash(self, email):
+ return hashlib.md5(email.lower().strip().encode()).hexdigest()
+
+ def get_tags(self, list_id):
+ tags = []
+ offset = 0
+ count = 100
+
+ while True:
+ response = self.get(
+ f"/3.0/lists/{list_id}/tag-search?count={count}&offset={offset}"
+ )
+ tags.extend(response["tags"])
+
+ total_items = response["total_items"]
+ if offset + count >= total_items:
+ break
+
+ offset += count
+
+ return tags
+
+ def subscribe(self, *, list_id, email, name, merge_fields, tags):
+ subscriber_hash = self._get_subscriber_hash(email)
+ return self.put(
+ f"/3.0/lists/{list_id}/members/{subscriber_hash}",
+ {
+ "email_address": email,
+ "status": "pending",
+ "merge_fields": merge_fields,
+ "tags": tags,
+ },
+ )
+
+ def get_subscriber(self, *, list_id, email):
+ return self.get(f"/3.0/lists/{list_id}/members/{email}")
+
+
+def _get_client():
+ host = settings.MAILCHIMP_HOST
+ api_key = settings.MAILCHIMP_API_KEY
+
+ if not host:
+ raise ValueError(
+ "MAILCHIMP_HOST is not set. Set this to the base API host for your Mailchimp account, for example: us22.api.mailchimp.com"
+ )
+ if not api_key:
+ raise ValueError("MAILCHIMP_API_KEY is not set. Set this to your Mailchimp API key.")
+
+ return MailchimpClient(host=host, api_key=api_key)
+
+
+def update_segments():
+ client = _get_client()
+ list_id = settings.MAILCHIMP_LIST_ID
+
+ tags = client.get_tags(list_id)
+ existing_tag_names = set(MailingListSegment.objects.values_list("title", flat=True))
+
+ # Process tags and create/update MailingListSegment objects
+ current_tag_names = set()
+ for tag in tags:
+ tag_name = tag["name"]
+ current_tag_names.add(tag_name)
+
+ MailingListSegment.objects.update_or_create(
+ title=tag_name,
+ )
+
+ # Delete MailingListSegment objects for tags that no longer exist
+ tags_to_delete = existing_tag_names - current_tag_names
+ MailingListSegment.objects.filter(title__in=tags_to_delete).delete()
+
+ return len(current_tag_names)
+
+
+def update_subscriber(email, name, tags, custom_fields):
+ client = _get_client()
+ try:
+ client.subscribe(
+ list_id=settings.MAILCHIMP_LIST_ID,
+ email=email,
+ name=name,
+ merge_fields=custom_fields,
+ tags=tags,
+ )
+ return StatusEnum.OK.value
+ except MailchimpError:
+ logger.warning("Failed to create subscriber in Mailchimp", exc_info=True)
+ return StatusEnum.BAD_REQUEST.value
diff --git a/subscribe/migrations/0005_rename_sync_permission.py b/subscribe/migrations/0005_rename_sync_permission.py
new file mode 100644
index 000000000..24c3733fd
--- /dev/null
+++ b/subscribe/migrations/0005_rename_sync_permission.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.18 on 2024-09-05 08:33
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('subscribe', '0004_remove_legacy_subscribe_page_models'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='mailinglistsegment',
+ options={'permissions': [('can_sync_from_mailchimp', 'Can sync from Mailchimp')]},
+ ),
+ ]
diff --git a/subscribe/models.py b/subscribe/models.py
index 7d26a7ec5..9f24af23f 100644
--- a/subscribe/models.py
+++ b/subscribe/models.py
@@ -12,7 +12,7 @@ class MailingListSegment(models.Model):
class Meta:
permissions = [
- ("can_sync_from_campaign_monitor", "Can sync from Campaign Monitor"),
+ ("can_sync_from_mailchimp", "Can sync from Mailchimp"),
]
def __str__(self):
diff --git a/subscribe/templates/wagtailadmin/campaign_monitor_sync.html b/subscribe/templates/wagtailadmin/mailchimp_sync.html
similarity index 76%
rename from subscribe/templates/wagtailadmin/campaign_monitor_sync.html
rename to subscribe/templates/wagtailadmin/mailchimp_sync.html
index dc70268ff..9e38ff7ed 100644
--- a/subscribe/templates/wagtailadmin/campaign_monitor_sync.html
+++ b/subscribe/templates/wagtailadmin/mailchimp_sync.html
@@ -1,9 +1,9 @@
{% extends "wagtailadmin/base.html" %}
-{% block titletag %}Commands: Sync Campaign Monitor{% endblock %}
+{% block titletag %}Commands: Sync Mailchimp{% endblock %}
{% block content %}
- {% include "wagtailadmin/shared/header.html" with title="Sync Campaign Monitor" icon="mail" %}
+ {% include "wagtailadmin/shared/header.html" with title="Sync Mailchimp" icon="mail" %}