-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from happychuks/main
feat: Newsletter Feature Added
- Loading branch information
Showing
23 changed files
with
491 additions
and
4 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from django.contrib import admin, messages | ||
from .models import Newsletter, Subscriber | ||
from .tasks import send_newsletter_via_email | ||
|
||
@admin.action(description='Send selected newsletters') | ||
def send_selected_newsletters(modeladmin, request, queryset): | ||
for newsletter in queryset: | ||
send_newsletter_via_email.delay() | ||
|
||
messages.success(request, f"{queryset.count()} newsletters have been scheduled for sending.") | ||
|
||
|
||
@admin.register(Newsletter) | ||
class NewsletterAdmin(admin.ModelAdmin): | ||
list_display = ['subject', 'is_sent', 'scheduled_send_time', 'last_sent'] | ||
readonly_fields = ['last_sent'] | ||
actions = [send_selected_newsletters] | ||
|
||
def save_model(self, request, obj, form, change): | ||
if change: # If editing an existing object | ||
obj.is_sent = False # Set 'is_sent' to False when editing | ||
super().save_model(request, obj, form, change) | ||
|
||
@admin.register(Subscriber) | ||
class SubscriberAdmin(admin.ModelAdmin): | ||
list_display = ['email', 'is_active', 'subscribed_at'] | ||
list_filter = ['is_active'] | ||
search_fields = ['email'] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from django.apps import AppConfig | ||
|
||
class NewsletterConfig(AppConfig): | ||
default_auto_field = 'django.db.models.BigAutoField' | ||
name = 'apps.newsletter' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from django import forms | ||
|
||
class SubscribeForm(forms.Form): | ||
email = forms.EmailField(label='Enter your email', required=True) |
18 changes: 18 additions & 0 deletions
18
server/apps/newsletter/migrations/0011_alter_newsletter_scheduled_send_time.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Generated by Django 5.0.8 on 2024-08-19 23:13 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('newsletter', '0010_newsletter_last_sent'), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name='newsletter', | ||
name='scheduled_send_time', | ||
field=models.DateTimeField(blank=True, null=True), | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from django.db import models | ||
from apps.common.models import BaseModel | ||
from django.conf import settings | ||
from django.utils import timezone | ||
|
||
# Create your models here. | ||
class Subscriber(BaseModel): | ||
email = models.EmailField(unique=True) | ||
is_active = models.BooleanField(default=True) | ||
subscribed_at = models.DateTimeField(auto_now_add=True) | ||
|
||
def __str__(self): | ||
return self.email | ||
|
||
class Newsletter(BaseModel): | ||
"""Model for storing newsletters.""" | ||
subject = models.CharField(max_length=255) | ||
content = models.TextField() | ||
is_sent = models.BooleanField(default=False) | ||
created_at = models.DateTimeField(auto_now_add=True) | ||
last_sent = models.DateTimeField(blank=True, null=True) | ||
scheduled_send_time = models.DateTimeField(blank=True, null=True) | ||
|
||
def __str__(self): | ||
return self.subject |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
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 | ||
|
||
@shared_task | ||
def send_newsletter_via_email(): | ||
now = timezone.now() | ||
# Get newsletters that need to be sent | ||
newsletters = Newsletter.objects.filter(scheduled_send_time__lte=now, is_sent=False) | ||
|
||
for newsletter in newsletters: | ||
subscribers = Subscriber.objects.filter(is_active=True) | ||
|
||
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' | ||
subscriber.email | ||
) | ||
|
||
content = newsletter.content.replace('{unsubscribe_link}', unsubscribe_link) | ||
|
||
send_mail( | ||
subject=newsletter.subject, | ||
message='', | ||
from_email=settings.DEFAULT_FROM_EMAIL, | ||
recipient_list=[subscriber.email], | ||
html_message=content | ||
) | ||
|
||
except Exception as e: | ||
print(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 | ||
subscriber_count = Subscriber.objects.filter(is_active=True).count() | ||
print(f'Newsletter sent to {subscriber_count} subscribers') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# tests.py | ||
from django.test import TestCase, Client | ||
from django.urls import reverse | ||
from .models import Subscriber | ||
from .forms import SubscribeForm | ||
|
||
class NewsletterViewsTest(TestCase): | ||
|
||
def setUp(self): | ||
self.client = Client() | ||
self.subscribe_url = reverse('subscribe') # Adjust if URL name is different | ||
self.unsubscribe_url = reverse('unsubscribe', args=['[email protected]']) | ||
|
||
def test_subscribe_view_get(self): | ||
response = self.client.get(self.subscribe_url) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertTemplateUsed(response, 'newsletter/subscribe.html') | ||
self.assertIsInstance(response.context['form'], SubscribeForm) | ||
|
||
def test_subscribe_view_post_valid(self): | ||
response = self.client.post(self.subscribe_url, {'email': '[email protected]'}) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertTemplateUsed(response, 'newsletter/success.html') | ||
self.assertEqual(Subscriber.objects.count(), 1) | ||
self.assertEqual(Subscriber.objects.get().email, '[email protected]') | ||
|
||
def test_subscribe_view_post_invalid(self): | ||
response = self.client.post(self.subscribe_url, {'email': ''}) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertTemplateUsed(response, 'newsletter/subscribe.html') | ||
self.assertFormError(response, 'form', 'email', 'This field is required.') | ||
|
||
def test_unsubscribe_view_valid(self): | ||
Subscriber.objects.create(email='[email protected]', is_active=True) | ||
response = self.client.get(reverse('unsubscribe', args=['[email protected]'])) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertTemplateUsed(response, 'newsletter/unsubscribe_success.html') | ||
self.assertFalse(Subscriber.objects.get(email='[email protected]').is_active) | ||
|
||
def test_unsubscribe_view_invalid(self): | ||
response = self.client.get(reverse('unsubscribe', args=['[email protected]'])) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertTemplateUsed(response, 'newsletter/unsubscribe_fail.html') | ||
self.assertEqual(Subscriber.objects.count(), 0) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from django.urls import path | ||
from . import views | ||
|
||
app_name = 'newsletter' | ||
|
||
urlpatterns = [ | ||
path('subscribe/', views.subscribe, name='subscribe'), | ||
path('unsubscribe/<str:email>/', views.unsubscribe, name='unsubscribe'), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
from django.shortcuts import render, redirect | ||
from django.http import HttpResponse | ||
from .models import Subscriber | ||
from .forms import SubscribeForm | ||
from django.views.decorators.csrf import csrf_exempt | ||
|
||
|
||
@csrf_exempt # Only use this if you can't handle CSRF token in your frontend | ||
def subscribe(request): | ||
if request.method == 'POST': | ||
form = SubscribeForm(request.POST) | ||
if form.is_valid(): | ||
email = form.cleaned_data['email'] | ||
Subscriber.objects.create(email=email) | ||
return HttpResponse('You have successfully subscribed.') | ||
else: | ||
form = SubscribeForm() | ||
|
||
return render(request, 'newsletter/subscribe.html', {'form': form}) | ||
|
||
def unsubscribe(request, email): | ||
try: | ||
subscriber = Subscriber.objects.get(email=email) | ||
subscriber.is_active = False | ||
subscriber.save() | ||
return render(request, 'newsletter/unsubscribe_success.html', {'email': email}) | ||
except Subscriber.DoesNotExist: | ||
return render(request, 'newsletter/unsubscribe_fail.html', {'email': None}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
from .base import * | ||
|
||
from .base import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from decouple import config | ||
|
||
SITE_URL=config('SITE_URL') | ||
|
||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' | ||
EMAIL_HOST = config('EMAIL_HOST') | ||
EMAIL_PORT = 465 | ||
EMAIL_USE_TLS = False | ||
EMAIL_USE_SSL = True | ||
EMAIL_HOST_USER = config('EMAIL_HOST_USER') | ||
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD') | ||
DEFAULT_FROM_EMAIL = '2077 Collective' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from django.http import JsonResponse | ||
from django.middleware.csrf import get_token | ||
|
||
def csrf_token_view(request): | ||
return JsonResponse({'csrfToken': get_token(request)}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.