- Updated Wagtail hooks to use gettext for translatable strings in the NewsletterCampaignViewSet and related help texts. - Added LOCALE_PATHS to settings for loading translation files. - Updated base.html to include favicon links. - Translated various strings in the wagtailsettings edit template to support internationalization. - Created and populated zh_Hant translation files for Django messages. - Added a favicon.ico file to the static directory.
673 lines
25 KiB
Python
673 lines
25 KiB
Python
from django import forms as django_forms
|
||
from django.db import models
|
||
from django.utils.translation import gettext_lazy as _
|
||
from wagtail.admin.panels import (
|
||
FieldPanel,
|
||
MultiFieldPanel,
|
||
|
||
# import PublishingPanel:
|
||
PublishingPanel,
|
||
)
|
||
|
||
# import RichTextField:
|
||
from wagtail.fields import RichTextField
|
||
|
||
# import DraftStateMixin, PreviewableMixin, RevisionMixin, TranslatableMixin:
|
||
from wagtail.models import (
|
||
DraftStateMixin,
|
||
PreviewableMixin,
|
||
RevisionMixin,
|
||
TranslatableMixin,
|
||
)
|
||
|
||
from wagtail.contrib.settings.models import (
|
||
BaseGenericSetting,
|
||
register_setting,
|
||
)
|
||
|
||
from wagtail.snippets.models import register_snippet
|
||
from wagtail.fields import StreamField
|
||
from wagtail import blocks
|
||
|
||
from .security import encrypt_text
|
||
|
||
|
||
@register_setting
|
||
class HeaderSettings(BaseGenericSetting):
|
||
logo_light = models.ForeignKey(
|
||
"wagtailimages.Image",
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="+",
|
||
help_text=_("Use on dark background (light logo)."),
|
||
verbose_name=_("Light Logo"),
|
||
)
|
||
logo_dark = models.ForeignKey(
|
||
"wagtailimages.Image",
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="+",
|
||
help_text=_("Use on light background (dark logo)."),
|
||
verbose_name=_("Dark Logo"),
|
||
)
|
||
site_name = models.CharField(max_length=255, blank=True, verbose_name=_("Site Name"))
|
||
extra_links = StreamField([
|
||
("link", blocks.StructBlock([
|
||
("label", blocks.CharBlock(label=_("Label"))),
|
||
("url", blocks.URLBlock(label=_("URL")))
|
||
]))
|
||
], use_json_field=True, blank=True, null=True, verbose_name=_("Extra Links"))
|
||
|
||
panels = [
|
||
MultiFieldPanel(
|
||
[
|
||
FieldPanel("logo_light"),
|
||
FieldPanel("logo_dark"),
|
||
FieldPanel("site_name"),
|
||
FieldPanel("extra_links"),
|
||
],
|
||
heading=_("Header Settings"),
|
||
),
|
||
]
|
||
|
||
class Meta:
|
||
verbose_name = _("Header Settings")
|
||
|
||
@register_setting
|
||
class NavigationSettings(BaseGenericSetting):
|
||
footer_links = StreamField([
|
||
("section", blocks.StructBlock([
|
||
("title", blocks.CharBlock(required=False, label=_("Section Title"))),
|
||
("links", blocks.ListBlock(blocks.StructBlock([
|
||
("label", blocks.CharBlock(label=_("Label"))),
|
||
("url", blocks.URLBlock(label=_("URL")))
|
||
]))),
|
||
]))
|
||
], use_json_field=True, blank=True, null=True, verbose_name=_("Footer Links"))
|
||
|
||
panels = [
|
||
FieldPanel("footer_links"),
|
||
]
|
||
|
||
class Meta:
|
||
verbose_name = _("Footer Navigation")
|
||
|
||
class SocialLinkBlock(blocks.StructBlock):
|
||
SOCIAL_MEDIA_CHOICES = [
|
||
("facebook", _("Facebook")),
|
||
("twitter", _("Twitter")),
|
||
("instagram", _("Instagram")),
|
||
("threads", _("Threads")),
|
||
("linkedin", _("LinkedIn")),
|
||
("youtube", _("YouTube")),
|
||
]
|
||
|
||
platform = blocks.ChoiceBlock(choices=SOCIAL_MEDIA_CHOICES, label=_("Platform"))
|
||
url = blocks.URLBlock(label=_("URL"))
|
||
|
||
class Meta:
|
||
icon = "link"
|
||
label = _("Social Link")
|
||
|
||
@register_setting
|
||
class SocialMediaSettings(BaseGenericSetting):
|
||
links = StreamField([
|
||
("link", SocialLinkBlock()),
|
||
], use_json_field=True, verbose_name=_("Social Links"))
|
||
|
||
panels = [FieldPanel("links")]
|
||
|
||
class Meta:
|
||
verbose_name = _("Social Media Settings")
|
||
|
||
|
||
@register_setting
|
||
class MailSmtpSettings(BaseGenericSetting):
|
||
smtp_relay_host = models.CharField(max_length=255, blank=True, verbose_name=_("SMTP Relay Host"))
|
||
smtp_relay_port = models.PositiveIntegerField(default=587, verbose_name=_("SMTP Relay Port"))
|
||
smtp_use_tls = models.BooleanField(default=True, verbose_name=_("Use TLS"))
|
||
smtp_use_ssl = models.BooleanField(
|
||
default=False,
|
||
help_text=_("Port 465 usually uses SSL (Implicit TLS); port 587 usually uses STARTTLS (TLS)."),
|
||
verbose_name=_("Use SSL"),
|
||
)
|
||
smtp_timeout_seconds = models.PositiveIntegerField(default=15, verbose_name=_("SMTP Timeout Seconds"))
|
||
smtp_username = models.CharField(max_length=255, blank=True, verbose_name=_("SMTP Username"))
|
||
smtp_password = models.TextField(blank=True, verbose_name=_("SMTP Password"))
|
||
|
||
panels = [
|
||
MultiFieldPanel(
|
||
[
|
||
FieldPanel("smtp_relay_host"),
|
||
FieldPanel("smtp_relay_port"),
|
||
FieldPanel("smtp_use_tls"),
|
||
FieldPanel("smtp_use_ssl"),
|
||
FieldPanel("smtp_timeout_seconds"),
|
||
FieldPanel("smtp_username"),
|
||
FieldPanel(
|
||
"smtp_password",
|
||
widget=django_forms.PasswordInput(render_value=False),
|
||
),
|
||
],
|
||
heading=_("SMTP Settings"),
|
||
),
|
||
]
|
||
|
||
class Meta:
|
||
verbose_name = _("SMTP Settings")
|
||
|
||
def save(self, *args, **kwargs):
|
||
if self.smtp_use_tls and self.smtp_use_ssl:
|
||
raise ValueError("smtp_use_tls and smtp_use_ssl cannot both be True.")
|
||
|
||
if self.pk and not self.smtp_password:
|
||
previous = type(self).objects.filter(pk=self.pk).only("smtp_password").first()
|
||
self.smtp_password = previous.smtp_password if previous else ""
|
||
elif self.smtp_password:
|
||
self.smtp_password = encrypt_text(self.smtp_password)
|
||
|
||
super().save(*args, **kwargs)
|
||
|
||
|
||
@register_setting
|
||
class NewsletterSystemSettings(BaseGenericSetting):
|
||
member_center_base_url = models.URLField(blank=True, verbose_name=_("Member Center Base URL"))
|
||
member_center_subscribe_path = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="/newsletter/subscribe",
|
||
verbose_name=_("Subscribe Path"),
|
||
)
|
||
member_center_confirm_path = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="/newsletter/confirm",
|
||
verbose_name=_("Confirm Path"),
|
||
)
|
||
member_center_unsubscribe_token_path = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="/newsletter/unsubscribe-token",
|
||
verbose_name=_("Unsubscribe Token Path"),
|
||
)
|
||
member_center_unsubscribe_path = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="/newsletter/unsubscribe",
|
||
verbose_name=_("Unsubscribe Path"),
|
||
)
|
||
member_center_subscriptions_path = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="/newsletter/subscriptions",
|
||
verbose_name=_("Subscriptions Path"),
|
||
)
|
||
member_center_oauth_token_path = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="/oauth/token",
|
||
verbose_name=_("OAuth Token Path"),
|
||
)
|
||
member_center_oauth_client_id = models.CharField(max_length=255, blank=True, verbose_name=_("OAuth Client ID"))
|
||
member_center_oauth_client_secret = models.TextField(blank=True, verbose_name=_("OAuth Client Secret"))
|
||
member_center_oauth_scope = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="newsletter:list.read",
|
||
verbose_name=_("OAuth Scope"),
|
||
)
|
||
member_center_oauth_audience = models.CharField(max_length=255, blank=True, verbose_name=_("OAuth Audience"))
|
||
member_center_one_click_unsubscribe_path = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="/api/subscriptions/unsubscribe",
|
||
verbose_name=_("One-Click Unsubscribe Path"),
|
||
)
|
||
member_center_tenant_id = models.CharField(max_length=128, blank=True, verbose_name=_("Tenant ID"))
|
||
member_center_list_id = models.CharField(max_length=128, blank=True, verbose_name=_("List ID"))
|
||
member_center_timeout_seconds = models.PositiveIntegerField(default=10, verbose_name=_("Member Center Timeout Seconds"))
|
||
|
||
send_engine_base_url = models.URLField(blank=True, verbose_name=_("Send Engine Base URL"))
|
||
send_engine_send_jobs_path = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="/api/send-jobs",
|
||
verbose_name=_("Send Jobs Path"),
|
||
)
|
||
send_engine_oauth_scope = models.CharField(max_length=255, blank=True, verbose_name=_("Send Engine OAuth Scope"))
|
||
send_engine_timeout_seconds = models.PositiveIntegerField(default=10, verbose_name=_("Send Engine Timeout Seconds"))
|
||
send_engine_retry_interval_seconds = models.PositiveIntegerField(default=300, verbose_name=_("Retry Interval Seconds"))
|
||
send_engine_retry_max_attempts = models.PositiveIntegerField(default=3, verbose_name=_("Retry Max Attempts"))
|
||
|
||
sender_name = models.CharField(max_length=255, blank=True, verbose_name=_("Sender Name"))
|
||
sender_email = models.EmailField(blank=True, verbose_name=_("Sender Email"))
|
||
reply_to_email = models.EmailField(blank=True, verbose_name=_("Reply-To Email"))
|
||
default_charset = models.CharField(max_length=50, default="utf-8", verbose_name=_("Default Charset"))
|
||
one_click_endpoint_path = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="/u/unsubscribe",
|
||
verbose_name=_("One-Click Endpoint Path"),
|
||
)
|
||
one_click_token_secret = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
help_text=_("One-click token signing secret. Leave blank to use Django SECRET_KEY."),
|
||
verbose_name=_("One-Click Token Secret"),
|
||
)
|
||
one_click_token_ttl_seconds = models.PositiveIntegerField(default=60 * 60 * 24 * 30, verbose_name=_("One-Click Token TTL Seconds"))
|
||
site_base_url = models.URLField(
|
||
blank=True,
|
||
help_text=_("Site base URL for scheduler sends, e.g. https://news.example.com"),
|
||
verbose_name=_("Site Base URL"),
|
||
)
|
||
|
||
panels = [
|
||
MultiFieldPanel(
|
||
[
|
||
FieldPanel("member_center_base_url"),
|
||
FieldPanel("member_center_subscribe_path"),
|
||
FieldPanel("member_center_confirm_path"),
|
||
FieldPanel("member_center_unsubscribe_token_path"),
|
||
FieldPanel("member_center_unsubscribe_path"),
|
||
FieldPanel("member_center_subscriptions_path"),
|
||
FieldPanel("member_center_oauth_token_path"),
|
||
FieldPanel("member_center_oauth_client_id"),
|
||
FieldPanel(
|
||
"member_center_oauth_client_secret",
|
||
widget=django_forms.PasswordInput(render_value=False),
|
||
),
|
||
FieldPanel("member_center_oauth_scope"),
|
||
FieldPanel("member_center_oauth_audience"),
|
||
FieldPanel("member_center_one_click_unsubscribe_path"),
|
||
FieldPanel("member_center_tenant_id"),
|
||
FieldPanel("member_center_list_id"),
|
||
FieldPanel("member_center_timeout_seconds"),
|
||
],
|
||
heading=_("Member Center"),
|
||
),
|
||
MultiFieldPanel(
|
||
[
|
||
FieldPanel("send_engine_base_url"),
|
||
FieldPanel("send_engine_send_jobs_path"),
|
||
FieldPanel("send_engine_oauth_scope"),
|
||
FieldPanel("send_engine_timeout_seconds"),
|
||
FieldPanel("send_engine_retry_interval_seconds"),
|
||
FieldPanel("send_engine_retry_max_attempts"),
|
||
],
|
||
heading=_("Send Engine"),
|
||
),
|
||
MultiFieldPanel(
|
||
[
|
||
FieldPanel("one_click_endpoint_path"),
|
||
FieldPanel("one_click_token_secret"),
|
||
FieldPanel("one_click_token_ttl_seconds"),
|
||
FieldPanel("site_base_url"),
|
||
],
|
||
heading=_("List-Unsubscribe One-Click"),
|
||
),
|
||
MultiFieldPanel(
|
||
[
|
||
FieldPanel("sender_name"),
|
||
FieldPanel("sender_email"),
|
||
FieldPanel("reply_to_email"),
|
||
FieldPanel("default_charset"),
|
||
],
|
||
heading=_("Newsletter Mail"),
|
||
),
|
||
]
|
||
|
||
class Meta:
|
||
verbose_name = _("Newsletter System Settings")
|
||
|
||
def save(self, *args, **kwargs):
|
||
if self.pk and not self.member_center_oauth_client_secret:
|
||
previous = type(self).objects.filter(pk=self.pk).only("member_center_oauth_client_secret").first()
|
||
self.member_center_oauth_client_secret = previous.member_center_oauth_client_secret if previous else ""
|
||
elif self.member_center_oauth_client_secret:
|
||
self.member_center_oauth_client_secret = encrypt_text(self.member_center_oauth_client_secret)
|
||
|
||
super().save(*args, **kwargs)
|
||
|
||
|
||
@register_setting
|
||
class SystemNotificationMailSettings(BaseGenericSetting):
|
||
contact_form_from_name = models.CharField(max_length=255, blank=True, verbose_name=_("Contact Form Sender Name"))
|
||
contact_form_from_email = models.EmailField(blank=True, verbose_name=_("Contact Form Sender Email"))
|
||
contact_form_reply_to_email = models.EmailField(blank=True, verbose_name=_("Contact Form Reply-To Email"))
|
||
contact_form_to_emails = models.TextField(
|
||
blank=True,
|
||
help_text=_("Multiple recipients separated by comma or newline."),
|
||
verbose_name=_("Contact Form Notification Recipients"),
|
||
)
|
||
contact_form_subject_prefix = models.CharField(max_length=255, blank=True, default="[Contact Us]", verbose_name=_("Contact Form Subject Prefix"))
|
||
contact_form_user_subject_template = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
default="已收到您的聯絡表單",
|
||
verbose_name=_("User Copy Subject Template"),
|
||
)
|
||
contact_form_user_text_template = models.TextField(
|
||
blank=True,
|
||
default=(
|
||
"您好 {{name}}:\n\n"
|
||
"我們已收到您的來信,以下為存檔資訊:\n"
|
||
"Email: {{email}}\n"
|
||
"聯絡方式: {{contact}}\n"
|
||
"問題類別: {{category}}\n\n"
|
||
"留言內容:\n{{message}}\n"
|
||
),
|
||
verbose_name=_("User Copy Text Template"),
|
||
)
|
||
contact_form_user_html_template = models.TextField(
|
||
blank=True,
|
||
default=(
|
||
"<p>您好 {{name}}:</p>"
|
||
"<p>我們已收到您的來信,以下為存檔資訊:</p>"
|
||
"<ul>"
|
||
"<li>Email: {{email}}</li>"
|
||
"<li>聯絡方式: {{contact}}</li>"
|
||
"<li>問題類別: {{category}}</li>"
|
||
"</ul>"
|
||
"<p>留言內容:</p>"
|
||
"<p>{{message}}</p>"
|
||
),
|
||
verbose_name=_("User Copy HTML Template"),
|
||
)
|
||
default_charset = models.CharField(max_length=50, default="utf-8", verbose_name=_("Default Charset"))
|
||
|
||
panels = [
|
||
MultiFieldPanel(
|
||
[
|
||
FieldPanel("contact_form_from_name"),
|
||
FieldPanel("contact_form_from_email"),
|
||
FieldPanel("contact_form_reply_to_email"),
|
||
FieldPanel("contact_form_to_emails"),
|
||
FieldPanel("contact_form_subject_prefix"),
|
||
FieldPanel("contact_form_user_subject_template"),
|
||
FieldPanel("contact_form_user_text_template"),
|
||
FieldPanel("contact_form_user_html_template"),
|
||
FieldPanel("default_charset"),
|
||
],
|
||
heading=_("Contact Us Notification Mail"),
|
||
),
|
||
]
|
||
|
||
class Meta:
|
||
verbose_name = _("System Notification Mail Settings")
|
||
|
||
def contact_form_recipient_list(self) -> list[str]:
|
||
raw = (self.contact_form_to_emails or "").replace("\n", ",")
|
||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||
|
||
|
||
class OneClickUnsubscribeAudit(models.Model):
|
||
token_hash = models.CharField(max_length=64, unique=True, db_index=True)
|
||
subscriber_id = models.CharField(max_length=128, blank=True)
|
||
list_id = models.CharField(max_length=128, blank=True)
|
||
site_id = models.CharField(max_length=128, blank=True)
|
||
campaign_id = models.CharField(max_length=128, blank=True)
|
||
status = models.CharField(max_length=32, blank=True)
|
||
response_status = models.PositiveIntegerField(null=True, blank=True)
|
||
response_payload = models.JSONField(default=dict, blank=True)
|
||
error_message = models.TextField(blank=True)
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
processed_at = models.DateTimeField(auto_now=True)
|
||
|
||
class Meta:
|
||
ordering = ["-created_at"]
|
||
|
||
|
||
class NewsletterCampaign(models.Model):
|
||
STATUS_DRAFT = "draft"
|
||
STATUS_SCHEDULED = "scheduled"
|
||
STATUS_SENDING = "sending"
|
||
STATUS_SENT = "sent"
|
||
STATUS_FAILED = "failed"
|
||
STATUS_CHOICES = [
|
||
(STATUS_DRAFT, _("Draft")),
|
||
(STATUS_SCHEDULED, _("Scheduled")),
|
||
(STATUS_SENDING, _("Sending")),
|
||
(STATUS_SENT, _("Sent")),
|
||
(STATUS_FAILED, _("Failed")),
|
||
]
|
||
|
||
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
||
list_id = models.CharField(max_length=128, blank=True, verbose_name=_("List ID"))
|
||
subject_template = models.CharField(max_length=255, verbose_name=_("Subject Template"))
|
||
html_template = models.TextField(verbose_name=_("HTML Template"))
|
||
text_template = models.TextField(blank=True, verbose_name=_("Text Template"))
|
||
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=STATUS_DRAFT, verbose_name=_("Status"))
|
||
scheduled_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Scheduled At"))
|
||
sent_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Sent At"))
|
||
last_error = models.TextField(blank=True, verbose_name=_("Last Error"))
|
||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
||
|
||
panels = [
|
||
FieldPanel("title"),
|
||
FieldPanel("list_id"),
|
||
FieldPanel("subject_template"),
|
||
FieldPanel("html_template"),
|
||
FieldPanel("text_template"),
|
||
FieldPanel("scheduled_at"),
|
||
]
|
||
|
||
class Meta:
|
||
ordering = ["-created_at"]
|
||
verbose_name = _("Newsletter Campaign")
|
||
verbose_name_plural = _("Newsletter Campaigns")
|
||
|
||
def __str__(self):
|
||
return self.title
|
||
|
||
def save(self, *args, **kwargs):
|
||
if self._state.adding and not (self.list_id or "").strip():
|
||
default_list_id = (NewsletterSystemSettings.load().member_center_list_id or "").strip()
|
||
if default_list_id:
|
||
self.list_id = default_list_id
|
||
super().save(*args, **kwargs)
|
||
|
||
|
||
class NewsletterDispatchRecord(models.Model):
|
||
campaign = models.ForeignKey(
|
||
NewsletterCampaign,
|
||
on_delete=models.CASCADE,
|
||
related_name="dispatch_records",
|
||
verbose_name=_("Campaign"),
|
||
)
|
||
subscriber_id = models.CharField(max_length=128, blank=True, verbose_name=_("Subscriber ID"))
|
||
email = models.EmailField(blank=True, verbose_name=_("Email"))
|
||
status = models.CharField(max_length=32, blank=True, verbose_name=_("Status"))
|
||
retry_count = models.PositiveIntegerField(default=0, verbose_name=_("Retry Count"))
|
||
next_retry_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Next Retry At"))
|
||
response_status = models.PositiveIntegerField(null=True, blank=True, verbose_name=_("Response Status"))
|
||
response_payload = models.JSONField(default=dict, blank=True, verbose_name=_("Response Payload"))
|
||
error_message = models.TextField(blank=True, verbose_name=_("Error Message"))
|
||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||
|
||
panels = [
|
||
FieldPanel("campaign"),
|
||
FieldPanel("subscriber_id"),
|
||
FieldPanel("email"),
|
||
FieldPanel("status"),
|
||
FieldPanel("retry_count"),
|
||
FieldPanel("next_retry_at"),
|
||
FieldPanel("response_status"),
|
||
FieldPanel("response_payload"),
|
||
FieldPanel("error_message"),
|
||
]
|
||
|
||
class Meta:
|
||
ordering = ["-created_at"]
|
||
verbose_name = _("Newsletter Dispatch Record")
|
||
verbose_name_plural = _("Newsletter Dispatch Records")
|
||
|
||
def __str__(self):
|
||
return f"{self.campaign_id}:{self.email or self.subscriber_id}"
|
||
|
||
|
||
@register_setting
|
||
class NewsletterTemplateSettings(BaseGenericSetting):
|
||
subscribe_subject_template = models.CharField(
|
||
max_length=255,
|
||
default="請確認您的電子報訂閱",
|
||
verbose_name=_("Subscribe Subject Template"),
|
||
)
|
||
subscribe_html_template = models.TextField(
|
||
default=(
|
||
"<p>您好,請點擊以下連結完成訂閱:</p>"
|
||
"<p><a href='{{confirm_url}}'>{{confirm_url}}</a></p>"
|
||
),
|
||
verbose_name=_("Subscribe HTML Template"),
|
||
)
|
||
subscribe_text_template = models.TextField(
|
||
default="您好,請點擊以下連結完成訂閱:{{confirm_url}}",
|
||
verbose_name=_("Subscribe Text Template"),
|
||
)
|
||
confirm_success_template = RichTextField(
|
||
blank=True,
|
||
default="<p>訂閱確認成功。</p>",
|
||
verbose_name=_("Confirm Success Template"),
|
||
)
|
||
confirm_failure_template = RichTextField(
|
||
blank=True,
|
||
default="<p>訂閱確認失敗,請稍後再試。</p>",
|
||
verbose_name=_("Confirm Failure Template"),
|
||
)
|
||
unsubscribe_intro_template = RichTextField(
|
||
blank=True,
|
||
default="<p>確認要退訂電子報嗎?</p>",
|
||
verbose_name=_("Unsubscribe Intro Template"),
|
||
)
|
||
unsubscribe_success_template = RichTextField(
|
||
blank=True,
|
||
default="<p>已完成退訂。</p>",
|
||
verbose_name=_("Unsubscribe Success Template"),
|
||
)
|
||
unsubscribe_failure_template = RichTextField(
|
||
blank=True,
|
||
default="<p>退訂失敗,請稍後再試。</p>",
|
||
verbose_name=_("Unsubscribe Failure Template"),
|
||
)
|
||
|
||
panels = [
|
||
MultiFieldPanel(
|
||
[
|
||
FieldPanel("subscribe_subject_template"),
|
||
FieldPanel("subscribe_html_template"),
|
||
FieldPanel("subscribe_text_template"),
|
||
],
|
||
heading=_("Subscribe Confirmation Email"),
|
||
),
|
||
MultiFieldPanel(
|
||
[
|
||
FieldPanel("confirm_success_template"),
|
||
FieldPanel("confirm_failure_template"),
|
||
FieldPanel("unsubscribe_intro_template"),
|
||
FieldPanel("unsubscribe_success_template"),
|
||
FieldPanel("unsubscribe_failure_template"),
|
||
],
|
||
heading=_("Page Templates"),
|
||
),
|
||
]
|
||
|
||
class Meta:
|
||
verbose_name = _("Newsletter Template Settings")
|
||
|
||
|
||
@register_snippet
|
||
class BannerSnippet(models.Model):
|
||
key = models.CharField(max_length=50, blank=True, help_text=_("Identifier key, e.g. home / category"), verbose_name=_("Key"))
|
||
title = models.CharField(max_length=255, blank=True, verbose_name=_("Title"))
|
||
image = models.ForeignKey(
|
||
"wagtailimages.Image",
|
||
null=True,
|
||
blank=False,
|
||
on_delete=models.SET_NULL,
|
||
related_name="+",
|
||
)
|
||
link_url = models.URLField(blank=True)
|
||
link_text = models.CharField(max_length=100, blank=True, verbose_name=_("Link Text"))
|
||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||
sort_order = models.PositiveIntegerField(default=0, verbose_name=_("Sort Order"))
|
||
|
||
panels = [
|
||
FieldPanel("key"),
|
||
FieldPanel("title"),
|
||
FieldPanel("image"),
|
||
FieldPanel("link_url"),
|
||
FieldPanel("link_text"),
|
||
FieldPanel("is_active"),
|
||
FieldPanel("sort_order"),
|
||
]
|
||
|
||
class Meta:
|
||
ordering = ["sort_order", "id"]
|
||
verbose_name = _("Banner")
|
||
verbose_name_plural = _("Banners")
|
||
|
||
def __str__(self):
|
||
return self.title or f"Banner {self.pk}"
|
||
|
||
@register_snippet
|
||
class FooterText(
|
||
DraftStateMixin,
|
||
RevisionMixin,
|
||
PreviewableMixin,
|
||
TranslatableMixin,
|
||
models.Model,
|
||
):
|
||
|
||
body = RichTextField()
|
||
|
||
panels = [
|
||
FieldPanel("body"),
|
||
PublishingPanel(),
|
||
]
|
||
|
||
def __str__(self):
|
||
return "Footer text"
|
||
|
||
def get_preview_template(self, request, mode_name):
|
||
return "base.html"
|
||
|
||
def get_preview_context(self, request, mode_name):
|
||
return {"footer_text": self.body}
|
||
|
||
class Meta(TranslatableMixin.Meta):
|
||
verbose_name_plural = _("Footer Text")
|
||
|
||
|
||
class ContactFormSubmission(models.Model):
|
||
CATEGORY_COLLABORATION = "collaboration"
|
||
CATEGORY_WEBSITE_ISSUE = "website_issue"
|
||
CATEGORY_CAREER = "career"
|
||
CATEGORY_OTHER = "other"
|
||
CATEGORY_CHOICES = [
|
||
(CATEGORY_COLLABORATION, _("Collaboration")),
|
||
(CATEGORY_WEBSITE_ISSUE, _("Website Issue")),
|
||
(CATEGORY_CAREER, _("Career")),
|
||
(CATEGORY_OTHER, _("Other")),
|
||
]
|
||
|
||
name = models.CharField(max_length=100, verbose_name=_("Name"))
|
||
email = models.EmailField(blank=True, verbose_name=_("Email"))
|
||
contact = models.CharField(max_length=255, verbose_name=_("Contact"))
|
||
category = models.CharField(max_length=32, choices=CATEGORY_CHOICES, verbose_name=_("Category"))
|
||
message = models.TextField(verbose_name=_("Message"))
|
||
source_page = models.CharField(max_length=512, blank=True, verbose_name=_("Source Page"))
|
||
ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name=_("IP Address"))
|
||
user_agent = models.TextField(blank=True, verbose_name=_("User Agent"))
|
||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||
|
||
class Meta:
|
||
ordering = ["-created_at"]
|
||
verbose_name = _("Contact Form Submission")
|
||
verbose_name_plural = _("Contact Form Submissions")
|
||
|
||
def __str__(self):
|
||
return f"{self.get_category_display()} - {self.name}"
|