Skip to content

Have staff write assignment emails as plain text - #145

Merged
pancakereport merged 3 commits into
mainfrom
email-plain-text
Oct 22, 2026
Merged

pancakereport merged 3 commits into
mainfrom
email-plain-text

Conversation

@pancakereport

@pancakereport pancakereport commented Oct 22, 2026 •

Copy link
Copy Markdown

Summary

Closes #140. Staff write assignment emails as plain text, and never see or write HTML. As agreed on the issue, this is plain text with variables, not Markdown.

  • Writing the message: a blank line starts a new paragraph, line breaks are kept, and http(s):// links become clickable.
  • The default message: lists the course, exam, room, seat, start time and duration on their own lines (Room: {{ROOM}} and so on), so staff can see and edit each one.
  • Two versions of every email, both built from what staff wrote:
    • HTML version: rendered by a Jinja template kept in the repo.
    • Plain-text version: real text. Before, it was the raw HTML source.
    • No checkbox: the "HTML Body?" checkbox is gone, since there's nothing left to choose.
  • Subject first: it comes before the message, prefilled with the exam's name ("Your Exam Seating Assignment for Midterm 1").
  • Preview: shows the email the chosen student would get, filled in by the server exactly as it's sent. It shows both the HTML version and the plain-text version.
  • Variables: listed at the bottom of the page, each with what it's filled in with.
  • Typos are refused: a placeholder that isn't a variable ({{ROOOM}}) is refused before anything is sent. Before, it reached every student exactly as typed.

Safety

Staff text is only ever data, and is never run as a Jinja template. Running it as one would let anyone who can write an email run code on the server.

  • How text reaches the template: it's split into paragraphs, line breaks, links and filled-in values, and assignment_email.html escapes each piece as it outputs it. HTML typed by staff shows up as text.
  • Links: only http:// and https:// URLs become links.
  • Preview: shown in a sandboxed <iframe>, so nothing in it can run scripts or reach the page.

Changes

  • server/services/email/templates/:
    • __init__.py:
      • VARIABLES, one list that the page, the check and the substitution all use.
      • unknown_variables, which finds placeholders that aren't variables.
      • render_text and render_html, which build the two versions.
      • get_email, which now just returns the starting subject and body.
    • assignment_email.html: the HTML template: paragraphs, line breaks and links.
    • assignment_inform_email.txt: the default message, replacing the old .html one.
  • server/services/email/__init__.py:
    • assignment_values: the values of the variables for one student.
    • substitute_about_assignment: returns the subject, text and HTML. Every send path uses it: all students, one student, test emails and the batch endpoint.
  • server/forms.py: the body_html field is removed, and both subject and body are checked for unknown variables. The new EmailPreviewForm is the part of EmailForm the preview needs.
  • server/views.py:
    • New endpoint: email_preview (/students/email/preview/) returns a student's email as JSON.
    • Prefill: the subject is filled in with the exam's name.
    • Single-student page: now keeps the variables in the message instead of filling them in for that student. The preview shows that student, who is preselected.
  • server/templates/email.html:
    • Order: the subject, then the message with a short note on how it's formatted.
    • "Preview and send a test": one student picker shared by the preview and test emails.
    • Then: "Send to students", and the table of variables.
  • server/static/css/style.css: styles for the preview, the note and the table.

Tests

  • tests/unit/test_email_substitution.py: rewritten for plain text. It covers:
    • paragraphs and line breaks
    • escaping values and staff-typed HTML
    • staff text not being run as a template ({{ 7 * 7 }}, {% if %}, {{ config }})
    • which text becomes a link (trailing punctuation, parentheses, javascript:, quotes)
    • unknown variables
    • every email having a text and an HTML version
  • New tests/unit/test_email_page.py: the plain-text prefill, the subject before the message, the variables table, unknown variables refused by both the page and the batch endpoint, and the preview endpoint.
  • New tests/e2e/test_email_preview.py: in Chrome, Preview shows the filled-in email and its link, and a typo shows an error instead.
  • Updated for the removed checkbox: test_email.py, test_email_test_send.py, test_email_batches.py and test_email_copies.py.

Locally: unit 425, e2e 17, a11y 4 passing.

🤖 Generated with Claude Code

The HTML email is built from the plain text by a template in the repo,
which escapes every piece of it; staff text is never run as a template.
The subject comes first with the exam's name, the variables are listed
on the page, unknown variables are refused, and the preview shows the
email a student would get, filled in by the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚀 Staging Preview Deployed!

URL: https://preview-seating-pr-145-137456085712.us-west1.run.app

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.96907% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 80.01%. Comparing base (369643f) to head (332065d).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
server/services/email/templates/__init__.py 98.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #145      +/-   ##
==========================================
+ Coverage   79.64%   80.01%   +0.36%     
==========================================
  Files          31       31              
  Lines        2398     2442      +44     
==========================================
+ Hits         1910     1954      +44     
  Misses        488      488              
Flag Coverage Δ
e2e 33.87% <31.95%> (+0.17%) ⬆️
unit 80.01% <98.96%> (+0.36%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

pancakereport and others added 2 commits October 22, 2026 16:33
…eir own lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… period

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pancakereport
pancakereport marked this pull request as ready for review October 22, 2026 23:46
@pancakereport
pancakereport merged commit a6260f2 into main Oct 22, 2026
13 checks passed
@pancakereport
pancakereport deleted the email-plain-text branch October 22, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Email UX: Don't Have Staff Write HTML

1 participant