From 51629c5c42270a346885057a441095c964101cc1 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 30 Jun 2026 03:55:42 +0200 Subject: Fix events CRUD for standalone events and add events to admin menu - event_params now permits title, description, is_primary - event_information helper lists all node.events, not just the first - Occurrence.generate handles nil node (standalone events) - Page.aggregate order_by title uses correlated subquery to avoid GROUP BY conflict with tag-filter path; order_direction whitelisted to ASC/DESC to prevent SQL injection - Events link added to admin menu bar - events/index shows title, is_primary; drops latitude/longitude columns --- app/views/admin/_menu.html.erb | 1 + 1 file changed, 1 insertion(+) (limited to 'app/views/admin/_menu.html.erb') diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 6dba085..c87c5f7 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -4,5 +4,6 @@ search <%= link_to 'Nodes', nodes_path, selected?('nodes') %> <%= link_to 'Assets', assets_path, selected?('assets') %> +<%= link_to 'Events', events_path, selected?('events') %> <%= link_to 'User', users_path, selected?('users') %> <%= link_to 'Navigation', menu_items_path, selected?('menu_items') %> >  -- cgit v1.3 From edb7f16ea65328c30e5be758a069337ade6a95ba Mon Sep 17 00:00:00 2001 From: erdgeist Date: Thu, 9 Jul 2026 20:13:49 +0200 Subject: Remove subnavigation after the last consumer has gone --- app/views/admin/_menu.html.erb | 2 +- app/views/layouts/admin.html.erb | 3 --- public/stylesheets/admin.css | 21 --------------------- 3 files changed, 1 insertion(+), 25 deletions(-) (limited to 'app/views/admin/_menu.html.erb') diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index c87c5f7..4302987 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -6,4 +6,4 @@ <%= link_to 'Assets', assets_path, selected?('assets') %> <%= link_to 'Events', events_path, selected?('events') %> <%= link_to 'User', users_path, selected?('users') %> -<%= link_to 'Navigation', menu_items_path, selected?('menu_items') %> >  +<%= link_to 'Navigation', menu_items_path, selected?('menu_items') %> diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 79b1380..fb7d6da 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -25,9 +25,6 @@ -
<% if flash[:notice].present? || flash[:error].present? %> diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index fa36c18..38c9e5a 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -133,7 +133,6 @@ input[type=radio] { } #main_navigation a, -#sub_navigation a, #metadata a, #overview_toggle a { letter-spacing: 1px; @@ -149,26 +148,6 @@ input[type=radio] { background-color: #ff9600; } -/* #sub_navigation covers 13 views' Show/Preview/Revisions/metadata - links (see `grep -rn "content_for :subnavigation" app/views/`), - including nodes#edit -- so #page_editor's own subnav links are - already covered here and are not part of the new scoped-underline - rule above. Proposed for full removal per the redesign notes; the - 13-file list is the confirmation needed before scoping that work. */ -#sub_navigation a { - color: #969696; -} - -#sub_navigation a:hover { - color: #ffffff; - background-color: #ff9600; -} - -#sub_navigation a.selected { - color: #ffffff; - background-color: #ff9600; -} - .admin_content_spacer { clear: both; margin-bottom: 40px; -- cgit v1.3 From b271648f89cba7cafafa1b73b1658b1c1bc0e4b0 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sun, 12 Jul 2026 23:42:43 +0200 Subject: Rebuild the admin dashboard: icon nav, search, signposts, widgets 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. --- app/controllers/admin_controller.rb | 21 +-- app/helpers/nodes_helper.rb | 12 ++ app/models/node.rb | 6 +- app/views/admin/_menu.html.erb | 14 +- app/views/admin/index.html.erb | 210 ++++++++++-------------------- test/controllers/admin_controller_test.rb | 20 --- test/controllers/nodes_controller_test.rb | 20 +++ test/models/node_test.rb | 24 ++++ 8 files changed, 134 insertions(+), 193 deletions(-) (limited to 'app/views/admin/_menu.html.erb') diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 8445997..37fd78b 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -5,25 +5,8 @@ class AdminController < ApplicationController before_action :login_required def index - @drafts = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL") - .limit(50).order("updated_at desc") - - @drafts_count = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL").count - - @recent_changes = Node.where( - "updated_at < ? AND updated_at > ? AND parent_id IS NOT NULL", - Time.now, Time.now - 14.days - ).limit(50).order("updated_at desc") - - ordered_with_level = Node.root.self_and_descendants_ordered_with_level - @sitemap_depth = {} - ordered_with_level.each { |node, level| @sitemap_depth[node.id] = level } - @sitemap = ordered_with_level.map(&:first).reject(&:update?) - - @mynodes = Node.joins(:pages) - .where("pages.user_id = ? or pages.editor_id = ?", current_user, current_user) - .order("updated_at desc") - .distinct.first(50) + @drafts = Node.drafts_and_autosaves(current_user_id: current_user.id).limit(5) + @recent_changes = Node.recently_changed.limit(5) end def conventions diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb index 1268b63..5884c8c 100644 --- a/app/helpers/nodes_helper.rb +++ b/app/helpers/nodes_helper.rb @@ -28,6 +28,18 @@ module NodesHelper User.all.map {|u| [u.login, u.id]} end + def node_last_editor(node) + editor = node.draft&.editor || node.head&.editor + return nil unless editor + editor == current_user ? t("editor_self") : editor.login + end + + def node_head_editor(node) + editor = node.head&.editor + return nil unless editor + editor == current_user ? t("publisher_self") : editor.login + end + DEFAULT_EVENT_TAG_BY_PAGE_TAG = { 'erfa-detail' => 'open-day', 'chaostreff-detail' => 'open-day' diff --git a/app/models/node.rb b/app/models/node.rb index aa2f7f3..1d0a089 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -377,10 +377,10 @@ class Node < ApplicationRecord end def self.recently_changed - where( - "nodes.updated_at < ? AND nodes.updated_at > ? AND nodes.parent_id IS NOT NULL", + includes(:head).where( + "pages.updated_at < ? AND pages.updated_at > ? AND nodes.parent_id IS NOT NULL", Time.now, Time.now - 14.days - ) + ).order("pages.updated_at desc").references(:head) end protected diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 4302987..970429d 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -1,9 +1,5 @@ -<%= language_selector %> -<%= button_to 'Logout', logout_path, method: :delete %> -<%= link_to 'Overview', admin_path %> -search -<%= link_to 'Nodes', nodes_path, selected?('nodes') %> -<%= link_to 'Assets', assets_path, selected?('assets') %> -<%= link_to 'Events', events_path, selected?('events') %> -<%= link_to 'User', users_path, selected?('users') %> -<%= link_to 'Navigation', menu_items_path, selected?('menu_items') %> +<%= link_to icon("home", library: "tabler", "aria-hidden": true), admin_path, "aria-label": "Dashboard" %> +<%= icon("search", library: "tabler", "aria-hidden": true) %> +<%= button_to logout_path, method: :delete, aria: { label: "Log out" } do %> + <%= icon("logout", library: "tabler", "aria-hidden": true) %> +<% end %> diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index c67ccb3..bdb555e 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb @@ -1,155 +1,81 @@ -
-

Quick links

-
<%= link_to 'Create post or page', new_node_path, :only_path => false %>
- - -
<%= link_to("Upload a new asset", new_asset_path, :only_path => false) %>
+

cccms dashboard

+ -

- recent changes - my work - current drafts (<%= @drafts_count %>) - site map -

+
+ <%= link_to new_node_path, class: "action_button" do %> + <%= icon("plus", library: "tabler", "aria-hidden": true) %> Create post + <% end %> + <%= link_to chapters_nodes_path, class: "action_button" do %> + <%= icon("map-pin", library: "tabler", "aria-hidden": true) %> Find a chapter + <% end %> + <%= link_to new_asset_path, class: "action_button" do %> + <%= icon("upload", library: "tabler", "aria-hidden": true) %> Upload asset + <% end %> + <%= link_to sitemap_nodes_path, class: "action_button" do %> + <%= icon("list-tree", library: "tabler", "aria-hidden": true) %> Add a page in the page tree + <% end %> +
-
- -

Recent Changes

+
+
+

Drafts and autosaves

+
    + <% @drafts.each do |node| %> +
  • +
    + <%= link_to title_for_node(node), node_path(node) %> + <%= link_to_path("#{node.unique_name} ↗", node.unique_name) %> +
    + + <% if (editor = node_last_editor(node)) %><%= editor %>, <% end %><%= relative_time_phrase(node.updated_at) %> + +
  • + <% end %> +
+ <%= link_to "See all drafts →", drafts_nodes_path %> +
- - - - - - - - +
+

Recent changes

+
    <% @recent_changes.each do |node| %> -
"> - - - - - - +
  • +
    + <%= link_to title_for_node(node), node_path(node) %> + <%= link_to_path("#{node.unique_name} ↗", node.unique_name) %> +
    + + <%= (editor = node_head_editor(node)) ? t("published_by", editor: editor) : t("published") %>, <%= relative_time_phrase(node.head.updated_at) %> + +
  • <% end %> -
    IDTitleActionsLocked byRev.
    <%= node.id %> -

    <%= link_to title_for_node(node), node_path(node) %>

    -

    <%= link_to_path(node.unique_name, node.unique_name) %>

    -
    - <%= link_to 'show', node_path(node) %> - <%= link_to 'Revisions', node_revisions_path(node) %> - - <%= node.lock_owner.login if node.lock_owner %> - - <%= link_to ( node.draft ? node.draft.revision : (node.head ? node.head.revision : "EMPTY" ) ), node_revisions_path(node) %> -
    + + <%= link_to "See all recent changes →", recent_nodes_path %> +
    -
    - -

    My Work

    - - - - - - - - - - <% @mynodes.each do |node| %> - "> - - - - - - +
    +

    Housekeeping

    +
    + <%= link_to nodes_path, class: "action_button" do %> + <%= icon("list", library: "tabler", "aria-hidden": true) %> Nodes <% end %> -
    IDTitleActionsLocked byRev.
    <%= node.id %> -

    <%= link_to title_for_node(node), node_path(node) %>

    -

    <%= link_to_path(node.unique_name, node.unique_name) %>

    -
    - <%= link_to 'show', node_path(node) %> - <%= link_to 'Revisions', node_revisions_path(node) %> - - <%= node.lock_owner.login if node.lock_owner %> - - <%= link_to ( node.draft ? node.draft.revision : (node.head ? node.head.revision : "EMPTY" ) ), node_revisions_path(node) %> -
    -
    - -
    - -

    Current Drafts & Autosaves (<%= @drafts_count %>)

    - - - - - - - - - - - <% @drafts.each do |node| %> - "> - - - - - - - + <%= link_to events_path, class: "action_button" do %> + <%= icon("calendar", library: "tabler", "aria-hidden": true) %> Events <% end %> -
    IDTitleActionsLocked byRev.Autosave
    <%= node.id %> -

    <%= link_to title_for_node(node), node_path(node) %>

    -

    <%= link_to_path(node.unique_name, node.unique_name) %>

    -
    - <%= link_to 'show', node_path(node) %> - <%= link_to 'Revisions', node_revisions_path(node) %> - <% if pair = node.available_layer_pairs.last %> - <%= link_to 'diff', diff_node_revisions_path(node, start_revision: pair.first, end_revision: pair.last) %> - <% end %> - - <%= node.lock_owner.login if node.lock_owner %> - - <%= link_to ( node.draft ? node.draft.revision : (node.head ? node.head.revision : "EMPTY" ) ), node_revisions_path(node) %> - - <%= node.autosave ? "unsaved changes" : "" %> -
    -
    - -
    - -

    Sitemap

    - - - - - - - - - <% @sitemap.each do |node| %> - <% if !node.nil? %> - "> - - - - - + <%= link_to assets_path, class: "action_button" do %> + <%= icon("folder", library: "tabler", "aria-hidden": true) %> Assets + <% end %> + <%= link_to users_path, class: "action_button" do %> + <%= icon("users", library: "tabler", "aria-hidden": true) %> Users <% end %> + <%= link_to menu_items_path, class: "action_button" do %> + <%= icon("menu-2", library: "tabler", "aria-hidden": true) %> Navigation <% end %> -
    IDTitleActionsLocked by
    <%= node.id %> -

    <%= link_to title_for_node(node), node_path(node) %>

    -

    <%= link_to_path(node.unique_name, node.unique_name) %>

    -
    - <%= link_to 'create subpage', new_node_path(:parent_id => node.id) %> - <%= link_to 'Revisions', node_revisions_path(node) %> - - <%= node.lock_owner.login if node.lock_owner %> -
    +
    diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index 9beaf58..00a51e2 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -15,26 +15,6 @@ class AdminControllerTest < ActionController::TestCase assert_includes assigns(:drafts), node end - test "my work list shows each matching node only once, even with several revisions by the same user" do - login_as :quentin - user = User.find_by_login("quentin") - node = Node.root.children.create!(:slug => "dedup_test") - node.lock_for_editing!(user) - node.autosave!({:title => "v1"}, user) - node.save_draft!(user) - node.publish_draft! - node.lock_for_editing!(user) - node.autosave!({:title => "v2"}, user) - node.save_draft!(user) - node.publish_draft! - # three pages now exist on this node, all touched by quentin -- - # without DISTINCT, the join would return this node three times - - get :index - matches = assigns(:mynodes).select { |n| n.id == node.id } - assert_equal 1, matches.length - end - test "dashboard_search returns matching tags and nodes grouped separately" do node = Node.root.children.create!(:slug => "dashboard_search_test") node.find_or_create_draft(User.find_by_login("aaron")) diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index b43d2de..81e3f45 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb @@ -521,6 +521,26 @@ class NodesControllerTest < ActionController::TestCase assert_response :success end + test "mine shows each matching node only once, even with several revisions by the same user" do + login_as :quentin + user = User.find_by_login("quentin") + node = Node.root.children.create!(:slug => "dedup_test") + node.lock_for_editing!(user) + node.autosave!({:title => "v1"}, user) + node.save_draft!(user) + node.publish_draft! + node.lock_for_editing!(user) + node.autosave!({:title => "v2"}, user) + node.save_draft!(user) + node.publish_draft! + # three pages now exist on this node, all touched by quentin -- + # without DISTINCT, the join would return this node three times + + get :mine + matches = assigns(:nodes).select { |n| n.id == node.id } + assert_equal 1, matches.length + end + test "chapters combined with a search term does not raise an ambiguous column error" do login_as :quentin get :chapters, params: { :q => "Zombies" } diff --git a/test/models/node_test.rb b/test/models/node_test.rb index de540f8..0bce222 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb @@ -561,4 +561,28 @@ class NodeTest < ActiveSupport::TestCase result = Node.drafts_and_autosaves(current_user_id: @user1.id).to_a assert result.index(mine) < result.index(someone_elses_newer) end + + test "recently_changed includes a node whose head was recently published" do + node = Node.root.children.create!(:slug => "recent_changed_published") + node.find_or_create_draft(@user1) + node.autosave!({:title => "v1"}, @user1) + node.save_draft!(@user1) + node.publish_draft! + + assert_includes Node.recently_changed, node + end + + test "recently_changed excludes a node only touched by locking or unlocking after an old publish" do + node = Node.root.children.create!(:slug => "recent_changed_lock_only") + node.find_or_create_draft(@user1) + node.autosave!({:title => "v1"}, @user1) + node.save_draft!(@user1) + node.publish_draft! + node.head.update_column(:updated_at, 20.days.ago) + + node.lock_for_editing!(@user1) + node.unlock! + + assert_not_includes Node.recently_changed, node + end end -- cgit v1.3