Source code for mailing_lists.models

__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"


import json

from django.db import models, transaction
from django.contrib.auth.models import User
from django.conf import settings
from django.contrib.auth.models import Group
from django.urls import reverse

from mailchimp3 import MailChimp

from .constants import (
    MAIL_LIST_STATUSES,
    MAIL_LIST_STATUS_ACTIVE,
    MAILCHIMP_STATUSES,
    MAILCHIMP_SUBSCRIBED,
)
from .managers import MailListManager

from profiles.models import Profile
from scipost.behaviors import TimeStampedModel
from scipost.constants import NORMAL_CONTRIBUTOR
from scipost.models import Contributor


[docs]class MailchimpList(TimeStampedModel): """ This model is a copy of the current lists in the Mailchimp account. It will be used to map the Contributor's preferences to the Mailchimp's lists and keeping both up to date. """ name = models.CharField(max_length=255) internal_name = models.CharField(max_length=255, blank=True) supporting_text = models.TextField(blank=True) mailchimp_list_id = models.CharField(max_length=255, unique=True) status = models.CharField( max_length=255, choices=MAIL_LIST_STATUSES, default=MAIL_LIST_STATUS_ACTIVE ) subscriber_count = models.PositiveIntegerField(default=0) open_for_subscription = models.BooleanField(default=False) allowed_groups = models.ManyToManyField( Group, related_name="allowed_mailchimp_lists" ) objects = MailListManager() class Meta: ordering = ["status", "internal_name", "name"] def __str__(self): if self.internal_name: return self.internal_name return self.name
[docs] def get_absolute_url(self): return reverse("mailing_lists:list_detail", args=[self.mailchimp_list_id])
[docs] @transaction.atomic def update_members(self, status="subscribed"): """ Update the subscribers in the MailChimp account. """ # Extreme timeset value (1 minute) to allow for huge maillist subscribes client = MailChimp(settings.MAILCHIMP_API_USER, settings.MAILCHIMP_API_KEY) try: unsubscribe_emails = [] # Find all campaigns on the account campaigns = client.campaigns.all(get_all=True) for campaign in campaigns["campaigns"]: # All unsubscriptions are registered per campaign # Should be improved later on unsubscribers = client.reports.unsubscribes.all(campaign["id"], True) for unsubscriber in unsubscribers["unsubscribes"]: if unsubscriber["list_id"] == self.mailchimp_list_id: unsubscribe_emails.append(unsubscriber["email_address"]) except KeyError: # Call with MailChimp went wrong, returned invalid data return None # Unsubscribe *all* Contributors in the database if asked for updated_contributors = Profile.objects.filter( accepts_SciPost_emails=True, contributor__user__email__in=unsubscribe_emails ).update(accepts_SciPost_emails=False) # Check the current list of subscribers in MailChimp account subscribers_list = client.lists.members.all( self.mailchimp_list_id, True, fields="members.email_address" ) subscribers_list = [sub["email_address"] for sub in subscribers_list["members"]] # Retrieve *users* that are in the right group and didn't unsubscribe and # are not in the list yet. db_subscribers = ( User.objects.filter(contributor__isnull=False) .filter(is_active=True, contributor__status=NORMAL_CONTRIBUTOR) .filter( contributor__profile__accepts_SciPost_emails=True, groups__in=self.allowed_groups.all(), email__isnull=False, first_name__isnull=False, last_name__isnull=False, ) .exclude(email__in=subscribers_list) ) # Build batch data batch_data = {"operations": []} add_member_path = "lists/%s/members" % self.mailchimp_list_id for user in db_subscribers: batch_data["operations"].append( { "method": "POST", "path": add_member_path, "body": json.dumps( { "status": status, "status_if_new": status, "email_address": user.email, "merge_fields": { "FNAME": user.first_name, "LNAME": user.last_name, }, } ), } ) # Make the subscribe call post_response = client.batches.create(data=batch_data) list_data = client.lists.get(list_id=self.mailchimp_list_id) self.subscriber_count = list_data["stats"]["member_count"] self.save() return (updated_contributors, len(db_subscribers), post_response)
[docs]class MailchimpSubscription(TimeStampedModel): """ Track the Contributors' settings on wheter he/she wants to have an active subscription to a specific (public) list. """ active_list = models.ForeignKey( "mailing_lists.MailchimpList", on_delete=models.CASCADE ) contributor = models.ForeignKey( "scipost.Contributor", on_delete=models.CASCADE, related_name="mail_subscription", ) status = models.CharField( max_length=255, choices=MAILCHIMP_STATUSES, default=MAILCHIMP_SUBSCRIBED ) class Meta: unique_together = ( "active_list", "contributor", )