summaryrefslogtreecommitdiff
path: root/test/models
AgeCommit message (Collapse)Author
2026-07-27Surface held locks in the dashboard's work-in-progress widgeterdgeist
A lock with no draft or autosave behind it was invisible to everyone including its holder, until the next editor collided with it. work_in_progress now admits those nodes, and the widget names the holder rather than the last editor on any locked row. Also flips the ordering to stalest-first, so a backlog reads as a queue rather than a feed, and gives the heading a real total: the count came from the already-limited relation and could never exceed five.
2026-07-24Add TOTP enrollment and verification to User, witnessed in the action logerdgeist
2026-07-23Record asset deltas at publish, with changed assets as participantserdgeist
2026-07-23Witness asset uploads and out-of-band attaches in the action logerdgeist
2026-07-23Witness asset destruction, naming every node it stripserdgeist
2026-07-23Add action_participants, recording every node a trash/destroy toucheserdgeist
2026-07-22Add Node#attach_asset! attaching across all lifecycle rowserdgeist
2026-07-22Retire nodes#recent, superseded by the action logerdgeist
2026-07-22Fix Node#title ignoring autosaveerdgeist
head ? head.title : draft.title never accounted for a node with only an autosave, reuses editable_page
2026-07-21Render PDF headlines as document cards, not lightbox imageserdgeist
A starred PDF previously ran through the same crop-and-lightbox path a photo does -- exactly the awkward treatment explicit headline designation was meant to avoid. _headline_image.html.erb now branches on @headline_asset.pdf?: a PDF renders as a linked card (a :medium thumbnail, a file icon, its name), no gallery participation at all. The existing image-headline and gallery-fallback logic is otherwise unchanged, now scoped to an image-specific headline only. Other attached PDFs -- headlined or not -- list below as plain links, same reasoning as why non-headline photos still get a gallery trigger: an attached document shouldn't go invisible just because nothing's been starred yet. headline_image's own render guard needed widening to cover a page with PDFs attached but no images and no headline at all -- the one case none of the existing conditions accounted for.
2026-07-21PDFs currently should have no credits attached to them in displayerdgeist
2026-07-21More PDF headline eligibilityerdgeist
2026-07-21Make PDFs eligible for headline and allow searching assets by filenameerdgeist
2026-07-21Remove Page#update_assetserdgeist
Only caller left was the sort_images action removed last commit. Kept alive only as a test-setup convenience for diff_against coverage, which is what it actually was -- an implicit API with no deliberate contract, sitting there for anyone to call. Rewrote the one test to set up its fixture state directly via related_assets rather than through a method whose real job was already done.
2026-07-20Make headline images explicit, add asset creditserdgeist
- related_assets gains a `headline` boolean (DB-enforced: at most one per page), replacing "first image by position" as the headline rule. A rake task backfills the current first image on every live head/draft, so nothing changes visually until an editor changes it. - The image picker sidebar gets a star toggle reflecting the flag; the TinyMCE inline-image picker's badge now reads it too, instead of assuming position 0. - No headline chosen (or none attached) now falls back to the gallery-count caption itself becoming the lightbox trigger, instead of the gallery being unreachable. - Assets gain creator, source_url, and license_key (against a new config/asset_licenses.yml dictionary). asset_credit renders a degrading attribution line, reused as a hidden per-image glightbox caption so credit is one click away for every image, not only the headline's always-visible one. - Fixed: asset thumbnails rendered unconditionally regardless of whether a real variant exists on disk. Asset#has_variant? checks file existence, not content type -- some legacy PDFs have real pre-rewrite thumbnails a content-type check would have hidden. - assets#new/edit rebuilt onto the same node_description/node_content layout as assets#show, picking up the three new fields in the process.
2026-07-18Accept blank template names in the allowlist validationserdgeist
2026-07-18Give action log entries per-verb iconserdgeist
A muted glyph before each sentence makes both log surfaces scannable by verb: create, publish, move, trash, restore, destroy, and the discard pair. Rollback publishes get their own icon (history) distinct from ordinary promotion. Unknown verbs fall back to a dashed circle.
2026-07-18Annotate the History list with each revision's lifecycleerdgeist
Each row in nodes#show's History section now carries terse badges from the action log: created, published, and restored (rollback re-promotion), with date and actor, rendered from entry metadata alone. Backfilled entries wear the inferred marker, so reconstructed provenance stays distinguishable from witnessed history. A revision that was published and later restored shows both badges chronologically -- its true biography. Only create and publish entries carry page_id; trash, restore, and destroy annotate the node's own log zoom instead of any single revision, by design.
2026-07-18Give Trash a proper title on lazy constructionerdgeist
2026-07-18Keep the Trash out of the drafts surfaceserdgeist
2026-07-18Teach the action log reader the Trash vocabularyerdgeist
Sentences for trash (with the pre-trash path), restore_from_trash (with the destination), and destroy (with the final path, read from its flat create-symmetric key).
2026-07-18Add Trash affordances: cockpit, listing, dashboard entryerdgeist
nodes#show gains a Trash section on trashed nodes: provenance from the trash entry, a restore form whose parent picker pre-fills the old parent while it still lives, and permanent deletion. A Move-to-Trash button joins the status actions on living nodes. nodes#trashed lists trashed subtree roots with weight, provenance, and deletion; the dashboard housekeeping row links to it, and trash/destroy redirect there. Deletion from Trash now removes the whole subtree, deepest first, each node through a real destroy! so every per-node cascade runs -- amending the never-recursive rule for this one sanctioned path (both confirms state the count; the root entry carries destroyed_descendants). Bare Node#destroy still refuses children.
2026-07-17Add trash!, restore_from_trash!, and destroy_from_trash! with log entrieserdgeist
Also update the node action contract to include the trash related verbs.
2026-07-17Bootstrap the Trash node: reserved slug, identity, guardserdgeist
2026-07-17Refuse destroying nodes that still have childrenerdgeist
NestedTree's before_destroy silently delete_all'd the whole subtree, bypassing every per-node cleanup. Nodes are never destroyed recursively; descendants must be removed individually.
2026-07-17Allowlist aggregate order columns and template nameserdgeist
Page.aggregate interpolated order_by into SQL unchecked while already allowlisting order_direction; the column is now normalized and checked against the sortable columns, falling back to pages.id. Its values arrive from editor-authored aggregate shortcodes, so this was editor-gated, but the asymmetry was wrong regardless. template_name and default_template_name now validate inclusion in Page.custom_templates -- names render as filesystem paths, so only names actually present in the template directory are acceptable. Validated only on change: legacy rows whose template file has since vanished stay saveable, and valid_template's render-time fallback to standard_template continues to cover them. Two tests that wrote fabricated template names through the front door now arrange their state correctly (update_column for the stale-name fallback test, a real template for the update-persists test).
2026-07-17Allow updating migrated users without supplying a passworderdgeist
password_required? treated a blank crypted_password as "needs a password", but after bcrypt migration crypted_password is nil by design -- every subsequent save of a migrated or new user failed validation. The predicate now requires a password only when both the legacy and the bcrypt credential are absent, or when one is actually being set.
2026-07-17Implement transparent password hash migrationerdgeist
Add has_secure_password and bcrypt while retaining compatibility with legacy SHA-1 password hashes. Existing users are upgraded to password_digest on their next successful login. Add regression tests covering both legacy and modern authentication paths.
2026-07-17Add some more tests for the login basics while refactoringerdgeist
2026-07-17Improve test coverage for two-ordinal week selection in rrule humaniziererdgeist
2026-07-16Aggregate selected weeks in a more human friendly formerdgeist
Weekdays appearing on a subset of the weeks of the month are now aggregated into a simpler form.
2026-07-16managed RRULE constructs now include week selectionerdgeist
Improve the humanizer to also understand weekly patters that an editor can manually select, on top of the single week rules. Extend template and javascript controller to reflect these changes.
2026-07-16Condense the action log into a scannable tableerdgeist
Several minor improvements to the action log presentation: * Now a table with date and human readable presentation as rows is displayed * If no changes in a title were detected, the old version is omitted * The "inferred" flag is demoted to the end of the line * You can zoom in on the node's history directly from a log line * byline for the first publish action is preserved * Revisions are directly linked to when a new one i published
2026-07-16Add a reader for the action log at admin/logerdgeist
NodeActionsController#index lists entries newest-first, filterable by node_id or user_id -- the two zoom shapes the log was designed around. Rendering goes through NodeActionsHelper.action_summary, which builds one sentence per entry from metadata alone, so entries referencing deleted users or nodes render from their snapshots; live associations only upgrade names to links. Unknown verbs degrade to a generic sentence rather than an error, since the log outlives its vocabulary. The helper is the escaping boundary: every metadata value passes through h() before assembly. Actor names link to the log's own user zoom rather than the unused users page -- inspecting a suspicious user's other actions is the intended workflow. Publish entries with a translation_diff expose a collapsed per-locale change table linking out to the revision itself. Sentences live in en.yml/de.yml following the existing widget-string convention. nodes#show links to its node's zoomed log.
2026-07-16Record the full lifecycle contract in NodeAction entrieserdgeist
A contract comment above NodeAction.record! now specifies every verb's metadata shape. NodeAction.head_diff computes the publish diff between an outgoing head and its replacement -- default-locale title pair always, author/tags pairs and template/assets/abstract/ body flags only when changed, and a per-locale translation_diff with added/removed/changed status. It is a pure function of its two pages, shared by publish, rollback, and the future backfill, and reads translation rows directly so fallbacks never masquerade as content. publish entries carry via ("draft" or "revision"); restore_revision! is now transactional, takes the acting user, and logs through the same diff. Staged slug/parent changes applied at publish log a move entry with the path pair. Node creation logs a create entry with initial title and path. The draft-scoped translation_destroy writer is retired -- locale removal is recorded by the publish diff, where it becomes public fact.
2026-07-15Add NodeAction: an append-only log of who did what to a nodeerdgeist
node_id/page_id/user_id are lookup and ordering only -- all three nullify on delete, so an entry outlives its actor and its subject. Everything that must survive those deletions lives in a mandatory metadata jsonb written once at creation: the actor's username, the node's human-readable name (pinned to the default locale), and action-specific extras such as publish's title from/to. NodeAction.record! is the single constructor, so every entry gets the same baseline metadata without each call site re-implementing it. occurred_at is one field for live and backfilled entries alike; inferred_from distinguishes them -- nil means witnessed at the moment it happened, populated names how a backfilled entry was estimated. Instrumented so far: publish (crediting the actual publisher, threaded through from the controller -- previously nobody had the act of publishing recorded anywhere), revert's discard_autosave and destroy_draft branches, and translation destroy. publish_draft! now runs in a transaction so the promotion and its log entry land together. The remaining verbs follow once this mechanism has proven itself.
2026-07-14Extend static-asset cache-busting to the public layouterdgeist
mtime_busted_path only ever covered the four admin-only static files. The public layout has the identical exposure -- ccc.css, glightbox's vendored CSS/JS, and public.js are all served straight from public/ with no pipeline fingerprinting either -- just never got the same treatment.
2026-07-14Retire wipe_draft! and find_or_create_draft from production codeerdgeist
Both had already lost their reason to exist as production API: wipe_draft!'s one remaining callsite (nodes#show) was removed two sessions ago, and find_or_create_draft had zero production callers left at all -- confirmed by a fresh grep, not assumed -- every one of its ~65 call sites was test setup, unrelated to what those tests actually cover. wipe_draft! is deleted outright, along with its two tests -- the lock/draft/autosave cleanup it silently performed already has explicit, always-visible manual equivalents (Unlock, Discard Autosave, Destroy Draft), so nothing real is lost. find_or_create_draft moves to test_helper.rb as a plain method on ActiveSupport::TestCase, alongside the create_node_with_draft/ create_node_with_published_page helpers already living there -- extending the framework's own designated test-extension point rather than reopening the Node model from test code. Its three tests of real dispatch behavior (idempotency on repeat calls, and raising when a second user contends for the lock) are kept, since ~65 other tests depend on this helper actually working correctly; only the call syntax changed, from node.find_or_create_draft(user) to find_or_create_draft(node, user).
2026-07-13Add cache-buster using mtime of css and js files outside the asset pipelineerdgeist
2026-07-13clone_attributes_from: update translations in place, not delete+recreateerdgeist
Every promotion (autosave creation, draft creation, draft save) ran every locale's translation through delete-all-then-recreate unconditionally, giving every locale a fresh created_at/updated_at regardless of whether its content actually changed. Confirmed via Node.find(546).draft.translations -- both DE and EN shared one identical timestamp despite only EN having been edited. This silently defeated Page.find_with_outdated_translations' whole staleness comparison: every save reset every locale back into lockstep, so timestamps could never actually drift apart the way the feature depends on. The existing test for it only ever passed by manually backdating a timestamp with record_timestamps = false, bypassing normal save flow entirely -- not something that happens through ordinary editing. Now updates a matching locale's translation only when its own attributes actually differ, creates one genuinely new, and removes one genuinely gone from the source -- same end state, but real per-locale timestamps survive an unrelated save. search_vector is excluded from both the comparison and the copied attributes: it's DB-trigger-maintained from title/abstract, not real content, and comparing a precomputed tsvector risked a false 'changed' from representation noise alone. Also reloads the source's translations association, not just self's -- clone_attributes_from previously only guaranteed a fresh self, leaving any caller holding a page reference across an earlier mutation vulnerable to reading a stale cached association.
2026-07-13Carry over other-locale translations when creating an autosaveerdgeist
Node#autosave! only copied assets forward onto a newly created autosave, not translations. Invisible for single-locale nodes, but a node with an existing multi-locale head/draft that gets locked and edited in one locale would silently drop every other locale's translation from the new autosave -- and from there into the draft and eventually head, since save_draft! faithfully clones whatever the autosave actually holds. Fixed by using clone_attributes_from, which already does a complete clone (translations, assets, tags, template, published_at) -- the same method save_draft! already relies on two branches later, so no new mechanism, just closing the one place still doing a partial, hand-rolled version of it.
2026-07-13Asset picker: attach/detach/reorder UI, read-only view, autosave fixerdgeist
Replaces nodes#edit's old Images section -- a hidden panel dumping every image asset in the system unfiltered (#image_browser) plus a raw drag-and-drop box (#image_box) -- with a small search-and-click picker built on the endpoint from the last two commits. Attaching posts immediately and appends the new thumbnail via a cloned <template> -- icons only render correctly through the Rails helper server-side, so the template holds real, pre-rendered markup for JS to clone rather than duplicating raw SVG in a JS string. Reordering is jQuery UI sortable on the small attached list only, with a dedicated drag handle rather than the whole thumbnail. Two bugs caught while click-testing, fixed here rather than shipped and patched after: the search panel never closed after attaching an image, since the success handler re-triggered focus to keep it open for attaching several in a row -- which meant it just re-populated itself forever instead of signaling "done." Fixed to close explicitly; a click-outside-closes handler was added alongside it, matching the affordance the top-bar search already has. A real, independent, pre-existing data bug surfaced during the same testing: Node#autosave!'s first-time-creation branch never carried related assets forward from whatever page was previously current -- attach an image, let autosave fire once, and it silently landed on a fresh, assetless Page row. Long-dormant, not introduced by this work, just finally exercised by something that made it visible. Fixed inside the `unless self.autosave` guard specifically -- running this on every call, not just creation, would overwrite anything attached directly to an existing autosave in between, a worse bug than the one being fixed. nodes#show gains a read-only Images section, rendered only when a page actually has attached images, so an attachment can be confirmed present without entering the edit/lock cycle -- useful on its own, and specifically useful the next time an asset bug needs investigating. Its thumbnail CSS is shared with the edit view's picker via a class (.thumbnail_list) rather than duplicated under a second name.
2026-07-12Rebuild the admin dashboard: icon nav, search, signposts, widgetserdgeist
Replaces the old admin#index wizard -- accreted over years, never designed as a whole -- with the dashboard settled on this session: a three-icon nav (dashboard/search/log out, no locale selector), a nodes-first search bar, four task signposts, and two symmetric widgets (drafts/autosaves, recent changes) with a quiet housekeeping row beneath them. Node.recently_changed now filters and orders by the head page's own updated_at instead of the node's blanket timestamp, so a lock/unlock cycle with no actual publish no longer surfaces here, and the original publisher is no longer misattributed to someone else's housekeeping action. This also restores the "published" qualifier on each entry, which the query previously couldn't guarantee was true. @mynodes and its dedicated "My Work" table are retired along with the old wizard -- "Continue my work" is a link to the existing, already- correct NodesController#mine instead of a second, duplicate query. Its one dedicated test (dedup across multiple revisions by the same user) is ported to nodes_controller_test.rb, since mine already carries the same .distinct protection the old query did; it just had no test of its own until now.
2026-07-12Add a locale-aware relative-time helper; drop stale locale overrideserdgeist
Node.recently_changed and the new dashboard need "X ago" rendered correctly in both locales. Rails' own distance_of_time_in_words/ time_ago_in_words can't do this on their own -- the scope option changes which translation key is looked up, not the underlying grammar, and German's "vor" requires dative case, which is a different word form than the nominative plural Rails computes by default. relative_time_phrase/relative_distance is a small, from- scratch bucketing helper instead, with an explicit, hand-checked translation table per unit per locale. Also removes de.yml's datetime.distance_in_words and activerecord. errors blocks. Both used the old {{count}}/{{model}} interpolation syntax the current i18n gem no longer recognizes -- it silently leaves the literal placeholder text in the rendered output rather than erroring, which is why this went unnoticed until now. Both were shadowing rails-i18n's own current, correct translations for exactly these keys; deleting the local override lets the gem's version take over instead of maintaining a second, broken copy.
2026-07-12Extract drafts/recent query logic from controller into Nodeerdgeist
Node.drafts_and_autosaves and Node.recently_changed replace inline query logic in NodesController#drafts/#recent -- pure refactor, no behavior change for either action. The real reason: the dashboard's upcoming abridged widgets need the exact same queries the full pages already use, just limited and (for drafts) sorted with the current user's own locked nodes first. Better to share one method than let a widget and a full page quietly drift onto two versions of "what counts as a current draft." current_user_id: is an explicit, optional argument rather than a separate method -- ordering by lock ownership only applies when a user is given; omitted entirely, it's pure recency, exactly today's behavior. Needed Arel.sql wrapping a sanitized CASE expression before .order would accept it -- sanitize_sql_array only vouches for the values inside the string, a separate Rails safety check still refuses any string shaped like more than a plain column reference unless it's explicitly marked as already-vetted.
2026-07-12Give the sitemap its own view, with collapse and descendant countserdgeist
Extracted from admin#index's inline table into NodesController#sitemap. Nested <details>/<summary> per branch, one linear pass over the existing flat [node, level] list (no added queries) -- each node's own descendant count computed the same way, via a small stack rather than re-walking the tree per node. Branches under updates/, club/erfas, club/chaostreffs, and disclosure start collapsed by default (CccConventions::SITEMAP_COLLAPSED_PATHS); any branch currently collapsed, whether by that default or because someone just closed it, is highlighted via a plain :not([open]) selector -- no state tracked outside the DOM itself. Dropped the update?-post exclusion this view used to rely on -- no longer needed now that updates/ collapses instead of being filtered out, so its real children (previously silently absent) now show up correctly. admin#index's own, separate @sitemap query is unchanged; that view has no collapse mechanism to compensate and wasn't part of this.
2026-07-11Add drafts/recent/mine/chapters admin node viewserdgeist
Four NodesController actions -- drafts, recent, mine, chapters -- each building its own base scope, sharing one private method (index_matching) for search narrowing and pagination. Wizard rewrite to link into these instead of rendering its own tables is a separate, later step. Node.editor_search backs the shared "q" narrowing: an ILIKE substring match against title/abstract on whichever of head or draft is present, splitting the term on whitespace and requiring every word to match somewhere independently, not as one phrase, since real words can end up separated by markup in the underlying HTML. Deliberately separate from Node.search, the public content search, which stays tsvector-based and head-only. chapters generalizes into /admin/nodes/tags/:tags for an arbitrary tag list (OR'd, not AND'd), sharing the controller action but rendering its own template rather than branching inside one view.
2026-07-10Prevent a staged parent move from crashing on its own descendanterdgeist
publish_draft! called move_to_child_of before validating the new parent at all. Staging a node under one of its own descendants created a real, if transient, cycle in parent_id the instant that save landed -- and update_unique_names_of_children's after_save callback, which recurses down through parent_id with no cycle check and no depth limit, bounced between the two nodes forever, crashing the whole process with a SystemStackError rather than just producing bad data. Now rejected up front with a normal ActiveRecord::RecordInvalid.
2026-07-10Add the missing assertions two tests were silently running withouterdgeist
Both ran real code and checked nothing -- Minitest's "missing assertions" warning has been firing on every run this session. test_find_or_create_draft_if_draft_exists_and_is_owned_by_user called the method twice but never checked what came back; now confirms the second call returns the same draft rather than creating a new one, and leaves the lock and page count untouched. test_destroy_a_published_node destroyed a node and loaded the admin index but never checked the response, unlike its two neighbors covering the same destroy path (dangling pages, orphaned occurrences). Added the assert_response :success its own shape already implied. No behavior changed -- both were passing before for the same reason they're passing now, they just weren't proving anything.
2026-07-10Fix rrule/url column overflow on events#indexerdgeist
Long RRULEs previously overflowed their column with no wrap point; now escaped and rendered with a <wbr> after each semicolon, so a long rule wraps at a clause boundary instead of running off the table. Deliberately not truncated -- a cut-off RRULE's trailing clause (BYDAY, BYMONTH, etc.) is usually the most specific part. The url column is now a real link, truncated with an ellipsis at a fixed width -- deliberately no tooltip, since hovering a real link already shows the full address in the browser's own status bar. rrule_with_break_opportunities splits on the raw string's own semicolons before escaping each piece, not after -- escaping first and searching the result for semicolons also matches the ones inside &lt;/&gt; entities, corrupting anything containing a literal < or >.