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 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 def _consent_flags(contact: Contact) -> dict[str, bool]: return channel_preferences(contact) def _postal_from_post(post) -> dict: return Contact.make_postal_address( line1=post.get("address_line1", ""), line2=post.get("address_line2", ""), city=post.get("address_city", ""), state=post.get("address_state", ""), zip_code=post.get("address_zip", ""), country=post.get("address_country", "US"), ) @login_required def contact_list(request): contacts = Contact.objects.prefetch_related( Prefetch("consents", queryset=ConsentRecord.objects.all()) ).all() q = (request.GET.get("q") or "").strip() if q: contacts = contacts.filter( Q(first_name__icontains=q) | Q(last_name__icontains=q) | Q(email__icontains=q) | Q(phone__icontains=q) ) rows = list(contacts[:200]) for contact in rows: contact.consent_flags = _consent_flags(contact) return render( request, "contacts/list.html", {"contacts": rows, "q": q}, ) @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): contact = get_object_or_404( Contact.objects.prefetch_related("consents"), pk=pk ) if request.method == "POST": 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"]) set_channel_preferences( contact, { Channel.EMAIL: "consent_email" in request.POST, Channel.SMS: "consent_sms" in request.POST, Channel.POSTCARD: "consent_postcard" in request.POST, }, reason="portal_manual", ) messages.success(request, "Contact updated.") return redirect("contacts:detail", pk=contact.pk) prefs = _consent_flags(contact) return render( request, "contacts/detail.html", {"contact": contact, "prefs": prefs}, ) @login_required def contact_import(request): return render(request, "contacts/import.html") @require_GET def address_suggest(request): """ Backend proxy for Nominatim search. Browser JS must call this URL only — never Nominatim directly. """ q = (request.GET.get("q") or "").strip() if len(q) < 3: return JsonResponse({"results": []}) try: limit = int(request.GET.get("limit") or 5) except (TypeError, ValueError): limit = 5 try: results = suggest_addresses(q, limit=limit) except NominatimError as exc: return JsonResponse({"error": str(exc), "results": []}, status=502) return JsonResponse({"results": results})