Add detail page
This commit is contained in:
@@ -8,8 +8,8 @@ urlpatterns = [
|
||||
path("", views.IndexView.as_view(), name="index"),
|
||||
path("my-prints/", views.MyPrintsView.as_view(), name="my_prints"),
|
||||
path("p/<slug:slug>/confirm/<str:token>/", views.ConfirmEmailView.as_view(), name="confirm"),
|
||||
path("p/<slug:slug>/", views.SubmissionDetailView.as_view(), name="detail"),
|
||||
# Routes to be added as features land (see plan.md Section 7):
|
||||
# path("p/<slug:slug>/", views.SubmissionDetailView.as_view(), name="detail"),
|
||||
# path("p/<slug:slug>/status/", views.SubmissionStatusFragment.as_view(), name="status"),
|
||||
# path("p/<slug:slug>/resend/", views.ResendConfirmationView.as_view(), name="resend"),
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.db.models import Count, Q
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.html import format_html
|
||||
from django.views import View
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
||||
from apps.submissions.models import Submission, VerifiedEmail
|
||||
|
||||
@@ -60,6 +60,50 @@ class IndexView(ListView):
|
||||
return ctx
|
||||
|
||||
|
||||
class SubmissionDetailView(DetailView):
|
||||
"""Per-submission detail page at `/p/<slug>/` (plan.md §7.4 step 7).
|
||||
|
||||
Two access tiers, gated on whether the viewer owns the row:
|
||||
|
||||
- **Anyone with the slug** sees a minimal card -- slug, status badge,
|
||||
age. That's it. Keeps the URL safe to share, gives anonymous /
|
||||
non-owner visitors enough to confirm they're looking at the right
|
||||
submission without leaking source URLs, uploaded filenames,
|
||||
operator notes, or the submitter's notes-to-operator.
|
||||
- **Logged-in owner only** (`submission.submitted_by == request.user`)
|
||||
sees the full demo/detail-completed.html layout: status banner,
|
||||
source card, operator notes, the user's own notes_for_op, details
|
||||
sidebar with submitter / created / closed timestamps.
|
||||
|
||||
Guests never see the owner view -- they aren't logged in by
|
||||
definition, so condition (1) fails. They can still navigate to /p/X/
|
||||
via the URL we email them, they just get the minimal card. Once
|
||||
operator admin actions get wired, we'll grow a `?token=` fast-path
|
||||
for guests to view their own row, but that's plan.md §7.6 territory.
|
||||
"""
|
||||
|
||||
model = Submission
|
||||
template_name = "dashboard/detail.html"
|
||||
context_object_name = "submission"
|
||||
slug_url_kwarg = "slug"
|
||||
slug_field = "slug"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_related(
|
||||
"submitted_by", "requested_filament", "closed_by"
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
sub: Submission = self.object
|
||||
ctx["is_owner"] = (
|
||||
self.request.user.is_authenticated
|
||||
and sub.submitted_by_id is not None
|
||||
and sub.submitted_by_id == self.request.user.id
|
||||
)
|
||||
return ctx
|
||||
|
||||
|
||||
class ConfirmEmailView(View):
|
||||
"""Email-confirmation landing page (plan.md §7.4 step 8).
|
||||
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
"""Outgoing email -- plan.md §7 (state-transition side effects).
|
||||
|
||||
Two public functions:
|
||||
Three public functions, one per dedicated template:
|
||||
|
||||
send_confirmation_email(submission)
|
||||
Guest path: token-link emailed immediately after `SubmitView` creates
|
||||
an `identifying` row. The user must click within 24 h (plan.md §7.6)
|
||||
or the row is cleaned up.
|
||||
or the row is cleaned up. Template: `emails/confirmation.*`.
|
||||
|
||||
send_status_update_email(submission, *, previous_status=None)
|
||||
Generic notifier for any state transition the user should know about
|
||||
(queued, rejected, completed, failed). Callers pick when to fire it
|
||||
-- typically operator admin actions and the validation worker.
|
||||
send_verifying_email(submission)
|
||||
Fired by the `process_submissions` worker on the `processing ->
|
||||
verifying` success branch: "auto-checks cleared, awaiting operator
|
||||
review". Template: `emails/verifying.*`.
|
||||
|
||||
Both delegate to Django's email machinery. The backend is wired in
|
||||
send_rejection_email(submission, *, previous_status=None)
|
||||
Always fired on any transition into `status = rejected`, whether
|
||||
auto (validator failure from `processing`) or operator-driven (admin
|
||||
"Reject" action from `verifying`). The body renders
|
||||
`submission.operator_notes` verbatim so the user sees the same
|
||||
rejection reason in the email as on the public detail page.
|
||||
Template: `emails/rejected.*`.
|
||||
|
||||
All three delegate to Django's email machinery. The backend is wired in
|
||||
`hamprint/settings/base.py`: Mailtrap via `django-anymail` when
|
||||
`MAILTRAP_API_TOKEN` is present, console when not. Send failures are caught
|
||||
+ logged so a flaky transport never blocks the submission flow.
|
||||
@@ -104,19 +112,30 @@ def send_confirmation_email(sub: Submission) -> bool:
|
||||
return _send("confirmation", sub, {"confirm_url": confirm_url})
|
||||
|
||||
|
||||
def send_status_update_email(
|
||||
def send_rejection_email(
|
||||
sub: Submission, *, previous_status: str | None = None
|
||||
) -> bool:
|
||||
"""Notify the submitter that their submission moved to a new state.
|
||||
"""Notify the submitter that their submission was rejected.
|
||||
|
||||
Pass `previous_status` so the email can render "was X, now Y" when
|
||||
useful; omit it for first-time-ever notifications.
|
||||
Always fired on any transition into `status = rejected`:
|
||||
- automatic rejection from `processing` (URL/STL validation failure),
|
||||
- operator rejection from `verifying` (admin "Reject" action).
|
||||
|
||||
The email body renders `submission.operator_notes` verbatim -- that's
|
||||
the same string the auto-validator writes ("Automatic rejection: …")
|
||||
or that an operator types when clicking "Reject" in admin, so the
|
||||
user sees one consistent reason across the email + the public detail
|
||||
page.
|
||||
|
||||
`previous_status` is the state we left to land in `rejected`. Useful
|
||||
so the email can subtly distinguish "rejected before a human even
|
||||
looked" (from `processing`) vs. "rejected after operator review"
|
||||
(from `verifying`); both render with the same template.
|
||||
"""
|
||||
detail_url = f"{settings.SITE_URL}/p/{sub.slug}/"
|
||||
return _send(
|
||||
"status_update",
|
||||
"rejected",
|
||||
sub,
|
||||
{"detail_url": detail_url, "previous_status": previous_status},
|
||||
{"previous_status": previous_status},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.submissions.emails import send_status_update_email, send_verifying_email
|
||||
from apps.submissions.emails import send_rejection_email, send_verifying_email
|
||||
from apps.submissions.models import Submission
|
||||
from apps.submissions.validation import (
|
||||
ValidationError,
|
||||
@@ -71,7 +71,7 @@ class Command(BaseCommand):
|
||||
# closed_by stays NULL -- the validator did the rejecting,
|
||||
# not an operator (plan.md §5 / §7.3).
|
||||
sub.save()
|
||||
send_status_update_email(sub, previous_status="processing")
|
||||
send_rejection_email(sub, previous_status="processing")
|
||||
self.stdout.write(f"rejected {sub.slug}: {exc}")
|
||||
else:
|
||||
sub.status = Submission.Status.VERIFYING
|
||||
|
||||
Reference in New Issue
Block a user