develop #6
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -36,6 +36,11 @@ from .newsletter_scheduler import dispatch_campaign
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@require_GET
|
||||||
|
def health_check(request):
|
||||||
|
return JsonResponse({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
def _load_settings(request_or_site=None):
|
def _load_settings(request_or_site=None):
|
||||||
return (
|
return (
|
||||||
NewsletterSystemSettings.load(request_or_site=request_or_site),
|
NewsletterSystemSettings.load(request_or_site=request_or_site),
|
||||||
|
|||||||
@ -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, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import certifi
|
import certifi
|
||||||
@ -35,6 +36,55 @@ def env_list(name, default):
|
|||||||
return 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()
|
GA4_MEASUREMENT_ID = os.environ.get("GA4_MEASUREMENT_ID", "").strip()
|
||||||
SECRET_KEY = os.environ.get("SECRET_KEY", "").strip()
|
SECRET_KEY = os.environ.get("SECRET_KEY", "").strip()
|
||||||
|
|
||||||
@ -74,6 +124,7 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
@ -197,7 +248,10 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
|||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
|
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
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
|
# Default storage settings
|
||||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STORAGES
|
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STORAGES
|
||||||
@ -205,12 +259,13 @@ STORAGES = {
|
|||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
|
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"endpoint_url": os.environ.get("AWS_S3_ENDPOINT_URL"),
|
|
||||||
"access_key": os.environ.get("AWS_ACCESS_KEY_ID"),
|
"access_key": os.environ.get("AWS_ACCESS_KEY_ID"),
|
||||||
"secret_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
"secret_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
||||||
"bucket_name": os.environ.get("AWS_STORAGE_BUCKET_NAME"),
|
"bucket_name": os.environ.get("AWS_STORAGE_BUCKET_NAME"),
|
||||||
"region_name": os.environ.get("AWS_S3_REGION_NAME", default="us-east-1"),
|
"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": {
|
"staticfiles": {
|
||||||
@ -253,7 +308,35 @@ CSRF_TRUSTED_ORIGINS = env_list(
|
|||||||
default=[]
|
default=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
ALLOWED_HOSTS = env_list(
|
ALLOWED_HOSTS = build_allowed_hosts()
|
||||||
"ALLOWED_HOSTS",
|
|
||||||
default=[]
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ DEBUG = False
|
|||||||
# outdated JavaScript / CSS assets being served from cache
|
# outdated JavaScript / CSS assets being served from cache
|
||||||
# (e.g. after a Wagtail upgrade).
|
# (e.g. after a Wagtail upgrade).
|
||||||
# See https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#manifeststaticfilesstorage
|
# 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:
|
try:
|
||||||
from .local import *
|
from .local import *
|
||||||
|
|||||||
@ -14,6 +14,7 @@ urlpatterns = [
|
|||||||
path("django-admin/", admin.site.urls),
|
path("django-admin/", admin.site.urls),
|
||||||
path("admin/", include(wagtailadmin_urls)),
|
path("admin/", include(wagtailadmin_urls)),
|
||||||
path("documents/", include(wagtaildocs_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
|
# use <str:slug> so Unicode tag slugs (e.g. 台北美食) still resolve
|
||||||
path("tags/<str:slug>/", home_views.hashtag_search, name="hashtag_search"),
|
path("tags/<str:slug>/", home_views.hashtag_search, name="hashtag_search"),
|
||||||
path("search/", search_views.search, name="search"),
|
path("search/", search_views.search, name="search"),
|
||||||
|
|||||||
@ -6,3 +6,4 @@ psycopg[binary]
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
django-storages[boto3]
|
django-storages[boto3]
|
||||||
certifi
|
certifi
|
||||||
|
whitenoise
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user