"""Submit form view (plan.md §7.4). The view persists a `Submission` and, depending on whether the request is authenticated, sets the initial state machine state per plan.md §7.3: OAuth user -> processing (email already verified) guest with email -> identifying (waiting for confirmation link click) For the guest path, the confirmation email is sent **synchronously** after the DB commit so we know the Mailtrap API outcome before redirecting. The dashboard then shows a green "check your inbox" notice on success or a red "couldn't send email -- try Google sign-in" notice on failure. """ from __future__ import annotations import secrets from django.contrib import messages from django.db import IntegrityError, transaction from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.html import format_html from django.views.generic import CreateView from .emails import send_confirmation_email from .forms import SubmissionForm from .models import Submission class SubmitView(CreateView): """Public submit form. GET renders, POST creates a Submission.""" form_class = SubmissionForm template_name = "submissions/submit.html" success_url = reverse_lazy("dashboard:index") def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["user"] = self.request.user return kwargs def form_valid(self, form): submission: Submission = form.save(commit=False) # The slug is auto-generated in `Submission.save()` if blank, so we # don't need to set one here. The `except IntegrityError` below # handles the rare race where the just-generated slug collides with # a concurrent insert. if self.request.user.is_authenticated: submission.submitted_by = self.request.user submission.guest_email = None submission.email_confirmed = True submission.status = Submission.Status.PROCESSING else: submission.submitted_by = None # guest_email is already on the form's cleaned_data, ModelForm # populated it onto the instance. submission.email_confirmed = False submission.status = Submission.Status.IDENTIFYING submission.confirmation_token = secrets.token_urlsafe(32) submission.confirmation_sent_at = timezone.now() # Persist inside a tight atomic block so the row is committed BEFORE # we hit the email transport. That way a slow / failing Mailtrap API # call can never roll back a saved submission, and we get to look at # the result and surface it as a user-visible notice. with transaction.atomic(): try: submission.save() except IntegrityError: # Extremely rare slug collision on the unique index; clearing # `slug` makes `Submission.save()` regenerate it on retry. submission.slug = "" submission.save() self.object = submission if submission.status == Submission.Status.IDENTIFYING: self._notify_guest(submission) else: messages.success( self.request, format_html( "Submission {} accepted. " "We'll start validating it shortly.", submission.slug, ), ) return super().form_valid(form) # ---- guest-path notice --------------------------------------------------- def _notify_guest(self, submission: Submission) -> None: """Send the confirmation email and surface success / failure to the user via the messages framework. Green on 2xx from Mailtrap, red on any transport failure (with a "sign in with Google instead" hint).""" sent = send_confirmation_email(submission) if sent: messages.success( self.request, format_html( "Submission {slug} created. " "We've sent a confirmation link to {email} — " "click it within 24 hours to add your print to the queue.", slug=submission.slug, email=submission.guest_email, ), ) else: messages.error( self.request, format_html( "Submission {slug} was saved, " "but we couldn't send the confirmation email to {email}. " "Try " "signing in with Google instead — it skips email " "confirmation entirely.", slug=submission.slug, email=submission.guest_email, login_url=reverse("account_login"), ), )