summaryrefslogtreecommitdiff
path: root/app/controllers/nodes_controller.rb
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-13 23:33:11 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-13 23:33:11 +0200
commit8eab68f2c5a3c126e2fec2ecdfa7ace87ce0937b (patch)
treef446ebc26a7707c7b64a937aa51a155df146c80a /app/controllers/nodes_controller.rb
parent42714c697273a7117c6b355fab26c8c35e336ad1 (diff)
parentcdf5d9941ca866d437612d2f863eac6eb0b3db12 (diff)
Merge branch 'erdgeist-revive-events'
Diffstat (limited to 'app/controllers/nodes_controller.rb')
-rw-r--r--app/controllers/nodes_controller.rb248
1 files changed, 198 insertions, 50 deletions
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index 494887d..bff1733 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -1,19 +1,23 @@
1class NodesController < ApplicationController 1class NodesController < ApplicationController
2 2
3 # Private 3 # Private
4 4
5 layout 'admin' 5 layout 'admin'
6 6
7 before_action :login_required 7 before_action :login_required
8 before_action :find_node, :only => [ 8 before_action :find_node, :only => [
9 :show, 9 :show,
10 :edit, 10 :edit,
11 :update, 11 :update,
12 :destroy, 12 :destroy,
13 :publish, 13 :publish,
14 :unlock 14 :unlock,
15 :autosave,
16 :revert
15 ] 17 ]
16 18
19 around_action :pin_to_default_locale, :only => [:show, :edit, :update, :autosave]
20
17 def index 21 def index
18 @nodes = Node.root.descendants.includes(:head, :draft) 22 @nodes = Node.root.descendants.includes(:head, :draft)
19 .order('id DESC') 23 .order('id DESC')
@@ -22,90 +26,203 @@ class NodesController < ApplicationController
22 26
23 def new 27 def new
24 @node = Node.new node_params 28 @node = Node.new node_params
29 @selected_kind = CccConventions::NODE_KINDS.key?(params[:kind]) ? params[:kind] : "generic"
25 if params.has_key?(:parent_id) 30 if params.has_key?(:parent_id)
26 @parent_id = params[:parent_id] 31 @parent_id = params[:parent_id]
27 @parent_name = Node.find(@parent_id).title 32 @parent_name = Node.find(@parent_id).title
28 end 33 end
29 end 34 end
30 35
31 def create 36 def create
32 params[:title] ||= "" 37 params[:title] ||= ""
33 38
34 @node = Node.new 39 @node = Node.new
35 @node.parent_id = find_parent 40 @node.parent_id = find_parent
36 @node.slug = params[:title].parameterize.to_s 41 @node.slug = slug_for(params[:title])
37 42
43 config = CccConventions::NODE_KINDS[params[:kind]]
44
38 if @node.save 45 if @node.save
39 @node.draft.update(:title => params[:title]) 46 @node.draft.update(:title => params[:title])
40 case params[:kind] 47 Array(config && config[:tags]).each { |t| @node.draft.tag_list.add(t) }
41 when "update"
42 @node.draft.tag_list.add("update")
43 when "press_release"
44 @node.draft.tag_list.add("update", "pressemitteilung")
45 end
46 @node.draft.save! 48 @node.draft.save!
49
50 @node.update!(default_template_name: config[:template]) if config && config[:template]
51
47 redirect_to(edit_node_path(@node)) 52 redirect_to(edit_node_path(@node))
48 else 53 else
54 @selected_kind = CccConventions::NODE_KINDS.key?(params[:kind]) ? params[:kind] : "generic"
55 if params[:parent_id].present?
56 @parent_id = params[:parent_id]
57 @parent_name = Node.find(@parent_id).title
58 end
49 render :new 59 render :new
50 end 60 end
51 end 61 end
52 62
53 def show 63 def show
54 node = Node.find(params[:id]) 64 @page = @node.draft || @node.head
55 node.wipe_draft! 65 @translations = @page.translation_summary
56 @page = node.draft || node.head
57 end 66 end
58 67
59 def edit 68 def edit
60 begin 69 freshly_locked = @node.lock_owner != current_user
61 @draft = @node.find_or_create_draft( current_user ) 70 @node.lock_for_editing!( current_user )
62 rescue LockedByAnotherUser => e 71 @page = @node.autosave || @node.draft || @node.head
63 flash[:error] = e.message 72
64 if request.referer 73 if @node.autosave
65 redirect_to request.referer || node_path(@node) 74 flash.now[:notice] =
66 else 75 "This page has unsaved changes from a previous session, shown below. " \
67 redirect_to node_path(@node) 76 "Save to keep them, or use \"Discard Autosave\" below to go back to the last saved version."
68 end 77 elsif freshly_locked
78 flash.now[:notice] = "Node locked and ready to edit"
69 end 79 end
80 rescue LockedByAnotherUser => e
81 flash[:error] = e.message
82 redirect_to(request.referer || node_path(@node))
70 end 83 end
71 84
72 def update 85 def update
73 @node.update(node_params) 86 @node.update(node_params)
74 @draft = @node.find_or_create_draft current_user 87 @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user )
75 @draft.tag_list = params[:tag_list] 88 @node.save_draft!(current_user)
76 if @draft.update( page_params ) 89
77 flash[:notice] = "Draft has been saved: #{Time.now}" 90 flash[:notice] = "Draft saved. Publish your changes in the Status section once you're done."
78 respond_to do |format| 91 flash[:status_path] = node_path(@node)
79 format.html { redirect_to edit_node_path(@node) } 92
80 format.js 93 if @node.draft.translated_locales.size > 1
94 stale_locale = @node.draft.translated_locales.find do |locale|
95 @node.draft.outdated_translations?(locale: locale)
81 end 96 end
97 if stale_locale
98 flash[:stale_locale] = stale_locale
99 flash[:stale_locale_path] = edit_node_path(@node, locale: stale_locale)
100 end
101 end
102
103 if params[:commit] == "Save + Unlock + Exit"
104 @node.unlock!
105 redirect_to node_path(@node)
106 else
107 redirect_to edit_node_path(@node)
108 end
109 rescue LockedByAnotherUser => e
110 flash[:error] = e.message
111 redirect_to node_path(@node)
112 rescue ActiveRecord::RecordInvalid
113 @page = @node.autosave || @node.draft || @node.head
114 render :action => :edit
115 end
116
117 def autosave
118 @node.update(node_params)
119 @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user )
120 head :ok
121 rescue LockedByAnotherUser => e
122 render plain: e.message, status: :locked
123 rescue ActiveRecord::RecordInvalid => e
124 render plain: e.message, status: :unprocessable_entity
125 rescue StandardError => e
126 render plain: "Autosave failed", status: :internal_server_error
127 end
128
129 def revert
130 @node.lock_for_editing!(current_user)
131 @node.revert!(current_user)
132
133 if params[:return_to].present?
134 redirect_to safe_return_to(params[:return_to])
135 elsif @node.draft
136 redirect_to edit_node_path(@node)
82 else 137 else
83 render :action => :edit 138 redirect_to node_path(@node)
84 end 139 end
140 rescue LockedByAnotherUser => e
141 flash[:error] = e.message
142 redirect_to node_path(@node)
85 end 143 end
86 144
87 def destroy 145 def destroy
88 @node.destroy 146 @node.destroy
89 end 147 end
90 148
91 def publish 149 def publish
92 @node.publish_draft! 150 @node.publish_draft!
93 flash[:notice] = "Draft has been published" 151 flash[:notice] = "Draft has been published"
94 redirect_to node_path(@node) 152 redirect_to node_path(@node)
95 end 153 end
96 154
97 def unlock 155 def unlock
98 if @node.unlock! 156 if @node.unlock!
99 flash[:notice] = "Node unlocked" 157 flash[:notice] = "Node unlocked"
100 else 158 else
101 flash[:notice] = "Already unlocked" 159 flash[:notice] = "Already unlocked"
102 end 160 end
103 161
162 redirect_to node_path(@node)
163 end
164
165 def generate_shared_preview
166 @node = Node.find(params[:id])
167 if @node.draft
168 @node.draft.ensure_preview_token!
169 flash[:notice] = "Shareable preview link created - see below."
170 else
171 flash[:notice] = "Create or edit a draft first - shared preview links are only available for pages with an active draft."
172 end
173 redirect_to node_path(@node)
174 end
175
176 def revoke_shared_preview
177 @node = Node.find(params[:id])
178 @node.draft.revoke_preview_token! if @node.draft
179 flash[:notice] = "Shareable preview link revoked."
104 redirect_to node_path(@node) 180 redirect_to node_path(@node)
105 end 181 end
106 182
183 def parameterize_preview
184 render plain: slug_for(params[:title])
185 end
186
187 # Filter functions for admin views
188 def drafts
189 @nodes = index_matching(Node.drafts_and_autosaves)
190 end
191
192 def recent
193 @nodes = index_matching(Node.recently_changed)
194 end
195
196 def mine
197 base = Node.joins(:pages)
198 .where("pages.user_id = ? or pages.editor_id = ?", current_user, current_user)
199 .distinct
200 @nodes = index_matching(base)
201 end
202
203 def chapters
204 @kind_keys = Array(params[:kinds]) & %w[erfa chaostreff]
205 @kind_keys = %w[erfa chaostreff] if @kind_keys.empty?
206 tags = @kind_keys.flat_map { |key| CccConventions::NODE_KINDS[key][:tags] }
207 @nodes = nodes_matching_tags(tags)
208 end
209
210 def tags
211 tags = params[:tags].to_s.split(',').map(&:strip).reject(&:blank?)
212 @nodes = nodes_matching_tags(tags)
213 end
214
215 def sitemap
216 @sitemap = Node.root.self_and_descendants_ordered_with_level
217 @sitemap_descendant_counts = descendant_counts_for(@sitemap)
218 end
219
107 private 220 private
108 221
222 def slug_for(title)
223 title.to_s.parameterize
224 end
225
109 def node_params 226 def node_params
110 params.fetch(:node, {}).permit(:slug, :parent_id, :staged_slug, :staged_parent_id) 227 params.fetch(:node, {}).permit(:slug, :parent_id, :staged_slug, :staged_parent_id)
111 end 228 end
@@ -113,25 +230,56 @@ class NodesController < ApplicationController
113 def page_params 230 def page_params
114 params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name, :published_at, :user_id) 231 params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name, :published_at, :user_id)
115 end 232 end
116 233
117 def find_node 234 def find_node
118 @node = Node.find(params[:id]) 235 @node = Node.find(params[:id])
119 end 236 end
120 237
121 def find_parent 238 def find_parent
122 case params[:kind] 239 case params[:kind]
123 when "top_level"
124 Node.root.id
125 when "update"
126 Update.find_or_create_parent.id
127 when "press_release"
128 Update.find_or_create_parent.id
129 when "generic" 240 when "generic"
130 if params[:parent_id] && Node.find(params[:parent_id]) 241 if params[:parent_id] && Node.find(params[:parent_id])
131 params[:parent_id] 242 params[:parent_id]
132 else 243 else
133 nil 244 nil
134 end 245 end
246 else
247 config = CccConventions::NODE_KINDS[params[:kind]]
248 config && config[:parent] ? config[:parent].call.id : nil
249 end
250 end
251
252 def pin_to_default_locale
253 Globalize.with_locale(I18n.default_locale) { yield }
254 end
255
256 def descendant_counts_for(ordered_with_level)
257 counts = Hash.new(0)
258 stack = [] # [node, level, index]
259 ordered_with_level.each_with_index do |(node, level), index|
260 while stack.any? && stack.last[1] >= level
261 ancestor_node, _ancestor_level, ancestor_index = stack.pop
262 counts[ancestor_node.id] = index - ancestor_index - 1
263 end
264 stack << [node, level, index]
265 end
266 total = ordered_with_level.length
267 while stack.any?
268 ancestor_node, _ancestor_level, ancestor_index = stack.pop
269 counts[ancestor_node.id] = total - ancestor_index - 1
135 end 270 end
271 counts
272 end
273
274 def nodes_matching_tags(tags)
275 matching_pages = Page.tagged_with(tags, any: true).reselect(:id)
276 base = Node.where(head_id: matching_pages).or(Node.where(draft_id: matching_pages))
277 index_matching(base)
278 end
279
280 def index_matching(base_scope)
281 scope = base_scope.includes(:head, :draft)
282 scope = scope.merge(Node.editor_search(params[:q])) if params[:q].present?
283 scope.order("nodes.updated_at desc").paginate(page: params[:page], per_page: 25)
136 end 284 end
137end 285end