feat: Implement contact form submission feature with SMTP settings
- Added ContactFormSubmission model to store contact form submissions. - Created ContactForm for handling form submissions. - Implemented admin interface for managing contact form submissions. - Developed views and JavaScript for handling contact form submission via AJAX. - Added SMTP settings model for email configuration. - Created notification email templates for contact form submissions. - Updated frontend to include contact form modal and associated styles. - Added tests for contact form submission and validation.
This commit is contained in:
parent
4679cc70ef
commit
7a632c5ebd
@ -1,3 +1,20 @@
|
||||
from django.contrib import admin
|
||||
from .models import ContactFormSubmission
|
||||
|
||||
# Register your models here.
|
||||
|
||||
@admin.register(ContactFormSubmission)
|
||||
class ContactFormSubmissionAdmin(admin.ModelAdmin):
|
||||
list_display = ("created_at", "category", "name", "email", "contact")
|
||||
list_filter = ("category", "created_at")
|
||||
search_fields = ("name", "email", "contact", "message", "source_page")
|
||||
readonly_fields = (
|
||||
"name",
|
||||
"email",
|
||||
"contact",
|
||||
"category",
|
||||
"message",
|
||||
"source_page",
|
||||
"ip_address",
|
||||
"user_agent",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from .models import ContactFormSubmission
|
||||
|
||||
|
||||
class NewsletterSubscribeForm(forms.Form):
|
||||
@ -17,3 +18,12 @@ class NewsletterUnsubscribeForm(forms.Form):
|
||||
),
|
||||
)
|
||||
token = forms.CharField(max_length=1024, widget=forms.HiddenInput())
|
||||
|
||||
|
||||
class ContactForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ContactFormSubmission
|
||||
fields = ["name", "contact", "email", "category", "message", "source_page"]
|
||||
widgets = {
|
||||
"source_page": forms.HiddenInput(),
|
||||
}
|
||||
|
||||
117
innovedus_cms/base/migrations/0007_contactformsubmission.py
Normal file
117
innovedus_cms/base/migrations/0007_contactformsubmission.py
Normal file
@ -0,0 +1,117 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("base", "0006_oneclickunsubscribeaudit_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="MailSmtpSettings",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("smtp_relay_host", models.CharField(blank=True, max_length=255)),
|
||||
("smtp_relay_port", models.PositiveIntegerField(default=587)),
|
||||
("smtp_use_tls", models.BooleanField(default=True)),
|
||||
(
|
||||
"smtp_use_ssl",
|
||||
models.BooleanField(default=False, help_text="465 常用 SSL(Implicit TLS);587 常用 STARTTLS(TLS)。"),
|
||||
),
|
||||
("smtp_timeout_seconds", models.PositiveIntegerField(default=15)),
|
||||
("smtp_username", models.CharField(blank=True, max_length=255)),
|
||||
("smtp_password", models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "SMTP Settings",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SystemNotificationMailSettings",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("contact_form_from_name", models.CharField(blank=True, max_length=255)),
|
||||
("contact_form_from_email", models.EmailField(blank=True, max_length=254)),
|
||||
("contact_form_reply_to_email", models.EmailField(blank=True, max_length=254)),
|
||||
("contact_form_to_emails", models.TextField(blank=True, help_text="可填多個收件人,以逗號或換行分隔。")),
|
||||
("contact_form_subject_prefix", models.CharField(blank=True, default="[Contact Us]", max_length=255)),
|
||||
("contact_form_user_subject_template", models.CharField(blank=True, default="已收到您的聯絡表單", max_length=255)),
|
||||
(
|
||||
"contact_form_user_text_template",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
default="您好 {{name}}:\n\n我們已收到您的來信,以下為存檔資訊:\nEmail: {{email}}\n聯絡方式: {{contact}}\n問題類別: {{category}}\n\n留言內容:\n{{message}}\n",
|
||||
),
|
||||
),
|
||||
(
|
||||
"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>",
|
||||
),
|
||||
),
|
||||
("default_charset", models.CharField(default="utf-8", max_length=50)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "System Notification Mail Settings",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ContactFormSubmission",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=100)),
|
||||
("email", models.EmailField(blank=True, max_length=254)),
|
||||
("contact", models.CharField(max_length=255)),
|
||||
(
|
||||
"category",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("collaboration", "合作邀約"),
|
||||
("website_issue", "網站問題回報"),
|
||||
("career", "求職專區"),
|
||||
("other", "其他"),
|
||||
],
|
||||
max_length=32,
|
||||
),
|
||||
),
|
||||
("message", models.TextField()),
|
||||
("source_page", models.CharField(blank=True, max_length=512)),
|
||||
("ip_address", models.GenericIPAddressField(blank=True, null=True)),
|
||||
("user_agent", models.TextField(blank=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="newslettersystemsettings",
|
||||
name="smtp_password",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="newslettersystemsettings",
|
||||
name="smtp_relay_host",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="newslettersystemsettings",
|
||||
name="smtp_relay_port",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="newslettersystemsettings",
|
||||
name="smtp_timeout_seconds",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="newslettersystemsettings",
|
||||
name="smtp_use_ssl",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="newslettersystemsettings",
|
||||
name="smtp_use_tls",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="newslettersystemsettings",
|
||||
name="smtp_username",
|
||||
),
|
||||
]
|
||||
@ -29,6 +29,8 @@ from wagtail.fields import StreamField
|
||||
from wagtail import blocks
|
||||
|
||||
from .security import encrypt_text
|
||||
|
||||
|
||||
@register_setting
|
||||
class HeaderSettings(BaseGenericSetting):
|
||||
logo_light = models.ForeignKey(
|
||||
@ -115,6 +117,53 @@ class SocialMediaSettings(BaseGenericSetting):
|
||||
panels = [FieldPanel("links")]
|
||||
|
||||
|
||||
@register_setting
|
||||
class MailSmtpSettings(BaseGenericSetting):
|
||||
smtp_relay_host = models.CharField(max_length=255, blank=True)
|
||||
smtp_relay_port = models.PositiveIntegerField(default=587)
|
||||
smtp_use_tls = models.BooleanField(default=True)
|
||||
smtp_use_ssl = models.BooleanField(
|
||||
default=False,
|
||||
help_text="465 常用 SSL(Implicit TLS);587 常用 STARTTLS(TLS)。",
|
||||
)
|
||||
smtp_timeout_seconds = models.PositiveIntegerField(default=15)
|
||||
smtp_username = models.CharField(max_length=255, blank=True)
|
||||
smtp_password = models.TextField(blank=True)
|
||||
|
||||
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)
|
||||
@ -176,16 +225,6 @@ class NewsletterSystemSettings(BaseGenericSetting):
|
||||
send_engine_retry_interval_seconds = models.PositiveIntegerField(default=300)
|
||||
send_engine_retry_max_attempts = models.PositiveIntegerField(default=3)
|
||||
|
||||
smtp_relay_host = models.CharField(max_length=255, blank=True)
|
||||
smtp_relay_port = models.PositiveIntegerField(default=587)
|
||||
smtp_use_tls = models.BooleanField(default=True)
|
||||
smtp_use_ssl = models.BooleanField(
|
||||
default=False,
|
||||
help_text="465 常用 SSL(Implicit TLS);587 常用 STARTTLS(TLS)。",
|
||||
)
|
||||
smtp_timeout_seconds = models.PositiveIntegerField(default=15)
|
||||
smtp_username = models.CharField(max_length=255, blank=True)
|
||||
smtp_password = models.TextField(blank=True)
|
||||
sender_name = models.CharField(max_length=255, blank=True)
|
||||
sender_email = models.EmailField(blank=True)
|
||||
reply_to_email = models.EmailField(blank=True)
|
||||
@ -252,22 +291,12 @@ class NewsletterSystemSettings(BaseGenericSetting):
|
||||
),
|
||||
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),
|
||||
),
|
||||
FieldPanel("sender_name"),
|
||||
FieldPanel("sender_email"),
|
||||
FieldPanel("reply_to_email"),
|
||||
FieldPanel("default_charset"),
|
||||
],
|
||||
heading="SMTP / Mail",
|
||||
heading="Newsletter Mail",
|
||||
),
|
||||
]
|
||||
|
||||
@ -275,15 +304,6 @@ class NewsletterSystemSettings(BaseGenericSetting):
|
||||
verbose_name = "Newsletter System 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)
|
||||
|
||||
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 ""
|
||||
@ -293,6 +313,73 @@ class NewsletterSystemSettings(BaseGenericSetting):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@register_setting
|
||||
class SystemNotificationMailSettings(BaseGenericSetting):
|
||||
contact_form_from_name = models.CharField(max_length=255, blank=True)
|
||||
contact_form_from_email = models.EmailField(blank=True)
|
||||
contact_form_reply_to_email = models.EmailField(blank=True)
|
||||
contact_form_to_emails = models.TextField(
|
||||
blank=True,
|
||||
help_text="可填多個收件人,以逗號或換行分隔。",
|
||||
)
|
||||
contact_form_subject_prefix = models.CharField(max_length=255, blank=True, default="[Contact Us]")
|
||||
contact_form_user_subject_template = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
default="已收到您的聯絡表單",
|
||||
)
|
||||
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"
|
||||
),
|
||||
)
|
||||
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>"
|
||||
),
|
||||
)
|
||||
default_charset = models.CharField(max_length=50, default="utf-8")
|
||||
|
||||
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)
|
||||
@ -516,3 +603,32 @@ class FooterText(
|
||||
|
||||
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, "合作邀約"),
|
||||
(CATEGORY_WEBSITE_ISSUE, "網站問題回報"),
|
||||
(CATEGORY_CAREER, "求職專區"),
|
||||
(CATEGORY_OTHER, "其他"),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
email = models.EmailField(blank=True)
|
||||
contact = models.CharField(max_length=255)
|
||||
category = models.CharField(max_length=32, choices=CATEGORY_CHOICES)
|
||||
message = models.TextField()
|
||||
source_page = models.CharField(max_length=512, blank=True)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_category_display()} - {self.name}"
|
||||
|
||||
@ -15,7 +15,7 @@ from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from wagtail.rich_text import expand_db_html
|
||||
|
||||
from .models import NewsletterSystemSettings
|
||||
from .models import MailSmtpSettings, NewsletterSystemSettings, SystemNotificationMailSettings
|
||||
from .security import decrypt_text
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -444,35 +444,47 @@ class SendEngineClient:
|
||||
return APIResult(ok=False, status=0, data={}, error=str(exc))
|
||||
|
||||
|
||||
def send_subscribe_email(*, to_email: str, subject: str, text_body: str, html_body: str, config: NewsletterSystemSettings) -> int:
|
||||
from_email = build_from_email(config.sender_name, config.sender_email)
|
||||
reply_to = [config.reply_to_email] if config.reply_to_email else None
|
||||
|
||||
if not config.smtp_relay_host:
|
||||
def _build_smtp_connection(smtp_config: MailSmtpSettings):
|
||||
if not smtp_config.smtp_relay_host:
|
||||
raise ValueError("SMTP relay host is empty. Please save SMTP settings first.")
|
||||
if config.smtp_use_tls and config.smtp_use_ssl:
|
||||
if smtp_config.smtp_use_tls and smtp_config.smtp_use_ssl:
|
||||
raise ValueError("SMTP TLS and SSL cannot both be enabled.")
|
||||
|
||||
password = ""
|
||||
encrypted_password = (config.smtp_password or "").strip()
|
||||
encrypted_password = (smtp_config.smtp_password or "").strip()
|
||||
if encrypted_password:
|
||||
try:
|
||||
password = decrypt_text(encrypted_password)
|
||||
except Exception:
|
||||
password = ""
|
||||
|
||||
connection = get_connection(
|
||||
return get_connection(
|
||||
backend="django.core.mail.backends.smtp.EmailBackend",
|
||||
host=config.smtp_relay_host,
|
||||
port=config.smtp_relay_port,
|
||||
username=config.smtp_username or None,
|
||||
host=smtp_config.smtp_relay_host,
|
||||
port=smtp_config.smtp_relay_port,
|
||||
username=smtp_config.smtp_username or None,
|
||||
password=password or None,
|
||||
use_tls=bool(config.smtp_use_tls),
|
||||
use_ssl=bool(config.smtp_use_ssl),
|
||||
timeout=max(1, int(config.smtp_timeout_seconds or 15)),
|
||||
use_tls=bool(smtp_config.smtp_use_tls),
|
||||
use_ssl=bool(smtp_config.smtp_use_ssl),
|
||||
timeout=max(1, int(smtp_config.smtp_timeout_seconds or 15)),
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
|
||||
def send_subscribe_email(
|
||||
*,
|
||||
to_email: str,
|
||||
subject: str,
|
||||
text_body: str,
|
||||
html_body: str,
|
||||
config: NewsletterSystemSettings,
|
||||
smtp_config: MailSmtpSettings | None = None,
|
||||
) -> int:
|
||||
smtp_config = smtp_config or MailSmtpSettings.load()
|
||||
from_email = build_from_email(config.sender_name, config.sender_email)
|
||||
reply_to = [config.reply_to_email] if config.reply_to_email else None
|
||||
connection = _build_smtp_connection(smtp_config)
|
||||
|
||||
message = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_body,
|
||||
@ -490,12 +502,88 @@ def send_subscribe_email(*, to_email: str, subject: str, text_body: str, html_bo
|
||||
logger.info(
|
||||
"newsletter email send result sent_count=%s smtp_host=%s smtp_port=%s smtp_tls=%s smtp_ssl=%s smtp_timeout=%s from_email=%s to_email=%s",
|
||||
sent_count,
|
||||
config.smtp_relay_host,
|
||||
config.smtp_relay_port,
|
||||
bool(config.smtp_use_tls),
|
||||
bool(config.smtp_use_ssl),
|
||||
max(1, int(config.smtp_timeout_seconds or 15)),
|
||||
smtp_config.smtp_relay_host,
|
||||
smtp_config.smtp_relay_port,
|
||||
bool(smtp_config.smtp_use_tls),
|
||||
bool(smtp_config.smtp_use_ssl),
|
||||
max(1, int(smtp_config.smtp_timeout_seconds or 15)),
|
||||
from_email or settings.DEFAULT_FROM_EMAIL,
|
||||
to_email,
|
||||
)
|
||||
return sent_count
|
||||
|
||||
|
||||
def send_contact_notification_email(
|
||||
*,
|
||||
subject: str,
|
||||
text_body: str,
|
||||
html_body: str,
|
||||
notification_config: SystemNotificationMailSettings,
|
||||
smtp_config: MailSmtpSettings | None = None,
|
||||
) -> int:
|
||||
recipients = notification_config.contact_form_recipient_list()
|
||||
if not recipients:
|
||||
return 0
|
||||
|
||||
smtp_config = smtp_config or MailSmtpSettings.load()
|
||||
from_email = build_from_email(
|
||||
notification_config.contact_form_from_name,
|
||||
notification_config.contact_form_from_email,
|
||||
)
|
||||
reply_to = (
|
||||
[notification_config.contact_form_reply_to_email]
|
||||
if notification_config.contact_form_reply_to_email
|
||||
else None
|
||||
)
|
||||
connection = _build_smtp_connection(smtp_config)
|
||||
message = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_body,
|
||||
from_email=from_email or settings.DEFAULT_FROM_EMAIL,
|
||||
to=recipients,
|
||||
reply_to=reply_to,
|
||||
connection=connection,
|
||||
)
|
||||
if html_body:
|
||||
message.attach_alternative(html_body, "text/html")
|
||||
charset = (notification_config.default_charset or "utf-8").strip() or "utf-8"
|
||||
message.encoding = charset
|
||||
return message.send(fail_silently=False)
|
||||
|
||||
|
||||
def send_contact_user_email(
|
||||
*,
|
||||
to_email: str,
|
||||
subject: str,
|
||||
text_body: str,
|
||||
html_body: str,
|
||||
notification_config: SystemNotificationMailSettings,
|
||||
smtp_config: MailSmtpSettings | None = None,
|
||||
) -> int:
|
||||
if not (to_email or "").strip():
|
||||
return 0
|
||||
|
||||
smtp_config = smtp_config or MailSmtpSettings.load()
|
||||
from_email = build_from_email(
|
||||
notification_config.contact_form_from_name,
|
||||
notification_config.contact_form_from_email,
|
||||
)
|
||||
reply_to = (
|
||||
[notification_config.contact_form_reply_to_email]
|
||||
if notification_config.contact_form_reply_to_email
|
||||
else None
|
||||
)
|
||||
connection = _build_smtp_connection(smtp_config)
|
||||
message = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_body,
|
||||
from_email=from_email or settings.DEFAULT_FROM_EMAIL,
|
||||
to=[to_email.strip()],
|
||||
reply_to=reply_to,
|
||||
connection=connection,
|
||||
)
|
||||
if html_body:
|
||||
message.attach_alternative(html_body, "text/html")
|
||||
charset = (notification_config.default_charset or "utf-8").strip() or "utf-8"
|
||||
message.encoding = charset
|
||||
return message.send(fail_silently=False)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from .newsletter import (
|
||||
extract_token,
|
||||
@ -7,6 +8,7 @@ from .newsletter import (
|
||||
render_newsletter_html,
|
||||
verify_one_click_token,
|
||||
)
|
||||
from .models import ContactFormSubmission
|
||||
from .security import decrypt_text, encrypt_text
|
||||
|
||||
|
||||
@ -74,3 +76,39 @@ class NewsletterTemplateTests(TestCase):
|
||||
)
|
||||
self.assertIn('href="https://news.example.com/a"', rendered)
|
||||
self.assertIn('src="https://news.example.com/media/x.jpg"', rendered)
|
||||
|
||||
def test_contact_form_submit_saves_submission(self):
|
||||
response = self.client.post(
|
||||
reverse("contact_form_submit"),
|
||||
data={
|
||||
"name": "Tester",
|
||||
"contact": "tester@example.com",
|
||||
"email": "tester@example.com",
|
||||
"category": "other",
|
||||
"message": "hello",
|
||||
"source_page": "/",
|
||||
},
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(ContactFormSubmission.objects.count(), 1)
|
||||
submission = ContactFormSubmission.objects.first()
|
||||
self.assertEqual(submission.name, "Tester")
|
||||
self.assertEqual(submission.email, "tester@example.com")
|
||||
self.assertEqual(submission.category, "other")
|
||||
|
||||
def test_contact_form_submit_rejects_invalid_email(self):
|
||||
response = self.client.post(
|
||||
reverse("contact_form_submit"),
|
||||
data={
|
||||
"name": "Tester",
|
||||
"contact": "123",
|
||||
"email": "not-an-email",
|
||||
"category": "other",
|
||||
"message": "hello",
|
||||
"source_page": "/",
|
||||
},
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(ContactFormSubmission.objects.count(), 0)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from urllib.parse import urlencode
|
||||
import hashlib
|
||||
import json
|
||||
@ -9,27 +10,38 @@ from django.core.validators import validate_email
|
||||
from django.http import HttpResponseNotAllowed, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_GET, require_http_methods, require_POST
|
||||
from .forms import NewsletterSubscribeForm, NewsletterUnsubscribeForm
|
||||
from .forms import ContactForm, NewsletterSubscribeForm, NewsletterUnsubscribeForm
|
||||
from .models import (
|
||||
MailSmtpSettings,
|
||||
NewsletterCampaign,
|
||||
NewsletterSystemSettings,
|
||||
NewsletterTemplateSettings,
|
||||
OneClickUnsubscribeAudit,
|
||||
SystemNotificationMailSettings,
|
||||
)
|
||||
from .newsletter import (
|
||||
MemberCenterClient,
|
||||
build_from_email,
|
||||
extract_token,
|
||||
render_placeholders,
|
||||
send_contact_notification_email,
|
||||
send_contact_user_email,
|
||||
send_subscribe_email,
|
||||
)
|
||||
from .newsletter_scheduler import dispatch_campaign
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _load_settings():
|
||||
return NewsletterSystemSettings.load(), NewsletterTemplateSettings.load()
|
||||
|
||||
def _load_settings(request_or_site=None):
|
||||
return (
|
||||
NewsletterSystemSettings.load(request_or_site=request_or_site),
|
||||
NewsletterTemplateSettings.load(request_or_site=request_or_site),
|
||||
MailSmtpSettings.load(request_or_site=request_or_site),
|
||||
)
|
||||
|
||||
|
||||
def _build_context(*, title: str, message: str, success: bool):
|
||||
@ -40,6 +52,112 @@ def _build_context(*, title: str, message: str, success: bool):
|
||||
}
|
||||
|
||||
|
||||
def _render_contact_template(template: str, values: dict[str, str]) -> str:
|
||||
rendered = template or ""
|
||||
for key, value in values.items():
|
||||
rendered = rendered.replace(f"{{{{{key}}}}}", value)
|
||||
return rendered
|
||||
|
||||
|
||||
@require_POST
|
||||
def contact_form_submit(request):
|
||||
form = ContactForm(request.POST)
|
||||
if not form.is_valid():
|
||||
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
||||
return JsonResponse({"success": False, "errors": form.errors}, status=400)
|
||||
messages.error(request, "表單欄位未填完整,請確認後再送出。")
|
||||
return redirect(request.META.get("HTTP_REFERER") or "/")
|
||||
|
||||
submission = form.save(commit=False)
|
||||
submission.ip_address = request.META.get("REMOTE_ADDR") or ""
|
||||
submission.user_agent = request.META.get("HTTP_USER_AGENT", "")
|
||||
submission.save()
|
||||
notification_settings = SystemNotificationMailSettings.load(request_or_site=request)
|
||||
smtp_settings = MailSmtpSettings.load(request_or_site=request)
|
||||
subject_prefix = (notification_settings.contact_form_subject_prefix or "").strip()
|
||||
subject = f"{subject_prefix} {submission.get_category_display()}".strip()
|
||||
escaped_message_html = escape(submission.message).replace("\n", "<br>")
|
||||
text_body = (
|
||||
f"Name: {submission.name}\n"
|
||||
f"Email: {submission.email}\n"
|
||||
f"Contact: {submission.contact}\n"
|
||||
f"Category: {submission.get_category_display()}\n"
|
||||
f"Source Page: {submission.source_page}\n\n"
|
||||
f"Message:\n{submission.message}\n"
|
||||
)
|
||||
html_body = (
|
||||
"<p><strong>Name:</strong> "
|
||||
f"{escape(submission.name)}</p>"
|
||||
"<p><strong>Email:</strong> "
|
||||
f"{escape(submission.email)}</p>"
|
||||
"<p><strong>Contact:</strong> "
|
||||
f"{escape(submission.contact)}</p>"
|
||||
"<p><strong>Category:</strong> "
|
||||
f"{escape(submission.get_category_display())}</p>"
|
||||
"<p><strong>Source Page:</strong> "
|
||||
f"{escape(submission.source_page or '')}</p>"
|
||||
"<p><strong>Message:</strong></p>"
|
||||
f"<p>{escaped_message_html}</p>"
|
||||
)
|
||||
try:
|
||||
send_contact_notification_email(
|
||||
subject=subject,
|
||||
text_body=text_body,
|
||||
html_body=html_body,
|
||||
notification_config=notification_settings,
|
||||
smtp_config=smtp_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("contact form admin notification email failed: %s", exc)
|
||||
|
||||
if submission.email:
|
||||
values_text = {
|
||||
"name": submission.name,
|
||||
"email": submission.email,
|
||||
"contact": submission.contact,
|
||||
"category": submission.get_category_display(),
|
||||
"message": submission.message,
|
||||
"source_page": submission.source_page or "",
|
||||
}
|
||||
values_html = {
|
||||
"name": escape(submission.name),
|
||||
"email": escape(submission.email),
|
||||
"contact": escape(submission.contact),
|
||||
"category": escape(submission.get_category_display()),
|
||||
"message": escape(submission.message).replace("\n", "<br>"),
|
||||
"source_page": escape(submission.source_page or ""),
|
||||
}
|
||||
user_subject = _render_contact_template(
|
||||
notification_settings.contact_form_user_subject_template,
|
||||
values_text,
|
||||
)
|
||||
user_text = _render_contact_template(
|
||||
notification_settings.contact_form_user_text_template,
|
||||
values_text,
|
||||
)
|
||||
user_html = _render_contact_template(
|
||||
notification_settings.contact_form_user_html_template,
|
||||
values_html,
|
||||
)
|
||||
try:
|
||||
send_contact_user_email(
|
||||
to_email=submission.email,
|
||||
subject=user_subject,
|
||||
text_body=user_text,
|
||||
html_body=user_html,
|
||||
notification_config=notification_settings,
|
||||
smtp_config=smtp_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("contact form user copy email failed: %s", exc)
|
||||
|
||||
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
||||
return JsonResponse({"success": True})
|
||||
|
||||
messages.success(request, "感謝您的來信,我們已收到您的表單。")
|
||||
return redirect(request.META.get("HTTP_REFERER") or "/")
|
||||
|
||||
|
||||
def _extract_one_click_token(request):
|
||||
token = (request.GET.get("token") or "").strip()
|
||||
if token:
|
||||
@ -61,7 +179,7 @@ def _extract_one_click_token(request):
|
||||
|
||||
|
||||
def _handle_one_click_unsubscribe(*, request, token):
|
||||
system_settings, _ = _load_settings()
|
||||
system_settings, _, _ = _load_settings(request)
|
||||
|
||||
token_hash = hashlib.sha256(token.encode("utf-8")).hexdigest() if token else ""
|
||||
|
||||
@ -115,7 +233,7 @@ def _handle_one_click_unsubscribe(*, request, token):
|
||||
@require_http_methods(["POST"])
|
||||
def newsletter_subscribe(request):
|
||||
form = NewsletterSubscribeForm(request.POST)
|
||||
system_settings, template_settings = _load_settings()
|
||||
system_settings, template_settings, smtp_settings = _load_settings(request)
|
||||
|
||||
if not form.is_valid():
|
||||
return render(
|
||||
@ -174,6 +292,7 @@ def newsletter_subscribe(request):
|
||||
text_body=render_placeholders(template_settings.subscribe_text_template, values),
|
||||
html_body=render_placeholders(template_settings.subscribe_html_template, values),
|
||||
config=system_settings,
|
||||
smtp_config=smtp_settings,
|
||||
)
|
||||
except Exception:
|
||||
return render(
|
||||
@ -202,7 +321,7 @@ def newsletter_subscribe(request):
|
||||
def newsletter_confirm(request):
|
||||
token = (request.GET.get("token") or "").strip()
|
||||
email = (request.GET.get("email") or "").strip().lower()
|
||||
system_settings, template_settings = _load_settings()
|
||||
system_settings, template_settings, _ = _load_settings(request)
|
||||
|
||||
if not token:
|
||||
return render(
|
||||
@ -242,7 +361,7 @@ def newsletter_confirm(request):
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def newsletter_unsubscribe(request):
|
||||
system_settings, template_settings = _load_settings()
|
||||
system_settings, template_settings, _ = _load_settings(request)
|
||||
client = MemberCenterClient(system_settings)
|
||||
|
||||
if request.method == "GET":
|
||||
@ -379,6 +498,7 @@ def newsletter_smtp_test(request):
|
||||
return redirect(redirect_to)
|
||||
|
||||
settings_obj = NewsletterSystemSettings.load(request_or_site=request)
|
||||
smtp_settings = MailSmtpSettings.load(request_or_site=request)
|
||||
try:
|
||||
sent_count = send_subscribe_email(
|
||||
to_email=to_email,
|
||||
@ -386,6 +506,7 @@ def newsletter_smtp_test(request):
|
||||
text_body="這是一封測試信,代表 SMTP 設定可正常寄送。",
|
||||
html_body="<p>這是一封測試信,代表 SMTP 設定可正常寄送。</p>",
|
||||
config=settings_obj,
|
||||
smtp_config=smtp_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
messages.error(request, f"測試信寄送失敗:{exc}")
|
||||
|
||||
198
innovedus_cms/mysite/static/css/contact_form.css
Normal file
198
innovedus_cms/mysite/static/css/contact_form.css
Normal file
@ -0,0 +1,198 @@
|
||||
.contact-form-modal[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.contact-form-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.contact-form-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #0e1b42cc;
|
||||
}
|
||||
|
||||
.contact-form-modal__dialog {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(680px, calc(100vw - 48px));
|
||||
max-height: calc(100dvh - 48px);
|
||||
margin: 0 auto;
|
||||
padding: 20px 28px 24px;
|
||||
background: #0e1b42e6;
|
||||
color: #ffffff;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.contact-form-modal__close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact-form-modal__title {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.contact-form-modal__subtitle {
|
||||
margin: 12px 0 18px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
max-width: 415px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.contact-form,
|
||||
.contact-form * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.contact-form__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.contact-form__field {
|
||||
margin-bottom: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.contact-form__field label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.contact-form__field input,
|
||||
.contact-form__field select,
|
||||
.contact-form__field textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
background: #ffffff4d;
|
||||
color: #0e1b42;
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.contact-form__field input:focus,
|
||||
.contact-form__field select:focus,
|
||||
.contact-form__field textarea:focus {
|
||||
background: #ffffffcc;
|
||||
color: #0e1b42;
|
||||
}
|
||||
|
||||
.contact-form__field input::placeholder {
|
||||
color: #ffffffcc;
|
||||
}
|
||||
|
||||
.contact-form__field select {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.contact-form__field textarea {
|
||||
min-height: 180px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.contact-form__actions {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contact-form__actions button {
|
||||
min-width: 196px;
|
||||
height: 38px;
|
||||
border: 0;
|
||||
background: #ffffff;
|
||||
color: #0e1b42;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact-form__result {
|
||||
margin: 10px 0 0;
|
||||
min-height: 20px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.contact-form-modal__dialog {
|
||||
width: calc(90vw - 24px);
|
||||
max-height: calc(100dvh - 24px);
|
||||
margin: 0 auto;
|
||||
padding: 16px 12px 18px;
|
||||
}
|
||||
|
||||
.contact-form-modal__title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.contact-form-modal__subtitle {
|
||||
font-size: 16px;
|
||||
margin: 10px 0 14px;
|
||||
}
|
||||
|
||||
.contact-form__row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-form__field {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.contact-form__field label {
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.contact-form__field input,
|
||||
.contact-form__field select,
|
||||
.contact-form__field textarea {
|
||||
font-size: 13px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.contact-form__field select {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.contact-form__field textarea {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.contact-form__actions button {
|
||||
min-width: 160px;
|
||||
height: 34px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contact-form__result {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
70
innovedus_cms/mysite/static/js/contact_form.js
Normal file
70
innovedus_cms/mysite/static/js/contact_form.js
Normal file
@ -0,0 +1,70 @@
|
||||
(function () {
|
||||
const modal = document.querySelector("[data-contact-form]");
|
||||
if (!modal) return;
|
||||
|
||||
const form = modal.querySelector("[data-contact-form-body]");
|
||||
const result = modal.querySelector("[data-contact-result]");
|
||||
const categoryInput = modal.querySelector("#contact-form-category");
|
||||
const closeTargets = modal.querySelectorAll("[data-contact-close]");
|
||||
const triggers = document.querySelectorAll("[data-contact-trigger]");
|
||||
let lastActive = null;
|
||||
|
||||
const setOpen = (open) => {
|
||||
modal.hidden = !open;
|
||||
document.body.style.overflow = open ? "hidden" : "";
|
||||
if (!open && lastActive) {
|
||||
lastActive.focus();
|
||||
}
|
||||
};
|
||||
|
||||
triggers.forEach((el) => {
|
||||
el.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
lastActive = el;
|
||||
const preset = el.getAttribute("data-contact-category");
|
||||
if (preset && categoryInput) {
|
||||
categoryInput.value = preset;
|
||||
}
|
||||
if (result) result.textContent = "";
|
||||
setOpen(true);
|
||||
});
|
||||
});
|
||||
|
||||
closeTargets.forEach((el) => {
|
||||
el.addEventListener("click", () => setOpen(false));
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && !modal.hidden) {
|
||||
setOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
if (form) {
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const data = new FormData(form);
|
||||
|
||||
try {
|
||||
const response = await fetch(form.action, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
if (!response.ok || !payload.success) {
|
||||
if (result) result.textContent = "送出失敗,請確認欄位後再試一次。";
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) result.textContent = "已送出,我們會盡快與您聯繫。";
|
||||
form.reset();
|
||||
} catch (_) {
|
||||
if (result) result.textContent = "送出失敗,請稍後再試。";
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
@ -3,27 +3,49 @@
|
||||
if (!root) return;
|
||||
|
||||
const toggle = root.querySelector(".subscribe-fab__toggle");
|
||||
const input = root.querySelector(".subscribe-fab__input");
|
||||
const triggers = document.querySelectorAll("[data-subscribe-trigger]");
|
||||
if (!toggle) return;
|
||||
|
||||
const setOpen = (open) => {
|
||||
const focusInput = () => {
|
||||
if (!input) return;
|
||||
requestAnimationFrame(function () {
|
||||
input.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const setOpen = (open, shouldFocusInput) => {
|
||||
root.classList.toggle("is-open", open);
|
||||
toggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
if (open && shouldFocusInput) {
|
||||
focusInput();
|
||||
}
|
||||
};
|
||||
|
||||
toggle.addEventListener("click", function () {
|
||||
const isOpen = root.classList.contains("is-open");
|
||||
setOpen(!isOpen);
|
||||
setOpen(!isOpen, !isOpen);
|
||||
});
|
||||
|
||||
triggers.forEach(function (trigger) {
|
||||
trigger.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
setOpen(true, true);
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
if (event.target.closest("[data-subscribe-trigger]")) {
|
||||
return;
|
||||
}
|
||||
if (!root.contains(event.target)) {
|
||||
setOpen(false);
|
||||
setOpen(false, false);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape") {
|
||||
setOpen(false);
|
||||
setOpen(false, false);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/mysite.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/footer.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/subscribe_fab.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/contact_form.css' %}">
|
||||
|
||||
{% block extra_css %}
|
||||
{# Override this in templates to add extra stylesheets #}
|
||||
@ -54,11 +55,13 @@
|
||||
</main>
|
||||
|
||||
{% include "includes/footer.html" %}
|
||||
{% include "includes/contact_form.html" %}
|
||||
{% include "includes/subscribe_fab.html" %}
|
||||
|
||||
{# Global javascript #}
|
||||
<script type="text/javascript" src="{% static 'js/mysite.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/header.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/contact_form.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/subscribe_fab.js' %}"></script>
|
||||
{# Instagram embed script to render IG oEmbeds #}
|
||||
<script async src="https://www.instagram.com/embed.js"></script>
|
||||
|
||||
52
innovedus_cms/mysite/templates/includes/contact_form.html
Normal file
52
innovedus_cms/mysite/templates/includes/contact_form.html
Normal file
@ -0,0 +1,52 @@
|
||||
<div class="contact-form-modal" data-contact-form hidden>
|
||||
<div class="contact-form-modal__backdrop" data-contact-close></div>
|
||||
<div class="contact-form-modal__dialog" role="dialog" aria-modal="true" aria-label="聯絡表單">
|
||||
<button class="contact-form-modal__close" type="button" aria-label="關閉" data-contact-close>×</button>
|
||||
|
||||
<h2 class="contact-form-modal__title">聯絡我們</h2>
|
||||
<p class="contact-form-modal__subtitle">有任何問題或想要與我們交流的地方,都歡迎留言聯絡</p>
|
||||
|
||||
<form class="contact-form" method="post" action="{% url 'contact_form_submit' %}" data-contact-form-body>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="source_page" value="{{ request.get_full_path|default:'/' }}" />
|
||||
|
||||
<div class="contact-form__row">
|
||||
<div class="contact-form__field">
|
||||
<label for="contact-form-name">該如何稱呼您</label>
|
||||
<input id="contact-form-name" type="text" name="name" required placeholder="請留下您的姓名" />
|
||||
</div>
|
||||
<div class="contact-form__field">
|
||||
<label for="contact-form-contact">聯絡方式</label>
|
||||
<input id="contact-form-contact" type="text" name="contact" required placeholder="+886..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-form__field">
|
||||
<label for="contact-form-email">Email</label>
|
||||
<input id="contact-form-email" type="email" name="email" placeholder="name@example.com" />
|
||||
</div>
|
||||
|
||||
<div class="contact-form__field">
|
||||
<label for="contact-form-category">問題類別</label>
|
||||
<select id="contact-form-category" name="category" required>
|
||||
<option value="" selected disabled>請選擇問題類別</option>
|
||||
<option value="collaboration">合作邀約</option>
|
||||
<option value="website_issue">網站問題回報</option>
|
||||
<option value="career">求職專區</option>
|
||||
<option value="other">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="contact-form__field">
|
||||
<label for="contact-form-message">留言內容</label>
|
||||
<textarea id="contact-form-message" name="message" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="contact-form__actions">
|
||||
<button type="submit">送出</button>
|
||||
</div>
|
||||
|
||||
<p class="contact-form__result" data-contact-result aria-live="polite"></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -40,9 +40,9 @@
|
||||
<!-- <div class="footer-col footer-col--right"> -->
|
||||
<div class="footer-fixed-links">
|
||||
<ul>
|
||||
<li><a href="{% url 'newsletter_subscribe' %}">訂閱電子報</a></li>
|
||||
<li><a href="#">合作提案</a></li>
|
||||
<li><a href="#">聯絡我們</a></li>
|
||||
<li><a href="#" data-subscribe-trigger>訂閱電子報</a></li>
|
||||
<li><a href="#" data-contact-trigger data-contact-category="collaboration">合作提案</a></li>
|
||||
<li><a href="#" data-contact-trigger data-contact-category="other">聯絡我們</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ urlpatterns = [
|
||||
path("tags/<str:slug>/", home_views.hashtag_search, name="hashtag_search"),
|
||||
path("search/", search_views.search, name="search"),
|
||||
path("newsletter/subscribe/", base_views.newsletter_subscribe, name="newsletter_subscribe"),
|
||||
path("contact-form/submit/", base_views.contact_form_submit, name="contact_form_submit"),
|
||||
path("newsletter/confirm/", base_views.newsletter_confirm, name="newsletter_confirm"),
|
||||
path("newsletter/unsubscribe/", base_views.newsletter_unsubscribe, name="newsletter_unsubscribe"),
|
||||
path("u/unsubscribe", base_views.one_click_unsubscribe, name="one_click_unsubscribe"),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user