summaryrefslogtreecommitdiff
path: root/test
AgeCommit message (Collapse)Author
11 hoursMake revisions locale awareerdgeist
14 hoursAdd cache-buster using mtime of css and js files outside the asset pipelineerdgeist
18 hoursMake revision diffs locale-aware, defaulting to whichever locale changederdgeist
diff_against compared title/abstract/body under whatever I18n.locale happened to be ambient, with no concept of 'diff this translation specifically' -- so a change confined to one locale was invisible to Diff Head vs. Draft regardless of which locale you were looking at when you clicked it, exactly the 'yields nothing' complaint from earlier this session. Page#diff_against gains a locale: keyword, additive only -- nil preserves the exact original ambient-locale behavior every existing caller and test already depends on; passing a locale switches to reading each side's actual PageTranslation row directly, same fallback-free reasoning as Page#translation_summary. Page#locale_diff_summary reports one entry per locale present on either side, so an added or removed translation counts as a change even where content matches everywhere it exists on both. RevisionsController#diff now resolves a real locale before diffing -- defaulting to whichever locale actually changed, falling back to the default locale only when nothing did -- and the view carries that locale through every existing control (view toggle, layer-pair buttons, the revision-select form) so it and the view/layer-pair axis stay independently selectable rather than resetting each other.
18 hoursclone_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.
20 hoursFix shared preview redirecting to admin for an ordinary new drafterdgeist
superseded/currently_public used published_at presence as a proxy for page-row relevance, but published_at is carried forward onto every new draft descended from previously-published content (Page#clone_attributes_from, Node#save_draft!) -- so it read true for any draft on an already-published node, whether or not that specific draft had ever gone live itself. Compare draft_id/head_id identity directly instead; no timestamp involved. A stale (superseded) link now redirects to the live public page rather than an admin URL an anonymous holder can't reach anyway.
20 hoursWire real autosave into translation editingerdgeist
The shared TinyMCE setup initializes cccms.setup_autosave() on any page with a textarea.with_editor, unconditionally starting a 7-second interval that submits to the form's data-autosave-url -- which the translation edit form never set, so the interval PUT the current page URL itself and 404'd on a nonexistent route. Fixed by actually giving it something to talk to, rather than suppressing it: a real autosave endpoint, and update now goes through Node#autosave!/#save_draft! -- the same pipeline the primary editor uses, fixed earlier this session for exactly this kind of cross-locale carryover. This was the autosave-buffer parity already flagged as due after the proof of concept; the shared JS just forced the timing.
21 hoursAdd per-node translation management: list, edit, destroy, compareerdgeist
Replaces the old locale-switch-and-edit-the-same-screen workflow, which conflated presentation locale with content locale and let an editor silently drift into editing the wrong language with no persistent signal that anything had changed. Non-default-locale content now has its own explicit routes and screens, never sharing a route param with the ambient chrome locale. - PageTranslationsController: index/show/edit/update/destroy, scoped to Page.non_default_locales; update handles first-time creation too, so there's no separate new/create step. - Reads go through the actual PageTranslation row (Page#translation_summary), never through the Globalize fallback-bearing accessor -- fallback is correct for public rendering but wrong for editing, where a missing translation needs to look empty, not borrowed from another locale. - Translations ride on the page's own draft/head cycle; no independent publish state. - nodes#show gains a Translations section (per-locale Lock+Edit / Create+Lock, Destroy, a link into the read-only Compare view) and a locale indicator on its own default-locale content; nodes#edit, nodes#update, and nodes#autosave are pinned to the default locale via Globalize.with_locale regardless of the ambient route locale. - nodes#show no longer double-loads the node or calls wipe_draft! on every view (see previous commit for why that's now safe). - .preview_link_row is renamed .aligned_action_row now that it has a second real consumer.
23 hoursCarry 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.
32 hoursAsset 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.
34 hoursAdd loadOnFocus to initSearchPicker; asset search shows recent by defaulterdgeist
initSearchPicker's input handler has always short-circuited on an empty term -- correct for every existing picker, none of which want results appearing before anything's typed. The asset picker does want exactly that (show the last few uploaded images without requiring a search first), so this adds an opt-in loadOnFocus option instead of changing the shared default: it fires once when the input gains focus with no term yet, reusing the same request/render path a real search uses rather than a separate code path. The AJAX call itself was pulled out into a named runSearch function so both triggers could share it without duplicating the success/render logic. RelatedAssetsController#search now treats a blank term as "show the most recently created, unattached images" (limit 5) rather than returning nothing, matching the new client behavior. A real search term still returns up to 10 name-matched results, unchanged.
34 hoursAdd RelatedAssetsController: search, attach, detach, reordererdgeist
Backend for the asset-picker rebuild -- replaces the plan to dump every image asset in the system into a hidden, unfiltered browse panel on every node edit (the actual current behavior, confirmed by reading nodes/edit.html.erb directly) with a small, name-scoped search endpoint plus create/destroy/update for attach, detach, and reorder. No schema change needed -- RelatedAsset already had everything this requires (asset_id, page_id, an acts_as_list position). search excludes assets already attached to the page, keeping results meaningfully small given hundreds of assets total but only a handful per node in practice. create is find_or_create_by! rather than a bare create!, guarding against the same asset being attached twice from two separate search results. update leans on acts_as_list's own insert_at rather than custom position-shifting logic. Node#editable_page (autosave || draft || head) is extracted since this is now its third call site with identical logic -- deliberately not touching nodes#show, which resolves draft || head without autosave on purpose, a different and correct semantic for "current committed state" versus "what's actively being edited."
38 hoursNormalize action-button wording and add icons app-wideerdgeist
"New X" becomes "Create X" throughout (users, events, assets, menu items, nodes), matching the verb-first pattern the dashboard's own signposts already established, with a shared plus icon rather than a document-flavored one that only made sense next to "post". The "Destroy"/"destroy"/"Delete" family is normalized to "Destroy" everywhere, with a shared trash icon; occurrences#index and pages#index also pick up the destructive button class they'd been silently missing. Filter and Search convert from submit_tag to button_tag, the only way either can hold an icon alongside its label. Edit and the node editor's three dynamic labels (Continue Editing / Edit Draft / Lock + Edit) share one icon without touching their wording -- unlike Destroy's family, the state nuance in the text is real information, not just inconsistent phrasing.
38 hoursRebuild 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.
38 hoursAdd 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.
45 hoursAdd a grouped tag+node search endpoint for the upcoming dashboarderdgeist
AdminController#dashboard_search returns tags and nodes as separate, labeled groups (via ActsAsTaggableOn::Tag.named_like and Node.editor_search) rather than the flat per-node list every existing picker returns. initSearchPicker gains a renderResults callback option that, when given, replaces the default per-node rendering entirely -- lets the dashboard render its two labeled groups without a second, parallel picker implementation. resultsHeaderHtml (the "Press Enter to see all results" hint) is now threaded through as a third argument to renderResults, so a picker with custom rendering can still show it -- previously only the default per-node branch ever did. Tags link straight to the existing /admin/nodes/tags/:tag view rather than becoming an in-place filter chip.
47 hoursExtract 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.
2 daysGive 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.
3 daysAdd 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.
3 daysDo not offer to destroy the only draft of a never-published nodeerdgeist
4 daysPrevent 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.
4 daysFix current_user/uniq inconsistencies in admin#index, remove dead queryerdgeist
@mynodes used the bare @current_user ivar instead of the current_user method (works today only because login_required already forces that memoization; every other query and controller in this app uses the method), and .uniq, which is plain Enumerable#uniq here, not Relation#distinct -- it materializes every joined row into an Array before deduplicating, rather than deduplicating in SQL. @mypages had the same ivar pattern, no .order/.limit unlike every sibling query in this action, and -- confirmed via a full grep -- is referenced nowhere else in the codebase. Removed rather than bounded; nothing was ever rendering it.
4 daysAdd 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.
4 daysFix 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 >.
4 daysAdd test for backlink in revisions#indexerdgeist
4 daysInclude assets, tags and template in diff between revisionserdgeist
4 daysStop diff output from corrupting the document on structural tag changeserdgeist
sdiff aligned an inserted paragraph break correctly, but the renderer wrapped the raw </p>/<p> tokens in <ins> regardless -- invalid nesting that browsers silently repair by dropping the unmatched closing tag, leaving everything downstream rendered inside an <ins> with nothing left to close it. Same failure mode as the retired cacycle_diff.js, different cause. Tag tokens now render as a plain-text pilcrow with a tooltip naming the actual tag, never as literal markup inside <ins>/<del>. Applies to both inline and side-by-side. Side-by-side's new panel flattening a real paragraph break into a marker, rather than showing its own true structure, is a known, accepted trade-off for now -- not the same problem, and not fixed here.
4 daysEnsure that comparison view toggle is there for all diff modeserdgeist
4 daysDestroying Draft or Discarding Autosave drops you where you lefterdgeist
4 daysAdd head/draft/autosave layer comparison at three UI entry pointserdgeist
Node#resolve_page_reference and #available_layer_pairs let Page#diff_against compare named layers (head/draft/autosave), not just numbered revisions -- autosave was never part of Node#pages, so this was the missing piece. Wired into nodes#show's Status section, nodes#edit right after an autosave gets resurrected ("What changed?"), and the admin wizard's current-drafts table, which now also lists autosave-only nodes it previously never showed. revisions#diff hides the numbered-revision picker when comparing named layers (it can't represent them), shows a plain label instead, and offers buttons to switch between whichever other pairs make sense for the node's current state. Destroying the topmost layer is available directly from the diff view, reusing the existing revert! path. "Discard changes" is renamed "Discard Autosave" everywhere it appears, to match "Destroy Draft".
5 daysWhitespace fixeserdgeist
5 daysFix stale assertions and escaped body in revisions#showerdgeist
The table-based markup revisions#show's own test was asserting against (<strong>, <td>) was retired when this view moved to the node_description/node_content pattern; updated the assertions to match. Found in the same pass: @page.body had no raw(), so any real markup in a revision's body rendered as escaped entities on this page — same bug class as revisions#diff, just not yet fixed here.
5 daysRetire vendored cacycle_diff.js for a diff-lcs-based diff viewerdgeist
Revisions#diff now computes an inline or side-by-side word diff server-side (Page#diff_against, lib/html_word_diff.rb) instead of shipping raw/escaped content to the browser for a 2008-era vendored JS differ to mangle. View mode is picked via a `view` param, settable either on the diff page itself or directly from the revisions#index sticky bar next to "Diff revisions". Also fixes a pre-existing bug in RevisionsController#diff's single- revision branch, which set params[:start]/params[:end] instead of params[:start_revision]/params[:end_revision].
6 daysAdd revert!/discard and rebuild nodes#edit around the new hierarchyerdgeist
revert! discards exactly the topmost non-empty layer -- autosave if present, else draft -- and reveals whatever's beneath it, releasing the lock only once nothing is left to protect. Guards against destroying a brand-new, never-published node's only draft, which would violate the (head | draft) invariant every other method here assumes holds; the view's Destroy/Discard button is gated the same way. nodes#edit now calls lock_for_editing! instead of find_or_create_draft, and always displays autosave || draft || head, resurrecting an abandoned session's unsaved content by default with an explicit flash explaining what's shown and how to get back to the last saved version. The view drops content_for :subnavigation entirely: Show becomes "Unlock + Back", Preview stays a plain link, metadata's own <details> already replaced the old toggle, and Publish moves off this page for good, per the earlier decision to manage the publish lifecycle entirely from nodes#show. Save Draft and Save + Unlock + Exit appear both above and below the form, given the body field alone runs 600px on desktop.
6 daysFix authorship and published_at loss in autosave promotionerdgeist
Two real bugs surfaced by the full test suite, not by the new tests written for this feature -- both were latent the moment lock_for_editing! and save_draft! replaced find_or_create_draft, and only visible once an existing test exercised the exact path each one lived in. lock_for_editing! never stamped user/editor onto a draft that already existed when the lock was acquired. find_or_create_draft used to do this as part of acquiring the lock; splitting lock acquisition from draft creation dropped it entirely, since it looked like draft-creation logic rather than locking logic. Restored as an explicit step: claim authorship only if none is set yet, editorship unconditionally, matching the old behavior exactly. save_draft!'s "no draft yet" branch set user/editor before calling clone_attributes_from, whose first line is an unconditional self.reload -- silently discarding both, since neither had been persisted yet. clone_attributes_from also always copies published_at from its source without the ||= guard used for template_name, and the source here is always the autosave, whose published_at is never anything but nil -- meaning every single promotion, not just the first, was quietly resetting a published page's published_at, which publish_draft!'s own ||= Time.now would then treat as never-published and re-stamp. Fixed by running clone_attributes_from first on both branches, then applying user/editor/published_at afterward, exactly as the existing-draft branch already happened to do by accident. Four controller tests updated to insert a real put :update between get :edit and assertions that used to be true immediately after visiting edit -- deferred draft creation means edit alone no longer produces one, which is the intended consequence of this whole redesign, not something these tests were meant to catch.
6 daysFix node_content count in show-with-published-draft testerdgeist
Fallout from the earlier nodes#show heading change, which replaced the Title row with an <h1> -- unrelated to the autosave work in the preceding commits.
6 daysFix tests for the new autosave routingerdgeist
Route Save and autosave through the new Node methods update now promotes the current autosave into the draft via save_draft! rather than writing submitted params directly; autosave gets its own PUT member route so PATCH update can mean "deliberate save" specifically, matching every other custom action on this resource. Both now require the lock to already be held rather than acquiring it themselves, which is a deliberate narrowing: through the real UI this is always true, since neither can fire before edit has loaded. Two existing tests called update directly with no prior edit, which the old code tolerated by acquiring the lock as a side effect; updated to call edit first instead of loosening the guarantee back open. NodesController#edit itself is unchanged and still calls the old find_or_create_draft, so drafts are still created eagerly on entering edit for now -- switching that over is the next step, not this one.
6 daysAdd head/draft/autosave hierarchy to Nodeerdgeist
Introduces autosave_id as a third, unversioned layer above draft/head, with lock_for_editing!, autosave!, and save_draft! as the new entry points. Also fixes a real bug in wipe_draft!: its "no draft" branch unconditionally released the lock, which was safe when "no draft" only ever meant "nothing is happening" — no longer true now that a lock can exist with only an autosave beneath it. lock_for_editing! deliberately does not call wipe_draft! at all, for the same reason: an intruder calling it while a lock was genuinely held would otherwise silently steal it via wipe_draft!'s own unlock side effect, caught by the new two-user lock test.
8 daysFix test to match the current layouterdgeist
8 daysFix shared preview links breaking after a second publisherdgeist
A token's page could stop being node.head_id (superseded by a newer draft) while still being published_at.present? - the old check only compared against the current head, so a superseded page fell through to direct rendering instead of redirecting, serving a stale frozen snapshot indefinitely instead of the current live content. Also handles scheduled publishing correctly: a page can be head_id but not yet public? (published_at in the future) - that case must still render directly, not redirect into a 404 on the not-yet-live public URL.
8 daysStrengthen event/occurrence cascade test with real assertionserdgeist
test_can_remove_a_node_with_an_event previously just called node.destroy and get :index with no assertions at all - would pass whether or not occurrences were actually cleaned up, or even if index rendered correctly afterward. Now confirms occurrences genuinely exist before destroy (otherwise a passing post-destroy count of zero is meaningless - indistinguishable from "nothing to cascade in the first place"), scopes the count to this event specifically rather than a global Occurrence.count that could coincidentally pass regardless of whether this cascade works, and checks the trailing index request actually succeeds rather than just not raising. First real test of the occurrences.event_id FK constraint added earlier this session, not just the application-level dependent: :destroy.
8 daysRemove dead custom_rrule references after column droperdgeist
events#index still read event.custom_rrule - a live bug the column-drop migration introduced, not just stale test data. Caught by four test failures (three fixtures/direct attribute hashes still setting the removed column, one UnknownAttributeError from a stale fixture loaded before any test method runs), none of which were actually testing this view - "should get index" passed throughout with zero Event records present, meaning it could never have caught a per-row rendering bug. Strengthened to create one real event first, so a future stray reference to a dropped or renamed column fails loudly instead of silently passing on an empty table.
10 daysAdd self-service event creation from nodes#showerdgeist
nodes#show's events table now renders unconditionally (previously hidden entirely for a zero-event node) and gains an "add event" link. The suggested tag_list is derived from the page's own category tags via NodesHelper::DEFAULT_EVENT_TAG_BY_PAGE_TAG (erfa-detail/ chaostreff-detail -> open-day) rather than hardcoded, and degrades to a blank field for any node whose tags don't match - deliberately universal, not chapter-specific, since Updates have historically carried event dates the same way. events#new surfaces *why* the tag was pre-filled via flash.now (not flash - this is a same-request render, not a redirect), using an explicit auto_tag_source param passed alongside tag_list rather than having the controller re-derive the reason from node_id. No destroy link added to nodes#show's events list - deliberate, per existing subnav-semantics convention (destructive actions live on the resource's own views). Covered directly by test. Adds real coverage for EventsController, previously 100% commented-out scaffold, plus unit tests for the new tag-mapping helper and the weekday-abbreviation helpers from the prior commit.
14 daysAdd RRULE humanizer and wire events into nodes#showerdgeist
- app/models/concerns/rrule_humanizer.rb: new concern included into Event, renders recurring schedule as natural-language German or English from RRULE string; handles WEEKLY/MONTHLY, biweekly (INTERVAL=2), ordinal weekday positions (1TU, -1TH, -2WE), BYMONTH single-month exclusions (December pause convention); gracefully returns nil for COUNT/UNTIL/unrecognized shapes - test/models/concerns/rrule_humanizer_test.rb: 15 tests covering all distinct RRULE shapes found in production data - app/helpers/nodes_helper.rb: add event_schedule_text helper combining humanize_rrule with start_time formatting - app/views/nodes/show.html.erb: add events row, conditionally rendered when node has associated events - config/locales/de.yml, en.yml: add event_schedule_time, event_schedule_unrecognized, event_schedule_none keys
14 daysPhase 1: standalone events, external_url on nodeserdgeist
- Migration: node_id nullable on events and occurrences, add title/description/is_primary to events, external_url to nodes - Existing events marked is_primary: true (were all 1:1 with nodes) - Node: has_one :event -> has_many :events - Event: belongs_to :node optional, validates title presence for standalone events, is_primary uniqueness scoped to node_id, display_title helper falling back through node title - Occurrence: belongs_to :node optional, summary falls back to event.display_title - nodes_helper: event_information uses events.first (interim; will be replaced in Phase 3 event UI) - Tests: fix node.event -> node.events.first in event_test
14 daysFix aggregate shortcode syntax in content_controller_testerdgeist
Test used the old <aggregate ...> tag form; production templates already use the [aggregate ...] shortcode.
2026-06-27Stage 7: Rails 7.2 → 8.1 on Ruby 3.2.11erdgeist
- Bump Rails to 8.1.3 (Ruby unchanged at 3.2.11, new gemset rails8-upgrade) - config.load_defaults 8.1; merge app:update diffs for all environment files - Remove routing-filter 0.7.0; replace with native scope '(:locale)' in routes.rb and default_url_options in ApplicationController - Delete config/initializers/routing_filter_rails71_patch.rb - Replace vendored TinyMCE 3.x (~200 files) with tinymce-rails ~> 8.3; migrate admin_interface.js from jQuery .tinymce()/advanced theme to tinymce.init(); add config/tinymce.yml; note: TinyMCE 7+ is GPL - rails-i18n ~> 8.0 added explicitly (previously indirect dependency) - awesome_nested_set, acts-as-taggable-on pinned to git main/master (gemspec activerecord < 8.1 ceiling; no functional incompatibility; repin to version once upstream releases updated gemspecs) - globalize ~> 7.0, libxml-ruby ~> 5.0, nokogiri ~> 1.18, pg ~> 1.5 - sass-rails, coffee-rails, uglifier moved from :assets group to main (Sprockets 4 convention; :assets group no longer meaningful) - Node: head, draft, lock_owner marked belongs_to optional: true - Page: node, user, editor marked belongs_to optional: true - Static assets in public/images/ and public/javascripts/ referenced via plain HTML tags; Rails 8 load_defaults raises on pipeline helpers for undeclared assets - sessions_controller_test.rb: remove stale require and dead rescue_action - users_controller_test.rb: assert button[type=submit] not input[type=submit] (Rails 8 button_to renders <button> not <input>) - test_helper.rb: node.reload after children.create! (awesome_nested_set 3.9.0 does not refresh parent in memory after callback) - 129 runs, 339 assertions, 3 failures, 0 errors — identical baseline to 7.2
2026-06-27Stage 6 click-testing fixes and production setuperdgeist
- file_attachment.rb: delete old upload directory before writing replacement files; fixes orphaned variants when filename or mime type changes - assets/edit.html.erb: add file upload field and current file display; the form was previously empty and non-functional - admin.css: fix button_to hover styling; buttons now show orange hover to signal interactivity - test/controllers/users_controller_test.rb: assert input[type=submit] not anchor tag for destroy action (button_to change) - test/test_helper.rb: add I18n.locale reset in setup block - doc/rc.d_cccms: fix cccms_chdir, add start_precmd for log/pid dirs, PATH export for bash wrapper, user/pid/tcp_nopush unicorn fixes - doc/INSTALL.md: new installation guide covering all non-obvious steps - Remove parked search migration from doc/ (now in db/migrate/)
2026-06-26Stage 4: Rails 5.2 -> 6.1 on Ruby 2.7.2erdgeist
- routing-filter 0.6.3 -> 0.7.0 (Rails 6.1 compatibility) - RSS named routes rss_xml/rss_rdf added - RouteWithParams workarounds: will_paginate_patch, content_path shim, safe_path helper - Paperclip removed, replaced with FileAttachment concern (preserves URL scheme) - Assets resource moved to /admin/assets (Sprockets middleware conflict) - ApplicationRecord base class added, all models migrated - Strong parameters added to Assets, Occurrences, Events, MenuItems controllers - update_attributes -> update throughout - render :nothing -> head :ok/:not_found throughout - language_selector rewritten (removes :overwrite_params) - Environment files updated for Rails 6.1 (eager_load, public_file_server, ActionMailer) - Arel::Visitors::DepthFirst and Integer/Float duration patches removed from test_helper - AssetsController tests added (10 tests covering upload, variants, destroy, auth) - ImageMagick geometry: 460x250! for headline crop (not # which is invalid in IM6) 129 runs, 311 assertions, 5 failures (all pre-existing), 0 errors
2026-06-25Rails 5.2 test updateserdgeist
- Rename test/functional → test/controllers, test/unit → test/models - Remove test/performance/browsing_test.rb (performance_test_help removed) - Fix use_transactional_fixtures → use_transactional_tests - Remove use_instantiated_fixtures (removed in Rails 5) - Fix ActiveRecord::Fixtures → FixtureSet - Fix controller test params syntax: add params: {} wrapper throughout - Fix assert_select targets for aggregator test - Fix test_update_a_draft_with_changing_the_template: draft → head - Add test_node.reload after children.create! (awesome_nested_set bug) - Add before/after count pattern for create tests (transactional isolation) - Known failures: 5 tests affected by Rails 5 transactional test isolation
2026-06-25Upgrade to Rails 4.2.11.3erdgeist
- Bump rails 3.2.22.5 → 4.2.11.3 - Replace globalize3 with globalize ~> 5.0 (gem renamed at 5.0) - Upgrade routing-filter ~> 0.3 → ~> 0.6 - Upgrade sass-rails, coffee-rails to 4.x - Upgrade awesome_nested_set 2.x → 3.x (Rails 4 required) - Add jquery-rails for UJS support - Pin nokogiri ~> 1.10.10, loofah ~> 2.20.0, rails-html-sanitizer ~> 1.4.4 - Add config/secrets.yml (gitignored), eager_load, serve_static_files - Fix routes: add via: to all match calls, remove legacy catch-all routes - Add admin named route, fix rvm dotfiles - Fix ActiveRecord::FixtureSet rename in test_helper - Set active_support.test_order and active_record.raise_in_transactional_callbacks