summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/nodes_controller.rb45
-rw-r--r--app/models/node.rb22
-rw-r--r--app/views/nodes/_node_list.html.erb39
-rw-r--r--app/views/nodes/chapters.html.erb9
-rw-r--r--app/views/nodes/drafts.html.erb3
-rw-r--r--app/views/nodes/mine.html.erb3
-rw-r--r--app/views/nodes/recent.html.erb3
-rw-r--r--app/views/nodes/tags.html.erb3
-rw-r--r--config/routes.rb5
-rw-r--r--public/stylesheets/admin.css15
-rw-r--r--test/controllers/nodes_controller_test.rb104
-rw-r--r--test/models/node_test.rb26
12 files changed, 277 insertions, 0 deletions
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index 6fcd930..ede91ad 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -183,6 +183,39 @@ class NodesController < ApplicationController
183 render plain: slug_for(params[:title]) 183 render plain: slug_for(params[:title])
184 end 184 end
185 185
186 # Filter functions for admin views
187 def drafts
188 base = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL")
189 @nodes = index_matching(base)
190 end
191
192 def recent
193 base = Node.where(
194 "nodes.updated_at < ? AND nodes.updated_at > ? AND nodes.parent_id IS NOT NULL",
195 Time.now, Time.now - 14.days
196 )
197 @nodes = index_matching(base)
198 end
199
200 def mine
201 base = Node.joins(:pages)
202 .where("pages.user_id = ? or pages.editor_id = ?", current_user, current_user)
203 .distinct
204 @nodes = index_matching(base)
205 end
206
207 def chapters
208 @kind_keys = Array(params[:kinds]) & %w[erfa chaostreff]
209 @kind_keys = %w[erfa chaostreff] if @kind_keys.empty?
210 tags = @kind_keys.flat_map { |key| CccConventions::NODE_KINDS[key][:tags] }
211 @nodes = nodes_matching_tags(tags)
212 end
213
214 def tags
215 tags = params[:tags].to_s.split(',').map(&:strip).reject(&:blank?)
216 @nodes = nodes_matching_tags(tags)
217 end
218
186 private 219 private
187 220
188 def slug_for(title) 221 def slug_for(title)
@@ -214,4 +247,16 @@ class NodesController < ApplicationController
214 config && config[:parent] ? config[:parent].call.id : nil 247 config && config[:parent] ? config[:parent].call.id : nil
215 end 248 end
216 end 249 end
250
251 def nodes_matching_tags(tags)
252 matching_pages = Page.tagged_with(tags, any: true).reselect(:id)
253 base = Node.where(head_id: matching_pages).or(Node.where(draft_id: matching_pages))
254 index_matching(base)
255 end
256
257 def index_matching(base_scope)
258 scope = base_scope.includes(:head, :draft)
259 scope = scope.merge(Node.editor_search(params[:q])) if params[:q].present?
260 scope.order("nodes.updated_at desc").paginate(page: params[:page], per_page: 25)
261 end
217end 262end
diff --git a/app/models/node.rb b/app/models/node.rb
index 9cb4840..24f3cd0 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -345,6 +345,28 @@ class Node < ApplicationRecord
345 .distinct 345 .distinct
346 end 346 end
347 347
348 # This one is for admin-only views, where finding a draft is the point.
349 # Substring match on whichever of head/draft is present.
350 def self.editor_search(term)
351 words = term.to_s.split(/\s+/).reject(&:blank?)
352 return none if words.empty?
353
354 conditions = []
355 binds = {}
356
357 words.each_with_index do |word, i|
358 key = "term#{i}"
359 binds[key.to_sym] = "%#{sanitize_sql_like(word)}%"
360 conditions << "(head_translations.title ILIKE :#{key} OR head_translations.abstract ILIKE :#{key} " \
361 "OR draft_translations.title ILIKE :#{key} OR draft_translations.abstract ILIKE :#{key})"
362 end
363
364 joins("LEFT JOIN page_translations head_translations ON head_translations.page_id = nodes.head_id")
365 .joins("LEFT JOIN page_translations draft_translations ON draft_translations.page_id = nodes.draft_id")
366 .where(conditions.join(" AND "), binds)
367 .distinct
368 end
369
348 protected 370 protected
349 def lock_for! current_user 371 def lock_for! current_user
350 self.lock_owner = current_user 372 self.lock_owner = current_user
diff --git a/app/views/nodes/_node_list.html.erb b/app/views/nodes/_node_list.html.erb
new file mode 100644
index 0000000..03f38b4
--- /dev/null
+++ b/app/views/nodes/_node_list.html.erb
@@ -0,0 +1,39 @@
1<%= form_tag url_for(controller: params[:controller], action: params[:action]), method: :get, class: "node_search_form" do %>
2 <% Array(params[:kinds]).each do |kind| %>
3 <%= hidden_field_tag "kinds[]", kind %>
4 <% end %>
5 <%= hidden_field_tag :tags, params[:tags] if params[:tags].present? %>
6 <%= text_field_tag :q, params[:q], placeholder: "Search title, abstract, body…" %>
7 <%= submit_tag "Search", class: "action_button" %>
8 <% if params[:q].present? || params[:kinds].present? || params[:tags].present? %>
9 <%= link_to "Reset", url_for(controller: params[:controller], action: params[:action]) %>
10 <% end %>
11<% end %>
12
13<%= will_paginate @nodes %>
14<table class="node_table">
15 <tr class="header">
16 <th class="node_id">ID</th>
17 <th class="title">Title</th>
18 <th class="actions">Actions</th>
19 <th class="editor">Locked by</th>
20 <th class="revision">Rev.</th>
21 </tr>
22 <% @nodes.each do |node| %>
23 <tr class="<%= cycle("even", "odd") %>">
24 <td class="node_id"><%= node.id %></td>
25 <td class="title">
26 <h4><%= link_to title_for_node(node), node_path(node) %></h4>
27 <p><%= link_to_path(node.unique_name, node.unique_name) %></p>
28 </td>
29 <td class="actions">
30 <%= link_to 'show', node_path(node) %>
31 <%= link_to 'edit', edit_node_path(node) %>
32 <%= link_to 'revisions', node_revisions_path(node) %>
33 </td>
34 <td><%= node.lock_owner.login if node.lock_owner %></td>
35 <td><%= node.draft ? node.draft.revision : (node.head ? node.head.revision : "EMPTY") %></td>
36 </tr>
37 <% end %>
38</table>
39<%= will_paginate @nodes %>
diff --git a/app/views/nodes/chapters.html.erb b/app/views/nodes/chapters.html.erb
new file mode 100644
index 0000000..939f19c
--- /dev/null
+++ b/app/views/nodes/chapters.html.erb
@@ -0,0 +1,9 @@
1<h1>Chapters</h1>
2
3<%= form_tag chapters_nodes_path, method: :get do %>
4 <label><%= check_box_tag 'kinds[]', 'erfa', @kind_keys.include?('erfa') %> Erfa</label>
5 <label><%= check_box_tag 'kinds[]', 'chaostreff', @kind_keys.include?('chaostreff') %> Chaostreff</label>
6 <%= submit_tag "Filter", class: "action_button" %>
7<% end %>
8
9<%= render 'node_list' %>
diff --git a/app/views/nodes/drafts.html.erb b/app/views/nodes/drafts.html.erb
new file mode 100644
index 0000000..6d0830c
--- /dev/null
+++ b/app/views/nodes/drafts.html.erb
@@ -0,0 +1,3 @@
1<h1>Nodes with drafts or autosaves</h1>
2
3<%= render 'node_list' %>
diff --git a/app/views/nodes/mine.html.erb b/app/views/nodes/mine.html.erb
new file mode 100644
index 0000000..fc5680a
--- /dev/null
+++ b/app/views/nodes/mine.html.erb
@@ -0,0 +1,3 @@
1<h1>My work</h1>
2
3<%= render 'node_list' %>
diff --git a/app/views/nodes/recent.html.erb b/app/views/nodes/recent.html.erb
new file mode 100644
index 0000000..3602775
--- /dev/null
+++ b/app/views/nodes/recent.html.erb
@@ -0,0 +1,3 @@
1<h1>Recently changed</h1>
2
3<%= render 'node_list' %>
diff --git a/app/views/nodes/tags.html.erb b/app/views/nodes/tags.html.erb
new file mode 100644
index 0000000..0ebc6ce
--- /dev/null
+++ b/app/views/nodes/tags.html.erb
@@ -0,0 +1,3 @@
1<h1>Nodes tagged: <%= params[:tags] %></h1>
2
3<%= render 'node_list' %>
diff --git a/config/routes.rb b/config/routes.rb
index aebce90..2c165d2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -37,7 +37,12 @@ Cccms::Application.routes.draw do
37 37
38 resources :nodes do 38 resources :nodes do
39 collection do 39 collection do
40 get 'tags/:tags', action: :tags, as: :tags, constraints: { tags: /[^\/]+/ }
40 get :parameterize_preview 41 get :parameterize_preview
42 get :drafts
43 get :recent
44 get :mine
45 get :chapters
41 end 46 end
42 47
43 member do 48 member do
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 28b8494..aa8b288 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -748,6 +748,21 @@ form.button_to button[type="submit"] {
748 margin-bottom: 0; 748 margin-bottom: 0;
749} 749}
750 750
751.node_search_form {
752 display: flex;
753 align-items: center;
754 gap: 0.5rem;
755 margin-bottom: 0.75rem;
756}
757
758.node_search_form input[type=text] {
759 padding: 4px 12px;
760 box-sizing: border-box;
761 height: 2.25rem;
762 width: 22rem;
763 border-radius: 2px;
764}
765
751/* Layout only -- the at-rest visibility (wavy underline) for these 766/* Layout only -- the at-rest visibility (wavy underline) for these
752 links comes from the scoped rule in Base elements above. */ 767 links comes from the scoped rule in Base elements above. */
753.add_child_links { 768.add_child_links {
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index b563d4d..05cb195 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -471,4 +471,108 @@ class NodesControllerTest < ActionController::TestCase
471 assert_response :success 471 assert_response :success
472 assert_select "form.destructive", :count => 0 472 assert_select "form.destructive", :count => 0
473 end 473 end
474
475 test "drafts includes a never-published node with only a draft" do
476 node = Node.root.children.create!(:slug => "drafts_action_test")
477 login_as :quentin
478 get :drafts
479 assert_includes assigns(:nodes), node
480 end
481
482 test "chapters filters by kind, matching head or draft, and shows both by default" do
483 erfa_node = Node.root.children.create!(:slug => "chapters_erfa_test")
484 erfa_node.find_or_create_draft(@user1)
485 erfa_node.draft.tag_list = "erfa-detail"
486 erfa_node.draft.save!
487 erfa_node.publish_draft!
488
489 chaostreff_node = Node.root.children.create!(:slug => "chapters_chaostreff_test")
490 chaostreff_node.find_or_create_draft(@user1)
491 chaostreff_node.draft.tag_list = "chaostreff-detail"
492 chaostreff_node.draft.save!
493 chaostreff_node.publish_draft!
494
495 login_as :quentin
496
497 get :chapters, params: { :kinds => "erfa" }
498 assert_includes assigns(:nodes), erfa_node
499 assert_not_includes assigns(:nodes), chaostreff_node
500
501 get :chapters
502 assert_includes assigns(:nodes), erfa_node
503 assert_includes assigns(:nodes), chaostreff_node
504 end
505
506 test "recent combined with a search term does not raise an ambiguous column error" do
507 login_as :quentin
508 get :recent, params: { :q => "Zombies" }
509 assert_response :success
510 end
511
512 test "drafts combined with a search term does not raise an ambiguous column error" do
513 login_as :quentin
514 get :drafts, params: { :q => "Zombies" }
515 assert_response :success
516 end
517
518 test "mine combined with a search term does not raise an ambiguous column error" do
519 login_as :quentin
520 get :mine, params: { :q => "Zombies" }
521 assert_response :success
522 end
523
524 test "chapters combined with a search term does not raise an ambiguous column error" do
525 login_as :quentin
526 get :chapters, params: { :q => "Zombies" }
527 assert_response :success
528 end
529
530 test "tags path filters by an arbitrary raw tag, generalizing chapters" do
531 presse_node = Node.root.children.create!(:slug => "tags_path_presse_test")
532 presse_node.find_or_create_draft(@user1)
533 presse_node.draft.tag_list = "pressemitteilung"
534 presse_node.draft.save!
535 presse_node.publish_draft!
536
537 erfa_node = Node.root.children.create!(:slug => "tags_path_erfa_test")
538 erfa_node.find_or_create_draft(@user1)
539 erfa_node.draft.tag_list = "erfa-detail"
540 erfa_node.draft.save!
541 erfa_node.publish_draft!
542
543 login_as :quentin
544 get :tags, params: { :tags => "pressemitteilung" }
545
546 assert_includes assigns(:nodes), presse_node
547 assert_not_includes assigns(:nodes), erfa_node
548
549 assert_select "h1", "Nodes tagged: pressemitteilung"
550 assert_select "h1", :text => "Chapters", :count => 0
551 end
552
553 test "tags path with multiple tags matches any of them, not all" do
554 erfa_node = Node.root.children.create!(:slug => "tags_path_multi_erfa_test")
555 erfa_node.find_or_create_draft(@user1)
556 erfa_node.draft.tag_list = "erfa-detail"
557 erfa_node.draft.save!
558 erfa_node.publish_draft!
559
560 chaostreff_node = Node.root.children.create!(:slug => "tags_path_multi_chaostreff_test")
561 chaostreff_node.find_or_create_draft(@user1)
562 chaostreff_node.draft.tag_list = "chaostreff-detail"
563 chaostreff_node.draft.save!
564 chaostreff_node.publish_draft!
565
566 login_as :quentin
567 get :tags, params: {:tags => "erfa-detail,chaostreff-detail" }
568
569 assert_includes assigns(:nodes), erfa_node
570 assert_includes assigns(:nodes), chaostreff_node
571 end
572
573 test "chapters renders the curated heading" do
574 login_as :quentin
575 get :chapters
576 assert_select "h1", "Chapters"
577 end
474end 578end
diff --git a/test/models/node_test.rb b/test/models/node_test.rb
index 5626384..15e908b 100644
--- a/test/models/node_test.rb
+++ b/test/models/node_test.rb
@@ -511,4 +511,30 @@ class NodeTest < ActiveSupport::TestCase
511 a.reload 511 a.reload
512 assert_equal Node.root.id, a.parent_id 512 assert_equal Node.root.id, a.parent_id
513 end 513 end
514
515 test "editor_search matches a partial substring, not just a whole word" do
516 node = Node.root.children.create!(:slug => "editor_search_substring_test")
517 node.find_or_create_draft(@user1)
518 node.draft.update(:title => "Biometrics Conference")
519 node.publish_draft!
520
521 assert_includes Node.editor_search("bio"), node
522 assert_includes Node.editor_search("Conf"), node
523 end
524
525 test "editor_search returns an empty relation for a blank term, not every node" do
526 assert_equal 0, Node.editor_search("").count
527 assert_equal 0, Node.editor_search(nil).count
528 assert_equal 0, Node.editor_search(" ").count
529 end
530
531 test "editor_search requires every word to match, but each word can match a different field" do
532 node = Node.root.children.create!(:slug => "editor_search_multiword_test")
533 node.find_or_create_draft(@user1)
534 node.draft.update(:title => "Backspace e.V. Bamberg", :abstract => "Spiegelgraben 41, 96052 Bamberg")
535 node.publish_draft!
536
537 assert_includes Node.editor_search("Backspace Spiegelgraben"), node
538 assert_equal 0, Node.editor_search("Backspace Nonexistentstreet").count
539 end
514end 540end