66copied into district_notes by b3d9f47a25c1.
77
88Form comments are real data (dev has live testimony, e.g. the TN workshop),
9- so before dropping, every legacy form comment becomes a submission under a
10- catch-all 'legacy' form config:
9+ so before dropping, every legacy form comment becomes a submission:
1110
12- - tags are preserved verbatim, so tag-filtered galleries keep showing them;
13- only the per-portal admin queue groups them under 'legacy' (portal_id is
14- ON UPDATE CASCADE — re-attribute later with a plain UPDATE if wanted).
11+ - PORTAL ATTRIBUTION: gallery membership and moderation authority both key
12+ on portal_id, so each comment is attributed to its primary tag slug (the
13+ earliest-created tag == the portal page's slug in the legacy flow); a
14+ form config row is created per distinct slug. Untagged comments fall back
15+ to a catch-all 'legacy' config. All created configs get admin_teams='{}'
16+ (no team information exists here): they are moderatable only by
17+ review:review-all holders until an admin grants teams in the CMS — and
18+ they DO accept new public submissions (the portals are real pages), which
19+ is intended.
20+ - tags follow the new-submission convention: [portal_id, *other slugs].
1521- map attachments keep their LIVE document reference (legacy behavior);
16- clone-at-submission applies only to new submissions.
17- - moderation maps to the new bits preserving what the old public gate
18- showed: hidden = anything REJECTED (comment, commenter, or a tag);
19- nsfw = any moderation score >= 0.2 without an APPROVED override.
22+ map_is_clone=false marks them so takedown never demotes a real user's
23+ working map (this migration also adds that column).
24+ - moderation preserves the OLD public gate exactly: hidden = anything
25+ REJECTED (comment, commenter, or a tag) OR anything score-flagged
26+ (>= 0.2) without an explicit APPROVED override — the old gate excluded
27+ score-flagged rows from public view entirely, and converting them to
28+ merely-blurred would retroactively publish testimony no human ever
29+ approved. nsfw carries the score flag so an admin who un-hides one still
30+ gets the blur. Reviewers can unhide false positives from the queue.
2031- submission ids are the legacy comment ids offset past MAX(submissions.id),
2132 so a deploy where pr10..13 already collected new submissions can't collide.
33+ - negative-zone rows (skipped by b3d9f47a25c1 as never-renderable) are in
34+ neither district_notes nor submissions and are dropped with the tables —
35+ a deliberate one-way loss.
2236
2337Downgrade recreates the tables (final shape as of 0db008690d60 + da39a3ee5e6b)
2438empty — it does NOT reverse the conversion (converted rows simply remain in
@@ -90,18 +104,30 @@ def _timestamps():
90104
91105
92106def _convert_legacy_form_comments (bind ) -> None :
93- # Catch-all portal config, only when there is anything to migrate.
107+ # One config per portal the legacy data references: the comment's primary
108+ # (earliest) tag slug IS the portal page slug in the legacy flow, and
109+ # gallery membership + moderation authority both key on portal_id — a
110+ # catch-all portal would empty every legacy tag gallery. Untagged
111+ # comments fall back to 'legacy'.
94112 bind .execute (
95113 sa .text (
96114 f"""
97115 INSERT INTO comments.form_configs
98116 (portal_id, name, fields, required_fields,
99117 require_email_confirm, admin_teams)
100- SELECT 'legacy', 'Legacy submissions', { _ALL_FIELDS } ,
101- '{{}}', false, '{{}}'
102- WHERE EXISTS (
103- SELECT 1 FROM comments.comment c { _FORM_COMMENT_FILTER }
104- )
118+ SELECT DISTINCT
119+ COALESCE(t.primary_slug, 'legacy'),
120+ COALESCE(t.primary_slug, 'Legacy submissions'),
121+ { _ALL_FIELDS } , '{{}}'::varchar(64)[], false,
122+ '{{}}'::varchar(255)[]
123+ FROM comments.comment c
124+ LEFT JOIN LATERAL (
125+ SELECT (array_agg(tg.slug ORDER BY tg.id))[1] AS primary_slug
126+ FROM comments.comment_tag ct
127+ JOIN comments.tag tg ON tg.id = ct.tag_id
128+ WHERE ct.comment_id = c.id
129+ ) t ON true
130+ { _FORM_COMMENT_FILTER }
105131 ON CONFLICT (portal_id) DO NOTHING
106132 """
107133 )
@@ -122,7 +148,8 @@ def _convert_legacy_form_comments(bind) -> None:
122148 c.moderation_score AS c_score,
123149 cm.review_status::text AS m_status,
124150 cm.moderation_score AS m_score,
125- t.slugs, t.rejected_tag, t.score_flagged_tag, t.max_tag_score
151+ t.slugs, t.rejected_tag, t.score_flagged_tag, t.max_tag_score,
152+ COALESCE((t.slugs)[1], 'legacy') AS portal
126153 FROM comments.comment c
127154 LEFT JOIN comments.document_comment dc ON dc.comment_id = c.id
128155 LEFT JOIN comments.commenter cm ON cm.id = c.commenter_id
@@ -141,35 +168,46 @@ def _convert_legacy_form_comments(bind) -> None:
141168 ) t ON true
142169 LEFT JOIN document.document d ON d.document_id = dc.document_id
143170 WHERE dc.zone IS NULL
171+ ),
172+ score_flags AS (
173+ SELECT e.*,
174+ COALESCE(
175+ (e.c_score >= 0.2 AND e.c_status IS DISTINCT FROM 'APPROVED')
176+ OR (e.m_score >= 0.2 AND e.m_status IS DISTINCT FROM 'APPROVED')
177+ OR e.score_flagged_tag,
178+ false
179+ ) AS score_flagged,
180+ COALESCE(
181+ e.c_status = 'REJECTED'
182+ OR e.m_status = 'REJECTED'
183+ OR e.rejected_tag,
184+ false
185+ ) AS was_rejected
186+ FROM enriched e
144187 )
145188 INSERT INTO comments.submissions
146189 (id, portal_id, map_public_id, tags, status, submitted_at,
147- nsfw, hidden, flagged, moderation_score,
190+ nsfw, hidden, flagged, moderation_score, map_is_clone,
148191 created_at, updated_at)
149192 SELECT
150193 e.id + :offset,
151- 'legacy' ,
194+ e.portal ,
152195 e.public_id,
153- COALESCE(e.slugs, '{}'),
196+ ARRAY[e.portal]::varchar(255)[]
197+ || array_remove(COALESCE(e.slugs, '{}'), e.portal),
154198 'submitted',
155199 e.created_at,
156- COALESCE(
157- (e.c_score >= 0.2 AND e.c_status IS DISTINCT FROM 'APPROVED')
158- OR (e.m_score >= 0.2 AND e.m_status IS DISTINCT FROM 'APPROVED')
159- OR e.score_flagged_tag,
160- false
161- ),
162- COALESCE(
163- e.c_status = 'REJECTED'
164- OR e.m_status = 'REJECTED'
165- OR e.rejected_tag,
166- false
167- ),
200+ e.score_flagged,
201+ -- The OLD public gate fully excluded score-flagged rows that
202+ -- no human APPROVED; blurred-but-fetchable would widen their
203+ -- exposure retroactively, so they convert as hidden too.
204+ e.was_rejected OR e.score_flagged,
168205 e.review_flagged,
169206 GREATEST(e.c_score, e.m_score, e.max_tag_score),
207+ false,
170208 e.created_at,
171209 e.updated_at
172- FROM enriched e
210+ FROM score_flags e
173211 """
174212 ),
175213 {"offset" : offset },
@@ -179,7 +217,7 @@ def _convert_legacy_form_comments(bind) -> None:
179217 sa .text (
180218 f"""
181219 INSERT INTO comments.submissions_content (submission_id, field, value)
182- SELECT c.id + :offset, f.field, LEFT(f.value, 5000)
220+ SELECT c.id + :offset, f.field, LEFT(BTRIM( f.value) , 5000)
183221 FROM comments.comment c
184222 LEFT JOIN comments.commenter cm ON cm.id = c.commenter_id
185223 CROSS JOIN LATERAL (VALUES
@@ -214,6 +252,27 @@ def _convert_legacy_form_comments(bind) -> None:
214252
215253
216254def upgrade () -> None :
255+ # Whether the submission's map is a submission-owned frozen clone (new
256+ # prompt/form submissions) or a live reference to the author's working
257+ # document (drafts, converted legacy rows, later auto-collect modes).
258+ # Takedown may only demote the draft_status of CLONES.
259+ op .add_column (
260+ "submissions" ,
261+ sa .Column (
262+ "map_is_clone" ,
263+ sa .Boolean (),
264+ nullable = False ,
265+ server_default = "false" ,
266+ ),
267+ schema = "comments" ,
268+ )
269+ # Every pre-existing submitted row was created by clone-at-submission.
270+ op .execute (
271+ sa .text (
272+ "UPDATE comments.submissions SET map_is_clone = true "
273+ "WHERE status = 'submitted'"
274+ )
275+ )
217276 _convert_legacy_form_comments (op .get_bind ())
218277 op .drop_table ("document_comment" , schema = "comments" )
219278 op .drop_table ("comment_tag" , schema = "comments" )
@@ -227,6 +286,7 @@ def upgrade() -> None:
227286
228287
229288def downgrade () -> None :
289+ op .drop_column ("submissions" , "map_is_clone" , schema = "comments" )
230290 # Recreates the schema only; rows are unrecoverable without a backup.
231291 op .execute (
232292 sa .text (
0 commit comments