Improve campaign personalization and contact duplicate handling.
Add merge tags for email/SMS, paginated removable recipients, PCM event panels for postcard campaigns, and a portal modal when phone/address matches an existing contact.
This commit is contained in:
+121
-22
@@ -9,7 +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 contacts.services import find_matching_contact, upsert_contact
|
||||
from messaging.services import channel_preferences, set_channel_preferences
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ def contact_create(request):
|
||||
"consent_sms": False,
|
||||
"consent_postcard": True,
|
||||
}
|
||||
match_prompt = None
|
||||
if request.method == "POST":
|
||||
for key in list(form.keys()):
|
||||
if key.startswith("consent_"):
|
||||
@@ -77,6 +78,8 @@ def contact_create(request):
|
||||
else:
|
||||
form[key] = (request.POST.get(key) or "").strip()
|
||||
email = form["email"].lower()
|
||||
resolve = (request.POST.get("resolve_match") or "").strip()
|
||||
match_id = (request.POST.get("match_id") or "").strip()
|
||||
errors: list[str] = []
|
||||
if not form["first_name"]:
|
||||
errors.append("First name is required.")
|
||||
@@ -92,34 +95,77 @@ def contact_create(request):
|
||||
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(
|
||||
existing, reason = find_matching_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)
|
||||
# Phone/address collision (different email): ask user unless they chose.
|
||||
if (
|
||||
existing
|
||||
and reason in {"phone", "address"}
|
||||
and resolve not in {"update", "create"}
|
||||
):
|
||||
match_prompt = {
|
||||
"contact": existing,
|
||||
"reason": reason,
|
||||
"reason_label": "phone number"
|
||||
if reason == "phone"
|
||||
else "mailing address",
|
||||
}
|
||||
else:
|
||||
merge_into = None
|
||||
merge_phone_address = True
|
||||
if resolve == "update" and match_id:
|
||||
merge_into = Contact.objects.filter(pk=match_id).first()
|
||||
if merge_into is None:
|
||||
errors.append("Matched contact no longer exists.")
|
||||
elif resolve == "create":
|
||||
merge_phone_address = False
|
||||
elif reason == "email" and existing:
|
||||
merge_into = existing
|
||||
|
||||
if not errors:
|
||||
contact, created, used_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"],
|
||||
merge_phone_address=merge_phone_address,
|
||||
merge_into=merge_into,
|
||||
)
|
||||
set_channel_preferences(
|
||||
contact,
|
||||
{
|
||||
Channel.EMAIL: form["consent_email"],
|
||||
Channel.SMS: form["consent_sms"],
|
||||
Channel.POSTCARD: form["consent_postcard"],
|
||||
},
|
||||
reason="portal_manual",
|
||||
)
|
||||
if created:
|
||||
verb = "Added"
|
||||
elif used_reason == "email":
|
||||
verb = "Updated (same email)"
|
||||
elif resolve == "update":
|
||||
verb = f"Updated (matched by {reason or used_reason})"
|
||||
else:
|
||||
verb = f"Updated (matched by {used_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})
|
||||
|
||||
return render(
|
||||
request,
|
||||
"contacts/create.html",
|
||||
{"form": form, "match_prompt": match_prompt},
|
||||
)
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
@@ -128,9 +174,62 @@ def contact_detail(request, pk):
|
||||
Contact.objects.prefetch_related("consents"), pk=pk
|
||||
)
|
||||
if request.method == "POST":
|
||||
first_name = (request.POST.get("first_name") or "").strip()
|
||||
last_name = (request.POST.get("last_name") or "").strip()
|
||||
email = (request.POST.get("email") or "").strip().lower()
|
||||
phone = (request.POST.get("phone") or "").strip()
|
||||
errors: list[str] = []
|
||||
if not 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.")
|
||||
else:
|
||||
taken = (
|
||||
Contact.objects.filter(email__iexact=email)
|
||||
.exclude(pk=contact.pk)
|
||||
.exists()
|
||||
)
|
||||
if taken:
|
||||
errors.append("Another contact already uses that email.")
|
||||
if errors:
|
||||
for err in errors:
|
||||
messages.error(request, err)
|
||||
prefs = _consent_flags(contact)
|
||||
# Reflect submitted values so the user can fix them.
|
||||
contact.first_name = first_name
|
||||
contact.last_name = last_name
|
||||
contact.email = email
|
||||
contact.phone = phone
|
||||
contact.postal_address = _postal_from_post(request.POST)
|
||||
contact.notes = (request.POST.get("notes") or "").strip()
|
||||
return render(
|
||||
request,
|
||||
"contacts/detail.html",
|
||||
{"contact": contact, "prefs": prefs},
|
||||
)
|
||||
|
||||
contact.first_name = first_name
|
||||
contact.last_name = last_name
|
||||
contact.email = email
|
||||
contact.phone = phone
|
||||
contact.postal_address = _postal_from_post(request.POST)
|
||||
contact.notes = (request.POST.get("notes") or "").strip()
|
||||
contact.save(update_fields=["postal_address", "notes", "updated_at"])
|
||||
contact.save(
|
||||
update_fields=[
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"phone",
|
||||
"postal_address",
|
||||
"notes",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
set_channel_preferences(
|
||||
contact,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user