develop #6

Merged
warrenchen merged 13 commits from develop into main 2026-04-01 17:48:46 +00:00
6 changed files with 600 additions and 8 deletions
Showing only changes of commit 3142bbce7e - Show all commits

View File

@ -0,0 +1,502 @@
# Generated by Django 5.2.12 on 2026-03-27 14:27
import django.db.models.deletion
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0007_contactformsubmission'),
('wagtailimages', '0027_image_description'),
]
operations = [
migrations.AlterModelOptions(
name='contactformsubmission',
options={'ordering': ['-created_at'], 'verbose_name': 'Contact Form Submission', 'verbose_name_plural': 'Contact Form Submissions'},
),
migrations.AlterModelOptions(
name='newslettercampaign',
options={'ordering': ['-created_at'], 'verbose_name': 'Newsletter Campaign', 'verbose_name_plural': 'Newsletter Campaigns'},
),
migrations.AlterModelOptions(
name='newsletterdispatchrecord',
options={'ordering': ['-created_at'], 'verbose_name': 'Newsletter Dispatch Record', 'verbose_name_plural': 'Newsletter Dispatch Records'},
),
migrations.AlterModelOptions(
name='socialmediasettings',
options={'verbose_name': 'Social Media Settings'},
),
migrations.AlterField(
model_name='bannersnippet',
name='is_active',
field=models.BooleanField(default=True, verbose_name='Active'),
),
migrations.AlterField(
model_name='bannersnippet',
name='key',
field=models.CharField(blank=True, help_text='Identifier key, e.g. home / category', max_length=50, verbose_name='Key'),
),
migrations.AlterField(
model_name='bannersnippet',
name='link_text',
field=models.CharField(blank=True, max_length=100, verbose_name='Link Text'),
),
migrations.AlterField(
model_name='bannersnippet',
name='sort_order',
field=models.PositiveIntegerField(default=0, verbose_name='Sort Order'),
),
migrations.AlterField(
model_name='bannersnippet',
name='title',
field=models.CharField(blank=True, max_length=255, verbose_name='Title'),
),
migrations.AlterField(
model_name='contactformsubmission',
name='category',
field=models.CharField(choices=[('collaboration', 'Collaboration'), ('website_issue', 'Website Issue'), ('career', 'Career'), ('other', 'Other')], max_length=32, verbose_name='Category'),
),
migrations.AlterField(
model_name='contactformsubmission',
name='contact',
field=models.CharField(max_length=255, verbose_name='Contact'),
),
migrations.AlterField(
model_name='contactformsubmission',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Created At'),
),
migrations.AlterField(
model_name='contactformsubmission',
name='email',
field=models.EmailField(blank=True, max_length=254, verbose_name='Email'),
),
migrations.AlterField(
model_name='contactformsubmission',
name='ip_address',
field=models.GenericIPAddressField(blank=True, null=True, verbose_name='IP Address'),
),
migrations.AlterField(
model_name='contactformsubmission',
name='message',
field=models.TextField(verbose_name='Message'),
),
migrations.AlterField(
model_name='contactformsubmission',
name='name',
field=models.CharField(max_length=100, verbose_name='Name'),
),
migrations.AlterField(
model_name='contactformsubmission',
name='source_page',
field=models.CharField(blank=True, max_length=512, verbose_name='Source Page'),
),
migrations.AlterField(
model_name='contactformsubmission',
name='user_agent',
field=models.TextField(blank=True, verbose_name='User Agent'),
),
migrations.AlterField(
model_name='headersettings',
name='extra_links',
field=wagtail.fields.StreamField([('link', 2)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'label': 'Label'}), 1: ('wagtail.blocks.URLBlock', (), {'label': 'URL'}), 2: ('wagtail.blocks.StructBlock', [[('label', 0), ('url', 1)]], {})}, null=True, verbose_name='Extra Links'),
),
migrations.AlterField(
model_name='headersettings',
name='logo_dark',
field=models.ForeignKey(blank=True, help_text='Use on light background (dark logo).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='Dark Logo'),
),
migrations.AlterField(
model_name='headersettings',
name='logo_light',
field=models.ForeignKey(blank=True, help_text='Use on dark background (light logo).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='Light Logo'),
),
migrations.AlterField(
model_name='headersettings',
name='site_name',
field=models.CharField(blank=True, max_length=255, verbose_name='Site Name'),
),
migrations.AlterField(
model_name='mailsmtpsettings',
name='smtp_password',
field=models.TextField(blank=True, verbose_name='SMTP Password'),
),
migrations.AlterField(
model_name='mailsmtpsettings',
name='smtp_relay_host',
field=models.CharField(blank=True, max_length=255, verbose_name='SMTP Relay Host'),
),
migrations.AlterField(
model_name='mailsmtpsettings',
name='smtp_relay_port',
field=models.PositiveIntegerField(default=587, verbose_name='SMTP Relay Port'),
),
migrations.AlterField(
model_name='mailsmtpsettings',
name='smtp_timeout_seconds',
field=models.PositiveIntegerField(default=15, verbose_name='SMTP Timeout Seconds'),
),
migrations.AlterField(
model_name='mailsmtpsettings',
name='smtp_use_ssl',
field=models.BooleanField(default=False, help_text='Port 465 usually uses SSL (Implicit TLS); port 587 usually uses STARTTLS (TLS).', verbose_name='Use SSL'),
),
migrations.AlterField(
model_name='mailsmtpsettings',
name='smtp_use_tls',
field=models.BooleanField(default=True, verbose_name='Use TLS'),
),
migrations.AlterField(
model_name='mailsmtpsettings',
name='smtp_username',
field=models.CharField(blank=True, max_length=255, verbose_name='SMTP Username'),
),
migrations.AlterField(
model_name='navigationsettings',
name='footer_links',
field=wagtail.fields.StreamField([('section', 5)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'label': 'Section Title', 'required': False}), 1: ('wagtail.blocks.CharBlock', (), {'label': 'Label'}), 2: ('wagtail.blocks.URLBlock', (), {'label': 'URL'}), 3: ('wagtail.blocks.StructBlock', [[('label', 1), ('url', 2)]], {}), 4: ('wagtail.blocks.ListBlock', (3,), {}), 5: ('wagtail.blocks.StructBlock', [[('title', 0), ('links', 4)]], {})}, null=True, verbose_name='Footer Links'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Created At'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='html_template',
field=models.TextField(verbose_name='HTML Template'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='last_error',
field=models.TextField(blank=True, verbose_name='Last Error'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='list_id',
field=models.CharField(blank=True, max_length=128, verbose_name='List ID'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='scheduled_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Scheduled At'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='sent_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Sent At'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='status',
field=models.CharField(choices=[('draft', 'Draft'), ('scheduled', 'Scheduled'), ('sending', 'Sending'), ('sent', 'Sent'), ('failed', 'Failed')], default='draft', max_length=16, verbose_name='Status'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='subject_template',
field=models.CharField(max_length=255, verbose_name='Subject Template'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='text_template',
field=models.TextField(blank=True, verbose_name='Text Template'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='title',
field=models.CharField(max_length=255, verbose_name='Title'),
),
migrations.AlterField(
model_name='newslettercampaign',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Updated At'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='campaign',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dispatch_records', to='base.newslettercampaign', verbose_name='Campaign'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Created At'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='email',
field=models.EmailField(blank=True, max_length=254, verbose_name='Email'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='error_message',
field=models.TextField(blank=True, verbose_name='Error Message'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='next_retry_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Next Retry At'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='response_payload',
field=models.JSONField(blank=True, default=dict, verbose_name='Response Payload'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='response_status',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Response Status'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='retry_count',
field=models.PositiveIntegerField(default=0, verbose_name='Retry Count'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='status',
field=models.CharField(blank=True, max_length=32, verbose_name='Status'),
),
migrations.AlterField(
model_name='newsletterdispatchrecord',
name='subscriber_id',
field=models.CharField(blank=True, max_length=128, verbose_name='Subscriber ID'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='default_charset',
field=models.CharField(default='utf-8', max_length=50, verbose_name='Default Charset'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_base_url',
field=models.URLField(blank=True, verbose_name='Member Center Base URL'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_confirm_path',
field=models.CharField(blank=True, default='/newsletter/confirm', max_length=255, verbose_name='Confirm Path'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_list_id',
field=models.CharField(blank=True, max_length=128, verbose_name='List ID'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_oauth_audience',
field=models.CharField(blank=True, max_length=255, verbose_name='OAuth Audience'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_oauth_client_id',
field=models.CharField(blank=True, max_length=255, verbose_name='OAuth Client ID'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_oauth_client_secret',
field=models.TextField(blank=True, verbose_name='OAuth Client Secret'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_oauth_scope',
field=models.CharField(blank=True, default='newsletter:list.read', max_length=255, verbose_name='OAuth Scope'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_oauth_token_path',
field=models.CharField(blank=True, default='/oauth/token', max_length=255, verbose_name='OAuth Token Path'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_one_click_unsubscribe_path',
field=models.CharField(blank=True, default='/api/subscriptions/unsubscribe', max_length=255, verbose_name='One-Click Unsubscribe Path'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_subscribe_path',
field=models.CharField(blank=True, default='/newsletter/subscribe', max_length=255, verbose_name='Subscribe Path'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_subscriptions_path',
field=models.CharField(blank=True, default='/newsletter/subscriptions', max_length=255, verbose_name='Subscriptions Path'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_tenant_id',
field=models.CharField(blank=True, max_length=128, verbose_name='Tenant ID'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_timeout_seconds',
field=models.PositiveIntegerField(default=10, verbose_name='Member Center Timeout Seconds'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_unsubscribe_path',
field=models.CharField(blank=True, default='/newsletter/unsubscribe', max_length=255, verbose_name='Unsubscribe Path'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='member_center_unsubscribe_token_path',
field=models.CharField(blank=True, default='/newsletter/unsubscribe-token', max_length=255, verbose_name='Unsubscribe Token Path'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='one_click_endpoint_path',
field=models.CharField(blank=True, default='/u/unsubscribe', max_length=255, verbose_name='One-Click Endpoint Path'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='one_click_token_secret',
field=models.CharField(blank=True, help_text='One-click token signing secret. Leave blank to use Django SECRET_KEY.', max_length=255, verbose_name='One-Click Token Secret'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='one_click_token_ttl_seconds',
field=models.PositiveIntegerField(default=2592000, verbose_name='One-Click Token TTL Seconds'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='reply_to_email',
field=models.EmailField(blank=True, max_length=254, verbose_name='Reply-To Email'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='send_engine_base_url',
field=models.URLField(blank=True, verbose_name='Send Engine Base URL'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='send_engine_oauth_scope',
field=models.CharField(blank=True, max_length=255, verbose_name='Send Engine OAuth Scope'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='send_engine_retry_interval_seconds',
field=models.PositiveIntegerField(default=300, verbose_name='Retry Interval Seconds'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='send_engine_retry_max_attempts',
field=models.PositiveIntegerField(default=3, verbose_name='Retry Max Attempts'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='send_engine_send_jobs_path',
field=models.CharField(blank=True, default='/api/send-jobs', max_length=255, verbose_name='Send Jobs Path'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='send_engine_timeout_seconds',
field=models.PositiveIntegerField(default=10, verbose_name='Send Engine Timeout Seconds'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='sender_email',
field=models.EmailField(blank=True, max_length=254, verbose_name='Sender Email'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='sender_name',
field=models.CharField(blank=True, max_length=255, verbose_name='Sender Name'),
),
migrations.AlterField(
model_name='newslettersystemsettings',
name='site_base_url',
field=models.URLField(blank=True, help_text='Site base URL for scheduler sends, e.g. https://news.example.com', verbose_name='Site Base URL'),
),
migrations.AlterField(
model_name='newslettertemplatesettings',
name='confirm_failure_template',
field=wagtail.fields.RichTextField(blank=True, default='<p>訂閱確認失敗,請稍後再試。</p>', verbose_name='Confirm Failure Template'),
),
migrations.AlterField(
model_name='newslettertemplatesettings',
name='confirm_success_template',
field=wagtail.fields.RichTextField(blank=True, default='<p>訂閱確認成功。</p>', verbose_name='Confirm Success Template'),
),
migrations.AlterField(
model_name='newslettertemplatesettings',
name='subscribe_html_template',
field=models.TextField(default="<p>您好,請點擊以下連結完成訂閱:</p><p><a href='{{confirm_url}}'>{{confirm_url}}</a></p>", verbose_name='Subscribe HTML Template'),
),
migrations.AlterField(
model_name='newslettertemplatesettings',
name='subscribe_subject_template',
field=models.CharField(default='請確認您的電子報訂閱', max_length=255, verbose_name='Subscribe Subject Template'),
),
migrations.AlterField(
model_name='newslettertemplatesettings',
name='subscribe_text_template',
field=models.TextField(default='您好,請點擊以下連結完成訂閱:{{confirm_url}}', verbose_name='Subscribe Text Template'),
),
migrations.AlterField(
model_name='newslettertemplatesettings',
name='unsubscribe_failure_template',
field=wagtail.fields.RichTextField(blank=True, default='<p>退訂失敗,請稍後再試。</p>', verbose_name='Unsubscribe Failure Template'),
),
migrations.AlterField(
model_name='newslettertemplatesettings',
name='unsubscribe_intro_template',
field=wagtail.fields.RichTextField(blank=True, default='<p>確認要退訂電子報嗎?</p>', verbose_name='Unsubscribe Intro Template'),
),
migrations.AlterField(
model_name='newslettertemplatesettings',
name='unsubscribe_success_template',
field=wagtail.fields.RichTextField(blank=True, default='<p>已完成退訂。</p>', verbose_name='Unsubscribe Success Template'),
),
migrations.AlterField(
model_name='socialmediasettings',
name='links',
field=wagtail.fields.StreamField([('link', 2)], block_lookup={0: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('facebook', 'Facebook'), ('twitter', 'Twitter'), ('instagram', 'Instagram'), ('threads', 'Threads'), ('linkedin', 'LinkedIn'), ('youtube', 'YouTube')], 'label': 'Platform'}), 1: ('wagtail.blocks.URLBlock', (), {'label': 'URL'}), 2: ('wagtail.blocks.StructBlock', [[('platform', 0), ('url', 1)]], {})}, verbose_name='Social Links'),
),
migrations.AlterField(
model_name='systemnotificationmailsettings',
name='contact_form_from_email',
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact Form Sender Email'),
),
migrations.AlterField(
model_name='systemnotificationmailsettings',
name='contact_form_from_name',
field=models.CharField(blank=True, max_length=255, verbose_name='Contact Form Sender Name'),
),
migrations.AlterField(
model_name='systemnotificationmailsettings',
name='contact_form_reply_to_email',
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact Form Reply-To Email'),
),
migrations.AlterField(
model_name='systemnotificationmailsettings',
name='contact_form_subject_prefix',
field=models.CharField(blank=True, default='[Contact Us]', max_length=255, verbose_name='Contact Form Subject Prefix'),
),
migrations.AlterField(
model_name='systemnotificationmailsettings',
name='contact_form_to_emails',
field=models.TextField(blank=True, help_text='Multiple recipients separated by comma or newline.', verbose_name='Contact Form Notification Recipients'),
),
migrations.AlterField(
model_name='systemnotificationmailsettings',
name='contact_form_user_html_template',
field=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'),
),
migrations.AlterField(
model_name='systemnotificationmailsettings',
name='contact_form_user_subject_template',
field=models.CharField(blank=True, default='已收到您的聯絡表單', max_length=255, verbose_name='User Copy Subject Template'),
),
migrations.AlterField(
model_name='systemnotificationmailsettings',
name='contact_form_user_text_template',
field=models.TextField(blank=True, default='您好 {{name}}\n\n我們已收到您的來信,以下為存檔資訊:\nEmail: {{email}}\n聯絡方式: {{contact}}\n問題類別: {{category}}\n\n留言內容:\n{{message}}\n', verbose_name='User Copy Text Template'),
),
migrations.AlterField(
model_name='systemnotificationmailsettings',
name='default_charset',
field=models.CharField(default='utf-8', max_length=50, verbose_name='Default Charset'),
),
]

View File

@ -36,6 +36,11 @@ from .newsletter_scheduler import dispatch_campaign
logger = logging.getLogger(__name__)
@require_GET
def health_check(request):
return JsonResponse({"status": "ok"})
def _load_settings(request_or_site=None):
return (
NewsletterSystemSettings.load(request_or_site=request_or_site),

View File

@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import socket
try:
import certifi
@ -35,6 +36,55 @@ def env_list(name, default):
return default
def env_bool(name, default=False):
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
def env_optional(name, default=None):
value = os.environ.get(name)
if value is None:
return default
normalized = value.strip()
if normalized == "" or normalized.lower() in {"none", "null"}:
return None
return normalized
def detect_private_ip():
"""
Return the primary private IPv4 address for this container when available.
"""
configured_ip = os.environ.get("PRIVATE_IP", "").strip()
if configured_ip:
return configured_ip
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# No packets are sent; this asks the OS which interface would be used.
sock.connect(("10.255.255.255", 1))
return sock.getsockname()[0]
except OSError:
return ""
finally:
sock.close()
def build_allowed_hosts():
hosts = env_list("ALLOWED_HOSTS", default=[])
private_ip = detect_private_ip()
for host in ("localhost", "127.0.0.1", private_ip):
if host and host not in hosts:
hosts.append(host)
return hosts
GA4_MEASUREMENT_ID = os.environ.get("GA4_MEASUREMENT_ID", "").strip()
SECRET_KEY = os.environ.get("SECRET_KEY", "").strip()
@ -74,6 +124,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@ -197,7 +248,10 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATIC_URL = "/static/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = f'{os.environ.get("AWS_S3_ENDPOINT_URL")}/{os.environ.get("AWS_STORAGE_BUCKET_NAME")}/'
MEDIA_URL = (
(os.environ.get("MEDIA_URL", "").strip() if os.environ.get("MEDIA_URL") else "")
or f'{os.environ.get("AWS_S3_ENDPOINT_URL")}/{os.environ.get("AWS_STORAGE_BUCKET_NAME")}/'
)
# Default storage settings
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STORAGES
@ -205,12 +259,13 @@ STORAGES = {
"default": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
"OPTIONS": {
"endpoint_url": os.environ.get("AWS_S3_ENDPOINT_URL"),
"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"),
"addressing_style": "path",
"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": {
@ -253,7 +308,35 @@ CSRF_TRUSTED_ORIGINS = env_list(
default=[]
)
ALLOWED_HOSTS = env_list(
"ALLOWED_HOSTS",
default=[]
)
ALLOWED_HOSTS = build_allowed_hosts()
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
"loggers": {
"django": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"django.request": {
"handlers": ["console"],
"level": "CRITICAL",
"propagate": False,
},
"wagtail": {
"handlers": ["console"],
"level": "INFO",
"propagate": True,
},
},
}

View File

@ -6,7 +6,7 @@ DEBUG = False
# outdated JavaScript / CSS assets being served from cache
# (e.g. after a Wagtail upgrade).
# See https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#manifeststaticfilesstorage
STORAGES["staticfiles"]["BACKEND"] = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
STORAGES["staticfiles"]["BACKEND"] = "whitenoise.storage.CompressedManifestStaticFilesStorage"
try:
from .local import *

View File

@ -14,6 +14,7 @@ urlpatterns = [
path("django-admin/", admin.site.urls),
path("admin/", include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_urls)),
path("health", base_views.health_check, name="health_check"),
# use <str:slug> so Unicode tag slugs (e.g. 台北美食) still resolve
path("tags/<str:slug>/", home_views.hashtag_search, name="hashtag_search"),
path("search/", search_views.search, name="search"),

View File

@ -6,3 +6,4 @@ psycopg[binary]
python-dotenv
django-storages[boto3]
certifi
whitenoise