Skip to content

Commit

Permalink
Merge pull request #245 from 2077-Collective/feat/newsletter
Browse files Browse the repository at this point in the history
Feat/newsletter
  • Loading branch information
losndu authored Jan 17, 2025
2 parents 4927e6b + 1765a73 commit d537948
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 14 deletions.
35 changes: 29 additions & 6 deletions server/apps/newsletter/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from typing import Optional, Dict, Any
import logging
from tenacity import retry, stop_after_attempt, wait_exponential

logger = logging.getLogger(__name__)

class BeehiivService:
def __init__(self):
Expand All @@ -19,33 +23,42 @@ def __init__(self):
"Content-Type": "application/json"
}

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def create_subscriber(self, email: str, is_active: bool = True) -> Dict[str, Any]:
"""
Create a new subscriber in Beehiiv
Create a new subscriber in Beehiiv.
"""
endpoint = f"{self.base_url}/publications/{self.publication_id}/subscriptions"

data = {
"email": email,
"reactivate_existing": True,
"send_welcome_email": True,
"utm_source": "django_website",
"utm_source": "2077 Research",
"status": "active" if is_active else "inactive"
}

try:
logger.info(f"Creating Beehiiv subscriber: {email}")
response = requests.post(endpoint, headers=self.headers, json=data, timeout=10)
response.raise_for_status()
data = response.json()
if 'id' not in data:
raise ValueError("Invalid response from Beehiiv API")

# Check for "invalid" status
if data.get('data', {}).get('status') == 'invalid':
logger.warning(f"Beehiiv API returned an invalid status for {email}")
raise ValueError("Beehiiv API returned an invalid status. The email may need verification.")

logger.info(f"Successfully created Beehiiv subscriber: {email}")
return data
except requests.exceptions.RequestException as e:
logger.error(f"Failed to create Beehiiv subscriber {email}: {str(e)}")
raise Exception(f"Failed to create Beehiiv subscriber: {str(e)}") from e

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def update_subscriber_status(self, email: str, is_active: bool) -> Dict[str, Any]:
"""
Update subscriber status in Beehiiv
Update subscriber status in Beehiiv.
"""
endpoint = f"{self.base_url}/publications/{self.publication_id}/subscriptions/email:{email}"

Expand All @@ -54,24 +67,34 @@ def update_subscriber_status(self, email: str, is_active: bool) -> Dict[str, Any
}

try:
logger.info(f"Updating Beehiiv subscriber status: {email}")
response = requests.patch(endpoint, headers=self.headers, json=data, timeout=10)
response.raise_for_status()
data = response.json()

if 'id' not in data:
logger.warning(f"Invalid response from Beehiiv API for {email}")
raise ValueError("Invalid response from Beehiiv API")

logger.info(f"Successfully updated Beehiiv subscriber status: {email}")
return data
except requests.exceptions.RequestException as e:
logger.error(f"Failed to update Beehiiv subscriber {email}: {str(e)}")
raise Exception(f"Failed to update Beehiiv subscriber: {str(e)}") from e

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def delete_subscriber(self, email: str) -> bool:
"""
Delete a subscriber from Beehiiv
Delete a subscriber from Beehiiv.
"""
endpoint = f"{self.base_url}/publications/{self.publication_id}/subscriptions/email:{email}"

try:
logger.info(f"Deleting Beehiiv subscriber: {email}")
response = requests.delete(endpoint, headers=self.headers, timeout=10)
response.raise_for_status()
logger.info(f"Successfully deleted Beehiiv subscriber: {email}")
return True
except requests.exceptions.RequestException as e:
logger.error(f"Failed to delete Beehiiv subscriber {email}: {str(e)}")
raise Exception(f"Failed to delete Beehiiv subscriber: {str(e)}") from e
31 changes: 26 additions & 5 deletions server/apps/newsletter/tasks.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
from celery import shared_task
from django.core.mail import EmailMessage, send_mail
from django.conf import settings
from .models import Newsletter, Subscriber
from django.utils import timezone
from django.utils.html import format_html
from .models import Newsletter, Subscriber
from .services import BeehiivService
import logging

logger = logging.getLogger(__name__)

@shared_task
def send_newsletter_via_email():
"""
Send newsletters to active subscribers.
"""
now = timezone.now()
# Get newsletters that need to be sent
newsletters = Newsletter.objects.filter(scheduled_send_time__lte=now, is_sent=False)
Expand All @@ -16,7 +23,6 @@ def send_newsletter_via_email():

for subscriber in subscribers:
try:

unsubscribe_link = format_html(
'{}/newsletter/unsubscribe/{}/',
settings.SITE_URL, # Ensure this is set in your settings, e.g., 'http://127.0.0.1:8000'
Expand All @@ -34,13 +40,28 @@ def send_newsletter_via_email():
)

except Exception as e:
print(f"Error sending email to {subscriber.email}: {e}")
logger.error(f"Error sending email to {subscriber.email}: {e}")

# Mark newsletter as sent
newsletter.is_sent = True
newsletter.last_sent = timezone.now()
newsletter.save()

# Return a success message
# Log a success message
subscriber_count = Subscriber.objects.filter(is_active=True).count()
print(f'Newsletter sent to {subscriber_count} subscribers')
logger.info(f'Newsletter sent to {subscriber_count} subscribers')

@shared_task
def sync_to_beehiiv_task(email: str):
"""
Sync a subscriber to Beehiiv.
"""
beehiiv = BeehiivService()
try:
beehiiv.create_subscriber(email, is_active=True)
except ValueError as e:
# Handle Beehiiv "invalid" status
logger.warning(f"Beehiiv sync warning for {email}: {str(e)}")
except Exception as e:
# Log the error
logger.error(f"Error syncing {email} to Beehiiv: {str(e)}")
24 changes: 21 additions & 3 deletions server/apps/newsletter/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from django.db import IntegrityError
from django.shortcuts import render
from .models import Subscriber
import logging
from .tasks import sync_to_beehiiv_task

logger = logging.getLogger(__name__)

@csrf_exempt
def subscribe(request):
Expand All @@ -18,12 +22,26 @@ def subscribe(request):

try:
# Create a new subscriber
Subscriber.objects.create(email=email, is_active=True)
subscriber, created = Subscriber.objects.get_or_create(email=email, defaults={'is_active': True})

if not created:
# Handle case where email is already subscribed
return JsonResponse({'message': 'Email already subscribed'}, status=200)

# Enqueue the sync task with Beehiiv
sync_to_beehiiv_task.delay(email)

return JsonResponse({'message': 'Subscription successful'}, status=200)

except IntegrityError:
# Handle case where email is already subscribed
return JsonResponse({'message': 'Email already subscribed'}, status=400)
# Handle database integrity errors
logger.error(f"IntegrityError while subscribing {email}")
return JsonResponse({'message': 'An error occurred during subscription'}, status=500)

except Exception as e:
# Log the error and return a generic error message
logger.error(f"Error during subscription: {str(e)}")
return JsonResponse({'message': 'An error occurred during subscription'}, status=500)

# If the request method is not POST, return a 405 Method Not Allowed response
return JsonResponse({'message': 'Method not allowed'}, status=405)
Expand Down

0 comments on commit d537948

Please sign in to comment.