generated from westfarn/web_django_template
CI / test (pull_request) Successful in 35s
Shoppers can register, save shipping details, and view order history while cards stay on Stripe. EasyPost tracker updates (including numbers from Pirate Ship) and 1–5 star reviews are limited to buyers. The contact form now only asks for email and a message.
250 lines
8.3 KiB
Python
250 lines
8.3 KiB
Python
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):
|
|
featured = []
|
|
from django.apps import apps as django_apps
|
|
|
|
if django_apps.is_installed("shop"):
|
|
from shop.models import Product
|
|
|
|
featured = list(
|
|
Product.objects.filter(is_published=True)
|
|
.select_related("image")
|
|
.prefetch_related("colors")[:6]
|
|
)
|
|
return render(request, "public/home.html", {"featured_products": featured})
|
|
|
|
|
|
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: /account/",
|
|
"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"))
|
|
if django_apps.is_installed("shop"):
|
|
paths.append(("shop:list", "0.8", "weekly"))
|
|
if django_apps.is_installed("events"):
|
|
paths.append(("events:list", "0.8", "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
|
|
submitted_email = data["email"].lower()
|
|
contact_obj, _created, match_reason = upsert_contact(
|
|
email=submitted_email,
|
|
source=Contact.Source.CONTACT_FORM,
|
|
merge_phone_address=False,
|
|
)
|
|
ConsentRecord.objects.update_or_create(
|
|
contact=contact_obj,
|
|
channel=Channel.EMAIL,
|
|
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
|
|
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)
|