mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:11:08 -05:00
Refactor test utilities and enhance ASGI settings
- Cleaned up and standardized assertions in ApiTestMixin for API response validation. - Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE. - Removed unused imports and improved formatting in settings.py. - Refactored URL patterns in urls.py for better readability and organization. - Enhanced view functions in views.py for consistency and clarity. - Added .flake8 configuration for linting and style enforcement. - Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
@@ -1,36 +1,39 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.sites.models import Site
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from .models import EmailConfiguration
|
||||
|
||||
|
||||
@admin.register(EmailConfiguration)
|
||||
class EmailConfigurationAdmin(admin.ModelAdmin):
|
||||
list_display = ('site', 'from_name', 'from_email', 'reply_to', 'updated_at')
|
||||
list_select_related = ('site',)
|
||||
search_fields = ('site__domain', 'from_name', 'from_email', 'reply_to')
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
list_display = (
|
||||
"site",
|
||||
"from_name",
|
||||
"from_email",
|
||||
"reply_to",
|
||||
"updated_at",
|
||||
)
|
||||
list_select_related = ("site",)
|
||||
search_fields = ("site__domain", "from_name", "from_email", "reply_to")
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('site',)
|
||||
}),
|
||||
('Email Settings', {
|
||||
'fields': (
|
||||
'api_key',
|
||||
('from_name', 'from_email'),
|
||||
'reply_to'
|
||||
),
|
||||
'description': 'Configure the email settings. The From field in emails will appear as "From Name <from@email.com>"'
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
})
|
||||
(None, {"fields": ("site",)}),
|
||||
(
|
||||
"Email Settings",
|
||||
{
|
||||
"fields": ("api_key", ("from_name", "from_email"), "reply_to"),
|
||||
"description": 'Configure the email settings. The From field in emails will appear as "From Name <from@email.com>"',
|
||||
},
|
||||
),
|
||||
(
|
||||
"Timestamps",
|
||||
{"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related('site')
|
||||
return super().get_queryset(request).select_related("site")
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
if db_field.name == "site":
|
||||
kwargs["queryset"] = Site.objects.all().order_by('domain')
|
||||
kwargs["queryset"] = Site.objects.all().order_by("domain")
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.core.mail.message import sanitize_address
|
||||
from .services import EmailService
|
||||
from .models import EmailConfiguration
|
||||
|
||||
|
||||
class ForwardEmailBackend(BaseEmailBackend):
|
||||
def __init__(self, fail_silently=False, **kwargs):
|
||||
super().__init__(fail_silently=fail_silently)
|
||||
self.site = kwargs.get('site', None)
|
||||
self.site = kwargs.get("site", None)
|
||||
|
||||
def send_messages(self, email_messages):
|
||||
"""
|
||||
@@ -23,7 +23,7 @@ class ForwardEmailBackend(BaseEmailBackend):
|
||||
sent = self._send(message)
|
||||
if sent:
|
||||
num_sent += 1
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return num_sent
|
||||
@@ -33,11 +33,14 @@ class ForwardEmailBackend(BaseEmailBackend):
|
||||
if not email_message.recipients():
|
||||
return False
|
||||
|
||||
# Get the first recipient (ForwardEmail API sends to one recipient at a time)
|
||||
# Get the first recipient (ForwardEmail API sends to one recipient at a
|
||||
# time)
|
||||
to_email = email_message.to[0]
|
||||
|
||||
# Get site from connection or instance
|
||||
if hasattr(email_message, 'connection') and hasattr(email_message.connection, 'site'):
|
||||
if hasattr(email_message, "connection") and hasattr(
|
||||
email_message.connection, "site"
|
||||
):
|
||||
site = email_message.connection.site
|
||||
else:
|
||||
site = self.site
|
||||
@@ -49,11 +52,16 @@ class ForwardEmailBackend(BaseEmailBackend):
|
||||
try:
|
||||
config = EmailConfiguration.objects.get(site=site)
|
||||
except EmailConfiguration.DoesNotExist:
|
||||
raise ValueError(f"Email configuration not found for site: {site.domain}")
|
||||
raise ValueError(
|
||||
f"Email configuration not found for site: {
|
||||
site.domain}"
|
||||
)
|
||||
|
||||
# Get the from email, falling back to site's default if not provided
|
||||
if email_message.from_email:
|
||||
from_email = sanitize_address(email_message.from_email, email_message.encoding)
|
||||
from_email = sanitize_address(
|
||||
email_message.from_email, email_message.encoding
|
||||
)
|
||||
else:
|
||||
from_email = config.default_from_email
|
||||
|
||||
@@ -62,13 +70,16 @@ class ForwardEmailBackend(BaseEmailBackend):
|
||||
|
||||
# Get reply-to from message headers or use default
|
||||
reply_to = None
|
||||
if hasattr(email_message, 'reply_to') and email_message.reply_to:
|
||||
if hasattr(email_message, "reply_to") and email_message.reply_to:
|
||||
reply_to = email_message.reply_to[0]
|
||||
elif hasattr(email_message, 'extra_headers') and 'Reply-To' in email_message.extra_headers:
|
||||
reply_to = email_message.extra_headers['Reply-To']
|
||||
elif (
|
||||
hasattr(email_message, "extra_headers")
|
||||
and "Reply-To" in email_message.extra_headers
|
||||
):
|
||||
reply_to = email_message.extra_headers["Reply-To"]
|
||||
|
||||
# Get message content
|
||||
if email_message.content_subtype == 'html':
|
||||
if email_message.content_subtype == "html":
|
||||
# If it's HTML content, we'll send it as text for now
|
||||
# You could extend this to support HTML emails if needed
|
||||
text = email_message.body
|
||||
@@ -82,10 +93,10 @@ class ForwardEmailBackend(BaseEmailBackend):
|
||||
text=text,
|
||||
from_email=from_email,
|
||||
reply_to=reply_to,
|
||||
site=site
|
||||
site=site,
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return False
|
||||
|
||||
@@ -4,53 +4,51 @@ from django.contrib.sites.models import Site
|
||||
from django.test import RequestFactory, Client
|
||||
from allauth.account.models import EmailAddress
|
||||
from accounts.adapters import CustomAccountAdapter
|
||||
from email_service.services import EmailService
|
||||
from django.conf import settings
|
||||
import uuid
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Test all email flows in the application'
|
||||
help = "Test all email flows in the application"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(enforce_csrf_checks=False) # Disable CSRF for testing
|
||||
# Disable CSRF for testing
|
||||
self.client = Client(enforce_csrf_checks=False)
|
||||
self.adapter = CustomAccountAdapter()
|
||||
self.site = Site.objects.get_current()
|
||||
|
||||
|
||||
# Generate unique test data
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
self.test_username = f'testuser_{unique_id}'
|
||||
self.test_email = f'test_{unique_id}@thrillwiki.com'
|
||||
self.test_[PASSWORD-REMOVED]"
|
||||
self.new_[PASSWORD-REMOVED]"
|
||||
self.test_username = f"testuser_{unique_id}"
|
||||
self.test_email = f"test_{unique_id}@thrillwiki.com"
|
||||
self.test_password = "[PASSWORD-REMOVED]"
|
||||
self.new_password = "[PASSWORD-REMOVED]"
|
||||
|
||||
# Add testserver to ALLOWED_HOSTS
|
||||
if 'testserver' not in settings.ALLOWED_HOSTS:
|
||||
settings.ALLOWED_HOSTS.append('testserver')
|
||||
if "testserver" not in settings.ALLOWED_HOSTS:
|
||||
settings.ALLOWED_HOSTS.append("testserver")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Starting email flow tests...\n')
|
||||
self.stdout.write("Starting email flow tests...\n")
|
||||
|
||||
# Clean up any existing test users
|
||||
User.objects.filter(email__endswith='@thrillwiki.com').delete()
|
||||
User.objects.filter(email__endswith="@thrillwiki.com").delete()
|
||||
|
||||
# Test registration email
|
||||
self.test_registration()
|
||||
|
||||
# Create a test user for other flows
|
||||
user = User.objects.create_user(
|
||||
username=f'testuser2_{str(uuid.uuid4())[:8]}',
|
||||
email=f'test2_{str(uuid.uuid4())[:8]}@thrillwiki.com',
|
||||
password=self.test_password
|
||||
username=f"testuser2_{str(uuid.uuid4())[:8]}",
|
||||
email=f"test2_{str(uuid.uuid4())[:8]}@thrillwiki.com",
|
||||
password=self.test_password,
|
||||
)
|
||||
EmailAddress.objects.create(
|
||||
user=user,
|
||||
email=user.email,
|
||||
primary=True,
|
||||
verified=True
|
||||
user=user, email=user.email, primary=True, verified=True
|
||||
)
|
||||
|
||||
# Log in the test user
|
||||
@@ -62,89 +60,137 @@ class Command(BaseCommand):
|
||||
self.test_password_reset(user)
|
||||
|
||||
# Cleanup
|
||||
User.objects.filter(email__endswith='@thrillwiki.com').delete()
|
||||
self.stdout.write(self.style.SUCCESS('All email flow tests completed!\n'))
|
||||
User.objects.filter(email__endswith="@thrillwiki.com").delete()
|
||||
self.stdout.write(self.style.SUCCESS("All email flow tests completed!\n"))
|
||||
|
||||
def test_registration(self):
|
||||
"""Test registration email flow"""
|
||||
self.stdout.write('Testing registration email...')
|
||||
self.stdout.write("Testing registration email...")
|
||||
try:
|
||||
# Use dj-rest-auth registration endpoint
|
||||
response = self.client.post('/api/auth/registration/', {
|
||||
'username': self.test_username,
|
||||
'email': self.test_email,
|
||||
'password1': self.test_password,
|
||||
'password2': self.test_password
|
||||
}, content_type='application/json')
|
||||
|
||||
response = self.client.post(
|
||||
"/api/auth/registration/",
|
||||
{
|
||||
"username": self.test_username,
|
||||
"email": self.test_email,
|
||||
"password1": self.test_password,
|
||||
"password2": self.test_password,
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201, 204]:
|
||||
self.stdout.write(self.style.SUCCESS('Registration email test passed!\n'))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Registration email test passed!\n")
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f'Registration returned status {response.status_code}: {response.content.decode()}\n'
|
||||
f"Registration returned status {
|
||||
response.status_code}: {
|
||||
response.content.decode()}\n"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'Registration email test failed: {str(e)}\n'))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"Registration email test failed: {
|
||||
str(e)}\n"
|
||||
)
|
||||
)
|
||||
|
||||
def test_password_change(self, user):
|
||||
"""Test password change using dj-rest-auth"""
|
||||
self.stdout.write('Testing password change email...')
|
||||
self.stdout.write("Testing password change email...")
|
||||
try:
|
||||
response = self.client.post('/api/auth/password/change/', {
|
||||
'old_password': self.test_password,
|
||||
'new_password1': self.new_password,
|
||||
'new_password2': self.new_password
|
||||
}, content_type='application/json')
|
||||
|
||||
response = self.client.post(
|
||||
"/api/auth/password/change/",
|
||||
{
|
||||
"old_password": self.test_password,
|
||||
"new_password1": self.new_password,
|
||||
"new_password2": self.new_password,
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.stdout.write(self.style.SUCCESS('Password change email test passed!\n'))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Password change email test passed!\n")
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f'Password change returned status {response.status_code}: {response.content.decode()}\n'
|
||||
f"Password change returned status {
|
||||
response.status_code}: {
|
||||
response.content.decode()}\n"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'Password change email test failed: {str(e)}\n'))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"Password change email test failed: {
|
||||
str(e)}\n"
|
||||
)
|
||||
)
|
||||
|
||||
def test_email_change(self, user):
|
||||
"""Test email change verification"""
|
||||
self.stdout.write('Testing email change verification...')
|
||||
self.stdout.write("Testing email change verification...")
|
||||
try:
|
||||
new_email = f'newemail_{str(uuid.uuid4())[:8]}@thrillwiki.com'
|
||||
response = self.client.post('/api/auth/email/', {
|
||||
'email': new_email
|
||||
}, content_type='application/json')
|
||||
|
||||
new_email = f"newemail_{str(uuid.uuid4())[:8]}@thrillwiki.com"
|
||||
response = self.client.post(
|
||||
"/api/auth/email/",
|
||||
{"email": new_email},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.stdout.write(self.style.SUCCESS('Email change verification test passed!\n'))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Email change verification test passed!\n")
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f'Email change returned status {response.status_code}: {response.content.decode()}\n'
|
||||
f"Email change returned status {
|
||||
response.status_code}: {
|
||||
response.content.decode()}\n"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'Email change verification test failed: {str(e)}\n'))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"Email change verification test failed: {
|
||||
str(e)}\n"
|
||||
)
|
||||
)
|
||||
|
||||
def test_password_reset(self, user):
|
||||
"""Test password reset using dj-rest-auth"""
|
||||
self.stdout.write('Testing password reset email...')
|
||||
self.stdout.write("Testing password reset email...")
|
||||
try:
|
||||
# Request password reset
|
||||
response = self.client.post('/api/auth/password/reset/', {
|
||||
'email': user.email
|
||||
}, content_type='application/json')
|
||||
|
||||
response = self.client.post(
|
||||
"/api/auth/password/reset/",
|
||||
{"email": user.email},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.stdout.write(self.style.SUCCESS('Password reset email test passed!\n'))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Password reset email test passed!\n")
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f'Password reset returned status {response.status_code}: {response.content.decode()}\n'
|
||||
f"Password reset returned status {
|
||||
response.status_code}: {
|
||||
response.content.decode()}\n"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'Password reset email test failed: {str(e)}\n'))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"Password reset email test failed: {
|
||||
str(e)}\n"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.mail import send_mail, get_connection
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
from email_service.models import EmailConfiguration
|
||||
from email_service.services import EmailService
|
||||
from email_service.backends import ForwardEmailBackend
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Test the email service functionality'
|
||||
help = "Test the email service functionality"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--to',
|
||||
"--to",
|
||||
type=str,
|
||||
help='Recipient email address (optional, defaults to current user\'s email)',
|
||||
help="Recipient email address (optional, defaults to current user's email)",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-key',
|
||||
"--api-key",
|
||||
type=str,
|
||||
help='ForwardEmail API key (optional, will use configured value)',
|
||||
help="ForwardEmail API key (optional, will use configured value)",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--from-email',
|
||||
"--from-email",
|
||||
type=str,
|
||||
help='Sender email address (optional, will use configured value)',
|
||||
help="Sender email address (optional, will use configured value)",
|
||||
)
|
||||
|
||||
def get_config(self):
|
||||
@@ -35,58 +35,62 @@ class Command(BaseCommand):
|
||||
site = Site.objects.get(id=settings.SITE_ID)
|
||||
config = EmailConfiguration.objects.get(site=site)
|
||||
return {
|
||||
'api_key': config.api_key,
|
||||
'from_email': config.default_from_email,
|
||||
'site': site
|
||||
"api_key": config.api_key,
|
||||
"from_email": config.default_from_email,
|
||||
"site": site,
|
||||
}
|
||||
except (Site.DoesNotExist, EmailConfiguration.DoesNotExist):
|
||||
# Try environment variables
|
||||
api_key = os***REMOVED***iron.get('FORWARD_EMAIL_API_KEY')
|
||||
from_email = os***REMOVED***iron.get('FORWARD_EMAIL_FROM')
|
||||
|
||||
api_key = os.environ.get("FORWARD_EMAIL_API_KEY")
|
||||
from_email = os.environ.get("FORWARD_EMAIL_FROM")
|
||||
|
||||
if not api_key or not from_email:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'No configuration found in database or environment variables.\n'
|
||||
'Please either:\n'
|
||||
'1. Configure email settings in Django admin, or\n'
|
||||
'2. Set environment variables FORWARD_EMAIL_API_KEY and FORWARD_EMAIL_FROM, or\n'
|
||||
'3. Provide --api-key and --from-email arguments'
|
||||
))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"No configuration found in database or environment variables.\n"
|
||||
"Please either:\n"
|
||||
"1. Configure email settings in Django admin, or\n"
|
||||
"2. Set environment variables FORWARD_EMAIL_API_KEY and FORWARD_EMAIL_FROM, or\n"
|
||||
"3. Provide --api-key and --from-email arguments"
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
return {
|
||||
'api_key': api_key,
|
||||
'from_email': from_email,
|
||||
'site': Site.objects.get(id=settings.SITE_ID)
|
||||
"api_key": api_key,
|
||||
"from_email": from_email,
|
||||
"site": Site.objects.get(id=settings.SITE_ID),
|
||||
}
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS('Starting email service tests...'))
|
||||
self.stdout.write(self.style.SUCCESS("Starting email service tests..."))
|
||||
|
||||
# Get configuration
|
||||
config = self.get_config()
|
||||
if not config and not (options['api_key'] and options['from_email']):
|
||||
self.stdout.write(self.style.ERROR('No email configuration available. Tests aborted.'))
|
||||
if not config and not (options["api_key"] and options["from_email"]):
|
||||
self.stdout.write(
|
||||
self.style.ERROR("No email configuration available. Tests aborted.")
|
||||
)
|
||||
return
|
||||
|
||||
# Use provided values or fall back to config
|
||||
api_key = options['api_key'] or config['api_key']
|
||||
from_email = options['from_email'] or config['from_email']
|
||||
site = config['site']
|
||||
api_key = options["api_key"] or config["api_key"]
|
||||
from_email = options["from_email"] or config["from_email"]
|
||||
site = config["site"]
|
||||
|
||||
# If no recipient specified, use the from_email address for testing
|
||||
to_email = options['to'] or 'test@thrillwiki.com'
|
||||
to_email = options["to"] or "test@thrillwiki.com"
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Using configuration:'))
|
||||
self.stdout.write(f' From: {from_email}')
|
||||
self.stdout.write(f' To: {to_email}')
|
||||
self.stdout.write(self.style.SUCCESS("Using configuration:"))
|
||||
self.stdout.write(f" From: {from_email}")
|
||||
self.stdout.write(f" To: {to_email}")
|
||||
self.stdout.write(f' API Key: {"*" * len(api_key)}')
|
||||
self.stdout.write(f' Site: {site.domain}')
|
||||
self.stdout.write(f" Site: {site.domain}")
|
||||
|
||||
try:
|
||||
# 1. Test site configuration
|
||||
config = self.test_site_configuration(api_key, from_email)
|
||||
|
||||
|
||||
# 2. Test direct service
|
||||
self.test_email_service_directly(to_email, config.site)
|
||||
|
||||
@@ -96,118 +100,145 @@ class Command(BaseCommand):
|
||||
# 4. Test Django email backend
|
||||
self.test_email_backend(to_email, config.site)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('\nAll tests completed successfully! 🎉'))
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("\nAll tests completed successfully! 🎉")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'\nTests failed: {str(e)}'))
|
||||
self.stdout.write(self.style.ERROR(f"\nTests failed: {str(e)}"))
|
||||
|
||||
def test_site_configuration(self, api_key, from_email):
|
||||
"""Test creating and retrieving site configuration"""
|
||||
self.stdout.write('\nTesting site configuration...')
|
||||
|
||||
self.stdout.write("\nTesting site configuration...")
|
||||
|
||||
try:
|
||||
# Get or create default site
|
||||
site = Site.objects.get_or_create(
|
||||
id=settings.SITE_ID,
|
||||
defaults={
|
||||
'domain': 'example.com',
|
||||
'name': 'example.com'
|
||||
}
|
||||
defaults={"domain": "example.com", "name": "example.com"},
|
||||
)[0]
|
||||
|
||||
# Create or update email configuration
|
||||
config, created = EmailConfiguration.objects.update_or_create(
|
||||
site=site,
|
||||
defaults={
|
||||
'api_key': api_key,
|
||||
'default_from_email': from_email
|
||||
}
|
||||
"api_key": api_key,
|
||||
"default_from_email": from_email,
|
||||
},
|
||||
)
|
||||
|
||||
action = 'Created new' if created else 'Updated existing'
|
||||
self.stdout.write(self.style.SUCCESS(f'✓ {action} site configuration'))
|
||||
action = "Created new" if created else "Updated existing"
|
||||
self.stdout.write(self.style.SUCCESS(f"✓ {action} site configuration"))
|
||||
return config
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'✗ Site configuration failed: {str(e)}'))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"✗ Site configuration failed: {
|
||||
str(e)}"
|
||||
)
|
||||
)
|
||||
raise
|
||||
|
||||
def test_api_endpoint(self, to_email):
|
||||
"""Test sending email via the API endpoint"""
|
||||
self.stdout.write('\nTesting API endpoint...')
|
||||
|
||||
self.stdout.write("\nTesting API endpoint...")
|
||||
|
||||
try:
|
||||
# Make request to the API endpoint
|
||||
response = requests.post(
|
||||
'http://127.0.0.1:8000/api/email/send-email/',
|
||||
"http://127.0.0.1:8000/api/email/send-email/",
|
||||
json={
|
||||
'to': to_email,
|
||||
'subject': 'Test Email via API',
|
||||
'text': 'This is a test email sent via the API endpoint.'
|
||||
"to": to_email,
|
||||
"subject": "Test Email via API",
|
||||
"text": "This is a test email sent via the API endpoint.",
|
||||
},
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout=60)
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.stdout.write(self.style.SUCCESS('✓ API endpoint test successful'))
|
||||
self.stdout.write(self.style.SUCCESS("✓ API endpoint test successful"))
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f'✗ API endpoint test failed with status {response.status_code}: {response.text}'
|
||||
f"✗ API endpoint test failed with status {
|
||||
response.status_code}: {
|
||||
response.text}"
|
||||
)
|
||||
)
|
||||
raise Exception(f"API test failed: {response.text}")
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
'✗ API endpoint test failed: Could not connect to server. '
|
||||
'Make sure the Django development server is running.'
|
||||
"✗ API endpoint test failed: Could not connect to server. "
|
||||
"Make sure the Django development server is running."
|
||||
)
|
||||
)
|
||||
raise Exception("Could not connect to Django server")
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'✗ API endpoint test failed: {str(e)}'))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"✗ API endpoint test failed: {
|
||||
str(e)}"
|
||||
)
|
||||
)
|
||||
raise
|
||||
|
||||
def test_email_backend(self, to_email, site):
|
||||
"""Test sending email via Django's email backend"""
|
||||
self.stdout.write('\nTesting Django email backend...')
|
||||
|
||||
self.stdout.write("\nTesting Django email backend...")
|
||||
|
||||
try:
|
||||
# Create a connection with site context
|
||||
backend = ForwardEmailBackend(fail_silently=False, site=site)
|
||||
|
||||
|
||||
# Debug output
|
||||
self.stdout.write(f' Debug: Using from_email: {site.email_config.default_from_email}')
|
||||
self.stdout.write(f' Debug: Using to_email: {to_email}')
|
||||
|
||||
self.stdout.write(
|
||||
f" Debug: Using from_email: {
|
||||
site.email_config.default_from_email}"
|
||||
)
|
||||
self.stdout.write(f" Debug: Using to_email: {to_email}")
|
||||
|
||||
send_mail(
|
||||
subject='Test Email via Backend',
|
||||
message='This is a test email sent via the Django email backend.',
|
||||
subject="Test Email via Backend",
|
||||
message="This is a test email sent via the Django email backend.",
|
||||
from_email=site.email_config.default_from_email, # Explicitly set from_email
|
||||
recipient_list=[to_email],
|
||||
fail_silently=False,
|
||||
connection=backend
|
||||
connection=backend,
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS('✓ Email backend test successful'))
|
||||
self.stdout.write(self.style.SUCCESS("✓ Email backend test successful"))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'✗ Email backend test failed: {str(e)}'))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"✗ Email backend test failed: {
|
||||
str(e)}"
|
||||
)
|
||||
)
|
||||
raise
|
||||
|
||||
def test_email_service_directly(self, to_email, site):
|
||||
"""Test sending email directly via EmailService"""
|
||||
self.stdout.write('\nTesting EmailService directly...')
|
||||
|
||||
self.stdout.write("\nTesting EmailService directly...")
|
||||
|
||||
try:
|
||||
response = EmailService.send_email(
|
||||
to=to_email,
|
||||
subject='Test Email via Service',
|
||||
text='This is a test email sent directly via the EmailService.',
|
||||
site=site
|
||||
subject="Test Email via Service",
|
||||
text="This is a test email sent directly via the EmailService.",
|
||||
site=site,
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("✓ Direct EmailService test successful")
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS('✓ Direct EmailService test successful'))
|
||||
return response
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'✗ Direct EmailService test failed: {str(e)}'))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"✗ Direct EmailService test failed: {
|
||||
str(e)}"
|
||||
)
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -43,7 +43,8 @@ class Migration(migrations.Migration):
|
||||
(
|
||||
"site",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="sites.site"
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="sites.site",
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -55,7 +56,10 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name="EmailConfigurationEvent",
|
||||
fields=[
|
||||
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
|
||||
(
|
||||
"pgh_id",
|
||||
models.AutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("pgh_label", models.TextField(help_text="The event label.")),
|
||||
("id", models.BigIntegerField()),
|
||||
|
||||
@@ -3,11 +3,15 @@ from django.contrib.sites.models import Site
|
||||
from core.history import TrackedModel
|
||||
import pghistory
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class EmailConfiguration(TrackedModel):
|
||||
api_key = models.CharField(max_length=255)
|
||||
from_email = models.EmailField()
|
||||
from_name = models.CharField(max_length=255, help_text="The name that will appear in the From field of emails")
|
||||
from_name = models.CharField(
|
||||
max_length=255,
|
||||
help_text="The name that will appear in the From field of emails",
|
||||
)
|
||||
reply_to = models.EmailField()
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -7,9 +7,20 @@ from .models import EmailConfiguration
|
||||
import json
|
||||
import base64
|
||||
|
||||
|
||||
class EmailService:
|
||||
@staticmethod
|
||||
def send_email(*, to: str, subject: str, text: str, from_email: str = None, html: str = None, reply_to: str = None, request = None, site = None):
|
||||
def send_email(
|
||||
*,
|
||||
to: str,
|
||||
subject: str,
|
||||
text: str,
|
||||
from_email: str = None,
|
||||
html: str = None,
|
||||
reply_to: str = None,
|
||||
request=None,
|
||||
site=None,
|
||||
):
|
||||
# Get the site configuration
|
||||
if site is None and request is not None:
|
||||
site = get_current_site(request)
|
||||
@@ -20,23 +31,28 @@ class EmailService:
|
||||
# Fetch the email configuration for the current site
|
||||
email_config = EmailConfiguration.objects.get(site=site)
|
||||
api_key = email_config.api_key
|
||||
|
||||
|
||||
# Use provided from_email or construct from config
|
||||
if not from_email:
|
||||
from_email = f"{email_config.from_name} <{email_config.from_email}>"
|
||||
elif '<' not in from_email:
|
||||
# If from_email is provided but doesn't include a name, add the configured name
|
||||
from_email = f"{
|
||||
email_config.from_name} <{
|
||||
email_config.from_email}>"
|
||||
elif "<" not in from_email:
|
||||
# If from_email is provided but doesn't include a name, add the
|
||||
# configured name
|
||||
from_email = f"{email_config.from_name} <{from_email}>"
|
||||
|
||||
|
||||
# Use provided reply_to or fall back to config
|
||||
if not reply_to:
|
||||
reply_to = email_config.reply_to
|
||||
|
||||
except EmailConfiguration.DoesNotExist:
|
||||
raise ImproperlyConfigured(f"Email configuration is missing for site: {site.domain}")
|
||||
raise ImproperlyConfigured(
|
||||
f"Email configuration is missing for site: {site.domain}"
|
||||
)
|
||||
|
||||
# Ensure the reply_to address is clean
|
||||
reply_to = sanitize_address(reply_to, 'utf-8')
|
||||
reply_to = sanitize_address(reply_to, "utf-8")
|
||||
|
||||
# Format data for the API
|
||||
data = {
|
||||
@@ -74,7 +90,8 @@ class EmailService:
|
||||
f"{settings.FORWARD_EMAIL_BASE_URL}/v1/emails",
|
||||
json=data,
|
||||
headers=headers,
|
||||
timeout=60)
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
# Debug output
|
||||
print(f"Response Status: {response.status_code}")
|
||||
@@ -83,7 +100,10 @@ class EmailService:
|
||||
|
||||
if response.status_code != 200:
|
||||
error_message = response.text if response.text else "Unknown error"
|
||||
raise Exception(f"Failed to send email (Status {response.status_code}): {error_message}")
|
||||
raise Exception(
|
||||
f"Failed to send email (Status {
|
||||
response.status_code}): {error_message}"
|
||||
)
|
||||
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@@ -2,5 +2,5 @@ from django.urls import path
|
||||
from .views import SendEmailView
|
||||
|
||||
urlpatterns = [
|
||||
path('send-email/', SendEmailView.as_view(), name='send-email'),
|
||||
path("send-email/", SendEmailView.as_view(), name="send-email"),
|
||||
]
|
||||
|
||||
@@ -5,6 +5,7 @@ from rest_framework.permissions import AllowAny
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from .services import EmailService
|
||||
|
||||
|
||||
class SendEmailView(APIView):
|
||||
permission_classes = [AllowAny] # Allow unauthenticated access
|
||||
|
||||
@@ -16,30 +17,33 @@ class SendEmailView(APIView):
|
||||
from_email = data.get("from_email") # Optional
|
||||
|
||||
if not all([to, subject, text]):
|
||||
return Response({
|
||||
"error": "Missing required fields",
|
||||
"required_fields": ["to", "subject", "text"]
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{
|
||||
"error": "Missing required fields",
|
||||
"required_fields": ["to", "subject", "text"],
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
try:
|
||||
# Get the current site
|
||||
site = get_current_site(request)
|
||||
|
||||
|
||||
# Send email using the site's configuration
|
||||
response = EmailService.send_email(
|
||||
to=to,
|
||||
subject=subject,
|
||||
text=text,
|
||||
from_email=from_email, # Will use site's default if None
|
||||
site=site
|
||||
site=site,
|
||||
)
|
||||
|
||||
return Response({
|
||||
"message": "Email sent successfully",
|
||||
"response": response
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
return Response(
|
||||
{"message": "Email sent successfully", "response": response},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return Response({
|
||||
"error": str(e)
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return Response(
|
||||
{"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user