diff --git a/innovedus_cms/base/migrations/0008_alter_contactformsubmission_options_and_more.py b/innovedus_cms/base/migrations/0008_alter_contactformsubmission_options_and_more.py new file mode 100644 index 0000000..30e15cc --- /dev/null +++ b/innovedus_cms/base/migrations/0008_alter_contactformsubmission_options_and_more.py @@ -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='

訂閱確認失敗,請稍後再試。

', verbose_name='Confirm Failure Template'), + ), + migrations.AlterField( + model_name='newslettertemplatesettings', + name='confirm_success_template', + field=wagtail.fields.RichTextField(blank=True, default='

訂閱確認成功。

', verbose_name='Confirm Success Template'), + ), + migrations.AlterField( + model_name='newslettertemplatesettings', + name='subscribe_html_template', + field=models.TextField(default="

您好,請點擊以下連結完成訂閱:

{{confirm_url}}

", 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='

退訂失敗,請稍後再試。

', verbose_name='Unsubscribe Failure Template'), + ), + migrations.AlterField( + model_name='newslettertemplatesettings', + name='unsubscribe_intro_template', + field=wagtail.fields.RichTextField(blank=True, default='

確認要退訂電子報嗎?

', verbose_name='Unsubscribe Intro Template'), + ), + migrations.AlterField( + model_name='newslettertemplatesettings', + name='unsubscribe_success_template', + field=wagtail.fields.RichTextField(blank=True, default='

已完成退訂。

', 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='

您好 {{name}}:

我們已收到您的來信,以下為存檔資訊:

留言內容:

{{message}}

', 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'), + ), + ] diff --git a/innovedus_cms/base/views.py b/innovedus_cms/base/views.py index 7b1cdf9..18729e3 100644 --- a/innovedus_cms/base/views.py +++ b/innovedus_cms/base/views.py @@ -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), diff --git a/innovedus_cms/mysite/settings/base.py b/innovedus_cms/mysite/settings/base.py index 8968b4d..a7d563c 100644 --- a/innovedus_cms/mysite/settings/base.py +++ b/innovedus_cms/mysite/settings/base.py @@ -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, + }, + }, +} diff --git a/innovedus_cms/mysite/settings/production.py b/innovedus_cms/mysite/settings/production.py index efc862f..e1bda3c 100644 --- a/innovedus_cms/mysite/settings/production.py +++ b/innovedus_cms/mysite/settings/production.py @@ -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 * diff --git a/innovedus_cms/mysite/urls.py b/innovedus_cms/mysite/urls.py index d06a296..f060942 100644 --- a/innovedus_cms/mysite/urls.py +++ b/innovedus_cms/mysite/urls.py @@ -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 so Unicode tag slugs (e.g. 台北美食) still resolve path("tags//", home_views.hashtag_search, name="hashtag_search"), path("search/", search_views.search, name="search"), diff --git a/innovedus_cms/requirements.txt b/innovedus_cms/requirements.txt index ab34ab3..cc2b783 100644 --- a/innovedus_cms/requirements.txt +++ b/innovedus_cms/requirements.txt @@ -6,3 +6,4 @@ psycopg[binary] python-dotenv django-storages[boto3] certifi +whitenoise