feat(newsletter): Add permission to send newsletter campaigns and update model options

This commit is contained in:
warrenchen 2026-04-28 02:26:29 +09:00
parent c8bcdb0ee6
commit 4303c1f5db
5 changed files with 57 additions and 12 deletions

View File

@ -0,0 +1,20 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("base", "0008_alter_contactformsubmission_options_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="newslettercampaign",
options={
"ordering": ["-created_at"],
"permissions": [("send_newslettercampaign", "Can send newsletter campaign")],
"verbose_name": "Newsletter Campaign",
"verbose_name_plural": "Newsletter Campaigns",
},
),
]

View File

@ -459,6 +459,9 @@ class NewsletterCampaign(models.Model):
ordering = ["-created_at"] ordering = ["-created_at"]
verbose_name = _("Newsletter Campaign") verbose_name = _("Newsletter Campaign")
verbose_name_plural = _("Newsletter Campaigns") verbose_name_plural = _("Newsletter Campaigns")
permissions = [
("send_newslettercampaign", "Can send newsletter campaign"),
]
def __str__(self): def __str__(self):
return self.title return self.title

View File

@ -5,7 +5,8 @@ import json
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.core.exceptions import ValidationError from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.validators import validate_email from django.core.validators import validate_email
from django.http import HttpResponseNotAllowed, JsonResponse from django.http import HttpResponseNotAllowed, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
@ -35,6 +36,8 @@ from .newsletter_scheduler import dispatch_campaign
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SEND_NEWSLETTER_PERMISSION = "base.send_newslettercampaign"
@require_GET @require_GET
def health_check(request): def health_check(request):
@ -464,9 +467,12 @@ def one_click_unsubscribe(request):
) )
@staff_member_required @login_required
@require_GET @require_GET
def newsletter_campaign_send_now(request, campaign_id: int): def newsletter_campaign_send_now(request, campaign_id: int):
if not request.user.has_perm(SEND_NEWSLETTER_PERMISSION):
raise PermissionDenied
campaign = get_object_or_404(NewsletterCampaign, pk=campaign_id) campaign = get_object_or_404(NewsletterCampaign, pk=campaign_id)
if campaign.status == NewsletterCampaign.STATUS_SENDING: if campaign.status == NewsletterCampaign.STATUS_SENDING:
messages.error(request, "Campaign is currently sending.") messages.error(request, "Campaign is currently sending.")

View File

@ -10,6 +10,8 @@ from wagtail.snippets.views.snippets import CreateView, SnippetViewSet
from .models import NewsletterCampaign, NewsletterDispatchRecord, NewsletterSystemSettings from .models import NewsletterCampaign, NewsletterDispatchRecord, NewsletterSystemSettings
SEND_NEWSLETTER_PERMISSION = "base.send_newslettercampaign"
class NewsletterCampaignCreateView(CreateView): class NewsletterCampaignCreateView(CreateView):
def get_initial(self): def get_initial(self):
@ -89,7 +91,7 @@ register_snippet(NewsletterDispatchRecordViewSet)
def newsletter_campaign_listing_buttons(snippet, user, next_url=None): def newsletter_campaign_listing_buttons(snippet, user, next_url=None):
if not isinstance(snippet, NewsletterCampaign): if not isinstance(snippet, NewsletterCampaign):
return return
if not user.is_staff: if not user.has_perm(SEND_NEWSLETTER_PERMISSION):
return return
if snippet.status == NewsletterCampaign.STATUS_SENDING: if snippet.status == NewsletterCampaign.STATUS_SENDING:
return return

View File

@ -55,6 +55,28 @@ def env_optional(name, default=None):
return normalized return normalized
def build_media_storage_options():
options = {
"access_key": os.environ.get("AWS_ACCESS_KEY_ID"),
"secret_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
"bucket_name": os.environ.get("AWS_STORAGE_BUCKET_NAME"),
"region_name": os.environ.get("AWS_S3_REGION_NAME", default="us-east-1"),
"default_acl": env_optional("AWS_S3_DEFAULT_ACL"),
"querystring_auth": env_bool("AWS_S3_QUERYSTRING_AUTH", default=True),
"custom_domain": env_optional("AWS_S3_CUSTOM_DOMAIN"),
}
endpoint_url = env_optional("AWS_S3_ENDPOINT_URL")
if endpoint_url:
options["endpoint_url"] = endpoint_url
addressing_style = env_optional("AWS_S3_ADDRESSING_STYLE")
if addressing_style:
options["addressing_style"] = addressing_style
return options
def detect_private_ip(): def detect_private_ip():
""" """
Return the primary private IPv4 address for this container when available. Return the primary private IPv4 address for this container when available.
@ -258,15 +280,7 @@ MEDIA_URL = (
STORAGES = { STORAGES = {
"default": { "default": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage", "BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
"OPTIONS": { "OPTIONS": build_media_storage_options(),
"access_key": os.environ.get("AWS_ACCESS_KEY_ID"),
"secret_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
"bucket_name": os.environ.get("AWS_STORAGE_BUCKET_NAME"),
"region_name": os.environ.get("AWS_S3_REGION_NAME", default="us-east-1"),
"default_acl": env_optional("AWS_S3_DEFAULT_ACL"),
"querystring_auth": env_bool("AWS_S3_QUERYSTRING_AUTH", default=True),
"custom_domain": env_optional("AWS_S3_CUSTOM_DOMAIN"),
},
}, },
"staticfiles": { "staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",