Initial commit

This commit is contained in:
ai_ml_operations
2026-09-06 04:27:41 -07:00
commit 8a97e3fbe2
302 changed files with 34038 additions and 0 deletions
View File
+1
View File
@@ -0,0 +1 @@
from django.contrib import admin # noqa: F401
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class PublicConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "public"
+79
View File
@@ -0,0 +1,79 @@
import re
from django.conf import settings
def _phone_tel(phone: str) -> str:
digits = re.sub(r"[^\d+]", "", phone or "")
return digits
def site_branding(request):
phone = settings.CONTACT_PHONE or ""
public_site_url = (getattr(settings, "PUBLIC_SITE_URL", None) or "").rstrip("/")
if not public_site_url and getattr(request, "build_absolute_uri", None):
public_site_url = request.build_absolute_uri("/").rstrip("/")
return {
"SITE_NAME": settings.SITE_NAME,
"SITE_TAGLINE": settings.SITE_TAGLINE,
"PUBLIC_SITE_URL": public_site_url,
"CONTACT_PHONE": phone,
"CONTACT_PHONE_TEL": _phone_tel(phone),
"CONTACT_EMAIL": settings.CONTACT_EMAIL or "",
"CONTACT_ADDRESS": getattr(settings, "CONTACT_ADDRESS", "") or "",
"CONTACT_SERVICE_AREA": getattr(
settings,
"CONTACT_SERVICE_AREA",
"",
),
"CREDIT_NAME": getattr(settings, "CREDIT_NAME", "AI ML Operations, LLC"),
"CREDIT_URL": getattr(
settings, "CREDIT_URL", "https://aimloperations.com"
),
"SITE_UNDER_CONSTRUCTION": settings.SITE_UNDER_CONSTRUCTION,
}
def tianji_tracking(request):
match = getattr(request, "resolver_match", None)
url_name = getattr(match, "url_name", "") or ""
namespace = getattr(match, "namespace", "") or ""
full = f"{namespace}:{url_name}" if namespace else url_name
nav_section = ""
if full == "dashboard:home":
nav_section = "dashboard"
elif namespace == "leads":
nav_section = "leads"
elif full == "analytics:report":
nav_section = "analytics"
elif namespace == "contacts":
nav_section = "contacts"
elif full == "directmail:postcard_designer":
nav_section = "postcard"
elif namespace == "email_sms":
nav_section = "campaigns"
elif namespace == "directmail":
nav_section = "directmail"
elif namespace == "blog":
nav_section = "blog"
elif namespace == "payments":
nav_section = "payments"
elif full == "social:account_list":
nav_section = "social_accounts"
elif namespace == "social":
nav_section = "social"
return {
"tianji_enabled": getattr(settings, "TIANJI_ENABLED", False)
and bool(getattr(settings, "TIANJI_WEBSITE_ID", ""))
and bool(getattr(settings, "TIANJI_TRACKER_URL", "")),
"tianji_tracker_url": getattr(
settings,
"TIANJI_TRACKER_URL",
"https://tianji.aimloperations.com/tracker.js",
),
"tianji_website_id": getattr(settings, "TIANJI_WEBSITE_ID", ""),
"page_name": url_name,
"nav_section": nav_section,
}
+133
View File
@@ -0,0 +1,133 @@
"""Shared context for branded HTML/text emails (public site palette)."""
from __future__ import annotations
import html
import logging
import re
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
logger = logging.getLogger(__name__)
_URL_RE = re.compile(r"(https?://[^\s<]+)")
_LOGO_STATIC_PATH = "brand/logo.png"
def _absolute_static_url(site_url: str, relative: str) -> str:
"""
Build an absolute URL for a static asset.
Prefer the hashed Manifest URL when available. Fall back to the stable
path when the manifest is missing (dj-queue worker does not run
collectstatic) so sending mail never crashes.
"""
try:
path = staticfiles_storage.url(relative)
except ValueError:
logger.debug(
"staticfiles manifest miss for %s; using unhashed URL", relative
)
static_prefix = settings.STATIC_URL or "/static/"
path = f"{static_prefix}{relative.lstrip('/')}"
if path.startswith("http://") or path.startswith("https://"):
return path
if not path.startswith("/"):
path = f"/{path}"
return f"{site_url}{path}"
def _tagline_with_site_link(tagline: str, site_url: str) -> str:
"""Escape the tagline; keep a hook for per-client domain linking."""
raw = (tagline or "").strip()
if not raw:
return ""
return html.escape(raw)
def email_brand_context(**extra):
site_url = (getattr(settings, "PUBLIC_SITE_URL", None) or "").rstrip("/")
if not site_url:
site_url = "https://example.com"
logo_url = _absolute_static_url(site_url, _LOGO_STATIC_PATH)
brand_name = getattr(settings, "SITE_NAME", None) or "Your Company"
brand_legal = getattr(settings, "CREDIT_NAME", None) or brand_name
tagline = getattr(settings, "SITE_TAGLINE", None) or ""
host_label = site_url.replace("https://", "").replace("http://", "")
return {
"site_url": site_url,
"logo_url": logo_url,
"brand_name": brand_name,
"brand_legal": brand_legal,
"brand_tagline": tagline,
"brand_tagline_html": _tagline_with_site_link(tagline, site_url),
"host_label": host_label,
**extra,
}
def plain_text_to_email_html(text: str) -> str:
"""Escape plain text and turn paragraphs / URLs into simple HTML."""
raw = (text or "").replace("\r\n", "\n").strip()
if not raw:
return ""
blocks: list[str] = []
for para in re.split(r"\n\s*\n", raw):
lines = [html.escape(line) for line in para.split("\n")]
joined = "<br>\n".join(lines)
joined = _URL_RE.sub(
r'<a href="\1" style="color:#00626c;text-decoration:underline;">\1</a>',
joined,
)
blocks.append(
f'<p style="margin:0 0 16px;color:#212121;font-size:15px;'
f'line-height:1.6;">{joined}</p>'
)
return "\n".join(blocks)
_HTML_TAG_RE = re.compile(
r"<\s*(p|div|br|span|strong|em|b|i|u|a|img|h[1-6]|ul|ol|li|font|table)\b",
re.I,
)
def sanitize_email_html(raw: str) -> str:
"""Light cleanup for staff-authored HTML (Quill) before sending."""
text = raw or ""
text = re.sub(r"(?is)<script[^>]*>.*?</script>", "", text)
text = re.sub(r"(?is)<iframe[^>]*>.*?</iframe>", "", text)
text = re.sub(r"(?is)<object[^>]*>.*?</object>", "", text)
text = re.sub(r"(?i)\son\w+\s*=\s*([\"']).*?\1", "", text)
text = re.sub(r"(?i)\son\w+\s*=\s*[^\s>]+", "", text)
text = re.sub(r"(?i)javascript:", "", text)
return text.strip()
def campaign_body_to_email_html(body: str) -> str:
"""Render campaign body for email — HTML as-is when Quill markup, else plain."""
raw = (body or "").strip()
if not raw:
return ""
if _HTML_TAG_RE.search(raw):
return sanitize_email_html(raw)
return plain_text_to_email_html(raw)
def campaign_body_to_plain_text(body: str) -> str:
"""Plain-text alternative for multipart emails."""
from django.utils.html import strip_tags
raw = (body or "").strip()
if not raw:
return ""
if _HTML_TAG_RE.search(raw):
text = strip_tags(sanitize_email_html(raw))
return html.unescape(re.sub(r"[ \t]+\n", "\n", text)).strip()
return raw
+115
View File
@@ -0,0 +1,115 @@
from django import forms
from django.conf import settings
class ContactForm(forms.Form):
INTEREST_CHOICES = [
("general", "General inquiry"),
("quote", "Request a quote"),
("support", "Support"),
("other", "Something else"),
]
first_name = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={"class": "form-input", "id": "contact-first-name"}),
)
last_name = forms.CharField(
max_length=100,
required=False,
widget=forms.TextInput(attrs={"class": "form-input", "id": "contact-last-name"}),
)
email = forms.EmailField(
widget=forms.EmailInput(attrs={"class": "form-input", "id": "contact-email"}),
)
phone = forms.CharField(
max_length=32,
required=False,
widget=forms.TextInput(attrs={"class": "form-input", "id": "contact-phone"}),
)
address_line1 = forms.CharField(
max_length=200,
required=False,
label="Street address",
widget=forms.TextInput(
attrs={
"class": "form-input",
"id": "contact-address-line1",
"autocomplete": "off",
"data-ac": "line1",
}
),
)
address_line2 = forms.CharField(
max_length=200,
required=False,
label="Apt / suite",
widget=forms.TextInput(
attrs={
"class": "form-input",
"id": "contact-address-line2",
"autocomplete": "address-line2",
"data-ac": "line2",
}
),
)
address_city = forms.CharField(
max_length=100,
required=False,
label="City",
widget=forms.TextInput(
attrs={
"class": "form-input",
"id": "contact-address-city",
"autocomplete": "address-level2",
"data-ac": "city",
}
),
)
address_state = forms.CharField(
max_length=32,
required=False,
label="State",
widget=forms.TextInput(
attrs={
"class": "form-input",
"id": "contact-address-state",
"autocomplete": "address-level1",
"data-ac": "state",
}
),
)
address_zip = forms.CharField(
max_length=20,
required=False,
label="ZIP",
widget=forms.TextInput(
attrs={
"class": "form-input",
"id": "contact-address-zip",
"autocomplete": "postal-code",
"data-ac": "zip",
}
),
)
interest = forms.ChoiceField(
choices=INTEREST_CHOICES,
widget=forms.Select(attrs={"class": "form-input", "id": "contact-interest"}),
)
message = forms.CharField(
widget=forms.Textarea(attrs={"class": "form-input", "id": "contact-message"}),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if settings.RECAPTCHA_PUBLIC_KEY and settings.RECAPTCHA_PRIVATE_KEY:
from django_recaptcha.fields import ReCaptchaField
from django_recaptcha.widgets import ReCaptchaV3
self.fields["captcha"] = ReCaptchaField(widget=ReCaptchaV3)
class NotifyForm(forms.Form):
email = forms.EmailField(
widget=forms.EmailInput(attrs={"class": "form-input"}),
)
+48
View File
@@ -0,0 +1,48 @@
from django.conf import settings
from django.shortcuts import redirect
class UnderConstructionMiddleware:
"""
When SITE_UNDER_CONSTRUCTION is true, redirect public traffic to the holding page.
Exempt: health, static/media, admin, auth login/logout, SMS webhook, and the
under-construction page itself (so notify-me POSTs work).
"""
EXEMPT_PREFIXES = (
"/healthz",
"/static/",
"/media/",
"/admin/",
"/accounts/login",
"/accounts/logout",
"/under-construction",
"/portal/messaging/webhooks/",
"/unsubscribe/",
"/api/address-suggest",
"/robots.txt",
"/sitemap.xml",
)
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if settings.SITE_UNDER_CONSTRUCTION and not self._is_exempt(request):
return redirect("public:under_construction")
return self.get_response(request)
def _is_exempt(self, request) -> bool:
path = request.path
if any(path.startswith(prefix) for prefix in self.EXEMPT_PREFIXES):
return True
user = getattr(request, "user", None)
if (
user is not None
and getattr(user, "is_authenticated", False)
and getattr(user, "is_staff", False)
and path.startswith("/portal/")
):
return True
return False
View File
+1
View File
@@ -0,0 +1 @@
+78
View File
@@ -0,0 +1,78 @@
"""Outbound notifications for public-site events."""
import logging
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import get_template
from django.urls import reverse
from leads.models import Lead
from public.email_branding import email_brand_context
logger = logging.getLogger(__name__)
def notify_admins_of_contact_form(lead: Lead) -> bool:
"""
Email CONTACT_EMAIL when someone submits the public contact form.
Returns True if a message was sent. Failures are logged; callers should
not block the visitor success path on delivery errors.
"""
to_email = (settings.CONTACT_EMAIL or "").strip()
if not to_email:
logger.warning("CONTACT_EMAIL unset; skipping contact-form notification")
return False
contact = lead.contact
name = contact.full_name or "(no name)"
phone = contact.phone or "(none)"
email = contact.email or "(none)"
message = (lead.message or "").strip() or "(no message)"
site = (settings.PUBLIC_SITE_URL or "").rstrip("/")
portal_path = reverse("leads:detail", kwargs={"pk": lead.pk})
portal_url = f"{site}{portal_path}" if site else portal_path
postal = contact.postal_address or {}
address_bits = [
postal.get("line1") or "",
postal.get("line2") or "",
", ".join(
part
for part in [
postal.get("city") or "",
postal.get("state") or "",
postal.get("zip") or "",
]
if part
),
]
address = "\n".join(bit for bit in address_bits if bit) or "(none)"
ctx = email_brand_context(
name=name,
email=email,
phone=phone,
address=address,
message=message,
portal_url=portal_url,
)
text_content = get_template("emails/contact_email.txt").render(ctx)
html_content = get_template("emails/contact_email.html").render(ctx)
mail = EmailMultiAlternatives(
subject=f"New contact form inquiry from {name}",
body=text_content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[to_email],
reply_to=[contact.email] if contact.email else None,
)
mail.attach_alternative(html_content, "text/html")
try:
mail.send(fail_silently=False)
except Exception:
logger.exception("Failed to send contact-form notification to %s", to_email)
return False
return True
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
<title>{% block title %}{{ brand_name }}{% endblock %}</title>
<!--[if mso]>
<style type="text/css">
body, table, td { font-family: Arial, Helvetica, sans-serif !important; }
</style>
<![endif]-->
<style type="text/css">
body, table, td, a {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
body {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
background-color: #f4f7f7;
color: #212121;
font-family: "Work Sans", Poppins, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
a { color: #00626c; }
.email-btn {
display: inline-block;
padding: 12px 24px;
background-color: #00626c;
color: #ffffff !important;
text-decoration: none;
border-radius: 4px;
font-weight: 600;
font-size: 14px;
letter-spacing: 0.3px;
}
.muted { color: #6b7280; font-size: 13px; line-height: 1.5; }
.field-label { color: #6b7280; font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px; margin: 0 0 4px; }
.field-value { color: #212121; font-size: 15px; margin: 0 0 16px; line-height: 1.5; }
</style>
</head>
<body style="margin:0;padding:0;background-color:#f4f7f7;">
{% block preheader %}{% endblock %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color:#f4f7f7;">
<tr>
<td align="center" style="padding:32px 16px;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="max-width:600px;background-color:#ffffff;border:1px solid #d9e3e4;">
<tr>
<td align="center" style="padding:28px 24px 20px;border-bottom:1px solid #d9e3e4;">
{% if logo_url %}
<a href="{{ site_url|default:'https://mkdrealtor.com' }}" style="text-decoration:none;">
<img src="{{ logo_url }}" alt="{{ brand_name|default:'Your Company' }} · EXIT Realty" width="160" style="display:block;width:160px;max-width:70%;height:auto;">
</a>
{% else %}
<p style="margin:0;font-size:20px;font-weight:700;letter-spacing:0.5px;color:#00626c;">
{{ brand_name }}
</p>
{% endif %}
{% if brand_name %}
<p style="margin:12px 0 0;font-size:15px;font-weight:600;color:#212121;">{{ brand_name }}</p>
{% endif %}
{% if brand_tagline_html %}
<p style="margin:4px 0 0;font-size:12px;color:#6b7280;">{{ brand_tagline_html|safe }}</p>
{% elif brand_tagline %}
<p style="margin:4px 0 0;font-size:12px;color:#6b7280;">{{ brand_tagline }}</p>
{% endif %}
{% block header_extra %}{% endblock %}
</td>
</tr>
<tr>
<td style="height:3px;line-height:3px;font-size:0;background-color:#00626c;">&nbsp;</td>
</tr>
<tr>
<td style="padding:28px 28px 8px;color:#212121;font-size:15px;line-height:1.6;font-family:'Work Sans',Poppins,-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">
{% block content %}{% endblock %}
</td>
</tr>
<tr>
<td style="padding:8px 28px 28px;color:#6b7280;font-size:12px;line-height:1.5;text-align:center;border-top:1px solid #d9e3e4;font-family:'Work Sans',Poppins,-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">
{% block footer %}
<p style="margin:16px 0 8px;color:#6b7280;">
{% block footer_note %}{% endblock %}
</p>
<p style="margin:0 0 4px;color:#6b7280;">
&copy; {% now "Y" %} {{ brand_name }}. All rights reserved.
</p>
<p style="margin:0;color:#6b7280;">
{% if brand_tagline_html %}
{{ brand_tagline_html|safe }}
{% else %}
<a href="{{ site_url|default:'https://mkdrealtor.com' }}" style="color:#00626c;text-decoration:none;">{{ host_label|default:"mkdrealtor.com" }}</a>
{% if brand_tagline %}
&nbsp;·&nbsp; {{ brand_tagline }}
{% endif %}
{% endif %}
</p>
{% endblock %}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,40 @@
{% extends "emails/base_email.html" %}
{% block title %}{{ subject }}{% endblock %}
{% block content %}
<p style="margin:0 0 16px;color:#212121;">Your {{ channel_display }} campaign has finished sending.</p>
<p style="margin:0 0 8px;font-size:18px;font-weight:600;color:#00626c;">{{ campaign_name }}</p>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin:20px 0;">
<tr>
<td style="padding:8px 0;border-bottom:1px solid #d9e3e4;color:#6b7280;font-size:13px;">Recipients</td>
<td align="right" style="padding:8px 0;border-bottom:1px solid #d9e3e4;color:#212121;font-size:15px;font-weight:600;">{{ total }}</td>
</tr>
<tr>
<td style="padding:8px 0;border-bottom:1px solid #d9e3e4;color:#6b7280;font-size:13px;">Sent</td>
<td align="right" style="padding:8px 0;border-bottom:1px solid #d9e3e4;color:#212121;font-size:15px;font-weight:600;">{{ sent }}</td>
</tr>
<tr>
<td style="padding:8px 0;border-bottom:1px solid #d9e3e4;color:#6b7280;font-size:13px;">Delivered</td>
<td align="right" style="padding:8px 0;border-bottom:1px solid #d9e3e4;color:#212121;font-size:15px;font-weight:600;">{{ delivered }}</td>
</tr>
<tr>
<td style="padding:8px 0;border-bottom:1px solid #d9e3e4;color:#6b7280;font-size:13px;">Failed / bounced</td>
<td align="right" style="padding:8px 0;border-bottom:1px solid #d9e3e4;color:#212121;font-size:15px;font-weight:600;">{{ failed }}</td>
</tr>
<tr>
<td style="padding:8px 0;color:#6b7280;font-size:13px;">Suppressed</td>
<td align="right" style="padding:8px 0;color:#212121;font-size:15px;font-weight:600;">{{ suppressed }}</td>
</tr>
</table>
{% if report_url %}
<p style="margin:24px 0 0;">
<a class="email-btn" href="{{ report_url }}" style="display:inline-block;padding:12px 24px;background-color:#00626c;color:#ffffff !important;text-decoration:none;border-radius:4px;font-weight:600;font-size:14px;">Open campaign report</a>
</p>
{% endif %}
{% endblock %}
{% block footer_note %}Campaign summary from {{ brand_name|default:"Monica Dhillon" }}.{% endblock %}
@@ -0,0 +1,16 @@
Campaign sent: {{ campaign_name }}
Your {{ channel_display }} campaign "{{ campaign_name }}" has finished sending.
Recipients: {{ total }}
Sent: {{ sent }}
Delivered: {{ delivered }}
Failed / bounced: {{ failed }}
Suppressed: {{ suppressed }}
{% if report_url %}Report: {{ report_url }}
{% endif %}
{{ brand_name|default:"Monica Dhillon" }}
{{ site_url|default:"https://mkdrealtor.com" }}
{% if brand_tagline %}{{ brand_tagline }}{% endif %}
@@ -0,0 +1,33 @@
{% extends "emails/base_email.html" %}
{% block title %}New Contact Request{% endblock %}
{% block content %}
<p style="margin:0 0 16px;color:#212121;">Hello,</p>
<p style="margin:0 0 24px;color:#212121;">A new contact request was submitted on the site.</p>
<p class="field-label" style="color:#6b7280;font-size:12px;text-transform:uppercase;letter-spacing:0.6px;margin:0 0 4px;">Name</p>
<p class="field-value" style="color:#212121;font-size:15px;margin:0 0 16px;"><strong>{{ name }}</strong></p>
<p class="field-label" style="color:#6b7280;font-size:12px;text-transform:uppercase;letter-spacing:0.6px;margin:0 0 4px;">Email</p>
<p class="field-value" style="color:#212121;font-size:15px;margin:0 0 16px;">
<a href="mailto:{{ email }}" style="color:#00626c;text-decoration:none;">{{ email }}</a>
</p>
<p class="field-label" style="color:#6b7280;font-size:12px;text-transform:uppercase;letter-spacing:0.6px;margin:0 0 4px;">Phone</p>
<p class="field-value" style="color:#212121;font-size:15px;margin:0 0 16px;">{{ phone }}</p>
<p class="field-label" style="color:#6b7280;font-size:12px;text-transform:uppercase;letter-spacing:0.6px;margin:0 0 4px;">Address</p>
<p class="field-value" style="color:#212121;font-size:15px;margin:0 0 16px;white-space:pre-wrap;">{{ address }}</p>
<p class="field-label" style="color:#6b7280;font-size:12px;text-transform:uppercase;letter-spacing:0.6px;margin:0 0 4px;">Message</p>
<p class="field-value" style="color:#212121;font-size:15px;margin:0 0 16px;white-space:pre-wrap;">{{ message }}</p>
{% if portal_url %}
<p style="margin:24px 0 0;">
<a class="email-btn" href="{{ portal_url }}" style="display:inline-block;padding:12px 24px;background-color:#00626c;color:#ffffff !important;text-decoration:none;border-radius:4px;font-weight:600;font-size:14px;">View in portal</a>
</p>
{% endif %}
{% endblock %}
{% block footer_note %}This is an automated message from {{ brand_name }}.{% endblock %}
@@ -0,0 +1,17 @@
New contact form inquiry — {{ brand_name|default:"Monica Dhillon" }}
Name: {{ name }}
Email: {{ email }}
Phone: {{ phone }}
Address:
{{ address }}
Message:
{{ message }}
{% if portal_url %}View in portal: {{ portal_url }}
{% endif %}
{{ brand_name|default:"Monica Dhillon" }}
{{ site_url|default:"https://mkdrealtor.com" }}
{% if brand_tagline %}{{ brand_tagline }}{% endif %}
@@ -0,0 +1,25 @@
{% extends "emails/base_email.html" %}
{% block title %}{{ title }}{% endblock %}
{% block header_extra %}
{% if title %}
<p style="margin:16px 0 0;font-size:18px;font-weight:600;color:#212121;line-height:1.4;">{{ title }}</p>
{% endif %}
{% endblock %}
{% block content %}
{{ content_html|safe }}
{% endblock %}
{% block footer_note %}
{% if prefs_url or one_click_url %}
{% if prefs_url %}
<a href="{{ prefs_url }}" style="color:#00626c;text-decoration:underline;">Manage preferences</a>
{% endif %}
{% if prefs_url and one_click_url %}&nbsp;·&nbsp;{% endif %}
{% if one_click_url %}
<a href="{{ one_click_url }}" style="color:#00626c;text-decoration:underline;">Unsubscribe from email</a>
{% endif %}
{% endif %}
{% endblock %}
@@ -0,0 +1,10 @@
{% if title %}{{ title }}
{% endif %}{{ content }}
{% if prefs_url %}Manage preferences: {{ prefs_url }}
{% endif %}{% if one_click_url %}Unsubscribe from email: {{ one_click_url }}
{% endif %}{{ brand_name|default:"Monica Dhillon" }}
{{ site_url|default:"https://mkdrealtor.com" }}
{% if brand_tagline %}{{ brand_tagline }}{% endif %}
+46
View File
@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% load static %}
{% block title %}404 · {{ SITE_NAME }}{% endblock %}
{% block body %}
<div class="page">
<section class="section section-single bg-gray-800 primary-overlay" style="background-image: url({% static 'images/bg-image-7.jpg' %});">
<div class="section-single-inner">
<div class="section-single-dummy"></div>
<div class="section-single-main">
<div class="container">
<div class="row row-30">
<div class="col-sm-6 text-center text-sm-left">
<p class="text-extra-large">404</p>
</div>
<div class="col-sm-6">
<div class="section-single-main-content">
<h6 class="title-decorated title-decorated-lg">Page not found</h6>
<p><span class="text-width-2">That page may have moved or the link is outdated. Lets get you back on track.</span></p>
<a class="button button-lg button-primary button-winona" href="{% url 'public:home' %}">Back to home</a>
<a class="button button-lg button-default-outline button-winona" href="{% url 'public:contact' %}" style="margin-left:8px">Contact Monica</a>
</div>
</div>
</div>
</div>
</div>
<div class="footer-minimal section-single-footer">
<div class="container">
<div class="footer-minimal-inner">
<a class="brand" href="{% url 'public:home' %}">
<img src="{% static 'brand/exit_logo.png' %}" alt="EXIT Realty · {{ SITE_NAME }}" width="151" height="44"/>
</a>
<div class="footer-aside-copy" style="text-align:left">
<p class="rights">
<span>&copy;&nbsp;</span><span>{% now "Y" %}</span><span>&nbsp;</span><span>{{ SITE_NAME }} · MKDRealtor.com</span>
</p>
<p class="footer-made-by">
Made by <a href="{{ CREDIT_URL }}" target="_blank" rel="noopener noreferrer">{{ CREDIT_NAME }}</a>
</p>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% endblock %}
+59
View File
@@ -0,0 +1,59 @@
{% extends "base.html" %}
{% load static %}
{% block title %}About {{ SITE_NAME }}{% endblock %}
{% block og_title %}About {{ SITE_NAME }}{% endblock %}
{% block twitter_title %}About {{ SITE_NAME }}{% endblock %}
{% block meta_description %}About {{ SITE_NAME }}. {{ SITE_TAGLINE }}. {{ CONTACT_SERVICE_AREA }}.{% endblock %}
{% block og_description %}About {{ SITE_NAME }}. {{ SITE_TAGLINE }}. {{ CONTACT_SERVICE_AREA }}.{% endblock %}
{% block twitter_description %}About {{ SITE_NAME }}. {{ SITE_TAGLINE }}. {{ CONTACT_SERVICE_AREA }}.{% endblock %}
{% block structured_data %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "AboutPage",
"name": "About {{ SITE_NAME|escapejs }}",
"url": "{{ PUBLIC_SITE_URL }}{% url 'public:about' %}",
"description": "{{ SITE_TAGLINE|escapejs }}",
"mainEntity": {
"@type": "Organization",
"name": "{{ SITE_NAME|escapejs }}",
"url": "{{ PUBLIC_SITE_URL }}/",
"telephone": "{{ CONTACT_PHONE|escapejs }}",
"email": "{{ CONTACT_EMAIL|escapejs }}"
}
}
</script>
{% endblock %}
{% block content %}
<section class="breadcrumbs-custom bg-image context-dark" style="background-image: url({% static 'images/breadcrumbs-image-1.jpg' %});">
<div class="breadcrumbs-custom-inner">
<div class="container breadcrumbs-custom-container">
<div class="breadcrumbs-custom-main">
<p class="breadcrumbs-custom-subtitle title-decorated">About</p>
<h1 class="text-uppercase breadcrumbs-custom-title">{{ SITE_NAME }}</h1>
</div>
<ul class="breadcrumbs-custom-path">
<li><a href="{% url 'public:home' %}">Home</a></li>
<li class="active">About</li>
</ul>
</div>
</div>
</section>
<section class="section section-lg">
<div class="container">
<div class="row row-50">
<div class="col-lg-8">
<h2>Who we are</h2>
<p>{% if SITE_TAGLINE %}{{ SITE_TAGLINE }}. {% endif %}Replace this paragraph with the clients story, team, and what customers should expect.</p>
<p>{% if CONTACT_SERVICE_AREA %}{{ CONTACT_SERVICE_AREA }}. {% endif %}Every site seeded from this template gets a unique public app — swap photos, copy, and service pages per brand.</p>
<a class="button button-primary button-winona" href="{% url 'public:contact' %}">Contact us</a>
</div>
<div class="col-lg-4">
{% if CONTACT_PHONE %}<p><strong>Phone</strong><br><a href="tel:{{ CONTACT_PHONE_TEL }}">{{ CONTACT_PHONE }}</a></p>{% endif %}
{% if CONTACT_EMAIL %}<p><strong>Email</strong><br><a href="mailto:{{ CONTACT_EMAIL }}">{{ CONTACT_EMAIL }}</a></p>{% endif %}
{% if CONTACT_ADDRESS %}<p><strong>Address</strong><br>{{ CONTACT_ADDRESS }}</p>{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}
+198
View File
@@ -0,0 +1,198 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Contact {{ SITE_NAME }} · Get in Touch{% endblock %}
{% block og_title %}Contact {{ SITE_NAME }} · Get in Touch{% endblock %}
{% block twitter_title %}Contact {{ SITE_NAME }} · Get in Touch{% endblock %}
{% block meta_description %}Contact {{ SITE_NAME }}. {{ CONTACT_SERVICE_AREA }}. Call{% if CONTACT_PHONE %} {{ CONTACT_PHONE }}{% endif %}{% if CONTACT_EMAIL %} or email {{ CONTACT_EMAIL }}{% endif %}.{% endblock %}
{% block og_description %}Contact {{ SITE_NAME }}. {{ CONTACT_SERVICE_AREA }}. Call{% if CONTACT_PHONE %} {{ CONTACT_PHONE }}{% endif %}{% if CONTACT_EMAIL %} or email {{ CONTACT_EMAIL }}{% endif %}.{% endblock %}
{% block twitter_description %}Contact {{ SITE_NAME }}. {{ CONTACT_SERVICE_AREA }}. Call{% if CONTACT_PHONE %} {{ CONTACT_PHONE }}{% endif %}{% if CONTACT_EMAIL %} or email {{ CONTACT_EMAIL }}{% endif %}.{% endblock %}
{% block structured_data %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ContactPage",
"name": "Contact {{ SITE_NAME|escapejs }}",
"url": "{{ PUBLIC_SITE_URL }}{% url 'public:contact' %}",
"description": "Contact {{ SITE_NAME|escapejs }}. {{ CONTACT_SERVICE_AREA|escapejs }}.",
"mainEntity": {
"@type": "Organization",
"name": "{{ SITE_NAME|escapejs }}",
"url": "{{ PUBLIC_SITE_URL }}/",
"telephone": "{{ CONTACT_PHONE|escapejs }}",
"email": "{{ CONTACT_EMAIL|escapejs }}",
{% if CONTACT_ADDRESS %}"address": "{{ CONTACT_ADDRESS|escapejs }}",{% endif %}
"areaServed": "{{ CONTACT_SERVICE_AREA|escapejs }}",
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer service",
"telephone": "{{ CONTACT_PHONE|escapejs }}",
"email": "{{ CONTACT_EMAIL|escapejs }}",
"areaServed": "{{ CONTACT_SERVICE_AREA|escapejs }}",
"availableLanguage": "English"
}
}
}
</script>
{% endblock %}
{% block content %}
<section class="breadcrumbs-custom bg-image context-dark" style="background-image: url({% static 'images/breadcrumbs-image-1.jpg' %});">
<div class="breadcrumbs-custom-inner">
<div class="container breadcrumbs-custom-container">
<div class="breadcrumbs-custom-main">
<p class="breadcrumbs-custom-subtitle title-decorated">Contact</p>
<h1 class="text-uppercase breadcrumbs-custom-title">Get in touch</h1>
</div>
<ul class="breadcrumbs-custom-path">
<li><a href="{% url 'public:home' %}">Home</a></li>
<li class="active">Contact</li>
</ul>
</div>
</div>
</section>
<section class="section section-sm">
<div class="container">
<div class="layout-bordered">
{% if CONTACT_PHONE %}
<div class="layout-bordered-item wow-outer">
<div class="layout-bordered-item-inner wow slideInUp">
<div class="icon icon-lg mdi mdi-phone text-primary"></div>
<ul class="list-0"><li><a class="link-default" href="tel:{{ CONTACT_PHONE_TEL }}">{{ CONTACT_PHONE }}</a></li></ul>
</div>
</div>
{% endif %}
{% if CONTACT_EMAIL %}
<div class="layout-bordered-item wow-outer">
<div class="layout-bordered-item-inner wow slideInUp">
<div class="icon icon-lg mdi mdi-email text-primary"></div>
<a class="link-default" href="mailto:{{ CONTACT_EMAIL }}">{{ CONTACT_EMAIL }}</a>
</div>
</div>
{% endif %}
{% if CONTACT_ADDRESS or CONTACT_SERVICE_AREA %}
<div class="layout-bordered-item wow-outer">
<div class="layout-bordered-item-inner wow slideInUp">
<div class="icon icon-lg mdi mdi-map-marker text-primary"></div>
<span class="link-default">{% if CONTACT_ADDRESS %}{{ CONTACT_ADDRESS }}{% else %}{{ CONTACT_SERVICE_AREA }}{% endif %}</span>
</div>
</div>
{% endif %}
</div>
</div>
</section>
<section class="section bg-gray-100 contact-form-section">
<div class="container section-lg">
<div class="row">
<div class="col-lg-8">
<h3 class="text-uppercase">Send a message</h3>
<form class="rd-form contact-message-form" method="post" action="{% url 'public:contact' %}">
{% csrf_token %}
<div class="row row-10">
<div class="col-md-6">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.first_name.id_for_label }}">First name</label>
{{ form.first_name }}
{{ form.first_name.errors }}
</div>
</div>
<div class="col-md-6">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.last_name.id_for_label }}">Last name</label>
{{ form.last_name }}
{{ form.last_name.errors }}
</div>
</div>
<div class="col-md-6">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.email.id_for_label }}">Email</label>
{{ form.email }}
{{ form.email.errors }}
</div>
</div>
<div class="col-md-6">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.phone.id_for_label }}">Phone</label>
{{ form.phone }}
{{ form.phone.errors }}
</div>
</div>
<div class="col-12">
<p class="form-label-outside" style="margin:8px 0 4px">Mailing address <span style="font-weight:400;color:#6b7280">(optional — for postcards)</span></p>
</div>
<div class="col-12" data-address-autocomplete data-suggest-url="{% url 'address_suggest' %}">
<div class="form-wrap address-ac-wrap">
<label class="form-label-outside" for="{{ form.address_line1.id_for_label }}">Street address</label>
{{ form.address_line1 }}
{{ form.address_line1.errors }}
</div>
<div class="row row-10">
<div class="col-md-6">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.address_line2.id_for_label }}">Apt / suite</label>
{{ form.address_line2 }}
{{ form.address_line2.errors }}
</div>
</div>
<div class="col-md-6">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.address_city.id_for_label }}">City</label>
{{ form.address_city }}
{{ form.address_city.errors }}
</div>
</div>
<div class="col-md-6">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.address_state.id_for_label }}">State</label>
{{ form.address_state }}
{{ form.address_state.errors }}
</div>
</div>
<div class="col-md-6">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.address_zip.id_for_label }}">ZIP</label>
{{ form.address_zip }}
{{ form.address_zip.errors }}
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.interest.id_for_label }}">I am interested in</label>
{{ form.interest }}
{{ form.interest.errors }}
</div>
</div>
<div class="col-12">
<div class="form-wrap">
<label class="form-label-outside" for="{{ form.message.id_for_label }}">Message</label>
{{ form.message }}
{{ form.message.errors }}
</div>
</div>
{% if form.captcha %}
<div class="col-12">{{ form.captcha }}{{ form.captcha.errors }}</div>
{% endif %}
</div>
<p style="font-size:13px;color:#6b7280;margin:12px 0 20px;">By submitting, you agree I may contact you about your inquiry by email and SMS (if you provide a phone number). You can change preferences or unsubscribe anytime from links in messages, or reply STOP to SMS.{% if form.captcha %} Protected by reCAPTCHA.{% endif %}</p>
<button class="button button-primary button-winona" type="submit" data-tianji-event="contact_form_submit">Send message</button>
</form>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'css/address-autocomplete.css' %}">
{% endblock %}
{% block tracking_events %}
{% if "sent" in request.GET %}
<script>
if (window.aimlTrackWhenReady) {
window.aimlTrackWhenReady('contact_form_success');
}
</script>
{% endif %}
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/address-autocomplete.js' %}"></script>
{% endblock %}
+80
View File
@@ -0,0 +1,80 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ SITE_NAME }}{% if SITE_TAGLINE %} · {{ SITE_TAGLINE }}{% endif %}{% endblock %}
{% block og_title %}{{ SITE_NAME }}{% endblock %}
{% block twitter_title %}{{ SITE_NAME }}{% endblock %}
{% block meta_description %}{{ SITE_NAME }}{% if SITE_TAGLINE %} — {{ SITE_TAGLINE }}.{% endif %} {{ CONTACT_SERVICE_AREA }}.{% endblock %}
{% block og_description %}{{ SITE_NAME }}{% if SITE_TAGLINE %} — {{ SITE_TAGLINE }}.{% endif %} {{ CONTACT_SERVICE_AREA }}.{% endblock %}
{% block twitter_description %}{{ SITE_NAME }}{% if SITE_TAGLINE %} — {{ SITE_TAGLINE }}.{% endif %} {{ CONTACT_SERVICE_AREA }}.{% endblock %}
{% block structured_data %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "{{ SITE_NAME|escapejs }}",
"url": "{{ PUBLIC_SITE_URL }}/",
"image": "{{ PUBLIC_SITE_URL }}{% static 'images/slider-minimal-slide-1-1920x968.jpg' %}",
"telephone": "{{ CONTACT_PHONE|escapejs }}",
"email": "{{ CONTACT_EMAIL|escapejs }}"{% if CONTACT_ADDRESS %},
"address": "{{ CONTACT_ADDRESS|escapejs }}"{% endif %}{% if CONTACT_SERVICE_AREA %},
"areaServed": "{{ CONTACT_SERVICE_AREA|escapejs }}"{% endif %}
}
</script>
{% endblock %}
{% block content %}
<div class="swiper-container swiper-slider swiper-slider-minimal" data-loop="false" data-slide-effect="fade" data-autoplay="false" data-simulate-touch="true">
<div class="swiper-wrapper">
<div class="swiper-slide" data-slide-bg="{% static 'images/slider-minimal-slide-1-1920x968.jpg' %}">
<div class="container">
<div class="jumbotron-classic-content">
<div class="wow-outer">
<div class="title-docor-text font-weight-bold title-decorated text-uppercase wow slideInLeft text-white">{{ SITE_NAME }}</div>
</div>
<h1 class="text-uppercase text-white font-weight-bold wow-outer"><span class="wow slideInDown" data-wow-delay=".2s">{% if SITE_TAGLINE %}{{ SITE_TAGLINE }}{% else %}Welcome{% endif %}</span></h1>
<p class="text-white wow-outer"><span class="wow slideInDown" data-wow-delay=".35s">{% if CONTACT_SERVICE_AREA %}{{ CONTACT_SERVICE_AREA }}. {% endif %}Tell us what you need — we will follow up promptly.</span></p>
<div class="wow-outer button-outer">
<a class="button button-md button-primary button-winona wow slideInDown" href="{% url 'public:contact' %}" data-wow-delay=".4s" data-tianji-event="hero_cta" data-tianji-event-destination="contact">Start a conversation</a>
</div>
</div>
</div>
</div>
</div>
</div>
<section class="section section-lg">
<div class="container">
<h2 class="text-uppercase text-center wow-outer"><span class="wow slideInDown">How we can help</span></h2>
<div class="row row-50">
<div class="col-md-4 wow-outer">
<article class="box-chloe wow slideInUp">
<div class="box-chloe__icon linearicons-phone"></div>
<div class="box-chloe__main">
<h3 class="box-chloe__title">Talk with us</h3>
<p>A real person answers. Share your goals and we will map the next step.</p>
</div>
</article>
</div>
<div class="col-md-4 wow-outer">
<article class="box-chloe wow slideInUp" data-wow-delay=".05s">
<div class="box-chloe__icon linearicons-register"></div>
<div class="box-chloe__main">
<h3 class="box-chloe__title">Stay informed</h3>
<p>Opt in on the contact form for email, SMS, or mail — only the channels you choose.</p>
</div>
</article>
</div>
<div class="col-md-4 wow-outer">
<article class="box-chloe wow slideInUp" data-wow-delay=".1s">
<div class="box-chloe__icon linearicons-apartment"></div>
<div class="box-chloe__main">
<h3 class="box-chloe__title">Work with us</h3>
<p>Replace this copy with your services. Brand, photos, and pages are unique to each client site.</p>
</div>
</article>
</div>
</div>
<div class="text-center" style="margin-top:24px">
<a class="button button-primary button-winona" href="{% url 'public:about' %}">About us</a>
</div>
</div>
</section>
{% endblock %}
+50
View File
@@ -0,0 +1,50 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Terms of Service · {{ SITE_NAME }}{% endblock %}
{% block og_title %}Terms of Service · {{ SITE_NAME }}{% endblock %}
{% block twitter_title %}Terms of Service · {{ SITE_NAME }}{% endblock %}
{% block meta_description %}Terms of Service for {{ SITE_NAME }} / MKDRealtor.com — how we use analytics, protect your information, and never sell or share personal data with third parties for marketing.{% endblock %}
{% block og_description %}Terms of Service for {{ SITE_NAME }} / MKDRealtor.com — how we use analytics, protect your information, and never sell or share personal data with third parties for marketing.{% endblock %}
{% block twitter_description %}Terms of Service for {{ SITE_NAME }} / MKDRealtor.com — how we use analytics, protect your information, and never sell or share personal data with third parties for marketing.{% endblock %}
{% block content %}
<section class="breadcrumbs-custom bg-image context-dark" style="background-image: url({% static 'images/breadcrumbs-image-1.jpg' %});">
<div class="breadcrumbs-custom-inner">
<div class="container breadcrumbs-custom-container">
<div class="breadcrumbs-custom-main">
<p class="breadcrumbs-custom-subtitle title-decorated">Legal</p>
<h1 class="text-uppercase breadcrumbs-custom-title">Terms of Service</h1>
</div>
<ul class="breadcrumbs-custom-path">
<li><a href="{% url 'public:home' %}">Home</a></li>
<li class="active">Terms</li>
</ul>
</div>
</div>
</section>
<section class="section section-lg">
<div class="container legal-prose">
<p class="legal-lead">These Terms of Service describe how {{ SITE_NAME }} (“we,” “us”) operates MKDRealtor.com and related pages. By using this site you agree to these terms.</p>
<h2>Analytics and site performance</h2>
<p>We use analytics to monitor site performance, understand how visitors use the site, and improve content and reliability. Analytics may collect technical information such as pages viewed, approximate location derived from IP address, device/browser type, and referral source. This information is used only to operate and improve the site.</p>
<h2>We do not sell or share your data</h2>
<p>We never sell your personal information. We do not share or sell user data to third parties for their advertising or marketing. Contact-form submissions and related lead information are used only to respond to your inquiry and to run our real-estate practice.</p>
<h2>Information you provide</h2>
<p>When you submit the contact form or other requests, you may share your name, email, phone, mailing address, and message. We use that information to follow up with you and, with your consent, for related outreach (email, SMS, or postcard where applicable). You can change preferences or unsubscribe using the links in our messages, or by contacting us.</p>
<h2>Cookies and similar technologies</h2>
<p>We may use cookies or similar technologies needed for the site to work (for example session and security) and for analytics as described above. The analytics notice on the site acknowledges this use. For questions about cookies or analytics, contact us using the details below.</p>
<h2>Contact</h2>
<p>
{% if CONTACT_EMAIL %}<a class="link-default" href="mailto:{{ CONTACT_EMAIL }}">{{ CONTACT_EMAIL }}</a>{% endif %}
{% if CONTACT_PHONE %}{% if CONTACT_EMAIL %} · {% endif %}<a class="link-default" href="tel:{{ CONTACT_PHONE_TEL }}">{{ CONTACT_PHONE }}</a>{% endif %}
</p>
<p><a class="button button-primary button-winona" href="{% url 'public:contact' %}">Contact Monica</a></p>
<p class="legal-updated">Last updated: {% now "F j, Y" %}</p>
</div>
</section>
{% endblock %}
@@ -0,0 +1,56 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Under construction · {{ SITE_NAME }}{% endblock %}
{% block body %}
<div class="page">
<section class="section section-single bg-gray-800 primary-overlay" style="background-image: url({% static 'images/bg-image-4.jpg' %});">
<div class="section-single-inner">
<div class="section-single-dummy"></div>
<div class="section-single-main">
<div class="container">
<div class="row row-30 justify-content-center justify-content-sm-start">
<div class="col-sm-10 col-md-9 col-lg-7 col-xl-6">
<h6 class="title-decorated title-decorated-lg">Were getting ready to launch</h6>
<p>{{ SITE_NAME }} · MKDRealtor.coms new site is almost here. Leave your email and Ill let you know when its live — or reach out now if you need help buying or selling.</p>
<div class="rd-mailform-wrap">
<form class="rd-form form-inline" method="post" action="{% url 'public:under_construction' %}">
{% csrf_token %}
<div class="form-wrap">
<input class="form-input" id="{{ form.email.id_for_label }}" type="email" name="{{ form.email.name }}" required value="{{ form.email.value|default:'' }}">
<label class="form-label" for="{{ form.email.id_for_label }}">Your e-mail</label>
{{ form.email.errors }}
</div>
<div class="form-button">
<button class="button button-primary button-winona" type="submit" data-tianji-event="notify_me_submit">Notify me</button>
</div>
</form>
</div>
<p style="margin-top:20px">
<a class="button button-default-outline button-winona" href="{% url 'public:contact' %}" data-tianji-event="holding_contact">Contact Monica now</a>
</p>
</div>
</div>
</div>
</div>
<div class="footer-minimal section-single-footer">
<div class="container">
<div class="footer-minimal-inner">
<a class="brand" href="{% url 'public:home' %}">
<img src="{% static 'brand/exit_logo.png' %}" alt="EXIT Realty · {{ SITE_NAME }}" width="151" height="44"/>
</a>
<div class="footer-aside-copy" style="text-align:left">
<p class="rights">
<span>&copy;&nbsp;</span><span>{% now "Y" %}</span><span>&nbsp;</span><span>{{ SITE_NAME }} · MKDRealtor.com</span>
{% if CONTACT_PHONE %}<span>&nbsp;·&nbsp;</span><a href="tel:{{ CONTACT_PHONE_TEL }}">{{ CONTACT_PHONE }}</a>{% endif %}
</p>
<p class="footer-made-by">
Made by <a href="{{ CREDIT_URL }}" target="_blank" rel="noopener noreferrer">{{ CREDIT_NAME }}</a>
</p>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% endblock %}
@@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}Communication preferences · {{ SITE_NAME }}{% endblock %}
{% block content %}
<section class="section section-lg">
<div class="container" style="max-width:560px;">
<h3 class="text-uppercase">Communication preferences</h3>
{% if not valid %}
<p>This preferences link is invalid or has expired.</p>
<p style="font-size:13px;color:#6b7280;">If you still get messages, reply STOP on SMS or contact Monica directly.</p>
<p style="margin-top:24px">
<a class="button button-primary button-winona" href="{% url 'public:contact' %}">Contact Monica</a>
<a class="button button-gray-bordered button-winona" href="{% url 'public:home' %}">Back to home</a>
</p>
{% else %}
<p>You are subscribed as <strong>{{ identity }}</strong>.</p>
<form method="post" action="{% url 'public:unsubscribe' token=token %}">
{% csrf_token %}
<div class="form-wrap">
<label class="checkbox-inline">
<input type="checkbox" name="consent_email" value="1" {% if prefs.email %}checked{% endif %}>
Email marketing
</label>
</div>
<div class="form-wrap">
<label class="checkbox-inline">
<input type="checkbox" name="consent_sms" value="1" {% if prefs.sms %}checked{% endif %}>
SMS updates
</label>
</div>
<div class="form-wrap">
<label class="checkbox-inline">
<input type="checkbox" name="consent_postcard" value="1" {% if prefs.postcard %}checked{% endif %}>
Postcard mailings
</label>
</div>
<p style="font-size:13px;color:#6b7280;margin-top:16px;">
SMS: reply <strong>STOP</strong> anytime. Changes take effect immediately.
Postcard mailings also need a postal address on file.
</p>
<div style="margin-top:24px;display:flex;flex-wrap:wrap;gap:12px;">
<button class="button button-primary button-winona" type="submit" name="action" value="save">
Save preferences
</button>
<button class="button button-gray-bordered button-winona" type="submit" name="action" value="unsubscribe_all">
Unsubscribe from all
</button>
</div>
</form>
<p style="margin-top:32px">
<a class="button button-gray-bordered button-winona" href="{% url 'public:home' %}">Back to home</a>
</p>
{% endif %}
</div>
</section>
{% endblock %}
+21
View File
@@ -0,0 +1,21 @@
from django.urls import path
from public import views
app_name = "public"
urlpatterns = [
path("", views.home, name="home"),
path("about/", views.about, name="about"),
path("contact/", views.contact, name="contact"),
path("terms/", views.terms, name="terms"),
path("robots.txt", views.robots_txt, name="robots_txt"),
path("sitemap.xml", views.sitemap_xml, name="sitemap_xml"),
path("under-construction/", views.under_construction, name="under_construction"),
path("unsubscribe/<str:token>/", views.unsubscribe, name="unsubscribe"),
path(
"unsubscribe/<str:token>/one-click/",
views.unsubscribe_one_click,
name="unsubscribe_one_click",
),
]
+257
View File
@@ -0,0 +1,257 @@
from django.conf import settings
from django.contrib import messages
from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET, require_http_methods
from analytics.services import attribute_lead_from_request
from contacts.models import Channel, ConsentRecord, Contact
from contacts.services import upsert_contact
from leads.models import Lead
from contacts.consent import (
channel_preferences,
parse_unsubscribe_token,
process_unsubscribe_token,
set_channel_preferences,
unsubscribe_all,
)
from public.forms import ContactForm, NotifyForm
from public.notifications import notify_admins_of_contact_form
def _public_site_base(request) -> str:
base = (settings.PUBLIC_SITE_URL or "").rstrip("/")
if base:
return base
return request.build_absolute_uri("/").rstrip("/")
def home(request):
return render(request, "public/home.html")
def about(request):
return render(request, "public/about.html")
def terms(request):
return render(request, "public/terms.html")
@require_GET
def robots_txt(request):
site = _public_site_base(request)
body = "\n".join(
[
"User-agent: *",
"Allow: /",
"Disallow: /portal/",
"Disallow: /accounts/",
"Disallow: /admin/",
"Disallow: /api/",
f"Sitemap: {site}/sitemap.xml",
"",
]
)
return HttpResponse(body, content_type="text/plain; charset=utf-8")
@require_GET
def sitemap_xml(request):
site = _public_site_base(request)
paths = [
("public:home", "1.0", "weekly"),
("public:about", "0.8", "monthly"),
("public:contact", "0.9", "monthly"),
("public:terms", "0.5", "yearly"),
]
from django.apps import apps as django_apps
if django_apps.is_installed("blog"):
paths.append(("blog:list", "0.7", "weekly"))
urls = []
for name, priority, changefreq in paths:
path = reverse(name)
urls.append(
" <url>\n"
f" <loc>{site}{path}</loc>\n"
f" <changefreq>{changefreq}</changefreq>\n"
f" <priority>{priority}</priority>\n"
" </url>"
)
body = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
+ "\n".join(urls)
+ "\n</urlset>\n"
)
return HttpResponse(body, content_type="application/xml; charset=utf-8")
@require_http_methods(["GET", "POST"])
def contact(request):
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
data = form.cleaned_data
postal = Contact.make_postal_address(
line1=data.get("address_line1") or "",
line2=data.get("address_line2") or "",
city=data.get("address_city") or "",
state=data.get("address_state") or "",
zip_code=data.get("address_zip") or "",
)
submitted_email = data["email"].lower()
contact_obj, _created, match_reason = upsert_contact(
email=submitted_email,
first_name=data["first_name"],
last_name=data.get("last_name") or "",
phone=data.get("phone") or "",
postal_address=postal
if Contact.postal_address_has_content(postal)
else None,
source=Contact.Source.CONTACT_FORM,
)
ConsentRecord.objects.update_or_create(
contact=contact_obj,
channel=Channel.EMAIL,
defaults={"opted_in": True, "reason": "contact_form"},
)
if (data.get("phone") or "").strip():
ConsentRecord.objects.update_or_create(
contact=contact_obj,
channel=Channel.SMS,
defaults={"opted_in": True, "reason": "contact_form"},
)
if Contact.postal_address_has_content(postal):
ConsentRecord.objects.get_or_create(
contact=contact_obj,
channel=Channel.POSTCARD,
defaults={"opted_in": True, "reason": "contact_form"},
)
interest = data.get("interest") or ""
interest_label = dict(ContactForm.INTEREST_CHOICES).get(interest, interest)
body = data.get("message") or ""
if interest_label:
body = f"Interest: {interest_label}\n\n{body}".strip()
if (
match_reason in {"phone", "address"}
and (contact_obj.email or "").lower() != submitted_email
):
body = (
f"Submitted email: {submitted_email} "
f"(merged by {match_reason} with "
f"{contact_obj.email or 'existing contact'})\n\n{body}"
).strip()
lead = Lead.objects.create(
contact=contact_obj,
message=body,
status=Lead.Status.NEW,
)
attribute_lead_from_request(request, lead)
notify_admins_of_contact_form(lead)
messages.success(request, "Thanks — we will be in touch soon.")
return redirect(f"{reverse('public:contact')}?sent=1")
else:
form = ContactForm()
return render(request, "public/contact.html", {"form": form})
@require_http_methods(["GET", "POST"])
def under_construction(request):
"""Direct route (also used by middleware). Accepts notify-me emails."""
if request.method == "POST":
form = NotifyForm(request.POST)
if form.is_valid():
email = form.cleaned_data["email"].lower()
Contact.objects.get_or_create(
email=email,
defaults={
"first_name": "",
"source": Contact.Source.NOTIFY_ME,
},
)
messages.success(request, "You're on the list — we'll email when we launch.")
return redirect("public:under_construction")
else:
form = NotifyForm()
return render(request, "public/under_construction.html", {"form": form})
@csrf_exempt
@require_http_methods(["GET", "POST"])
def unsubscribe_one_click(request, token: str):
"""
One-click opt-out for the channel encoded in the token.
CSRF-exempt so mail clients can POST List-Unsubscribe=One-Click (RFC 8058).
"""
ok = process_unsubscribe_token(token)
if not ok:
return render(request, "public/unsubscribe.html", {"valid": False})
return redirect("public:unsubscribe", token=token)
@require_http_methods(["GET", "POST"])
def unsubscribe(request, token: str):
"""
Signed-token preference center for email / SMS / postcard.
GET ?one_click=1 opts out the token channel then redirects here.
POST saves checkboxes or unsubscribes from all channels.
"""
contact, token_channel = parse_unsubscribe_token(token)
if not contact:
return render(
request,
"public/unsubscribe.html",
{"valid": False},
)
if request.method == "GET" and request.GET.get("one_click") in {
"1",
"true",
"yes",
}:
process_unsubscribe_token(token)
return redirect("public:unsubscribe", token=token)
if request.method == "POST":
action = (request.POST.get("action") or "save").strip()
if action == "unsubscribe_all":
unsubscribe_all(contact, reason="preferences_unsubscribe_all")
messages.success(
request, "You are unsubscribed from all marketing channels."
)
else:
prefs = {
Channel.EMAIL: "consent_email" in request.POST,
Channel.SMS: "consent_sms" in request.POST,
Channel.POSTCARD: "consent_postcard" in request.POST,
}
set_channel_preferences(
contact, prefs, reason="preferences_save"
)
messages.success(request, "Your communication preferences were saved.")
return redirect("public:unsubscribe", token=token)
contact = Contact.objects.prefetch_related("consents").get(pk=contact.pk)
prefs = channel_preferences(contact)
identity = contact.email or contact.phone or contact.full_name or "your profile"
return render(
request,
"public/unsubscribe.html",
{
"valid": True,
"contact": contact,
"identity": identity,
"prefs": prefs,
"token_channel": token_channel,
"token": token,
},
)
def page_not_found(request, exception):
return render(request, "public/404.html", status=404)