From c10b68f23efb4f5be9ea20860df62ae2a11ee9ee Mon Sep 17 00:00:00 2001 From: boryanagoncharenko <3010723+boryanagoncharenko@users.noreply.github.com> Date: Sun, 16 Feb 2025 13:55:37 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9C=20Add=20tags=20to=20mailchimp=20us?= =?UTF-8?q?ers=20(again)=20(#6158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add tags to mailchimp users #3995 * Refactoring * Improve Mailchimp calls * Add slides #3995 --- app.py | 5 ++ website/auth.py | 40 --------- website/auth_pages.py | 16 +--- website/classes.py | 2 + website/for_teachers.py | 6 ++ website/newsletter.py | 180 ++++++++++++++++++++++++++++++++++++++++ website/profile.py | 29 +++---- 7 files changed, 205 insertions(+), 73 deletions(-) create mode 100644 website/newsletter.py diff --git a/app.py b/app.py index 518d2a67799..7289ec886e7 100644 --- a/app.py +++ b/app.py @@ -55,6 +55,7 @@ from website.log_fetcher import log_fetcher from website.frontend_types import Adventure, Program, ExtraStory, SaveInfo from website.flask_hedy import g_db +from website.newsletter import add_used_slides_to_subscription logConfig(LOGGING_CONFIG) logger = logging.getLogger(__name__) @@ -2482,6 +2483,10 @@ def get_slides(level): if not SLIDES[g.lang].get_slides_for_level(level, keyword_language): return utils.error_page(error=404, ui_message="Slides do not exist!") + email = current_user().get('email') + if email: + add_used_slides_to_subscription(email) + slides = SLIDES[g.lang].get_slides_for_level(level, keyword_language) return render_template('slides.html', level=level, slides=slides) diff --git a/website/auth.py b/website/auth.py index 6e2e38069d9..af1065f3958 100644 --- a/website/auth.py +++ b/website/auth.py @@ -1,13 +1,10 @@ -import json import logging import os -import re import urllib from functools import wraps import bcrypt import boto3 -import requests from botocore.exceptions import ClientError as email_error from botocore.exceptions import NoCredentialsError from flask import request, session, redirect @@ -20,7 +17,6 @@ from safe_format import safe_format from utils import is_debug_mode, timems, times from website import querylog - TOKEN_COOKIE_NAME = config["session"]["cookie_name"] # A special value in the session, if this is set and we hit a 403 on the @@ -36,42 +32,6 @@ env = os.getenv("HEROKU_APP_NAME") -MAILCHIMP_API_URL = None -MAILCHIMP_API_HEADERS = {} -if os.getenv("MAILCHIMP_API_KEY") and os.getenv("MAILCHIMP_AUDIENCE_ID"): - # The domain in the path is the server name, which is contained in the Mailchimp API key - MAILCHIMP_API_URL = ( - "https://" - + os.getenv("MAILCHIMP_API_KEY").split("-")[1] - + ".api.mailchimp.com/3.0/lists/" - + os.getenv("MAILCHIMP_AUDIENCE_ID") - ) - MAILCHIMP_API_HEADERS = { - "Content-Type": "application/json", - "Authorization": "apikey " + os.getenv("MAILCHIMP_API_KEY"), - } - - -def mailchimp_subscribe_user(email, country): - # Request is always for teachers as only they can subscribe to newsletters - request_body = {"email_address": email, "status": "subscribed", "tags": [country, "teacher"]} - r = requests.post(MAILCHIMP_API_URL + "/members", headers=MAILCHIMP_API_HEADERS, data=json.dumps(request_body)) - - subscription_error = None - if r.status_code != 200 and r.status_code != 400: - subscription_error = True - # We can get a 400 if the email is already subscribed to the list. We should ignore this error. - if r.status_code == 400 and not re.match(".*already a list member", r.text): - subscription_error = True - # If there's an error in subscription through the API, we report it to the main email address - if subscription_error: - send_email( - config["email"]["sender"], - "ERROR - Subscription to Hedy newsletter on signup", - email, - "
" + email + "
Status:" + str(r.status_code) + " Body:" + r.text + "", - ) - @querylog.timed def check_password(password, hash): diff --git a/website/auth_pages.py b/website/auth_pages.py index 857ca10d4c2..bd60011d1d5 100644 --- a/website/auth_pages.py +++ b/website/auth_pages.py @@ -7,8 +7,8 @@ from safe_format import safe_format from hedy_content import ALL_LANGUAGES, COUNTRIES from utils import extract_bcrypt_rounds, is_heroku, is_testing_request, timems, times, remove_class_preview +from website.newsletter import create_subscription from website.auth import ( - MAILCHIMP_API_URL, RESET_LENGTH, SESSION_LENGTH, TOKEN_COOKIE_NAME, @@ -20,13 +20,11 @@ forget_current_user, is_admin, is_teacher, - mailchimp_subscribe_user, make_salt, password_hash, prepare_user_db, remember_current_user, requires_login, - send_email, send_email_template, send_localized_email_template, validate_signup_data, @@ -180,17 +178,7 @@ def signup(self): user, resp = self.store_new_account(body, body["email"].strip().lower()) if not is_testing_request(request) and "subscribe" in body: - # If we have a Mailchimp API key, we use it to add the subscriber through the API - if MAILCHIMP_API_URL: - mailchimp_subscribe_user(user["email"], body["country"]) - # Otherwise, we send an email to notify about the subscription to the main email address - else: - send_email( - config["email"]["sender"], - "Subscription to Hedy newsletter on signup", - user["email"], - "
" + user["email"] + "
", - ) + create_subscription(user["email"], body.get("country")) # We automatically login the user cookie = make_salt() diff --git a/website/classes.py b/website/classes.py index 9b85220c50d..9c85a78103e 100644 --- a/website/classes.py +++ b/website/classes.py @@ -9,6 +9,7 @@ from website.flask_helpers import render_template from website.auth import current_user, is_teacher, requires_login, requires_teacher, \ refresh_current_user_from_db, is_second_teacher +from website.newsletter import add_class_created_to_subscription from .database import Database from .website_module import WebsiteModule, route @@ -50,6 +51,7 @@ def create_class(self, user): } self.db.store_class(Class) + add_class_created_to_subscription(user['email']) response = {"id": Class["id"]} return make_response(response, 200) diff --git a/website/for_teachers.py b/website/for_teachers.py index 556f8df913e..e7debed6fdd 100644 --- a/website/for_teachers.py +++ b/website/for_teachers.py @@ -19,6 +19,7 @@ from website.server_types import SortedAdventure from website.flask_helpers import render_template +from website.newsletter import add_class_customized_to_subscription from website.auth import ( is_admin, is_teacher, @@ -888,6 +889,7 @@ def add_adventure(self, user, level): customizations["other_settings"].remove("hide_parsons") self.db.update_class_customizations(customizations) + add_class_customized_to_subscription(user['email']) available_adventures = self.get_unused_adventures(adventures, teacher_adventures, adventure_names) return render_partial('customize-class/partial-sortable-adventures.html', @@ -925,6 +927,7 @@ def remove_adventure_from_class(self, user): is_command_adventure=adventure_id in hedy_content.KEYWORDS_ADVENTURES) adventures[int(level)].remove(sorted_adventure) self.db.update_class_customizations(customizations) + add_class_customized_to_subscription(user['email']) available_adventures = self.get_unused_adventures(adventures, teacher_adventures, adventure_names) return render_partial('customize-class/partial-sortable-adventures.html', @@ -968,6 +971,7 @@ def sort_adventures_in_class(self, user): self.reorder_adventures(adventures[int(level)], from_sorted_adv_class=True) self.reorder_adventures(customizations['sorted_adventures'][level]) self.db.update_class_customizations(customizations) + add_class_customized_to_subscription(user['email']) return render_partial('customize-class/partial-sortable-adventures.html', level=level, @@ -1345,6 +1349,7 @@ def update_customizations(self, user, class_id): } self.db.update_class_customizations(customizations) + add_class_customized_to_subscription(user['email']) response = {"success": gettext("class_customize_success")} return make_response(response, 200) @@ -1773,6 +1778,7 @@ def add_adventure_to_class_level(self, user, class_id, adventure_id, level, remo self.reorder_adventures(customizations['sorted_adventures'][level]) self.db.update_class_customizations(customizations) + add_class_customized_to_subscription(user['email']) @route("/create-adventure/", methods=["POST"]) @route("/create-adventure/{email}
Status:{r.status_code} Body:{r.text}") + return not subscription_error + + +def get_mailchimp_subscriber(email): + request_path = f'{MAILCHIMP_API_URL}/members/{get_subscriber_hash(email)}' + return requests.get(request_path, headers=MAILCHIMP_API_HEADERS, timeout=MAILCHIMP_TIMEOUT_SECONDS) + + +def update_mailchimp_tags(email, tags): + request_path = f'{MAILCHIMP_API_URL}/members/{get_subscriber_hash(email)}/tags' + return requests.post( + request_path, + headers=MAILCHIMP_API_HEADERS, + data=json.dumps({'tags': tags}), + timeout=MAILCHIMP_TIMEOUT_SECONDS + ) + + +def delete_mailchimp_subscriber(email): + request_path = f'{MAILCHIMP_API_URL}/members/{get_subscriber_hash(email)}' + requests.delete(request_path, headers=MAILCHIMP_API_HEADERS, timeout=MAILCHIMP_TIMEOUT_SECONDS) + + +def get_subscriber_hash(email): + """ Hashes the email with md5 to avoid emails with unescaped characters triggering errors """ + return hashlib.md5(email.encode("utf-8")).hexdigest() diff --git a/website/profile.py b/website/profile.py index a68dbd1eadd..5d82a485e47 100644 --- a/website/profile.py +++ b/website/profile.py @@ -1,7 +1,5 @@ import datetime -import hashlib -import requests from flask import make_response, request, session from website.flask_helpers import gettext_with_fallback as gettext @@ -9,17 +7,15 @@ from hedy_content import ALL_KEYWORD_LANGUAGES, ALL_LANGUAGES, COUNTRIES from utils import is_testing_request, timems, valid_email from website.auth import ( - MAILCHIMP_API_HEADERS, - MAILCHIMP_API_URL, SESSION_LENGTH, create_verify_link, - mailchimp_subscribe_user, make_salt, password_hash, remember_current_user, requires_login, send_email_template, ) +from website.newsletter import update_subscription from .database import Database from .website_module import WebsiteModule, route @@ -71,7 +67,7 @@ def update_profile(self, user): if "email" in body: email = body["email"].strip().lower() old_user_email = user.get("email") - if email != user.get("email"): + if email != old_user_email: exists = self.db.user_by_email(email) if exists: return make_response(gettext("exists_email"), 403) @@ -95,19 +91,6 @@ def update_profile(self, user): # Add a notification to the response, still process the changes print(f"Profile changes processed for {user['username']}, mail sending invalid") - # We check whether the user is in the Mailchimp list. - if not is_testing_request(request) and MAILCHIMP_API_URL: - # We hash the email with md5 to avoid emails with unescaped characters triggering errors - request_path = ( - MAILCHIMP_API_URL + "/members/" + hashlib.md5(old_user_email.encode("utf-8")).hexdigest() - ) - r = requests.get(request_path, headers=MAILCHIMP_API_HEADERS) - # If user is subscribed, we remove the old email from the list and add the new one - if r.status_code == 200: - r = requests.delete(request_path, headers=MAILCHIMP_API_HEADERS) - self.db.get_username_role(user["username"]) - mailchimp_subscribe_user(email, body["country"]) - username = user["username"] updates = {} @@ -117,6 +100,14 @@ def update_profile(self, user): else: updates[field] = None + if not is_testing_request(request): + current_email = user.get('email') + current_country = user.get('country') + new_email = body.get('email', '').strip().lower() + new_country = body.get('country') + if current_email != new_email or current_country != new_country: + update_subscription(current_email, new_email, new_country) + if updates: self.db.update_user(username, updates)