Skip to content

ENG-3300: Add label column to MessagingTemplate for multi-template support#7900

Open
JadeCara wants to merge 18 commits intomainfrom
ENG-3300/multi-template-support
Open

ENG-3300: Add label column to MessagingTemplate for multi-template support#7900
JadeCara wants to merge 18 commits intomainfrom
ENG-3300/multi-template-support

Conversation

@JadeCara
Copy link
Copy Markdown
Contributor

@JadeCara JadeCara commented Apr 10, 2026

Ticket ENG-3300

Description Of Changes

Enables multiple named templates per MessagingActionType by adding a label column to the MessagingTemplate model. This is the backend foundation for the multi-template selector UI (correspondence templates like "Request for ID", "Status Update", "Case Closure").

Key decisions:

  • label is NOT NULL with a UniqueConstraint(type, label) — no two templates of the same type can share a label
  • Column has default="Default" so existing code paths that create templates without a label still work
  • Migration backfills existing rows from DEFAULT_MESSAGING_TEMPLATES dict labels, with title-case fallback for non-default types and dedup suffixing for duplicates
  • Request schemas default label to "Default" for backward compatibility — existing FE code continues to work without changes
  • New get_templates_by_type() query returns all templates for a given type (vs existing _basic_messaging_template_by_type which returns only one)
  • Duplicate label validation at the application layer gives clear 400 errors instead of raw IntegrityError

Dependency: Part of RMP-142 (Data Subject Correspondence). Requires ENG-3299 to merge first for the CORRESPONDENCE entry in DEFAULT_MESSAGING_TEMPLATES — this PR's migration backfill will work correctly regardless of merge order.

Code Changes

  • Model (messaging_template.py): Added label column with default="Default" and UniqueConstraint("type", "label")
  • Migration (new d71c7d274c04): 3-phase migration — add nullable column → backfill all rows → set NOT NULL + unique constraint. Handles dedup for multiple templates sharing the same type.
  • Schemas (messaging.py): Added label to MessagingTemplateWithPropertiesBase, MessagingTemplateDefault, MessagingTemplateWithPropertiesBodyParams (default "Default"), MessagingTemplateWithPropertiesPatchBodyParams (optional)
  • CRUD service (messaging_crud_service.py): New get_templates_by_type() and _validate_unique_label(). Wired label through all create/update/patch/defaults paths with backward-compatible fallbacks.
  • Endpoints (messaging_endpoints.py): Updated get_basic_messaging_templates and get_messaging_template_by_id to return label from the DB model instead of hardcoded dict lookup.
  • Tests (test_messaging_crud_service.py): Removed tests that relied on multiple templates sharing the same (type, label) — no longer valid with the unique constraint. Rewrote conflicting-template test to use distinct labels.

Steps to Confirm

Environment note: Steps 1–6 can be run against OSS fides. Steps 7–13 require the /plus/ routes, so switch to the fidesplus environment before continuing.

Migration round-trip (OSS fides or fidesplus):

  1. alembic upgrade head — verify the label column exists on messaging_template table
  2. Query: SELECT type, label FROM messaging_template ORDER BY type; — all rows should have labels matching DEFAULT_MESSAGING_TEMPLATES (e.g., "Subject identity verification", "Privacy request received")
  3. Verify constraint: SELECT conname FROM pg_constraint WHERE conname = 'uq_messaging_template_type_label'; — should return 1 row
  4. alembic downgrade -1 — verify column and constraint are removed
  5. alembic upgrade head — verify idempotent backfill (same labels as step 2)

Backward compatibility (no label in request):
6. PUT /api/v1/messaging/templates with existing payload (no label field, body is a list) — should succeed, templates get label from defaults or existing DB value
7. POST /api/v1/plus/messaging/templates/subject_identity_verification with body {"content": {"subject": "test", "body": "test"}, "is_enabled": false} (no label) — should succeed, label defaults to "Default"

Multi-template support:
8. POST /api/v1/plus/messaging/templates/subject_identity_verification with body {"label": "Custom verification", "content": {"subject": "test", "body": "test"}, "is_enabled": false} — should succeed with label "Custom verification"
9. Repeat step 8 with same label — should return 400 with message "A template with type 'subject_identity_verification' and label 'Custom verification' already exists."
10. Repeat step 8 with a different label (e.g., "Another verification") — should succeed (multiple templates, same type, different labels)

Label in responses:
11. GET /api/v1/messaging/templates — each template in response should have a label field
12. GET /api/v1/plus/messaging/templates/summary — each item should include label
13. GET /api/v1/messaging/templates/{id} — response should include label

Manual Test Results

All 13 steps verified manually against local OSS fides (steps 1–6) and fidesplus (steps 7–13) by author.

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • No UX review needed
  • Followup issues:
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
  • Documentation:
    • No documentation updates required

🤖 Generated with Claude Code

JadeCara and others added 2 commits April 10, 2026 15:39
…pport

Add a `label` column to the MessagingTemplate model enabling multiple
named templates per MessagingActionType. Includes migration with backfill
from DEFAULT_MESSAGING_TEMPLATES, unique constraint on (type, label),
dedup logic for existing rows, and label wiring through all CRUD paths.

- Model: add label column + UniqueConstraint(type, label)
- Migration: 3-phase (nullable add → backfill → NOT NULL + constraint)
- Schemas: label on all response schemas, optional with default on request
- CRUD: new get_templates_by_type(), _validate_unique_label(), label
  wired through create/update/patch/defaults paths
- Backward compatible: label defaults to "Default" if not provided

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Apr 15, 2026 11:19pm
fides-privacy-center Ignored Ignored Apr 15, 2026 11:19pm

Request Review

JadeCara and others added 5 commits April 10, 2026 15:47
- Add label to all 5 messaging template test fixtures (NOT NULL constraint)
- Use template.label from DB instead of hardcoded dict lookup in endpoints
- Fix migration docstring revision ID mismatch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Column default="Default" ensures existing code paths that create
MessagingTemplate without explicitly providing a label still work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JadeCara and others added 2 commits April 13, 2026 15:51
Remove tests that relied on multiple templates sharing the same
(type, label), which is now prevented by uq_messaging_template_type_label.
Rewrite conflicting-template test to use distinct labels inline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

❌ Patch coverage is 89.58333% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.05%. Comparing base (c6fe17d) to head (4807dd8).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...es/api/service/messaging/messaging_crud_service.py 87.50% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7900      +/-   ##
==========================================
- Coverage   85.06%   85.05%   -0.01%     
==========================================
  Files         629      629              
  Lines       40860    40900      +40     
  Branches     4748     4756       +8     
==========================================
+ Hits        34759    34789      +30     
- Misses       5029     5037       +8     
- Partials     1072     1074       +2     

☔ View full report in Codecov by Sentry.
📢 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.

JadeCara and others added 5 commits April 13, 2026 17:48
- Use `is not None` instead of `or` for label fallback to avoid
  silently swallowing empty-string labels
- Split test_get_templates_by_type into separate match/no-match tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JadeCara and others added 2 commits April 15, 2026 12:34
When creating a property-specific messaging template without an explicit
label, auto-generate a unique label (e.g. "Subject identity verification (2)")
instead of colliding with the DB unique constraint on (type, label).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JadeCara JadeCara marked this pull request as ready for review April 15, 2026 21:25
@JadeCara JadeCara requested a review from a team as a code owner April 15, 2026 21:25
@JadeCara JadeCara requested review from nreyes-dev and removed request for a team April 15, 2026 21:25
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Add label column to MessagingTemplate

This PR is well-structured. The migration follows the safe nullable→backfill→not-null pattern, the service layer provides good backward-compatibility (preserving labels on PUT when clients don't send one), uniqueness is enforced at both the application and DB level, and the test coverage in TestMessagingTemplateLabels is thorough.

A few issues worth addressing before merge:

Medium — Fix before merge

1. Model-level default="Default" is misleading (messaging_template.py:156)
The string "Default" doesn't match any label in DEFAULT_MESSAGING_TEMPLATES. If a row is ever created via ORM without an explicit label, it silently gets "Default" rather than the meaningful human-readable label. The service layer always supplies a label, so this default is redundant and could cause confusion. Recommend removing it entirely or at least aligning it with what the service layer would assign.

2. Empty-string label bypasses uniqueness check in patch_property_specific_template (messaging_crud_service.py:313)
if new_label and ... treats "" as falsy, so a PATCH with {"label": ""} skips _validate_unique_label and would store an empty string (satisfying NOT NULL but semantically invalid). Should be if new_label is not None and .... Should be paired with a Pydantic validator in the schema.

3. No blank-string validation on label in Pydantic schemas (messaging.py:184)
label: str | None = None accepts "" and whitespace-only strings. A min_length=1 or @field_validator at the API boundary would catch this before it reaches the service layer.

Low — Nits / FYI

4. TOCTOU window in create_property_specific_template_by_type (messaging_crud_service.py:355)
_next_available_label and _validate_unique_label are two separate reads. Under concurrent requests for the same type, both could select the same "available" label; one would then hit the DB unique constraint as an IntegrityError instead of the controlled MessagingTemplateValidationException. The DB constraint is the correct hard stop, but consider catching IntegrityError at the endpoint and converting it to a 422 so the API surface stays consistent.

5. DEFAULT_LABELS in migration is intentionally frozen, not "must stay in sync" (migration:61)
The comment "Must stay in sync with DEFAULT_MESSAGING_TEMPLATES" is subtly wrong — migrations are point-in-time and should never be updated. Clarifying the comment to "these values are frozen as of this migration and intentionally do not track future label renames" would prevent a future developer from editing the migration to "sync" it.

6. or fallback in get_all_basic_messaging_templates (messaging_crud_service.py:204)
template_from_db.label or template["label"] silently falls back on an empty-string label. Unlikely to matter given the NOT NULL constraint but worth using if ... is not None for clarity.


Overall the approach is sound. The two medium items (empty-string bypass and misleading ORM default) are worth fixing before merge; the rest are low-priority nits.

🔬 Codegraph: connected (46727 nodes)


💡 Write /code-review in a comment to re-run this review.

Comment thread src/fides/api/service/messaging/messaging_crud_service.py
@JadeCara
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. Addressed all findings:

  • R1 (model default): Changed default="Default"default="Unnamed" with a comment explaining it's a defensive fallback — the service layer always sets an explicit label, so this value should never appear in practice.
  • R2 (empty-string bypass in patch): Fixed — if new_label andif new_label is not None and.
  • R3 (blank-string validation): Added Field(None, min_length=1) to both MessagingTemplateWithPropertiesBodyParams.label and MessagingTemplateWithPropertiesPatchBodyParams.label.
  • R4 (TOCTOU race): Wrapped MessagingTemplate.create() in try/except IntegrityError that raises the same MessagingTemplateValidationException — concurrent duplicates now get a clean 400 instead of 500.
  • R5 (migration comment): Fixed (same as inline thread).
  • R6 (or fallback): Fixed (same as inline thread).

- Change model default from "Default" to "Unnamed" for clarity
- Fix migration comment to reflect frozen point-in-time values
- Add min_length=1 validation on label schema fields
- Wrap template create in try/except IntegrityError for race safety
- Drop unnecessary `or` fallback in get_all_basic_messaging_templates
- Fix falsy check to `is not None` in patch label validation
- Add parametrized test for auto-label numbering (covers while loop)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JadeCara JadeCara requested review from erosselli and removed request for nreyes-dev April 15, 2026 23:19
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.

1 participant