Skip to content

Commit

Permalink
Merge pull request #17 from happychuks/main
Browse files Browse the repository at this point in the history
feat: Newsletter Feature Added
  • Loading branch information
happychuks authored Aug 19, 2024
2 parents c246480 + e30f2c6 commit 5db604e
Show file tree
Hide file tree
Showing 23 changed files with 491 additions and 4 deletions.
Empty file.
29 changes: 29 additions & 0 deletions server/apps/newsletter/admin.py
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']

5 changes: 5 additions & 0 deletions server/apps/newsletter/apps.py
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'
4 changes: 4 additions & 0 deletions server/apps/newsletter/forms.py
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)
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.
25 changes: 25 additions & 0 deletions server/apps/newsletter/models.py
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
46 changes: 46 additions & 0 deletions server/apps/newsletter/tasks.py
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')
44 changes: 44 additions & 0 deletions server/apps/newsletter/tests.py
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)
9 changes: 9 additions & 0 deletions server/apps/newsletter/urls.py
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'),
]
28 changes: 28 additions & 0 deletions server/apps/newsletter/views.py
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})
5 changes: 5 additions & 0 deletions server/core/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
'task': 'apps.research.tasks.publish_scheduled_articles',
'schedule': crontab(minute='*/1'), # Runs every minute
},

'send-scheduled-newsletter': {
'task': 'apps.newsletter.tasks.send_newsletter_via_email',
'schedule': crontab(minute='*/1'), # Runs every minute
},
}

@app.task(bind=True)
Expand Down
2 changes: 2 additions & 0 deletions server/core/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
LOCAL_APPS = [
'apps.common',
'apps.research',
'apps.newsletter',
]

THIRD_PARTY_APPS = [
Expand Down Expand Up @@ -187,3 +188,4 @@
from .jazzmin import *
from .ckeditor import *
from .celery_config import *
from .mail import *
4 changes: 2 additions & 2 deletions server/core/config/celery_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Celery settings
CELERY_BROKER_URL = 'redis://localhost:6378/0' # Redis as a broker
CELERY_RESULT_BACKEND = 'redis://localhost:6378/0'
CELERY_BROKER_URL = 'redis://localhost:6379/0' # Redis as a broker
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
Expand Down
3 changes: 1 addition & 2 deletions server/core/config/local.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
from .base import *

from .base import *
12 changes: 12 additions & 0 deletions server/core/config/mail.py
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'
5 changes: 5 additions & 0 deletions server/core/token.py
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)})
3 changes: 3 additions & 0 deletions server/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from .token import csrf_token_view

urlpatterns = [
path('admin/', admin.site.urls),

# Custom URLS
path('', include('apps.research.urls')),
path('api/', include('apps.research.urls')),
path('newsletter/', include('apps.newsletter.urls')),
path('get-csrf-token/', csrf_token_view, name='csrf_token'),

# Ckeditor URL
path('ckeditor5/', include('django_ckeditor_5.urls')),
Expand Down
Loading

0 comments on commit 5db604e

Please sign in to comment.