docs: post-attendance flow v2 implementation plan
4 small TDD tasks (~120 LOC): display rename, attendance 3-button branch, Site Journal save+absences button, docs. Reuses Round C next_action pattern. HARD STOP after Task 4 — local verification by Konrad before any push (UX change to a daily-use path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
110545b11e
commit
29c36bede7
686
docs/plans/2026-05-15-post-attendance-flow-v2-plan.md
Normal file
686
docs/plans/2026-05-15-post-attendance-flow-v2-plan.md
Normal file
@ -0,0 +1,686 @@
|
||||
# Post-Attendance Flow v2 — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
|
||||
> (in-session) to implement this plan task-by-task. **HARD STOP after
|
||||
> Task 4 — do NOT push to origin or deploy. Hand back to Konrad for
|
||||
> manual local verification first.**
|
||||
|
||||
**Goal:** Replace the forced post-attendance SiteReport redirect with
|
||||
three explicit buttons (Log Work → dashboard / + Site Journal / +
|
||||
Absences), add a "Save Site Journal + Add Absences" path on the
|
||||
journal page, and rename the user-facing "Site Report" → "Site
|
||||
Journal" (display-only).
|
||||
|
||||
**Architecture:** Pure flow + display change. Reuses the existing
|
||||
Round C `next_action` POST mechanism (`core/views.py::attendance_log`,
|
||||
commit `8c749f3`). Display-only rename is the Path-A pattern (UI text
|
||||
only; `SiteReport` model/view/URL unchanged — exactly like "New
|
||||
Loan"→"Loan"). No model, migration, URL, or dependency changes.
|
||||
|
||||
**Tech Stack:** Django 5.2.7, Python 3.13, SQLite (local) / MySQL
|
||||
(prod). Bootstrap 5 templates.
|
||||
|
||||
**Design doc:** `docs/plans/2026-05-15-post-attendance-flow-v2-design.md`
|
||||
(commit `110545b`).
|
||||
|
||||
**Branch:** `ai-dev`. Baseline: HEAD `110545b`, 173/173 tests passing.
|
||||
|
||||
**Test command (Windows, Git Bash):**
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
|
||||
```
|
||||
On cmd.exe: `set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests -v 2`
|
||||
|
||||
**Pre-reading for the implementer:**
|
||||
- `docs/plans/2026-05-15-post-attendance-flow-v2-design.md` — full design (4 sections, all approved by Konrad)
|
||||
- `CLAUDE.md` — "UI-vs-DB naming drift" section (the Path-A display-rename pattern this follows) + "Django template comments {# #} SINGLE-LINE only" gotcha + the grep sanity-check one-liner
|
||||
- `core/views.py::attendance_log` lines ~744-781 — the Round C `next_action` branch being modified
|
||||
- `core/views.py::site_report_edit` lines ~867-885 — the POST save branch being modified
|
||||
|
||||
**CRITICAL — template comment gotcha:** Any multi-line `{# ... #}`
|
||||
renders as literal text on the page (silent, no error). After ANY
|
||||
template edit run:
|
||||
```
|
||||
grep -rn "^\s*{#" core/templates/ | awk -F: '$0 !~ /#}/ {print}'
|
||||
```
|
||||
Every match is a broken multi-line comment — fix to `{% comment %}…{% endcomment %}`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1 — Vocabulary rename: "Site Report" → "Site Journal" (display-only)
|
||||
|
||||
**Goal:** Every user-facing "Site Report" / "Log Today's Work" string
|
||||
reads "Site Journal". Zero code-identifier changes.
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/templates/core/site_report_edit.html` (h1, title, save button)
|
||||
- Modify: `core/templates/core/site_report_detail.html` (heading, title)
|
||||
- Modify: `core/templates/core/work_history.html` (clipboard tooltip/title text)
|
||||
- Modify: `core/views.py::site_report_edit` (success-toast string only)
|
||||
- Modify: `core/tests.py` (one render-assertion test)
|
||||
|
||||
### Step 1 — Write the failing test
|
||||
|
||||
Append to `core/tests.py` (end of file):
|
||||
|
||||
```python
|
||||
class SiteJournalRenameTests(TestCase):
|
||||
"""Display-only rename: the SiteReport edit/detail pages must show
|
||||
'Site Journal' to users. Model/view/URL identifiers are unchanged
|
||||
(Path-A pattern, like 'New Loan' displaying as 'Loan')."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.admin = User.objects.create_user(
|
||||
username='admin', password='pw', is_staff=True, is_superuser=True,
|
||||
)
|
||||
cls.project = Project.objects.create(name='Solar Farm Alpha')
|
||||
cls.worker = Worker.objects.create(
|
||||
name='W', id_number='1', monthly_salary=Decimal('6000'),
|
||||
)
|
||||
cls.log = WorkLog.objects.create(
|
||||
date=_date(2026, 5, 22), project=cls.project, supervisor=cls.admin,
|
||||
)
|
||||
cls.log.workers.add(cls.worker)
|
||||
|
||||
def setUp(self):
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
def test_edit_page_shows_site_journal_not_site_report(self):
|
||||
resp = self.client.get(f'/site-report/{self.log.id}/edit/')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'Site Journal')
|
||||
# The old user-facing strings must be gone from the rendered page
|
||||
self.assertNotContains(resp, "Log Today's Work")
|
||||
self.assertNotContains(resp, 'Save Site Report')
|
||||
```
|
||||
|
||||
### Step 2 — Run it, confirm it fails
|
||||
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.SiteJournalRenameTests -v 2
|
||||
```
|
||||
Expected: FAIL — page still says "Log Today's Work" / "Save Site Report".
|
||||
|
||||
### Step 3 — Apply the rename
|
||||
|
||||
In `core/templates/core/site_report_edit.html`:
|
||||
- `{% block title %}` — any "Site Report"/"Log Today's Work" → "Site Journal"
|
||||
- `<h1>` "Log Today's Work" → "Site Journal"
|
||||
- Save button: "Save Site Report" → "Save Site Journal"; if the
|
||||
template has `{% if is_creating %}Save Site Report{% else %}Update{% endif %}`,
|
||||
make it `{% if is_creating %}Save Site Journal{% else %}Update Site Journal{% endif %}`
|
||||
|
||||
In `core/templates/core/site_report_detail.html`:
|
||||
- Heading + `{% block title %}` "Site Report" → "Site Journal"
|
||||
|
||||
In `core/templates/core/work_history.html`:
|
||||
- The clipboard-icon link `title=` / tooltip text referencing "site
|
||||
report" → "site journal". Grep first: `grep -n "site report\|Site Report\|site-report" core/templates/core/work_history.html`
|
||||
(only change visible TEXT — leave `{% url 'site_report_edit' %}` and
|
||||
`{% url 'site_report_detail' %}` URL tags untouched).
|
||||
|
||||
In `core/views.py::site_report_edit` (around line 883), the toast:
|
||||
```python
|
||||
messages.success(
|
||||
request,
|
||||
f"Site journal saved for {work_log.project.name} on {work_log.date:%d %b %Y}.",
|
||||
)
|
||||
```
|
||||
|
||||
**Do NOT touch:** `SiteReport`, `site_report_edit`, `site_report_detail`,
|
||||
`/site-report/…` URLs, `work_log.site_report`, `SiteReportForm`,
|
||||
`core/site_report_schema.py`, the `{% url %}` tags.
|
||||
|
||||
### Step 4 — Template comment sanity check
|
||||
|
||||
```
|
||||
grep -rn "^\s*{#" core/templates/ | awk -F: '$0 !~ /#}/ {print}'
|
||||
```
|
||||
Expected: no output (or only pre-existing known-good — there should be none after the prior fixes).
|
||||
|
||||
### Step 5 — Run tests, confirm pass
|
||||
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.SiteJournalRenameTests -v 2
|
||||
```
|
||||
Expected: PASS.
|
||||
|
||||
Full suite:
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
|
||||
```
|
||||
Expected: 174 tests, OK (173 + 1 new). **If any EXISTING test fails
|
||||
because it asserted "Site Report" text, update that test's assertion
|
||||
to "Site Journal" — that string contract is intentionally changing.**
|
||||
|
||||
### Step 6 — Commit
|
||||
|
||||
```bash
|
||||
git add core/templates/core/site_report_edit.html core/templates/core/site_report_detail.html core/templates/core/work_history.html core/views.py core/tests.py
|
||||
git commit -m "rename(ui): 'Site Report' → 'Site Journal' (display-only, Path-A)
|
||||
|
||||
User-facing text only. SiteReport model / site_report_* views /
|
||||
/site-report/ URLs unchanged — same pattern as New Loan→Loan.
|
||||
Frees the word 'Journal' for the parked voice-transcript feature
|
||||
(which will be 'Voice Notes'). 1 render test."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2 — Attendance form: 3-way next_action branch + new button
|
||||
|
||||
**Goal:** Plain "Log Work" → dashboard. New "Log Work + Site Journal"
|
||||
→ site journal form. "Log Work + Add Absences" → unchanged. The 3
|
||||
buttons have clear primary/secondary visual hierarchy.
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/views.py::attendance_log` (lines ~754-781 — the Round C branch)
|
||||
- Modify: `core/templates/core/attendance_log.html` (lines ~174-182 — submit buttons)
|
||||
- Modify: `core/tests.py` (redirect-behaviour tests)
|
||||
|
||||
### Step 1 — Write the failing tests
|
||||
|
||||
Append to `core/tests.py`:
|
||||
|
||||
```python
|
||||
class PostAttendanceFlowV2Tests(TestCase):
|
||||
"""Flow v2: 'Log Work' → dashboard (was: forced site-report
|
||||
redirect). 'log_journal' → site journal form. 'log_absences'
|
||||
unchanged."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.admin = User.objects.create_user(
|
||||
username='admin', password='pw', is_staff=True, is_superuser=True,
|
||||
)
|
||||
cls.project = Project.objects.create(name='Solar Farm Alpha')
|
||||
cls.team = Team.objects.create(name='Team A', supervisor=cls.admin)
|
||||
cls.worker = Worker.objects.create(
|
||||
name='W', id_number='1', monthly_salary=Decimal('6000'),
|
||||
)
|
||||
cls.team.workers.add(cls.worker)
|
||||
|
||||
def setUp(self):
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
def _post(self, next_action):
|
||||
return self.client.post('/attendance/log/', data={
|
||||
'date': '2026-05-22',
|
||||
'project': self.project.id,
|
||||
'team': self.team.id,
|
||||
'workers': [self.worker.id],
|
||||
'next_action': next_action,
|
||||
})
|
||||
|
||||
def test_log_only_redirects_to_dashboard(self):
|
||||
"""REVERSAL of Phase A.1: plain Log Work now lands on the
|
||||
dashboard, NOT the site-report form."""
|
||||
resp = self._post('log_only')
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.url, '/')
|
||||
self.assertNotIn('/site-report/', resp.url)
|
||||
|
||||
def test_missing_next_action_defaults_to_dashboard(self):
|
||||
"""No next_action (e.g. old bookmark / form without the field)
|
||||
→ dashboard, same as log_only."""
|
||||
resp = self.client.post('/attendance/log/', data={
|
||||
'date': '2026-05-22',
|
||||
'project': self.project.id,
|
||||
'team': self.team.id,
|
||||
'workers': [self.worker.id],
|
||||
})
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.url, '/')
|
||||
|
||||
def test_log_journal_redirects_to_site_report_edit(self):
|
||||
resp = self._post('log_journal')
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertIn('/site-report/', resp.url)
|
||||
self.assertTrue(resp.url.endswith('/edit/'))
|
||||
|
||||
def test_log_absences_still_redirects_to_absences_prefilled(self):
|
||||
"""Round C path unchanged."""
|
||||
resp = self._post('log_absences')
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertIn('/absences/log/', resp.url)
|
||||
self.assertIn('date=2026-05-22', resp.url)
|
||||
self.assertIn(f'team={self.team.id}', resp.url)
|
||||
|
||||
def test_log_journal_survives_conflict_resolution(self):
|
||||
"""The conflict re-render carries next_action through the
|
||||
form.data.items loop (Round C). log_journal must survive it."""
|
||||
# Pre-create a conflicting WorkLog for the same worker+date
|
||||
clash = WorkLog.objects.create(
|
||||
date=_date(2026, 5, 22), project=self.project, supervisor=self.admin,
|
||||
)
|
||||
clash.workers.add(self.worker)
|
||||
# First POST → conflict screen (200, not redirect)
|
||||
resp = self._post('log_journal')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'name="next_action"')
|
||||
self.assertContains(resp, 'value="log_journal"')
|
||||
# Resolve via overwrite, carrying next_action
|
||||
resp2 = self.client.post('/attendance/log/', data={
|
||||
'date': '2026-05-22',
|
||||
'project': self.project.id,
|
||||
'team': self.team.id,
|
||||
'workers': [self.worker.id],
|
||||
'next_action': 'log_journal',
|
||||
'conflict_action': 'overwrite',
|
||||
})
|
||||
self.assertEqual(resp2.status_code, 302)
|
||||
self.assertIn('/site-report/', resp2.url)
|
||||
```
|
||||
|
||||
NOTE: `conflict_action='overwrite'` is the value the existing
|
||||
`attendance_log` conflict handler accepts (verified in the Round C
|
||||
work). If the test fails on an unknown conflict_action, grep
|
||||
`core/views.py` + `core/templates/core/attendance_log.html` for
|
||||
`conflict_action` to find the accepted values; the goal is just to
|
||||
prove the redirect survives the round-trip.
|
||||
|
||||
### Step 2 — Run, confirm failures
|
||||
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.PostAttendanceFlowV2Tests -v 2
|
||||
```
|
||||
Expected: `test_log_only_redirects_to_dashboard` and
|
||||
`test_missing_next_action_defaults_to_dashboard` FAIL (they currently
|
||||
redirect to /site-report/…); `test_log_journal_*` FAIL (no such
|
||||
branch yet).
|
||||
|
||||
### Step 3 — Rewrite the next_action branch in `attendance_log`
|
||||
|
||||
In `core/views.py`, replace the block from `next_action = request.POST.get('next_action', 'log_only')`
|
||||
down to (and including) the final `return redirect('home')` (~lines
|
||||
754-781) with:
|
||||
|
||||
```python
|
||||
# === ROUND C + FLOW v2: pick post-submit destination ===
|
||||
# The attendance form has THREE submit buttons, all named
|
||||
# `next_action`. Whichever the user clicked lands here:
|
||||
# - 'log_only' (default/missing) → dashboard (just done)
|
||||
# - 'log_journal' → Site Journal form
|
||||
# - 'log_absences' → /absences/log/ prefilled
|
||||
# Flow v2 (15 May 2026) REVERSED the old default: plain
|
||||
# "Log Work" no longer force-redirects into the journal —
|
||||
# that's now an explicit opt-in button. See
|
||||
# docs/plans/2026-05-15-post-attendance-flow-v2-design.md.
|
||||
next_action = request.POST.get('next_action', 'log_only')
|
||||
|
||||
if next_action == 'log_absences' and created_log_ids:
|
||||
# Pre-fill the absence form with the same date / team /
|
||||
# project just used (LAST log's date for date ranges).
|
||||
from urllib.parse import urlencode
|
||||
params = {'date': dates_to_log[-1].isoformat()}
|
||||
if team:
|
||||
params['team'] = team.id
|
||||
if project:
|
||||
params['project'] = project.id
|
||||
return redirect(f"{reverse('absence_log')}?{urlencode(params)}")
|
||||
|
||||
if next_action == 'log_journal' and created_log_ids:
|
||||
# Explicit opt-in: go to the Site Journal form for the
|
||||
# LAST created log (most recent date in a range).
|
||||
return redirect('site_report_edit', work_log_id=created_log_ids[-1])
|
||||
|
||||
# Default ('log_only', missing, unrecognised, or no logs
|
||||
# created because everything conflicted): straight to the
|
||||
# dashboard. The green "work log(s) created" toast already
|
||||
# confirms the save.
|
||||
return redirect('home')
|
||||
```
|
||||
|
||||
### Step 4 — Add the 3rd button + hierarchy in the template
|
||||
|
||||
In `core/templates/core/attendance_log.html`, the submit block
|
||||
(~lines 174-182) currently:
|
||||
```html
|
||||
<div class="d-grid gap-2 mt-5">
|
||||
<button type="submit" name="next_action" value="log_only" class="btn btn-lg btn-accent">
|
||||
<i class="fas fa-save me-2"></i>Log Work
|
||||
</button>
|
||||
<button type="submit" name="next_action" value="log_absences" class="btn btn-lg btn-outline-secondary"
|
||||
title="...">
|
||||
<i class="fas fa-user-clock me-2"></i>Log Work + Add Absences
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
Replace with (primary first, two secondary opt-ins, Konrad's order —
|
||||
Site Journal between Log Work and Absences):
|
||||
```html
|
||||
<div class="d-grid gap-2 mt-5">
|
||||
{% comment %}
|
||||
Flow v2: primary "Log Work" (just save → dashboard) anchors the
|
||||
eye. The two "+ ..." buttons are secondary opt-in extras. All
|
||||
three submit the SAME form; the next_action value routes the
|
||||
redirect in the view.
|
||||
{% endcomment %}
|
||||
<button type="submit" name="next_action" value="log_only" class="btn btn-lg btn-accent">
|
||||
<i class="fas fa-save me-2"></i>Log Work
|
||||
</button>
|
||||
<button type="submit" name="next_action" value="log_journal" class="btn btn-outline-secondary"
|
||||
title="Save this work log, then open the Site Journal form to record weather / progress for the same day.">
|
||||
<i class="fas fa-clipboard-check me-2"></i>Log Work + Site Journal
|
||||
</button>
|
||||
<button type="submit" name="next_action" value="log_absences" class="btn btn-outline-secondary"
|
||||
title="Save this work log, then jump to the absence form pre-filled with the same date / team / project.">
|
||||
<i class="fas fa-user-clock me-2"></i>Log Work + Add Absences
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
(Note: secondary buttons drop `btn-lg` so they read visually lighter
|
||||
than the primary — that's the hierarchy. Keep `btn-lg` only on the
|
||||
primary "Log Work".)
|
||||
|
||||
### Step 5 — Template comment sanity check
|
||||
|
||||
```
|
||||
grep -rn "^\s*{#" core/templates/ | awk -F: '$0 !~ /#}/ {print}'
|
||||
```
|
||||
Expected: no output.
|
||||
|
||||
### Step 6 — Run tests, confirm pass
|
||||
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.PostAttendanceFlowV2Tests -v 2
|
||||
```
|
||||
Expected: 5/5 PASS.
|
||||
|
||||
Full suite:
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
|
||||
```
|
||||
Expected: ~179 OK. **Any existing test that asserted attendance
|
||||
submit → /site-report/ by default MUST be updated** — that's the
|
||||
intentional reversal. Find them: `grep -n "site-report\|site_report_edit" core/tests.py`
|
||||
and fix assertions in non-flow-v2 tests that relied on the old
|
||||
forced redirect.
|
||||
|
||||
### Step 7 — Commit
|
||||
|
||||
```bash
|
||||
git add core/views.py core/templates/core/attendance_log.html core/tests.py
|
||||
git commit -m "feat(flow): attendance 3-button flow — Log Work → dashboard
|
||||
|
||||
Reverses Phase A.1's forced post-attendance SiteReport redirect.
|
||||
Plain 'Log Work' now lands on the dashboard. New 'Log Work + Site
|
||||
Journal' button (next_action=log_journal) opens the journal form
|
||||
explicitly. 'Log Work + Add Absences' unchanged. Primary/secondary
|
||||
button hierarchy so 3 buttons don't feel like a wall. 5 tests
|
||||
incl. conflict-path carry-through."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3 — Site Journal page: "Save + Add Absences" button
|
||||
|
||||
**Goal:** The Site Journal form gets a second submit button that
|
||||
saves the journal then jumps to the prefilled absence form.
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/views.py::site_report_edit` (POST success branch, ~line 880-885)
|
||||
- Modify: `core/templates/core/site_report_edit.html` (action row)
|
||||
- Modify: `core/tests.py`
|
||||
|
||||
### Step 1 — Write the failing tests
|
||||
|
||||
Append to `core/tests.py` (inside or after `PostAttendanceFlowV2Tests`
|
||||
— a new class is cleaner):
|
||||
|
||||
```python
|
||||
class SiteJournalSaveAbsencesTests(TestCase):
|
||||
"""The Site Journal form's 'Save + Add Absences' button saves the
|
||||
report then redirects to /absences/log/ prefilled from the
|
||||
WorkLog. Default save (no next_action) → dashboard."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.admin = User.objects.create_user(
|
||||
username='admin', password='pw', is_staff=True, is_superuser=True,
|
||||
)
|
||||
cls.project = Project.objects.create(name='Solar Farm Alpha')
|
||||
cls.team = Team.objects.create(name='Team A', supervisor=cls.admin)
|
||||
cls.worker = Worker.objects.create(
|
||||
name='W', id_number='1', monthly_salary=Decimal('6000'),
|
||||
)
|
||||
cls.team.workers.add(cls.worker)
|
||||
cls.log = WorkLog.objects.create(
|
||||
date=_date(2026, 5, 22), project=cls.project,
|
||||
team=cls.team, supervisor=cls.admin,
|
||||
)
|
||||
cls.log.workers.add(cls.worker)
|
||||
|
||||
def setUp(self):
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
def test_default_save_redirects_home(self):
|
||||
resp = self.client.post(f'/site-report/{self.log.id}/edit/', data={
|
||||
'weather': 'sunny',
|
||||
# no next_action
|
||||
})
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.url, '/')
|
||||
|
||||
def test_save_absences_redirects_to_prefilled_absence_form(self):
|
||||
resp = self.client.post(f'/site-report/{self.log.id}/edit/', data={
|
||||
'weather': 'sunny',
|
||||
'next_action': 'save_absences',
|
||||
})
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertIn('/absences/log/', resp.url)
|
||||
self.assertIn('date=2026-05-22', resp.url)
|
||||
self.assertIn(f'team={self.team.id}', resp.url)
|
||||
self.assertIn(f'project={self.project.id}', resp.url)
|
||||
# The report was actually saved
|
||||
self.log.refresh_from_db()
|
||||
self.assertTrue(hasattr(self.log, 'site_report'))
|
||||
```
|
||||
|
||||
### Step 2 — Run, confirm failure
|
||||
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.SiteJournalSaveAbsencesTests -v 2
|
||||
```
|
||||
Expected: `test_save_absences_*` FAILS (no such branch — it currently
|
||||
always redirects home).
|
||||
|
||||
### Step 3 — Add the next_action branch in `site_report_edit`
|
||||
|
||||
In `core/views.py::site_report_edit`, the POST success block currently
|
||||
(~lines 878-885):
|
||||
```python
|
||||
if instance.pk is None:
|
||||
instance.created_by = request.user
|
||||
instance.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Site journal saved for {work_log.project.name} on {work_log.date:%d %b %Y}.",
|
||||
)
|
||||
return redirect('home')
|
||||
```
|
||||
|
||||
Replace the `return redirect('home')` with:
|
||||
```python
|
||||
# Flow v2: a second submit button ("Save Site Journal +
|
||||
# Add Absences") carries next_action=save_absences. Mirror
|
||||
# the attendance form's absence-prefill exactly — same
|
||||
# query-string shape from the WorkLog we already have
|
||||
# select_related'd.
|
||||
if request.POST.get('next_action') == 'save_absences':
|
||||
from urllib.parse import urlencode
|
||||
params = {'date': work_log.date.isoformat()}
|
||||
if work_log.team_id:
|
||||
params['team'] = work_log.team_id
|
||||
if work_log.project_id:
|
||||
params['project'] = work_log.project_id
|
||||
return redirect(f"{reverse('absence_log')}?{urlencode(params)}")
|
||||
return redirect('home')
|
||||
```
|
||||
|
||||
(`reverse` is already imported in `core/views.py`. Confirm with
|
||||
`grep -n "from django.urls import" core/views.py`.)
|
||||
|
||||
### Step 4 — Add the button in the template
|
||||
|
||||
In `core/templates/core/site_report_edit.html`, the action row
|
||||
(~lines 168-176) currently:
|
||||
```html
|
||||
<div class="d-flex justify-content-between gap-2">
|
||||
<a href="{% url 'home' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i> Skip
|
||||
</a>
|
||||
<button type="submit" class="btn btn-accent">
|
||||
<i class="fas fa-check me-1"></i>
|
||||
{% if is_creating %}Save Site Journal{% else %}Update Site Journal{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
(The button text already says "Site Journal" after Task 1.)
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<div class="d-flex flex-wrap justify-content-between gap-2">
|
||||
<a href="{% url 'home' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i> Skip
|
||||
</a>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button type="submit" name="next_action" value="save_absences" class="btn btn-outline-secondary"
|
||||
title="Save the site journal, then jump to the absence form pre-filled with the same date / team / project.">
|
||||
<i class="fas fa-user-clock me-1"></i> Save + Add Absences
|
||||
</button>
|
||||
<button type="submit" name="next_action" value="save_only" class="btn btn-accent">
|
||||
<i class="fas fa-check me-1"></i>
|
||||
{% if is_creating %}Save Site Journal{% else %}Update Site Journal{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
(The primary save button gets an explicit `value="save_only"` so the
|
||||
two submit buttons are unambiguous; the view treats anything that
|
||||
isn't `save_absences` as "go home", so `save_only`/missing both work.)
|
||||
|
||||
### Step 5 — Template comment sanity check + run tests
|
||||
|
||||
```
|
||||
grep -rn "^\s*{#" core/templates/ | awk -F: '$0 !~ /#}/ {print}'
|
||||
```
|
||||
Expected: no output.
|
||||
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.SiteJournalSaveAbsencesTests -v 2
|
||||
```
|
||||
Expected: 2/2 PASS.
|
||||
|
||||
Full suite:
|
||||
```
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
|
||||
```
|
||||
Expected: ~181 OK.
|
||||
|
||||
### Step 6 — Commit
|
||||
|
||||
```bash
|
||||
git add core/views.py core/templates/core/site_report_edit.html core/tests.py
|
||||
git commit -m "feat(flow): Site Journal 'Save + Add Absences' button
|
||||
|
||||
site_report_edit POST now reads next_action — 'save_absences'
|
||||
saves the journal then redirects to /absences/log/ prefilled with
|
||||
the WorkLog's date/team/project (mirrors the attendance form's
|
||||
absence-prefill exactly). Default save still → dashboard. 2 tests."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — Docs: CLAUDE.md + parked-work.md
|
||||
|
||||
**Goal:** Record the Site Journal display-rename as a Path-A entry,
|
||||
and reserve "Voice Notes" for the parked voice feature so it doesn't
|
||||
collide with "Journal".
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md` (SiteReport Key-Models line + a Path-A note)
|
||||
- Modify: `docs/plans/parked-work.md` (Backburner section naming note)
|
||||
|
||||
### Step 1 — CLAUDE.md
|
||||
|
||||
Find the `**SiteReport**` bullet in the "Key Models" section. Append
|
||||
to it:
|
||||
> **UI label is "Site Journal"** (display-only rename, 15 May 2026 —
|
||||
> Path-A pattern: model/view/URL stay `SiteReport`/`site_report_*`/
|
||||
> `/site-report/`, only user-facing text says "Site Journal". Same
|
||||
> rationale as "New Loan"→"Loan".)
|
||||
|
||||
Find the "What's mid-flight" backburner bullet (top of file). Add a
|
||||
sentence:
|
||||
> The parked voice-transcript feature must NOT be called "Journal"
|
||||
> (that name now belongs to the structured site-progress form) —
|
||||
> use "Voice Notes".
|
||||
|
||||
### Step 2 — parked-work.md
|
||||
|
||||
In the "🧊 Backburner" section, in the Phase A.2 paragraph, add:
|
||||
> **Naming reservation:** when this is eventually built, it must be
|
||||
> called **"Voice Notes"** (or similar) — NOT "Journal". As of
|
||||
> 15 May 2026 "Site Journal" is the user-facing name of the
|
||||
> structured site-progress form (the `SiteReport` model). Two
|
||||
> "Journal"s would collide.
|
||||
|
||||
### Step 3 — Commit
|
||||
|
||||
```bash
|
||||
git add CLAUDE.md docs/plans/parked-work.md
|
||||
git commit -m "docs: record Site Journal rename (Path-A) + reserve 'Voice Notes'
|
||||
|
||||
CLAUDE.md SiteReport line notes the display-only UI rename.
|
||||
parked-work.md reserves 'Voice Notes' for the future voice
|
||||
feature so it never collides with 'Site Journal'."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛑 HARD STOP — hand back to Konrad
|
||||
|
||||
**After Task 4: DO NOT PUSH. DO NOT DEPLOY.**
|
||||
|
||||
Report to Konrad:
|
||||
- All tasks committed locally on `ai-dev` (list the commit SHAs)
|
||||
- Full suite count (expect ~181 green)
|
||||
- The exact manual verification steps from the design doc §
|
||||
"Verification (manual, local — Konrad)":
|
||||
1. `/attendance/log/` → **Log Work** → dashboard + toast (NOT journal)
|
||||
2. → **Log Work + Site Journal** → Site Journal form (titled "Site Journal")
|
||||
3. → **Log Work + Add Absences** → `/absences/log/` prefilled
|
||||
4. Site Journal form → **Save Site Journal** → dashboard
|
||||
5. → **Save + Add Absences** → `/absences/log/` prefilled
|
||||
6. Trigger an attendance conflict with "Log Work + Site Journal" clicked → resolve → still lands on Site Journal form
|
||||
- Remind Konrad: nothing reaches production until he runs these
|
||||
locally and explicitly says "push it".
|
||||
|
||||
Only after Konrad's explicit approval: push `ai-dev`, then the
|
||||
standard deploy (pull on VM → restart; **no migrations, no
|
||||
collectstatic** — pure template/view change; remember the
|
||||
restart-AFTER-pull ordering rule from CLAUDE.md).
|
||||
|
||||
---
|
||||
|
||||
## Notes for the implementer
|
||||
|
||||
- This is ~120 LOC across 4 tiny tasks. Each task is independently
|
||||
committable and TDD-driven.
|
||||
- The `next_action` mechanism is proven (Round C, commit `8c749f3`).
|
||||
You are extending it, not inventing it.
|
||||
- Display-rename is Path-A: **never** rename code identifiers
|
||||
(`SiteReport`, `site_report_edit`, the URLs). Only template text +
|
||||
the one toast string.
|
||||
- After EVERY template edit, run the `{#`-comment grep. This gotcha
|
||||
has bitten the project 16+ times.
|
||||
- If an existing test breaks because it asserted the OLD forced-
|
||||
redirect or the OLD "Site Report" text — that's expected. Update
|
||||
the assertion; the contract is intentionally changing. Do NOT
|
||||
weaken a flow-v2 test to accommodate a stale one.
|
||||
- No model, migration, URL, or dependency changes anywhere in this
|
||||
plan. If you find yourself writing a migration, stop — you've gone
|
||||
off-plan.
|
||||
Loading…
x
Reference in New Issue
Block a user