Skip to content

Commit

Permalink
feat(core): initialize sage_newsletter app features
Browse files Browse the repository at this point in the history
  • Loading branch information
sepehr-akbarzadeh committed Aug 14, 2024
1 parent 56d45b6 commit 6683a88
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 0 deletions.
Empty file added sage_newsletter/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions sage_newsletter/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .newsletter import NewsletterSubscriptionActions

__all__ = [
"NewsletterSubscriptionActions"
]
15 changes: 15 additions & 0 deletions sage_newsletter/actions/newsletter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.utils.translation import gettext_lazy as _


class NewsletterSubscriptionActions:
@staticmethod
def confirm_subscriptions(modeladmin, request, queryset):
queryset.update(confirmed=True)

confirm_subscriptions.short_description = _("Confirm selected subscriptions")

@staticmethod
def deactivate_subscriptions(modeladmin, request, queryset):
queryset.update(is_active=False)

deactivate_subscriptions.short_description = _("Deactivate selected subscriptions")
48 changes: 48 additions & 0 deletions sage_newsletter/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _

from .actions import NewsletterSubscriptionActions
from .models import NewsletterSubscriber


@admin.register(NewsletterSubscriber)
class NewsletterSubscriberAdmin(admin.ModelAdmin):
"""
Newsletter Subscriber Admin
"""

save_on_top = True
list_display = (
"email",
"date_subscribed",
"confirmed",
"preferences",
"frequency",
"language",
"is_active",
)
list_filter = (
"confirmed",
"preferences",
"frequency",
"language",
"is_active",
"gdpr_consent",
)
search_fields = ("email",)
readonly_fields = ("date_subscribed", "unsubscribe_token", "last_sent")
fieldsets = (
(
_("Subscriber Information"),
{"fields": ("email", "date_subscribed", "confirmed")},
),
(_("Preferences"), {"fields": ("preferences", "frequency", "language")}),
(
_("Subscription Status"),
{"fields": ("is_active", "gdpr_consent", "unsubscribe_token", "last_sent")},
),
)
actions = [
NewsletterSubscriptionActions.confirm_subscriptions,
NewsletterSubscriptionActions.deactivate_subscriptions,
]
8 changes: 8 additions & 0 deletions sage_newsletter/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class NewsletterConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "sage_newsletter"
verbose_name = _("Newsletter")
57 changes: 57 additions & 0 deletions sage_newsletter/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django import forms
from django.core.exceptions import ValidationError

from .models import NewsletterSubscriber


class NewsletterSubscriptionForm(forms.ModelForm):
"""
A Django ModelForm for handling newsletter subscriptions.
This form is associated with the NewsletterSubscriber model and is used for
subscribing users to a newsletter service. It handles both new subscriptions
and the reactivation of existing but inactive subscriptions.
The form only exposes the 'email' field for input, as it's the primary field
required for newsletter subscriptions.
Attributes:
Meta: An inner class that defines form-specific details like the associated model
and the fields to be included in the form.
"""

class Meta:
model = NewsletterSubscriber
fields = ["email"]

def clean_email(self):
"""
Validates and processes the email field input.
Checks if the provided email already exists in the database. If it does, the method
determines whether the associated subscription is active or inactive. Active
subscriptions will cause the method to raise a ValidationError, indicating that
the email is already in use. Inactive subscriptions will be reactivated.
Returns:
str: The cleaned email data.
Raises:
ValidationError: If the email address is already subscribed and active.
"""
email = self.cleaned_data.get("email")
try:
subscriber = NewsletterSubscriber.objects.get(email=email)
if subscriber.is_active:
raise ValidationError(
"This email address is already subscribed and active."
)
else:
# Mark the subscriber as reactivated and save
self.instance = subscriber
self.instance.is_active = True
self.instance.save()
except NewsletterSubscriber.DoesNotExist:
# Email not found, it's a new subscriber
pass
return email
Empty file.
20 changes: 20 additions & 0 deletions sage_newsletter/helpers/text_choices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.db import models
from django.utils.translation import gettext_lazy as _


class ContentPreferences(models.TextChoices):
NEWS = "NEWS", _("News")
DEALS = "DEALS", _("Deals")
TIPS = "TIPS", _("Tips")


class FrequencyPreferences(models.TextChoices):
DAILY = "DAILY", _("Daily")
WEEKLY = "WEEKLY", _("Weekly")
MONTHLY = "MONTHLY", _("Monthly")


class LanguagePreferences(models.TextChoices):
EN = "EN", _("English")
ES = "FA", _("Spanish")
FR = "AR", _("Arabic")
Empty file.
102 changes: 102 additions & 0 deletions sage_newsletter/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import uuid

from django.conf import settings
from django.db import models
from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _

from .helpers.text_choices import ContentPreferences, FrequencyPreferences


class NewsletterSubscriber(models.Model):
"""
Newsletter Subscriber
"""

email = models.EmailField(
unique=True,
verbose_name=_("Email Address"),
help_text="The email address of the subscriber.",
db_comment="Unique email address used for newsletter subscription.",
)
date_subscribed = models.DateTimeField(
default=tz.now,
verbose_name=_("Date Subscribed"),
help_text="The date and time when the subscription was created.",
db_comment="Timestamp of when the subscriber was added to the list.",
)
confirmed = models.BooleanField(
default=False,
verbose_name=_("Confirmed Subscription"),
help_text="Whether the subscriber has confirmed their email address.",
db_comment="Boolean flag indicating confirmed subscription status.",
)
unsubscribe_token = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True,
verbose_name=_("Unsubscribe Token"),
help_text="A unique token used for securely unsubscribing from the newsletter.",
db_comment="Unique token for secure unsubscribe functionality.",
)
preferences = models.CharField(
max_length=50,
choices=ContentPreferences.choices,
default="NEWS",
verbose_name=_("Content Preferences"),
help_text="The type of content the subscriber prefers to receive.",
db_comment="Subscriber's content preference selection.",
)
frequency = models.CharField(
max_length=50,
choices=FrequencyPreferences.choices,
default="WEEKLY",
verbose_name=_("Frequency Preferences"),
help_text="How often the subscriber wishes to receive the newsletter.",
db_comment="Subscriber's preferred frequency of newsletter delivery.",
)
language = models.CharField(
max_length=2,
choices=settings.LANGUAGES,
default=settings.LANGUAGE_CODE,
verbose_name=_("Language Preference"),
help_text="The preferred language for the newsletter.",
db_comment="Subscriber's preferred language for the newsletter.",
)
gdpr_consent = models.BooleanField(
default=False,
verbose_name=_("GDPR Consent"),
help_text="Whether the subscriber has given consent under GDPR.",
db_comment="Flag indicating GDPR consent has been given by the subscriber.",
)
last_sent = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Last Newsletter Sent"),
help_text="The date and time when the last newsletter was sent to this subscriber.",
db_comment="Timestamp of the last newsletter sent to the subscriber.",
)
is_active = models.BooleanField(
default=True,
verbose_name=_("Is Active"),
help_text="Whether the subscription is currently active.",
db_comment="Boolean flag indicating whether the subscription is active.",
)

objects = models.Manager()

class Meta:
"""
Meta
"""

verbose_name = _("Newsletter Subscriber")
verbose_name_plural = _("Newsletter Subscribers")
db_table = "sage_newsletter_subscriber"
db_table_comment = "Table for storing newsletter subscriber information."

def __str__(self):
return self.email

def __repr__(self):
return self.email
93 changes: 93 additions & 0 deletions sage_newsletter/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from django.contrib import messages
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import redirect, render
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView
from django.views.generic.base import ContextMixin

from .forms import NewsletterSubscriptionForm


class NewsletterViewMixin(ContextMixin):
"""
A mixin to add newsletter subscription functionality to a view.
This mixin provides functionalities for handling the newsletter subscription form.
It can be mixed into any Django view to add these capabilities.
"""

form_class = NewsletterSubscriptionForm
form_context_object = "newsletter_form"
success_url_name = None

def __init__(self, *args, **kwargs):
"""
Initialize the view.
Raises:
ImproperlyConfigured: If success_url_name is not set in the subclass.
"""
super().__init__(*args, **kwargs)
if not self.success_url_name:
raise ImproperlyConfigured(
f"{self.__class__.__name__} is missing the 'success_url_name' attribute. "
"You must define 'success_url_name' in your view."
)

def get_context_data(self, **kwargs):
"""
Inserts the form into the context dict for rendering.
This method extends the base `get_context_data` method to add the newsletter
subscription form to the context, making it available in the template.
Args:
**kwargs: Keyword arguments from the view.
Returns:
dict: The context dictionary with the form included.
"""
context = super().get_context_data(**kwargs)
context[self.form_context_object] = self.form_class()
return context

def post(self, request, *args, **kwargs):
"""
Handles POST requests for newsletter subscription.
This method processes the newsletter subscription form. If the form is valid,
it either adds a new subscription or reactivates an existing one. Appropriate
success messages are displayed to the user after processing.
Args:
request (HttpRequest): The request object.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
HttpResponseRedirect: Redirects to the specified URL on success.
HttpResponse: Renders the template with context on failure.
"""
form = self.form_class(request.POST)
if form.is_valid():
form.save()
if hasattr(form, "reactivated") and form.reactivated:
messages.success(
request,
_(
"We've reactivated your email address. Thanks for subscribing again!"
),
)
else:
messages.success(
request, _("You have successfully subscribed to the newsletter.")
)
return redirect(request.path)
self.object_list = self.get_queryset()

if isinstance(self, DetailView):
self.object = self.get_object()

context = self.get_context_data()
context[self.form_context_object] = form
return render(request, self.template_name, context)

0 comments on commit 6683a88

Please sign in to comment.