-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): initialize sage_newsletter app features
- Loading branch information
1 parent
56d45b6
commit 6683a88
Showing
11 changed files
with
348 additions
and
0 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,5 @@ | ||
from .newsletter import NewsletterSubscriptionActions | ||
|
||
__all__ = [ | ||
"NewsletterSubscriptionActions" | ||
] |
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,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") |
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,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, | ||
] |
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,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") |
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,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.
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,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.
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,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 |
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,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) |