| Age | Commit message (Collapse) | Author |
|
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.
|
|
|
|
|
|
|
|
The same related_assets.find_by(headline: true) query was written by
hand in three places -- content_helper, nodes#show, and the chapter
teaser partial (which was still using assets.first, the old
position-based rule, until now). Collapsed to one method, one query.
|
|
|
|
|
|
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.
|
|
GLightbox's own selector-based description feature never actually
resolved a selector in three different attempts -- literal text,
then a resolved-but-empty div, then apparently not firing at all
against the installed (unversioned) build. Replaced with a small
afterSlideLoad handler that reads a plain data-credit-selector
attribute and copies the real element's HTML into the slide's
description area directly, depending only on GLightbox handing back
a real DOM node per slide -- a much smaller contract than trusting
its own caption-resolution feature.
Renamed the credit div's class from glightbox-desc, since that name
is reserved by the library's own bundled CSS (display: none
!important) and can't be reliably overridden from here.
Also: distinguishes the credit line and the "N Bilder" caption from
ordinary body text (italic, muted, matching .author_and_date's
existing convention), and makes the lightbox's caption card follow
the site's light/dark toggle via Canvas/CanvasText, since the
library's bundled theme never adapts to it on its own.
|
|
self.assets = page.assets implicitly cleared the collection before
recreating it. Replacing it with an explicit loop, to preserve the
headline flag across clones, dropped that clearing -- every
clone_attributes_from call since has been appending related_assets
rather than replacing them. Restored with an explicit delete_all
before the copy.
update_assets (the sort_images reorder action) had the same gap for
headline specifically; it now carries the previous headline forward
when that asset is still present in the new list.
|
|
- 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.
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
Also update the node action contract to include the trash related verbs.
|
|
|
|
related_assets rows outlived their pages; head and draft carried a
dependent destroy that double-destroyed rows has_many :pages
already owns. Autosave keeps its own -- autosave pages carry no
node_id, so pages does not reach them.
|
|
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.
|
|
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).
|
|
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.
|
|
|
|
Weekdays appearing on a subset of the weeks of the month are now
aggregated into a simpler form.
|
|
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.
|
|
Also clean up some instance variable assignments on the way.
|
|
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
|
|
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.
|
|
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.
|
|
STYLES restructured from a single geometry: string per style to a
full args: array, since this fix needs -gravity and -extent
alongside -resize, which one geometry string can't express.
|
|
Sized generously (1600x1600>) rather than against today's cramped
490px content column specifically -- shrinking an oversized source
to fit a narrow container is free and looks fine; a source too
small for a future, wider redesign has no fix short of re-uploading
every image. Purely additive: nothing reads :large yet, and
generate_variants already loops over the whole STYLES hash
generically, so no other code needed to change for new uploads to
start getting it. Test suite's own hardcoded style lists updated in
the same commit so they don't quietly drift out of sync with what
the app actually defines.
|
|
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).
|
|
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.
|
|
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.
|
|
Its other two branches both require a day's inactivity before
touching anything; this one didn't, so a lock acquired seconds ago
with nothing drafted yet looked identical to one abandoned for a
week -- confirmed with a throwaway script showing a lock cleared
inside a single method call. Guarded on autosave.nil? specifically,
since the method's first line already guarantees any autosave
reaching this branch is a day old by construction.
|
|
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.
|
|
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.
|
|
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."
|
|
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.
|
|
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.
|
|
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.
|
|
search_vector is populated by a raw Postgres trigger created via
execute() in a migration -- invisible to schema.rb, which only
represents structure Rails understands. Any database rebuilt from
schema.rb rather than replayed migrations (test, a fresh db:setup,
disaster recovery) silently lost full-text search entirely, with no
error -- NULL @@ anything is never true in Postgres.
Page.ensure_search_vector_trigger!, called from an after_initialize
initializer, reinstalls the trigger via CREATE OR REPLACE on every
boot, in every environment. Idempotent and cheap.
|
|
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.
|
|
|
|
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".
|
|
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].
|
|
Editors' TinyMCE output can contain unclosed void elements like <br>,
which is valid HTML5 but invalid XML -- three different places assumed
the stricter rule and broke or silently misbehaved on the looser one.
The Atom feed's <content type="xhtml"> block required real, well-formed
XML structure but was handed a raw, unescaped body string; switched to
type="html" with CGI.escapeHTML, matching how title/summary already
handle the same content. rewrite_links_in_body used libxml's strict XML
parser to rewrite internal links to be locale-prefixed, which raised on
exactly this class of malformed markup -- silently, since the whole
method was wrapped in rescue; nil, meaning the link rewrite (not the
save) quietly failed with no error anywhere. Replaced with Nokogiri's
lenient HTML parser, which repairs malformed void elements rather than
rejecting them; also drops the bare rescue now that the actual failure
mode it was guarding against shouldn't occur, and fixes two adjacent
bugs found while in this method: a typo'd /sytem/uploads/ regex that
could never match, and a missing https:// exclusion alongside the
existing http:// one.
Also addresses stale flash messaging surfaced while testing the above:
update's save confirmation was being clobbered by edit's own "locked
and ready" notice on the very next request, since nothing distinguished
a fresh lock acquisition from a redirect back after saving. The save
confirmation now names the next step (publish from Status) and flags a
stale translation if one exists, using Page#outdated_translations?,
already present but previously unused by any controller.
|
|
A blank title failed presence and length validation simultaneously,
showing two redundant messages for one problem; length now skips
whenever slug is already blank, matching presence's own skip
condition. Separately, create's failure path never computed
@selected_kind/@parent_id/@parent_name the way new does, so
re-rendering after a validation error silently lost which kind and
parent had been chosen -- for the auto-tagging/auto-templating kinds,
that meant a corrected resubmission could silently produce a plain
generic node instead. required on the title field closes the common
case without a round trip; the server-side fix remains the actual
guarantee, since required is trivially bypassed.
|
|
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.
|