diff options
Diffstat (limited to 'CONTRIBUTING.md')
| -rw-r--r-- | CONTRIBUTING.md | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c26956c --- /dev/null +++ b/CONTRIBUTING.md | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | # Contributing to CCCMS | ||
| 2 | |||
| 3 | ## Stack | ||
| 4 | Rails 8.1, Ruby 3.2, PostgreSQL 16, ImageMagick 7 (`magick`, not `convert`). | ||
| 5 | No Sprockets pipeline for public or admin static assets except | ||
| 6 | `admin_bundle.js` — everything else is served straight from `public/` and | ||
| 7 | cache-busted via file mtime (`ApplicationHelper#mtime_busted_path`), not | ||
| 8 | fingerprinted content hashing. | ||
| 9 | |||
| 10 | ## Content model | ||
| 11 | - **Node** — the tree structure (`awesome_nested_set`-derived, migrating | ||
| 12 | toward a plain `parent_id` model — both may be present during that | ||
| 13 | transition). Holds three separate Page references: `head` (published), | ||
| 14 | `draft` (being worked on), `autosave` (in-progress typing). | ||
| 15 | - **Page** — one revision's content. `belongs_to :node`, except `autosave` | ||
| 16 | layers specifically: their `node_id` is deliberately `nil`, which is what | ||
| 17 | keeps them out of `node.pages` (the real revision history) entirely — | ||
| 18 | an autosave is not a revision. | ||
| 19 | - **Page::Translation** — Globalize's own generated class (`translates | ||
| 20 | :title, :abstract, :body` on `Page`). Not a top-level `PageTranslation` | ||
| 21 | constant, and it has `page_id`, not a `page` association reader. | ||
| 22 | |||
| 23 | ## Publishing lifecycle | ||
| 24 | There is exactly one path from an edit to a published change: | ||
| 25 | `autosave!` (writes into the autosave layer, creating it via | ||
| 26 | `clone_attributes_from` on the previous head/draft if it doesn't exist yet) | ||
| 27 | → `save_draft!` (clones the *entire* autosave into the draft layer, | ||
| 28 | wholesale — every locale, not just the one just edited) → `publish_draft!` | ||
| 29 | (promotes draft to head). Nothing writes to `draft` or `head` directly. | ||
| 30 | Locking (`lock_for_editing!`, raising `LockedByAnotherUser`) gates every | ||
| 31 | step; a lock can exist with no draft or autosave at all (acquisition is | ||
| 32 | deliberately separate from content creation). | ||
| 33 | |||
| 34 | ## Locale architecture | ||
| 35 | Presentation locale (which language the admin chrome or public page | ||
| 36 | renders in) and content-target locale (which translation a given screen is | ||
| 37 | reading or writing) are two different concerns and never share a | ||
| 38 | mechanism. The route's `:locale` segment governs the former only. The | ||
| 39 | primary node editor always targets the default locale, enforced via | ||
| 40 | `Globalize.with_locale` (never `I18n.locale`, which would leak into | ||
| 41 | generated URLs through `default_url_options`). Non-default-locale editing | ||
| 42 | uses a separately-named route param (`:translation_locale`), deliberately | ||
| 43 | distinct from `:locale` so the two can never be conflated by a link helper. | ||
| 44 | Globalize's configured fallback chain is correct for public rendering and | ||
| 45 | wrong for editing — anywhere a screen needs to know a translation's real, | ||
| 46 | unborrowed state, read the `Page::Translation` row directly rather than the | ||
| 47 | locale-dependent attribute accessor. | ||
| 48 | |||
| 49 | ## Assets | ||
| 50 | `FileAttachment` (`app/models/concerns/file_attachment.rb`) is a from- | ||
| 51 | scratch Paperclip replacement. `STYLES` is a hash of style name to a full | ||
| 52 | ImageMagick argument array (not just a geometry string — some styles need | ||
| 53 | `-gravity`/`-extent` alongside `-resize`, which a single string can't | ||
| 54 | express). `generate_variants` loops over `STYLES` generically; adding a | ||
| 55 | style needs no other code change, only `bundle exec rake | ||
| 56 | assets:regenerate_variants` to backfill existing assets. The URL contract | ||
| 57 | (`/system/uploads/:id/:style/:filename`) is stable and safe to parse | ||
| 58 | directly (the numeric id is always the second path segment) rather than | ||
| 59 | reconstructed through model methods. | ||
| 60 | |||
| 61 | ## Aggregator / shortcode system | ||
| 62 | `[aggregate ...]` in page body text is parsed by | ||
| 63 | `ContentHelper#aggregate?` into an options hash, then executed by | ||
| 64 | `Page.aggregate`. Only `tags`, `children` (`"direct"`/`"all"`, combined | ||
| 65 | with the current node), `order_by`/`order_direction`, and `limit` are | ||
| 66 | actually consumed. Only the first `[aggregate ...]` in a given body is | ||
| 67 | ever processed (`aggregate?` does a single, non-global match). | ||
| 68 | |||
| 69 | ## TinyMCE integration | ||
| 70 | `extended_valid_elements` must define a parent element and its commonly- | ||
| 71 | nested children symmetrically and explicitly together — leaving one | ||
| 72 | element on the schema's default rule while the other is explicitly | ||
| 73 | redefined can cause content loss on save, even when the child would be | ||
| 74 | perfectly valid under the untouched default alone. `valid_classes` denies | ||
| 75 | all classes by default (`'*': ''`); anything meant to render needs an | ||
| 76 | explicit per-element allowance. `content_style` injects CSS into the | ||
| 77 | editing iframe directly — the app's real public stylesheet is never loaded | ||
| 78 | there, so anything that needs to render correctly while editing needs its | ||
| 79 | own rule supplied this way. Constrained-input features (fixed placement | ||
| 80 | choices, for instance) are built as custom toolbar buttons with their own | ||
| 81 | dialog and `editor.insertContent()`, not the native image dialog or | ||
| 82 | `file_picker_callback` — those bring their own resize/edit affordances that | ||
| 83 | would undermine a genuinely fixed set of choices. | ||
| 84 | |||
| 85 | ## Testing | ||
| 86 | Shared test-only convenience methods (that need to operate across many | ||
| 87 | model/controller test files) live directly on `ActiveSupport::TestCase`, | ||
| 88 | in `test/test_helper.rb` — the framework's own designated extension point | ||
| 89 | for exactly this, not a monkey-patch of a production model class. | ||
