--
cgit v1.3
From 4705ef970469a852c7bdb4c097ba748e972c8f63 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Mon, 29 Jun 2026 16:26:31 +0200
Subject: Add chapter_detail template with events and external URL rendering
New public page template for erfa and chaostreff nodes. Renders the
node's external_url and lists associated events below the page content.
Seed script updated to set template_name: 'chapter_detail' on all
chapter drafts so the template is applied on publish.
---
.../page_templates/public/chapter_detail.html.erb | 23 ++++++++++++++++++++++
db/seeds/chapters.rb | 1 +
2 files changed, 24 insertions(+)
create mode 100644 app/views/custom/page_templates/public/chapter_detail.html.erb
(limited to 'app/views')
diff --git a/app/views/custom/page_templates/public/chapter_detail.html.erb b/app/views/custom/page_templates/public/chapter_detail.html.erb
new file mode 100644
index 0000000..71352d3
--- /dev/null
+++ b/app/views/custom/page_templates/public/chapter_detail.html.erb
@@ -0,0 +1,23 @@
+
\ No newline at end of file
+
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 7223219..2ce3853 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -48,7 +48,11 @@
<% @node.events.order(:start_time).each do |event| %>
-
<%= event_schedule_text(event) %><%= " (primary)" if event.is_primary? %>
<% end %>
--
cgit v1.3
From 206dc5c50a73c5402b90d7fdc8945d3ba9356758 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sat, 4 Jul 2026 01:24:55 +0200
Subject: Add self-service event creation from nodes#show
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.
---
app/controllers/events_controller.rb | 4 +
app/helpers/nodes_helper.rb | 14 +++
app/views/nodes/show.html.erb | 4 +-
test/controllers/events_controller_test.rb | 167 ++++++++++++++++++++-------
test/controllers/nodes_controller_test.rb | 33 ++++++
test/models/concerns/rrule_humanizer_test.rb | 15 +++
test/models/helpers/content_helper_test.rb | 4 +
test/models/helpers/nodes_helper_test.rb | 21 ++++
8 files changed, 219 insertions(+), 43 deletions(-)
(limited to 'app/views')
diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb
index 9bed5dd..b98a38e 100644
--- a/app/controllers/events_controller.rb
+++ b/app/controllers/events_controller.rb
@@ -37,6 +37,10 @@ class EventsController < ApplicationController
tag_list: params[:tag_list]
)
+ if params[:tag_list].present? && params[:auto_tag_source].present?
+ flash.now[:notice] = "Tag '#{params[:tag_list]}' was pre-filled because this page is tagged '#{params[:auto_tag_source]}'. You can remove it below."
+ end
+
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @event }
diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb
index 329bcc5..093bfc6 100644
--- a/app/helpers/nodes_helper.rb
+++ b/app/helpers/nodes_helper.rb
@@ -44,6 +44,20 @@ module NodesHelper
])
end
+ DEFAULT_EVENT_TAG_BY_PAGE_TAG = {
+ 'erfa-detail' => 'open-day',
+ 'chaostreff-detail' => 'open-day'
+ }.freeze
+
+ def default_event_tag_mapping(page)
+ page_tags = page.tag_list
+ DEFAULT_EVENT_TAG_BY_PAGE_TAG.find { |page_tag, _| page_tags.include?(page_tag) }
+ end
+
+ def default_event_tag_list(page)
+ default_event_tag_mapping(page)&.last
+ end
+
def event_schedule_text(event)
if event.rrule.present?
recurrence = event.humanize_rrule(I18n.locale)
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 2ce3853..c533a55 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -42,7 +42,6 @@
diff --git a/test/controllers/events_controller_test.rb b/test/controllers/events_controller_test.rb
index 14e534e..9371ca7 100644
--- a/test/controllers/events_controller_test.rb
+++ b/test/controllers/events_controller_test.rb
@@ -1,45 +1,130 @@
require 'test_helper'
class EventsControllerTest < ActionController::TestCase
- # test "should get index" do
- # get :index
- # assert_response :success
- # assert_not_nil assigns(:events)
- # end
- #
- # test "should get new" do
- # get :new
- # assert_response :success
- # end
- #
- # test "should create event" do
- # assert_difference('Event.count') do
- # post :create, params: { :event => { } }
- # end
- #
- # assert_redirected_to event_path(assigns(:event))
- # end
- #
- # test "should show event" do
- # get :show, params: { :id => events(:one).to_param }
- # assert_response :success
- # end
- #
- # test "should get edit" do
- # get :edit, params: { :id => events(:one).to_param }
- # assert_response :success
- # end
- #
- # test "should update event" do
- # put :update, params: { :id => events(:one).to_param, :event => { } }
- # assert_redirected_to event_path(assigns(:event))
- # end
- #
- # test "should destroy event" do
- # assert_difference('Event.count', -1) do
- # delete :destroy, params: { :id => events(:one).to_param }
- # end
- #
- # assert_redirected_to events_path
- # end
+
+ test "should get index" do
+ login_as :quentin
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:events)
+ end
+
+ test "should get new" do
+ login_as :quentin
+ get :new
+ assert_response :success
+ end
+
+ test "new pre-fills tag_list and explains it via flash when auto_tag_source is given" do
+ login_as :quentin
+ node = create_node_with_published_page
+
+ get :new, params: { node_id: node.id, tag_list: "open-day", auto_tag_source: "erfa-detail" }
+
+ assert_response :success
+ assert_equal "open-day", assigns(:event).tag_list.to_s
+ assert_match "open-day", flash[:notice]
+ assert_match "erfa-detail", flash[:notice]
+ end
+
+ test "new does not flash without an auto_tag_source" do
+ login_as :quentin
+
+ get :new, params: { tag_list: "open-day" }
+
+ assert_response :success
+ assert_nil flash[:notice]
+ end
+
+ test "new with no params renders a blank, unflashed form" do
+ login_as :quentin
+
+ get :new
+
+ assert_response :success
+ assert_nil assigns(:event).tag_list.presence
+ assert_nil flash[:notice]
+ end
+
+ test "should show event" do
+ login_as :quentin
+ node = create_node_with_published_page
+ event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ get :show, params: { id: event.id }
+ assert_response :success
+ end
+
+ test "should get edit" do
+ login_as :quentin
+ node = create_node_with_published_page
+ event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ get :edit, params: { id: event.id }
+ assert_response :success
+ end
+
+ test "should create event attached to a node" do
+ login_as :quentin
+ node = create_node_with_published_page
+
+ assert_difference('Event.count') do
+ post :create, params: {
+ event: {
+ node_id: node.id,
+ start_time: Time.now,
+ end_time: Time.now + 1.hour,
+ tag_list: "open-day"
+ }
+ }
+ end
+
+ assert_redirected_to edit_node_path(node)
+ assert_equal 'Event was successfully created.', flash[:notice]
+ end
+
+ test "should not create an event without a title or a node_id" do
+ login_as :quentin
+
+ assert_no_difference('Event.count') do
+ post :create, params: { event: { start_time: Time.now, end_time: Time.now + 1.hour } }
+ end
+
+ assert_response :success # re-renders :new, not a redirect
+ end
+
+ test "should honour return_to on create" do
+ login_as :quentin
+ node = create_node_with_published_page
+
+ post :create, params: {
+ event: { node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour },
+ return_to: node_path(node)
+ }
+
+ assert_redirected_to node_path(node)
+ end
+
+ test "should update event" do
+ login_as :quentin
+ node = create_node_with_published_page
+ event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ put :update, params: { id: event.id, event: { title: "Updated title" } }
+
+ assert_redirected_to events_path
+ assert_equal "Updated title", event.reload.title
+ end
+
+ test "should destroy event" do
+ login_as :quentin
+ node = create_node_with_published_page
+ event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ assert_difference('Event.count', -1) do
+ delete :destroy, params: { id: event.id }
+ end
+
+ assert_redirected_to events_url
+ end
end
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index 53799f1..f14e27c 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -379,4 +379,37 @@ class NodesControllerTest < ActionController::TestCase
get :index
end
+ test "show renders events row and add-link for zero-event chapter node" do
+ login_as :quentin
+ node = create_node_with_published_page
+ node.head.tag_list = "erfa-detail"
+ node.head.save!
+
+ get :show, params: { id: node.id }
+ assert_response :success
+ assert_select "a", text: "add event"
+ assert_select "a[href*='tag_list=open-day']"
+ assert_select "a[href*='auto_tag_source=erfa-detail']"
+ end
+
+ test "show renders events row without a tag default for untagged node" do
+ login_as :quentin
+ node = create_node_with_published_page
+
+ get :show, params: { id: node.id }
+ assert_response :success
+ assert_select "a", text: "add event"
+ assert_select "a[href*='tag_list=']", count: 0
+ end
+
+ test "show never renders a destroy link for events" do
+ login_as :quentin
+ node = create_node_with_published_page
+ Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ get :show, params: { id: node.id }
+ assert_response :success
+ assert_select "form.button_to.destructive", count: 0
+ end
+
end
diff --git a/test/models/concerns/rrule_humanizer_test.rb b/test/models/concerns/rrule_humanizer_test.rb
index 500dbc7..279ff73 100644
--- a/test/models/concerns/rrule_humanizer_test.rb
+++ b/test/models/concerns/rrule_humanizer_test.rb
@@ -81,4 +81,19 @@ class RruleHumanizerTest < ActiveSupport::TestCase
test "falls back to english for unknown locale" do
assert_equal "Every Tuesday", humanize("FREQ=WEEKLY;BYDAY=TU", :fr)
end
+
+ test "wday_abbr returns the correct German abbreviation for each day" do
+ monday = Time.parse("2026-07-06") # confirmed Monday
+ assert_equal "Mo", RruleHumanizer.wday_abbr(monday, :de)
+ assert_equal "Di", RruleHumanizer.wday_abbr(monday + 1.day, :de)
+ assert_equal "Mi", RruleHumanizer.wday_abbr(monday + 2.days, :de)
+ assert_equal "Do", RruleHumanizer.wday_abbr(monday + 3.days, :de)
+ assert_equal "Fr", RruleHumanizer.wday_abbr(monday + 4.days, :de)
+ assert_equal "Sa", RruleHumanizer.wday_abbr(monday + 5.days, :de)
+ assert_equal "So", RruleHumanizer.wday_abbr(monday + 6.days, :de)
+ end
+
+ test "wday_abbr falls back to :de for an unrecognized locale" do
+ assert_equal "Mo", RruleHumanizer.wday_abbr(Time.parse("2026-07-06"), :fr)
+ end
end
diff --git a/test/models/helpers/content_helper_test.rb b/test/models/helpers/content_helper_test.rb
index 2da82d7..a7ed478 100644
--- a/test/models/helpers/content_helper_test.rb
+++ b/test/models/helpers/content_helper_test.rb
@@ -1,4 +1,8 @@
require 'test_helper'
class ContentHelperTest < ActionView::TestCase
+ test "weekday_abbr delegates through the current I18n locale" do
+ I18n.locale = :de
+ assert_equal "Mo", weekday_abbr(Time.parse("2026-07-06"))
+ end
end
diff --git a/test/models/helpers/nodes_helper_test.rb b/test/models/helpers/nodes_helper_test.rb
index 13011de..5d91a88 100644
--- a/test/models/helpers/nodes_helper_test.rb
+++ b/test/models/helpers/nodes_helper_test.rb
@@ -1,4 +1,25 @@
require 'test_helper'
class NodesHelperTest < ActionView::TestCase
+ FakePage = Struct.new(:tag_list)
+
+ test "default_event_tag_mapping matches erfa-detail" do
+ page = FakePage.new(["erfa-detail"])
+ assert_equal ["erfa-detail", "open-day"], default_event_tag_mapping(page)
+ end
+
+ test "default_event_tag_mapping matches chaostreff-detail" do
+ page = FakePage.new(["chaostreff-detail"])
+ assert_equal ["chaostreff-detail", "open-day"], default_event_tag_mapping(page)
+ end
+
+ test "default_event_tag_mapping returns nil for unrelated tags" do
+ page = FakePage.new(["update"])
+ assert_nil default_event_tag_mapping(page)
+ end
+
+ test "default_event_tag_list is nil without a matching tag" do
+ page = FakePage.new([])
+ assert_nil default_event_tag_list(page)
+ end
end
--
cgit v1.3
From 9c630d186003a47563be2e3547485060dac7dd98 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sat, 4 Jul 2026 01:34:42 +0200
Subject: Retire event_information, superseded by nodes#show's event manager
The metadata-panel "Event" field on nodes#edit called event_information,
a second, tag-less implementation of "list events + add one" - now
redundant now that nodes#show has the tag-aware version with the
auto-tag flash explanation. Confirmed via grep this was the only call
site before removing both it and the method definition.
---
app/helpers/nodes_helper.rb | 15 ---------------
app/views/nodes/edit.html.erb | 3 ---
2 files changed, 18 deletions(-)
(limited to 'app/views')
diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb
index 093bfc6..2baf813 100644
--- a/app/helpers/nodes_helper.rb
+++ b/app/helpers/nodes_helper.rb
@@ -29,21 +29,6 @@ module NodesHelper
User.all.map {|u| [u.login, u.id]}
end
- def event_information
- events = @node.events.order(:start_time)
- items = events.map do |event|
- safe_join([
- "#{event.start_time&.to_fs(:db)} - #{event.end_time&.to_fs(:db)} > ",
- link_to('edit', edit_event_path(event)),
- ])
- end
- safe_join([
- safe_join(items, ' | '),
- ' > ',
- link_to('add event', new_event_path(:node_id => @node.id))
- ])
- end
-
DEFAULT_EVENT_TAG_BY_PAGE_TAG = {
'erfa-detail' => 'open-day',
'chaostreff-detail' => 'open-day'
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb
index 498fa1f..31f585c 100644
--- a/app/views/nodes/edit.html.erb
+++ b/app/views/nodes/edit.html.erb
@@ -15,9 +15,6 @@
<% end %>
-
Event
-
<%= event_information %>
-
Slug
<%= f.text_field(
--
cgit v1.3
From 1fa2f3821497d5529a6753cee84cf5ca679e8bcc Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sun, 5 Jul 2026 21:49:21 +0200
Subject: Fix admin search results colliding with other search widgets
layouts/admin.html.erb's top-bar search results div shared id
"search_results" with nodes#new/nodes#edit's parent-search widget -
since the layout renders on every admin page, whichever widget's JS
ran last silently won the shared ID, putting top-bar results into the
parent-search box on affected pages. Renamed to "menu_search_results"
and updated admin_search.js to match.
Also modernizes admin_search.js's event bindings from keyup/keydown/
keypress/paste/cut + .attr("value") to input + .val() throughout
(menu_items, parent_search, move_to_search) - the multi-event binding
was working around old IE compatibility that .val() + "input" already
handles correctly. parent_search's result rendering now matches
menu_search's convention (real
"));
- // Sometimes I don't get jquery; wrap() didn't work *sigh*
- // Guess I'll need a book someday or another framework
- var wrapper = $("");
- $(wrapper).append(link);
+ $(link).bind("click", parent_search.link_closure(results[result]));
- $("#search_results").append(wrapper);
+ $("#search_results").append(link);
found = true;
}
if (found)
@@ -166,51 +175,61 @@ parent_search = {
link_closure : function(node) {
var barf = function(){
- $("#parent_search_term").attr("value", node.title);
- $("#parent_id").attr("value", node.node_id);
+ $("#parent_search_term").val(node.title);
+ $("#parent_id").val(node.node_id).attr("data-unique-name", node.unique_name);
$('#search_results').slideUp();
$('#search_results').empty();
+ parent_search.update_resulting_path();
return false;
}
return barf;
},
- initialize_radio_buttons : function() {
- $("#kind_top_level").bind("change", function(){
- $("#parent_search_field").hide();
- });
+ update_resulting_path : function() {
+ var kind = $("input[name='kind']:checked");
+ var title = $("#title").val();
- $("#kind_update").bind("change", function(){
- $("#parent_search_field").hide();
- });
+ if (title === "") {
+ $("#resulting_path").text("—");
+ return;
+ }
- $("#kind_press_release").bind("change", function(){
- $("#parent_search_field").hide();
- });
+ var prefix = kind.val() === "generic"
+ ? ($("#parent_id").attr("data-unique-name") || "")
+ : (kind.attr("data-path-prefix") || "");
- $("#kind_generic").bind("change", function(){
- $("#parent_search_field").show();
- });
+ clearTimeout(parent_search.path_timeout);
+ parent_search.path_timeout = setTimeout(function() {
+ $.get("/nodes/parameterize_preview", { title: title }, function(slug) {
+ $("#resulting_path").text(window.location.origin + "/" + (prefix ? prefix + "/" : "") + slug);
+ });
+ }, 300);
+ },
+ initialize_radio_buttons : function() {
+ $("input[name='kind']").bind("change", function(){
+ if ($(this).val() === "generic") {
+ $("#parent_search_field").show();
+ } else {
+ $("#parent_search_field").hide();
+ }
+ parent_search.update_resulting_path();
+ });
}
}
move_to_search = {
initialize_search : function() {
- $("#move_to_search_term").bind("keyup", function() { move_to_search.do_search($(this))});
- $("#move_to_search_term").bind("keydown", function() { move_to_search.do_search($(this))});
- $("#move_to_search_term").bind("keypress", function() { move_to_search.do_search($(this))});
- $("#move_to_search_term").bind("paste", function() { move_to_search.do_search($(this))});
- $("#move_to_search_term").bind("cut", function() { move_to_search.do_search($(this))});
+ $("#move_to_search_term").bind("input", function() {move_to_search.do_search($(this))});
},
do_search : function(_this) {
- if (_this.attr("value")) {
+ if (_this.val()) {
$.ajax({
type: "GET",
url: ADMIN_MENU_SEARCH_URL,
- data: "search_term=" + _this.attr("value"),
+ data: "search_term=" + _this.val(),
dataType: "json",
success : function(results) {
move_to_search.show_results(results);
@@ -248,8 +267,8 @@ move_to_search = {
link_closure : function(node) {
var barf = function(){
- $("#move_to_search_term").attr("value", node.title);
- $("#node_staged_parent_id").attr("value", node.node_id);
+ $("#move_to_search_term").val(node.title);
+ $("#node_staged_parent_id").val(node.node_id);
$('#search_results').slideUp();
$('#search_results').empty();
return false;
--
cgit v1.3
From 9a7ce465b06462443ee82e8418f3c070de080ab2 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sun, 5 Jul 2026 21:52:11 +0200
Subject: Surface node-kind conventions in the admin UI
nodes#new: each kind's radio button now shows a label plus a smaller,
muted .field_hint explaining what happens automatically (parent,
tags, template) - previously a single undifferentiated string mixing
a short label with a long parenthetical, cluttering the smaller kinds
(erfa/chaostreff) worst. Title field gets the same hint treatment,
telling editors up front that the slug is auto-generated and where to
adjust it later - the previous silence here meant editors only
discovered slug generation after already committing to a title, with
no visible way to fix it.
New "Resulting path" row live-previews the full URL (origin + kind's
path prefix + a debounced call to the new parameterize_preview route,
reusing the exact same slug_for helper "create" now calls, so the
preview can never diverge from what actually gets created) as the
editor types, plus a copy-to-clipboard button. For "generic", the
prefix comes from whichever parent gets chosen via the search widget,
not the registry - parent_search.link_closure now stashes the chosen
node's unique_name as a data attribute for exactly this.
New read-only admin/conventions view dumps the NODE_KINDS registry as
a reference table for whoever's confused later about how a given kind
behaves - deliberately just renders the same label/hint text used on
nodes#new rather than re-describing parent/tags/template separately,
since decomposing the parent Proc would mean either invoking it
(Update's has a real side effect - creates the year folder) or
re-stating its logic a second time in a different shape, either of
which risks drifting from the actual behavior.
nodes#edit's Template field gets a one-line hint noting the value may
already be pre-filled based on how the node was created - the one
place the previous commit's new inheritance mechanism actually
surfaces on this view.
---
app/helpers/application_helper.rb | 4 ++++
app/views/nodes/edit.html.erb | 5 ++++-
app/views/nodes/new.html.erb | 34 +++++++++++++++++++---------------
config/routes.rb | 1 +
public/stylesheets/admin.css | 12 ++++++++++++
5 files changed, 40 insertions(+), 16 deletions(-)
(limited to 'app/views')
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0be66e9..87aa82c 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -11,4 +11,8 @@ module ApplicationHelper
end
end
end
+
+ def resolve_kind_text(value)
+ value.respond_to?(:call) ? value.call : value
+ end
end
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb
index 31f585c..2d0edc1 100644
--- a/app/views/nodes/edit.html.erb
+++ b/app/views/nodes/edit.html.erb
@@ -44,7 +44,10 @@
+ <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %>
+ Set automatically based on how this node was created - change it if needed.
+
- <%= radio_button_tag :kind, "top_level" %>
- Top Level
-
-
- <%= radio_button_tag :kind, "generic", :selected => true %>
- Generic ( can be created anywhere )
-
-
- <%= radio_button_tag :kind, "update" %>
- Update ( is automatically created in /updates/<%= Time.now.year.to_s %>/ and gets tag "update" )
-
+ <% CccConventions::NODE_KINDS.each do |kind, config| %>
- <%= radio_button_tag :kind, "press_release" %>
- Pressemitteilung ( is automatically created in /updates/<%= Time.now.year.to_s %>/ and gets tags "update, pressemitteilung" )
+ <%= radio_button_tag :kind, kind, kind == "generic",
+ data: { path_prefix: resolve_kind_text(config[:path_prefix]) } %>
+ <%= resolve_kind_text(config[:label]) %>
+ <% if config[:hint] %>
+ <%= resolve_kind_text(config[:hint]) %>
+ <% end %>
+ <% end %>
Title
-
<%= text_field_tag :title %>
+
<%= text_field_tag :title %>
+ A URL slug will be generated from this automatically. You can review or adjust it afterward under the "metadata" section's Slug field.
+
Parent
@@ -45,6 +41,14 @@
+
+
Resulting path
+
+ —
+
+ This preview updates as you type. The final path can still be changed afterward by editing the title (for the last segment) or moving the node to a different parent (for everything before it).
+
+
<%= submit_tag "Create" %>
diff --git a/config/routes.rb b/config/routes.rb
index cf4733c..6c07414 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -59,6 +59,7 @@ Cccms::Application.routes.draw do
match 'admin' => 'admin#index', :as => :admin, :via => :get
match 'admin/search' => 'admin#search', :as => :admin_search, :via => :get
match 'admin/menu_search' => 'admin#menu_search', :as => :admin_menu_search, :via => :get
+ match 'admin/conventions' => 'admin#conventions', :as => :admin_conventions, :via => :get
match 'search' => 'search#index', :as => :search, :via => :get
resources :users
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index a02bd61..f240bb2 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -508,6 +508,14 @@ table tr.header {
text-align: left;
}
+.field_hint {
+ display: block;
+ margin-top: 2px;
+ font-size: 0.75rem;
+ color: #969696;
+ padding-bottom: 4px;
+}
+
#search_widget {
position: absolute;
top: 20px;
@@ -527,18 +535,21 @@ table tr.header {
font-size: 18px;
}
+#menu_search_results p,
#search_results p {
margin: 0;
padding: 4px 4px 0 4px;
border-bottom: 1px solid #e8e8e8;
}
+#menu_search_results p a,
#search_results p a {
display: block;
font-weight: bold;
font-size: 0.95rem;
}
+#menu_search_results p span.result_path,
#search_results p span.result_path {
display: block;
margin-top: 2px;
@@ -548,6 +559,7 @@ table tr.header {
padding-bottom: 4px;
}
+#menu_search_results p.search_more,
#search_results p.search_more {
margin: 0;
padding: 6px 4px;
--
cgit v1.3
From 4c47031f9693fc04b01cbb005ad864cd1e3d2425 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Mon, 6 Jul 2026 04:15:46 +0200
Subject: Add erfa/chaostreff node-kind creation conventions, part 2
The following changes were already announced in the last commit, but
the files forgotten. Here's them actually attached.
lib/ccc_conventions.rb: NODE_KINDS registry replaces the kind-specific
branches previously scattered across nodes_controller#create (tags),
unique_path-position check). Each kind declares its parent lookup
(a Proc - Update's "update"/"press_release" continue to genuinely
find-or-create their year-folder via Update.find_or_create_parent;
erfa/chaostreff use a plain find_by_unique_name!, since their parent
nodes are fixed and a missing one should fail loudly, not be silently
created), its tags, and (new) its default template.
New read-only admin/conventions view dumps the NODE_KINDS registry as
a reference table for whoever's confused later about how a given kind
behaves - deliberately just renders the same label/hint text used on
nodes#new rather than re-describing parent/tags/template separately,
since decomposing the parent Proc would mean either invoking it
(Update's has a real side effect - creates the year folder) or
re-stating its logic a second time in a different shape, either of
which risks drifting from the actual behavior.
---
app/views/admin/conventions.html.erb | 11 +++++++++
lib/ccc_conventions.rb | 47 ++++++++++++++++++++++++++++++++++++
2 files changed, 58 insertions(+)
create mode 100644 app/views/admin/conventions.html.erb
create mode 100644 lib/ccc_conventions.rb
(limited to 'app/views')
diff --git a/app/views/admin/conventions.html.erb b/app/views/admin/conventions.html.erb
new file mode 100644
index 0000000..72f6214
--- /dev/null
+++ b/app/views/admin/conventions.html.erb
@@ -0,0 +1,11 @@
+
+ <% if @node.draft.preview_token.present? %>
+ <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %>
+ <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." } } %>
+ <% else %>
+ <%= button_to 'Create public preview link', generate_shared_preview_node_path(@node), method: :put %>
+ <% end %>
+
+
+ <% end %>
Author
<%= @page.user.try(:login) %>
diff --git a/config/routes.rb b/config/routes.rb
index 6c07414..26c3d4d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -38,6 +38,8 @@ Cccms::Application.routes.draw do
member do
put :unlock
put :publish
+ put :generate_shared_preview
+ put :revoke_shared_preview
end
resources :revisions do
@@ -50,6 +52,8 @@ Cccms::Application.routes.draw do
end
end
+ get 'preview/:token', to: 'shared_previews#show', as: :shared_preview
+
scope '/admin' do
resources :assets
end
--
cgit v1.3
From 36b5324900968afc7498950c14ad13be5c061a7c Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Mon, 6 Jul 2026 16:49:45 +0200
Subject: Restructure nodes#show and nodes#new around
node_description/node_content
nodes#show's flat
of label/value pairs grew unreadable once
Admin Preview and Public Preview were added alongside Public Link -
switched to the div.node_description/div.node_content pattern already
used by nodes#edit, grouped into People/Dates/Links/Revisions/Tags/
Events/Children sections rather than one undifferentiated list.
People/Dates/Links use a flex-based item layout, not a second
-
multiple independent tables sharing one outer width but different
column counts produced misaligned columns with no shared grid. Flex
items size to their own content with a shared minimum instead.
Revisions and Children are both collapsed via , no JS needed -
previously nodes#show didn't list a node's children at all, blocking
the ability to find/navigate erfa and chaostreff pages without already
knowing they're now standalone nodes. Revision list items link to
node_revision_path(@node, page) - the actual per-revision diff/restore
view, not the plain index.
nodes#new converted to the same pattern for visual consistency, plus a
few things surfaced along the way: submit buttons were unstyled
site-wide (a bare input[type=submit] with no border, fill, or hover
state - Create on this exact page was easy to miss entirely), fixed
with a bordered/bold treatment reusing the existing form.button_to
visual language rather than introducing a fourth button style. Title
regained the bold weight it had before the table rewrite dropped it
silently. Tag list grouping and full link/button semantic taxonomy
(show vs edit/add vs publish/revoke vs destroy, applied consistently
across every link on the page, not just buttons) are known follow-ups,
not attempted here.
---
app/views/nodes/new.html.erb | 57 ++++++------
app/views/nodes/show.html.erb | 204 ++++++++++++++++++++++++------------------
public/stylesheets/admin.css | 64 +++++++++----
3 files changed, 192 insertions(+), 133 deletions(-)
(limited to 'app/views')
diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb
index f557454..0a05325 100644
--- a/app/views/nodes/new.html.erb
+++ b/app/views/nodes/new.html.erb
@@ -6,13 +6,11 @@
<% end %>
-
What kind of node do you want to create?
-
<%= form_tag nodes_path do %>
-
-
-
Type
-
+
+
+
Type
+
<% CccConventions::NODE_KINDS.each do |kind, config| %>
+ <%= text_field_tag :title %>
A URL slug will be generated from this automatically. You can review or adjust it afterward under the "metadata" section's Slug field.
-
—This preview updates as you type. The final path can still be changed afterward by editing the title (for the last segment) or moving the node to a different parent (for everything before it).
-
<% CccConventions::NODE_KINDS.each do |kind, config| %>
- <%= radio_button_tag :kind, kind, kind == "generic",
+ <%= radio_button_tag :kind, kind, kind == @selected_kind,
data: { path_prefix: resolve_kind_text(config[:path_prefix]) } %>
<%= resolve_kind_text(config[:label]) %>
<% if config[:hint] %>
@@ -29,7 +29,7 @@
A URL slug will be generated from this automatically. You can review or adjust it afterward under the "metadata" section's Slug field.
+ Builds the field below automatically. If your pattern is more complex than this covers, just edit it directly below - these controls won't touch it unless you use them.
+
- <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %>
- Set automatically based on how this node was created - change it if needed.
-
+ <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %>
+ Set automatically based on how this node was created - change it if needed.
--
cgit v1.3
From b555a3e05c44f21067855b6bbafa70a5e32c9a40 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Wed, 8 Jul 2026 11:56:39 +0200
Subject: Promote page title to headline in nodes#show
---
app/views/nodes/show.html.erb | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
(limited to 'app/views')
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 71dbaa3..2ab7986 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -4,8 +4,8 @@
<% end %>
+
<%= title_for_node(@node) %>
-
People
@@ -128,9 +128,6 @@
<% end %>
-
Title
-
<%= sanitize(@page.title) %>
-
Abstract
<%= sanitize(@page.abstract) %>
--
cgit v1.3
From 43e7743ebff1b1f41bbb5ae39bcc5bed7d8982e3 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Wed, 8 Jul 2026 16:24:05 +0200
Subject: Autosave posts to its own endpoint surface lock loss immediately
Previously piggybacked on the main form's action and relied on a
.js.erb response to update #flash; now posts to the dedicated
autosave route with an explicit _method=put override, and reacts to
a 423 by stopping the timer and showing a persistent banner rather
than letting the next Save discover the conflict on its own.
---
app/views/nodes/edit.html.erb | 2 +-
public/javascripts/admin_interface.js | 38 ++++++++++++++++++++++++++++++++---
public/stylesheets/admin.css | 5 +++++
3 files changed, 41 insertions(+), 4 deletions(-)
(limited to 'app/views')
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb
index 4693569..1c6cc3a 100644
--- a/app/views/nodes/edit.html.erb
+++ b/app/views/nodes/edit.html.erb
@@ -5,7 +5,7 @@
<% end %>
-<%= form_for(@node) do |f| %>
+ <%= form_for(@node, html: { data: { autosave_url: autosave_node_path(@node), show_url: node_path(@node) } }) do |f| %>
<% if @node.errors.any? %>