diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-08-05 23:56:56 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-08-05 23:56:56 +0200 |
| commit | af2c6fd85e13139be3e3b2329a51c3a35acb3c40 (patch) | |
| tree | 02114279d6517ffc12b9920cc1d843937bff5a10 | |
| parent | a0a495d804319c3f7ad179baba7b87b41f1dc1f3 (diff) | |
Search assets and events, and order events by when they happen
| -rw-r--r-- | app/controllers/assets_controller.rb | 8 | ||||
| -rw-r--r-- | app/controllers/events_controller.rb | 7 | ||||
| -rw-r--r-- | app/models/asset.rb | 14 | ||||
| -rw-r--r-- | app/models/event.rb | 18 | ||||
| -rw-r--r-- | app/views/assets/index.html.erb | 2 | ||||
| -rw-r--r-- | app/views/events/index.html.erb | 2 | ||||
| -rw-r--r-- | app/views/nodes/_node_list.html.erb | 17 | ||||
| -rw-r--r-- | app/views/shared/_search_form.html.erb | 15 | ||||
| -rw-r--r-- | config/locales/de.yml | 2 | ||||
| -rw-r--r-- | config/locales/en.yml | 2 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 8 | ||||
| -rw-r--r-- | test/models/asset_test.rb | 27 | ||||
| -rw-r--r-- | test/models/event_test.rb | 25 |
13 files changed, 122 insertions, 25 deletions
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 | |||
| 6 | before_action :login_required | 6 | before_action :login_required |
| 7 | 7 | ||
| 8 | layout 'admin' | 8 | layout 'admin' |
| 9 | 9 | ||
| 10 | def index | 10 | def index |
| 11 | @assets = Asset.order('id DESC').paginate( | 11 | scope = params[:q].present? ? Asset.editor_search(params[:q]) : Asset.all |
| 12 | :page => params[:page], | 12 | @assets = scope.order("id DESC").paginate(:page => params[:page], :per_page => 20) |
| 13 | :per_page => 20 | ||
| 14 | ) | ||
| 15 | end | 13 | end |
| 16 | 14 | ||
| 17 | # GET /assets/1 | 15 | # 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 | |||
| 10 | # GET /events | 10 | # GET /events |
| 11 | # GET /events.xml | 11 | # GET /events.xml |
| 12 | def index | 12 | def index |
| 13 | @events = Event.order(:id) | 13 | scope = Event.order(Arel.sql("start_time DESC NULLS LAST")) |
| 14 | scope = scope.merge(Event.editor_search(params[:q])) if params[:q].present? | ||
| 14 | 15 | ||
| 15 | respond_to do |format| | 16 | respond_to do |format| |
| 16 | format.html { @events = @events.paginate(page: params[:page], per_page: 25) } | 17 | format.html { @events = scope.paginate(:page => params[:page], :per_page => 25) } |
| 17 | format.xml { render :xml => @events } | 18 | format.xml { render :xml => scope } |
| 18 | end | 19 | end |
| 19 | end | 20 | end |
| 20 | 21 | ||
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 | |||
| 82 | destroy! | 82 | destroy! |
| 83 | end | 83 | end |
| 84 | end | 84 | end |
| 85 | |||
| 86 | def self.editor_search(term) | ||
| 87 | words = term.to_s.split(/\s+/).reject(&:blank?) | ||
| 88 | return none if words.empty? | ||
| 89 | |||
| 90 | words.inject(all) do |scope, word| | ||
| 91 | like = "%#{sanitize_sql_like(word)}%" | ||
| 92 | scope.where( | ||
| 93 | "assets.name ILIKE :t OR assets.creator ILIKE :t OR " \ | ||
| 94 | "assets.upload_file_name ILIKE :t OR assets.source_url ILIKE :t OR " \ | ||
| 95 | "assets.upload_content_type ILIKE :t", :t => like | ||
| 96 | ) | ||
| 97 | end | ||
| 98 | end | ||
| 85 | end | 99 | 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 | |||
| 54 | destroyed | 54 | destroyed |
| 55 | end | 55 | end |
| 56 | 56 | ||
| 57 | def self.editor_search(term) | ||
| 58 | words = term.to_s.split(/\s+/).reject(&:blank?) | ||
| 59 | return none if words.empty? | ||
| 60 | |||
| 61 | words.inject(all) do |scope, word| | ||
| 62 | like = "%#{sanitize_sql_like(word)}%" | ||
| 63 | tagged = ActsAsTaggableOn::Tagging | ||
| 64 | .where(:taggable_type => base_class.name) | ||
| 65 | .joins(:tag).where("tags.name ILIKE ?", like) | ||
| 66 | .select(:taggable_id) | ||
| 67 | scope.where( | ||
| 68 | "events.title ILIKE :t OR events.description ILIKE :t OR " \ | ||
| 69 | "events.location ILIKE :t OR events.url ILIKE :t OR " \ | ||
| 70 | "events.id IN (:tagged)", :t => like, :tagged => tagged | ||
| 71 | ) | ||
| 72 | end | ||
| 73 | end | ||
| 74 | |||
| 57 | private | 75 | private |
| 58 | def generate_occurrences | 76 | def generate_occurrences |
| 59 | Occurrence.generate self | 77 | 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 @@ | |||
| 4 | :label => t(".create_asset"), | 4 | :label => t(".create_asset"), |
| 5 | :options => [[t(".create_asset"), new_asset_path]] %> | 5 | :options => [[t(".create_asset"), new_asset_path]] %> |
| 6 | 6 | ||
| 7 | <%= render "shared/search_form", :placeholder => t(".search_placeholder") %> | ||
| 8 | |||
| 7 | <%= will_paginate @assets %> | 9 | <%= will_paginate @assets %> |
| 8 | 10 | ||
| 9 | <table class="assets_table"> | 11 | <table class="assets_table"> |
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 @@ | |||
| 4 | :label => t(".create_event"), | 4 | :label => t(".create_event"), |
| 5 | :options => [[t(".create_event"), new_event_path]] %> | 5 | :options => [[t(".create_event"), new_event_path]] %> |
| 6 | 6 | ||
| 7 | <%= render "shared/search_form", :placeholder => t(".search_placeholder") %> | ||
| 8 | |||
| 7 | <%= will_paginate @events %> | 9 | <%= will_paginate @events %> |
| 8 | 10 | ||
| 9 | <table class="events_table"> | 11 | <table class="events_table"> |
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 @@ | |||
| 1 | <%= form_tag url_for(controller: params[:controller], action: params[:action]), method: :get, class: "node_search_form" do %> | 1 | <%= render "shared/search_form", |
| 2 | <% Array(params[:kinds]).each do |kind| %> | 2 | :placeholder => t(".search_placeholder"), |
| 3 | <%= hidden_field_tag "kinds[]", kind %> | 3 | :hidden => Array(params[:kinds]).map { |k| ["kinds[]", k] } + |
| 4 | <% end %> | 4 | (params[:tags].present? ? [["tags", params[:tags]]] : []) %> |
| 5 | <%= hidden_field_tag :tags, params[:tags] if params[:tags].present? %> | ||
| 6 | <%= text_field_tag :q, params[:q], placeholder: t(".search_placeholder") %> | ||
| 7 | <%= button_tag type: "submit", class: "action_button" do %> | ||
| 8 | <%= icon("search", library: "tabler", "aria-hidden": true) %> <%= t("admin.common.search") %> | ||
| 9 | <% end %> | ||
| 10 | <% if params[:q].present? || params[:kinds].present? || params[:tags].present? %> | ||
| 11 | <%= link_to t("admin.common.reset"), url_for(controller: params[:controller], action: params[:action]) %> | ||
| 12 | <% end %> | ||
| 13 | <% end %> | ||
| 14 | 5 | ||
| 15 | <%= will_paginate @nodes %> | 6 | <%= will_paginate @nodes %> |
| 16 | <table class="node_table"> | 7 | <table class="node_table"> |
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 @@ | |||
| 1 | <%= form_tag url_for(:controller => params[:controller], :action => params[:action]), | ||
| 2 | :method => :get, :class => "search_form" do %> | ||
| 3 | <% Array(local_assigns[:hidden]).each do |field_name, value| %> | ||
| 4 | <%= hidden_field_tag field_name, value %> | ||
| 5 | <% end %> | ||
| 6 | <%= text_field_tag :q, params[:q], :placeholder => placeholder %> | ||
| 7 | <%= button_tag :type => "submit", :class => "action_button" do %> | ||
| 8 | <%= icon("search", library: "tabler", "aria-hidden": true) %> | ||
| 9 | <%= t("admin.common.search") %> | ||
| 10 | <% end %> | ||
| 11 | <% if params[:q].present? || Array(local_assigns[:hidden]).any? %> | ||
| 12 | <%= link_to t("admin.common.reset"), | ||
| 13 | url_for(:controller => params[:controller], :action => params[:action]) %> | ||
| 14 | <% end %> | ||
| 15 | <% 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: | |||
| 581 | title: "Termine" | 581 | title: "Termine" |
| 582 | create_event: "Termin anlegen" | 582 | create_event: "Termin anlegen" |
| 583 | edit_link: "bearbeiten" | 583 | edit_link: "bearbeiten" |
| 584 | search_placeholder: "Titel, Beschreibung, Ort, Tags durchsuchen…" | ||
| 584 | show: | 585 | show: |
| 585 | title: "Termin" | 586 | title: "Termin" |
| 586 | title_for_node: "Termin für Node %{path}" | 587 | title_for_node: "Termin für Node %{path}" |
| @@ -691,6 +692,7 @@ de: | |||
| 691 | index: | 692 | index: |
| 692 | title: "Assets" | 693 | title: "Assets" |
| 693 | create_asset: "Asset anlegen" | 694 | create_asset: "Asset anlegen" |
| 695 | search_placeholder: "Name, Urheber, Dateiname, Typ durchsuchen…" | ||
| 694 | show: | 696 | show: |
| 695 | thumbnail: "Vorschaubild" | 697 | thumbnail: "Vorschaubild" |
| 696 | attached_to: "Angehängt an" | 698 | 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: | |||
| 528 | title: "Events" | 528 | title: "Events" |
| 529 | create_event: "Create event" | 529 | create_event: "Create event" |
| 530 | edit_link: "edit" | 530 | edit_link: "edit" |
| 531 | search_placeholder: "Search title, description, location, tags…" | ||
| 531 | edit: | 532 | edit: |
| 532 | title: "Editing event" | 533 | title: "Editing event" |
| 533 | change_node: "Change node" | 534 | change_node: "Change node" |
| @@ -653,6 +654,7 @@ en: | |||
| 653 | index: | 654 | index: |
| 654 | title: "Assets" | 655 | title: "Assets" |
| 655 | create_asset: "Create asset" | 656 | create_asset: "Create asset" |
| 657 | search_placeholder: "Search name, creator, filename, type…" | ||
| 656 | show: | 658 | show: |
| 657 | thumbnail: "Thumbnail" | 659 | thumbnail: "Thumbnail" |
| 658 | attached_to: "Attached to" | 660 | 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 { | |||
| 575 | } | 575 | } |
| 576 | 576 | ||
| 577 | .button_row .action_button, | 577 | .button_row .action_button, |
| 578 | .node_search_form .action_button { | 578 | .search_form .action_button { |
| 579 | margin-bottom: 0; | 579 | margin-bottom: 0; |
| 580 | } | 580 | } |
| 581 | 581 | ||
| @@ -1147,14 +1147,14 @@ form.button_to button[type="submit"] { | |||
| 1147 | margin-bottom: 0; | 1147 | margin-bottom: 0; |
| 1148 | } | 1148 | } |
| 1149 | 1149 | ||
| 1150 | .node_search_form { | 1150 | .search_form { |
| 1151 | display: flex; | 1151 | display: flex; |
| 1152 | align-items: center; | 1152 | align-items: center; |
| 1153 | gap: 0.5rem; | 1153 | gap: 0.5rem; |
| 1154 | margin-bottom: 0.75rem; | 1154 | margin-bottom: 0.75rem; |
| 1155 | } | 1155 | } |
| 1156 | 1156 | ||
| 1157 | .node_search_form input[type=text] { | 1157 | .search_form input[type=text] { |
| 1158 | padding: 4px 12px; | 1158 | padding: 4px 12px; |
| 1159 | box-sizing: border-box; | 1159 | box-sizing: border-box; |
| 1160 | height: 2.25rem; | 1160 | height: 2.25rem; |
| @@ -1163,7 +1163,7 @@ form.button_to button[type="submit"] { | |||
| 1163 | border-radius: 2px; | 1163 | border-radius: 2px; |
| 1164 | } | 1164 | } |
| 1165 | 1165 | ||
| 1166 | .node_search_form .action_button { | 1166 | .search_form .action_button { |
| 1167 | height: 2.25rem; | 1167 | height: 2.25rem; |
| 1168 | box-sizing: border-box; | 1168 | box-sizing: border-box; |
| 1169 | } | 1169 | } |
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 | |||
| 76 | assert asset.valid?, asset.errors.full_messages.to_sentence | 76 | assert asset.valid?, asset.errors.full_messages.to_sentence |
| 77 | assert_equal "image/png", asset.upload_content_type | 77 | assert_equal "image/png", asset.upload_content_type |
| 78 | end | 78 | end |
| 79 | |||
| 80 | def build_asset(name:, creator: nil) | ||
| 81 | asset = Asset.new(:name => name, :creator => creator) | ||
| 82 | asset.upload = Rack::Test::UploadedFile.new( | ||
| 83 | file_fixture("test_document.pdf"), "application/pdf") | ||
| 84 | asset.save! | ||
| 85 | asset | ||
| 86 | end | ||
| 87 | |||
| 88 | test "editor_search matches across columns" do | ||
| 89 | pdf = build_asset(:name => "Offener Brief") | ||
| 90 | other = build_asset(:name => "Nothing distinctive", :creator => "Someone") | ||
| 91 | |||
| 92 | assert_includes Asset.editor_search("brief"), pdf | ||
| 93 | assert_not_includes Asset.editor_search("brief"), other | ||
| 94 | assert_includes Asset.editor_search("pdf"), pdf, | ||
| 95 | "upload_content_type should be searchable" | ||
| 96 | assert_includes Asset.editor_search("someone"), other, | ||
| 97 | "creator should be searchable" | ||
| 98 | end | ||
| 99 | |||
| 100 | test "editor_search ANDs multiple words" do | ||
| 101 | build_asset(:name => "Offener Brief") | ||
| 102 | |||
| 103 | assert_not_empty Asset.editor_search("offener brief") | ||
| 104 | assert_empty Asset.editor_search("offener zzzznomatch") | ||
| 105 | end | ||
| 79 | end | 106 | 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 | |||
| 178 | assert event.save | 178 | assert event.save |
| 179 | assert_equal event.start_time, event.occurrences.first&.start_time | 179 | assert_equal event.start_time, event.occurrences.first&.start_time |
| 180 | end | 180 | end |
| 181 | |||
| 182 | test "editor_search matches across columns" do | ||
| 183 | match = Event.create!(:title => "Chaosradio Spezial", :location => "Zentrale") | ||
| 184 | other = Event.create!(:title => "Nothing distinctive") | ||
| 185 | |||
| 186 | assert_includes Event.editor_search("chaosradio"), match | ||
| 187 | assert_not_includes Event.editor_search("chaosradio"), other | ||
| 188 | assert_includes Event.editor_search("zentrale"), match, | ||
| 189 | "location should be searchable" | ||
| 190 | end | ||
| 191 | |||
| 192 | test "editor_search ANDs multiple words" do | ||
| 193 | Event.create!(:title => "Chaosradio Spezial") | ||
| 194 | |||
| 195 | assert_not_empty Event.editor_search("chaosradio spezial") | ||
| 196 | assert_empty Event.editor_search("chaosradio zzzznomatch") | ||
| 197 | end | ||
| 198 | |||
| 199 | test "editor_search finds an event by its tag" do | ||
| 200 | event = Event.create!(:title => "Nothing distinctive") | ||
| 201 | event.tag_list.add("open-day") | ||
| 202 | event.save! | ||
| 203 | |||
| 204 | assert_includes Event.editor_search("open-day"), event | ||
| 205 | end | ||
| 181 | end | 206 | end |
