From af2c6fd85e13139be3e3b2329a51c3a35acb3c40 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Wed, 5 Aug 2026 23:56:56 +0200 Subject: Search assets and events, and order events by when they happen --- app/controllers/assets_controller.rb | 8 +++----- app/controllers/events_controller.rb | 7 ++++--- app/models/asset.rb | 14 ++++++++++++++ app/models/event.rb | 18 ++++++++++++++++++ app/views/assets/index.html.erb | 2 ++ app/views/events/index.html.erb | 2 ++ app/views/nodes/_node_list.html.erb | 17 ++++------------- app/views/shared/_search_form.html.erb | 15 +++++++++++++++ config/locales/de.yml | 2 ++ config/locales/en.yml | 2 ++ public/stylesheets/admin.css | 8 ++++---- test/models/asset_test.rb | 27 +++++++++++++++++++++++++++ test/models/event_test.rb | 25 +++++++++++++++++++++++++ 13 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 app/views/shared/_search_form.html.erb diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index 801e0968..f8a7d1e4 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -6,12 +6,10 @@ class AssetsController < ApplicationController before_action :login_required layout 'admin' - + def index - @assets = Asset.order('id DESC').paginate( - :page => params[:page], - :per_page => 20 - ) + scope = params[:q].present? ? Asset.editor_search(params[:q]) : Asset.all + @assets = scope.order("id DESC").paginate(:page => params[:page], :per_page => 20) end # GET /assets/1 diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index d581de16..7ace5c1d 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -10,11 +10,12 @@ class EventsController < ApplicationController # GET /events # GET /events.xml def index - @events = Event.order(:id) + scope = Event.order(Arel.sql("start_time DESC NULLS LAST")) + scope = scope.merge(Event.editor_search(params[:q])) if params[:q].present? respond_to do |format| - format.html { @events = @events.paginate(page: params[:page], per_page: 25) } - format.xml { render :xml => @events } + format.html { @events = scope.paginate(:page => params[:page], :per_page => 25) } + format.xml { render :xml => scope } end end diff --git a/app/models/asset.rb b/app/models/asset.rb index b256b929..4d43b18f 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -82,4 +82,18 @@ class Asset < ApplicationRecord destroy! end end + + def self.editor_search(term) + words = term.to_s.split(/\s+/).reject(&:blank?) + return none if words.empty? + + words.inject(all) do |scope, word| + like = "%#{sanitize_sql_like(word)}%" + scope.where( + "assets.name ILIKE :t OR assets.creator ILIKE :t OR " \ + "assets.upload_file_name ILIKE :t OR assets.source_url ILIKE :t OR " \ + "assets.upload_content_type ILIKE :t", :t => like + ) + end + end end diff --git a/app/models/event.rb b/app/models/event.rb index 792ab448..e7823617 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -54,6 +54,24 @@ class Event < ApplicationRecord destroyed end + def self.editor_search(term) + words = term.to_s.split(/\s+/).reject(&:blank?) + return none if words.empty? + + words.inject(all) do |scope, word| + like = "%#{sanitize_sql_like(word)}%" + tagged = ActsAsTaggableOn::Tagging + .where(:taggable_type => base_class.name) + .joins(:tag).where("tags.name ILIKE ?", like) + .select(:taggable_id) + scope.where( + "events.title ILIKE :t OR events.description ILIKE :t OR " \ + "events.location ILIKE :t OR events.url ILIKE :t OR " \ + "events.id IN (:tagged)", :t => like, :tagged => tagged + ) + end + end + private def generate_occurrences Occurrence.generate self diff --git a/app/views/assets/index.html.erb b/app/views/assets/index.html.erb index af11f54f..d2c5d6cc 100644 --- a/app/views/assets/index.html.erb +++ b/app/views/assets/index.html.erb @@ -4,6 +4,8 @@ :label => t(".create_asset"), :options => [[t(".create_asset"), new_asset_path]] %> +<%= render "shared/search_form", :placeholder => t(".search_placeholder") %> + <%= will_paginate @assets %> diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index c4f0a7f9..14790526 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -4,6 +4,8 @@ :label => t(".create_event"), :options => [[t(".create_event"), new_event_path]] %> +<%= render "shared/search_form", :placeholder => t(".search_placeholder") %> + <%= will_paginate @events %>
diff --git a/app/views/nodes/_node_list.html.erb b/app/views/nodes/_node_list.html.erb index f3101058..55f396b7 100644 --- a/app/views/nodes/_node_list.html.erb +++ b/app/views/nodes/_node_list.html.erb @@ -1,16 +1,7 @@ -<%= form_tag url_for(controller: params[:controller], action: params[:action]), method: :get, class: "node_search_form" do %> - <% Array(params[:kinds]).each do |kind| %> - <%= hidden_field_tag "kinds[]", kind %> - <% end %> - <%= hidden_field_tag :tags, params[:tags] if params[:tags].present? %> - <%= text_field_tag :q, params[:q], placeholder: t(".search_placeholder") %> - <%= button_tag type: "submit", class: "action_button" do %> - <%= icon("search", library: "tabler", "aria-hidden": true) %> <%= t("admin.common.search") %> - <% end %> - <% if params[:q].present? || params[:kinds].present? || params[:tags].present? %> - <%= link_to t("admin.common.reset"), url_for(controller: params[:controller], action: params[:action]) %> - <% end %> -<% end %> +<%= render "shared/search_form", + :placeholder => t(".search_placeholder"), + :hidden => Array(params[:kinds]).map { |k| ["kinds[]", k] } + + (params[:tags].present? ? [["tags", params[:tags]]] : []) %> <%= will_paginate @nodes %>
diff --git a/app/views/shared/_search_form.html.erb b/app/views/shared/_search_form.html.erb new file mode 100644 index 00000000..bff6dfaa --- /dev/null +++ b/app/views/shared/_search_form.html.erb @@ -0,0 +1,15 @@ +<%= form_tag url_for(:controller => params[:controller], :action => params[:action]), + :method => :get, :class => "search_form" do %> + <% Array(local_assigns[:hidden]).each do |field_name, value| %> + <%= hidden_field_tag field_name, value %> + <% end %> + <%= text_field_tag :q, params[:q], :placeholder => placeholder %> + <%= button_tag :type => "submit", :class => "action_button" do %> + <%= icon("search", library: "tabler", "aria-hidden": true) %> + <%= t("admin.common.search") %> + <% end %> + <% if params[:q].present? || Array(local_assigns[:hidden]).any? %> + <%= link_to t("admin.common.reset"), + url_for(:controller => params[:controller], :action => params[:action]) %> + <% end %> +<% end %> diff --git a/config/locales/de.yml b/config/locales/de.yml index a4181b9b..df52da4c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -581,6 +581,7 @@ de: title: "Termine" create_event: "Termin anlegen" edit_link: "bearbeiten" + search_placeholder: "Titel, Beschreibung, Ort, Tags durchsuchen…" show: title: "Termin" title_for_node: "Termin für Node %{path}" @@ -691,6 +692,7 @@ de: index: title: "Assets" create_asset: "Asset anlegen" + search_placeholder: "Name, Urheber, Dateiname, Typ durchsuchen…" show: thumbnail: "Vorschaubild" attached_to: "Angehängt an" diff --git a/config/locales/en.yml b/config/locales/en.yml index 4cf7f61f..f67016b9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -528,6 +528,7 @@ en: title: "Events" create_event: "Create event" edit_link: "edit" + search_placeholder: "Search title, description, location, tags…" edit: title: "Editing event" change_node: "Change node" @@ -653,6 +654,7 @@ en: index: title: "Assets" create_asset: "Create asset" + search_placeholder: "Search name, creator, filename, type…" show: thumbnail: "Thumbnail" attached_to: "Attached to" diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index e6f31709..b959597f 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -575,7 +575,7 @@ form.button_to svg { } .button_row .action_button, -.node_search_form .action_button { +.search_form .action_button { margin-bottom: 0; } @@ -1147,14 +1147,14 @@ form.button_to button[type="submit"] { margin-bottom: 0; } -.node_search_form { +.search_form { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; } -.node_search_form input[type=text] { +.search_form input[type=text] { padding: 4px 12px; box-sizing: border-box; height: 2.25rem; @@ -1163,7 +1163,7 @@ form.button_to button[type="submit"] { border-radius: 2px; } -.node_search_form .action_button { +.search_form .action_button { height: 2.25rem; box-sizing: border-box; } diff --git a/test/models/asset_test.rb b/test/models/asset_test.rb index 2677681b..be4be3b9 100644 --- a/test/models/asset_test.rb +++ b/test/models/asset_test.rb @@ -76,4 +76,31 @@ class AssetTest < ActiveSupport::TestCase assert asset.valid?, asset.errors.full_messages.to_sentence assert_equal "image/png", asset.upload_content_type end + + def build_asset(name:, creator: nil) + asset = Asset.new(:name => name, :creator => creator) + asset.upload = Rack::Test::UploadedFile.new( + file_fixture("test_document.pdf"), "application/pdf") + asset.save! + asset + end + + test "editor_search matches across columns" do + pdf = build_asset(:name => "Offener Brief") + other = build_asset(:name => "Nothing distinctive", :creator => "Someone") + + assert_includes Asset.editor_search("brief"), pdf + assert_not_includes Asset.editor_search("brief"), other + assert_includes Asset.editor_search("pdf"), pdf, + "upload_content_type should be searchable" + assert_includes Asset.editor_search("someone"), other, + "creator should be searchable" + end + + test "editor_search ANDs multiple words" do + build_asset(:name => "Offener Brief") + + assert_not_empty Asset.editor_search("offener brief") + assert_empty Asset.editor_search("offener zzzznomatch") + end end diff --git a/test/models/event_test.rb b/test/models/event_test.rb index a5449a2a..49af8e38 100644 --- a/test/models/event_test.rb +++ b/test/models/event_test.rb @@ -178,4 +178,29 @@ class EventTest < ActiveSupport::TestCase assert event.save assert_equal event.start_time, event.occurrences.first&.start_time end + + test "editor_search matches across columns" do + match = Event.create!(:title => "Chaosradio Spezial", :location => "Zentrale") + other = Event.create!(:title => "Nothing distinctive") + + assert_includes Event.editor_search("chaosradio"), match + assert_not_includes Event.editor_search("chaosradio"), other + assert_includes Event.editor_search("zentrale"), match, + "location should be searchable" + end + + test "editor_search ANDs multiple words" do + Event.create!(:title => "Chaosradio Spezial") + + assert_not_empty Event.editor_search("chaosradio spezial") + assert_empty Event.editor_search("chaosradio zzzznomatch") + end + + test "editor_search finds an event by its tag" do + event = Event.create!(:title => "Nothing distinctive") + event.tag_list.add("open-day") + event.save! + + assert_includes Event.editor_search("open-day"), event + end end -- cgit v1.3