Instrument webhooks, fix compose layout, and ship social connect.
Add Grafana-friendly webhook request logging, Quill overflow fix, New Contact flow, in-place postcard campaign compose, and functional social account connect + composer.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
@@ -7,6 +9,7 @@ from django.views.decorators.http import require_GET, require_http_methods
|
||||
|
||||
from contacts.models import Channel, ConsentRecord, Contact
|
||||
from contacts.nominatim import NominatimError, suggest_addresses
|
||||
from contacts.services import upsert_contact
|
||||
from messaging.services import channel_preferences, set_channel_preferences
|
||||
|
||||
|
||||
@@ -48,6 +51,76 @@ def contact_list(request):
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def contact_create(request):
|
||||
form = {
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"address_line1": "",
|
||||
"address_line2": "",
|
||||
"address_city": "",
|
||||
"address_state": "",
|
||||
"address_zip": "",
|
||||
"address_country": "US",
|
||||
"notes": "",
|
||||
"consent_email": True,
|
||||
"consent_sms": False,
|
||||
"consent_postcard": True,
|
||||
}
|
||||
if request.method == "POST":
|
||||
for key in list(form.keys()):
|
||||
if key.startswith("consent_"):
|
||||
form[key] = key in request.POST
|
||||
else:
|
||||
form[key] = (request.POST.get(key) or "").strip()
|
||||
email = form["email"].lower()
|
||||
errors: list[str] = []
|
||||
if not form["first_name"]:
|
||||
errors.append("First name is required.")
|
||||
if not email:
|
||||
errors.append("Email is required.")
|
||||
else:
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
errors.append("Enter a valid email address.")
|
||||
postal = _postal_from_post(request.POST)
|
||||
has_postal = Contact.postal_address_has_content(postal)
|
||||
if form["consent_sms"] and not form["phone"]:
|
||||
errors.append("Phone is required for SMS consent.")
|
||||
if form["consent_postcard"] and not has_postal:
|
||||
# Soft: allow save but clear postcard consent if no address
|
||||
form["consent_postcard"] = False
|
||||
if not errors:
|
||||
contact, created, reason = upsert_contact(
|
||||
email=email,
|
||||
first_name=form["first_name"],
|
||||
last_name=form["last_name"],
|
||||
phone=form["phone"],
|
||||
postal_address=postal if has_postal else None,
|
||||
source=Contact.Source.MANUAL,
|
||||
notes_append=form["notes"],
|
||||
)
|
||||
set_channel_preferences(
|
||||
contact,
|
||||
{
|
||||
Channel.EMAIL: form["consent_email"],
|
||||
Channel.SMS: form["consent_sms"],
|
||||
Channel.POSTCARD: form["consent_postcard"],
|
||||
},
|
||||
reason="portal_manual",
|
||||
)
|
||||
verb = "Added" if created else f"Updated (matched by {reason or 'email'})"
|
||||
messages.success(request, f"{verb} {contact}.")
|
||||
return redirect("contacts:detail", pk=contact.pk)
|
||||
for err in errors:
|
||||
messages.error(request, err)
|
||||
return render(request, "contacts/create.html", {"form": form})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def contact_detail(request, pk):
|
||||
@@ -76,6 +149,7 @@ def contact_detail(request, pk):
|
||||
{"contact": contact, "prefs": prefs},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def contact_import(request):
|
||||
return render(request, "contacts/import.html")
|
||||
|
||||
Reference in New Issue
Block a user