summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/events_controller.rb6
-rw-r--r--app/helpers/node_actions_helper.rb87
-rw-r--r--app/models/event.rb77
-rw-r--r--app/models/node_action.rb30
-rw-r--r--app/views/node_actions/_change_details.html.erb6
5 files changed, 202 insertions, 4 deletions
diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb
index a322181d..818459c4 100644
--- a/app/controllers/events_controller.rb
+++ b/app/controllers/events_controller.rb
@@ -65,7 +65,7 @@ class EventsController < ApplicationController
65 @event = Event.new(event_params) 65 @event = Event.new(event_params)
66 66
67 respond_to do |format| 67 respond_to do |format|
68 if @event.save 68 if @event.save_witnessed(:actor => current_user)
69 flash[:notice] = t("flash.events.created") 69 flash[:notice] = t("flash.events.created")
70 format.html { redirect_to(safe_return_to(params[:return_to] || (@event.node ? edit_node_path(@event.node) : edit_event_path(@event)))) } 70 format.html { redirect_to(safe_return_to(params[:return_to] || (@event.node ? edit_node_path(@event.node) : edit_event_path(@event)))) }
71 format.xml { render :xml => @event, :status => :created, :location => @event } 71 format.xml { render :xml => @event, :status => :created, :location => @event }
@@ -82,7 +82,7 @@ class EventsController < ApplicationController
82 @event = Event.find(params[:id]) 82 @event = Event.find(params[:id])
83 83
84 respond_to do |format| 84 respond_to do |format|
85 if @event.update(event_params) 85 if @event.update_witnessed(event_params, :actor => current_user)
86 flash[:notice] = t("flash.events.updated") 86 flash[:notice] = t("flash.events.updated")
87 format.html { redirect_to(safe_return_to(params[:return_to] || events_path)) } 87 format.html { redirect_to(safe_return_to(params[:return_to] || events_path)) }
88 format.xml { head :ok } 88 format.xml { head :ok }
@@ -97,7 +97,7 @@ class EventsController < ApplicationController
97 # DELETE /events/1.xml 97 # DELETE /events/1.xml
98 def destroy 98 def destroy
99 @event = Event.find(params[:id]) 99 @event = Event.find(params[:id])
100 @event.destroy 100 @event.destroy_witnessed(:actor => current_user)
101 101
102 respond_to do |format| 102 respond_to do |format|
103 format.html { redirect_to(events_url) } 103 format.html { redirect_to(events_url) }
diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb
index 019a9cf7..672dda36 100644
--- a/app/helpers/node_actions_helper.rb
+++ b/app/helpers/node_actions_helper.rb
@@ -23,7 +23,10 @@ module NodeActionsHelper
23 "user_deactivate" => "user-off", 23 "user_deactivate" => "user-off",
24 "user_reactivate" => "user-check", 24 "user_reactivate" => "user-check",
25 "redaktion_grant" => "users-plus", 25 "redaktion_grant" => "users-plus",
26 "redaktion_revoke" => "users-minus" 26 "redaktion_revoke" => "users-minus",
27 "event_create" => "calendar-plus",
28 "event_update" => "calendar-event",
29 "event_destroy" => "calendar-x"
27 }.freeze 30 }.freeze
28 31
29 def verb_icon action 32 def verb_icon action
@@ -57,6 +60,7 @@ module NodeActionsHelper
57 def action_details? action 60 def action_details? action
58 m = action.metadata 61 m = action.metadata
59 return true if m["translation_diff"].present? 62 return true if m["translation_diff"].present?
63 return true if m["changes"].present? || m["description_changed"]
60 return true if m["title"].is_a?(Hash) && m.dig("title", "from") != m.dig("title", "to") 64 return true if m["title"].is_a?(Hash) && m.dig("title", "from") != m.dig("title", "to")
61 %w[author tags template_changed assets assets_changed assets_reordered 65 %w[author tags template_changed assets assets_changed assets_reordered
62 abstract_changed body_changed].any? { |key| m[key].present? } 66 abstract_changed body_changed].any? { |key| m[key].present? }
@@ -92,6 +96,59 @@ module NodeActionsHelper
92 items 96 items
93 end 97 end
94 98
99 def event_changes_list action
100 m = action.metadata
101 c = m["changes"] || {}
102 items = []
103
104 if c["title"]
105 items << t("node_actions.detail_title",
106 :from => c.dig("title", "from"), :to => c.dig("title", "to"))
107 end
108
109 if %w[rrule start_time end_time].any? { |field| c.key?(field) }
110 items << t("node_actions.detail_event_schedule",
111 :from => event_schedule_side(m, c, "from"),
112 :to => event_schedule_side(m, c, "to"))
113 end
114
115 if c["allday"]
116 items << t("node_actions.detail_event_allday",
117 :from => t("admin.common.#{c.dig("allday", "from") ? "yes" : "no"}"),
118 :to => t("admin.common.#{c.dig("allday", "to") ? "yes" : "no"}"))
119 end
120
121 %w[location url].each do |field|
122 next unless c[field]
123 items << t("node_actions.detail_event_#{field}",
124 :from => c.dig(field, "from").presence || t("node_actions.event_none"),
125 :to => c.dig(field, "to").presence || t("node_actions.event_none"))
126 end
127
128 if c["tags"]
129 items << t("node_actions.detail_tags",
130 :from => Array(c.dig("tags", "from")).join(", "),
131 :to => Array(c.dig("tags", "to")).join(", "))
132 end
133
134 if c["node_path"]
135 items << t("node_actions.detail_event_moved",
136 :from => c.dig("node_path", "from") || t("node_actions.event_none"),
137 :to => c.dig("node_path", "to") || t("node_actions.event_none"))
138 end
139
140 items << t("node_actions.detail_event_description") if m["description_changed"]
141 items << t("node_actions.detail_event_coordinates") if c["latitude"] || c["longitude"]
142 items
143 end
144
145 def event_schedule_side metadata, changes, side
146 attributes = %w[rrule start_time end_time].each_with_object({}) do |field, acc|
147 acc[field.to_sym] = changes.key?(field) ? changes.dig(field, side) : metadata[field]
148 end
149 event_schedule_text(Event.new(attributes)).presence || t("node_actions.event_none")
150 end
151
95 def translation_changes diff 152 def translation_changes diff
96 case diff["status"] 153 case diff["status"]
97 when "added" then [t("node_actions.locale_added", :title => diff.dig("title", "to"))] 154 when "added" then [t("node_actions.locale_added", :title => diff.dig("title", "to"))]
@@ -188,6 +245,12 @@ module NodeActionsHelper
188 participant ? link_to(name, edit_user_path(participant)) : name 245 participant ? link_to(name, edit_user_path(participant)) : name
189 end 246 end
190 247
248 def event_ref action
249 event = action.action_participants.detect { |p| p.subject_type == "Event" }&.subject
250 name = h(action.metadata["event_title"].presence || t("node_actions.unknown_event"))
251 event ? link_to(name, edit_event_path(event)) : name
252 end
253
191 def summarize_publish action 254 def summarize_publish action
192 if action.metadata["via"] == "revision" 255 if action.metadata["via"] == "revision"
193 t("node_actions.publish_rollback", 256 t("node_actions.publish_rollback",
@@ -301,4 +364,26 @@ module NodeActionsHelper
301 t("node_actions.user_create", :actor => actor_ref(action), 364 t("node_actions.user_create", :actor => actor_ref(action),
302 :target => user_participant_ref(action)).html_safe 365 :target => user_participant_ref(action)).html_safe
303 end 366 end
367
368 def summarize_event_create action
369 event_sentence(action, "event_create")
370 end
371
372 def summarize_event_update action
373 event_sentence(action, "event_update")
374 end
375
376 def summarize_event_destroy action
377 event_sentence(action, "event_destroy")
378 end
379
380 def event_sentence action, key
381 if action.node
382 t("node_actions.#{key}_on", :actor => actor_ref(action),
383 :event => event_ref(action), :subject => subject_ref(action)).html_safe
384 else
385 t("node_actions.#{key}", :actor => actor_ref(action),
386 :event => event_ref(action)).html_safe
387 end
388 end
304end 389end
diff --git a/app/models/event.rb b/app/models/event.rb
index b8651a81..7726f9bf 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -20,8 +20,85 @@ class Event < ApplicationRecord
20 title.presence || node&.head&.title || "Untitled event" 20 title.presence || node&.head&.title || "Untitled event"
21 end 21 end
22 22
23 def save_witnessed(actor:)
24 saved = false
25 transaction do
26 saved = save
27 raise ActiveRecord::Rollback unless saved
28 witness_event!("event_create", actor)
29 end
30 saved
31 end
32
33 def update_witnessed(attributes, actor:)
34 updated = false
35 transaction do
36 updated = update(attributes)
37 raise ActiveRecord::Rollback unless updated
38 changes = event_changes
39 witness_event!("event_update", actor, changes) if changes.any?
40 end
41 updated
42 end
43
44 def destroy_witnessed(actor:)
45 destroyed = false
46 transaction do
47 witness_event!("event_destroy", actor)
48 destroyed = destroy
49 raise ActiveRecord::Rollback unless destroyed
50 end
51 destroyed
52 end
53
23 private 54 private
24 def generate_occurrences 55 def generate_occurrences
25 Occurrence.generate self 56 Occurrence.generate self
26 end 57 end
58
59 def event_snapshot
60 {
61 :event_title => title.presence || node&.unique_name || "##{id}",
62 :start_time => start_time&.iso8601,
63 :end_time => end_time&.iso8601,
64 :allday => allday,
65 :rrule => rrule.presence,
66 :location => location.presence,
67 :url => url.presence,
68 :event_tags => tag_list.to_a.sort,
69 :path => node&.unique_name
70 }.compact
71 end
72
73 def event_changes
74 pairs = {}
75
76 saved_changes.except("updated_at", "created_at", "description")
77 .each do |attribute, (before, after)|
78 key, pair =
79 case attribute
80 when "node_id"
81 ["node_path", { "from" => Node.find_by(:id => before)&.unique_name,
82 "to" => Node.find_by(:id => after)&.unique_name }]
83 when "start_time", "end_time"
84 [attribute, { "from" => before&.iso8601, "to" => after&.iso8601 }]
85 when "tag_list"
86 ["tags", { "from" => Array(before).sort, "to" => Array(after).sort }]
87 else
88 [attribute, { "from" => before, "to" => after }]
89 end
90 pairs[key] = pair
91 end
92
93 extra = {}
94 extra[:changes] = pairs if pairs.any?
95 extra[:description_changed] = true if saved_changes.key?("description")
96 extra
97 end
98
99 def witness_event!(verb, actor, extra = {})
100 NodeAction.record!(:node => node, :participants => [node, self].compact,
101 :action => verb, :user => actor,
102 **event_snapshot, **extra)
103 end
27end 104end
diff --git a/app/models/node_action.rb b/app/models/node_action.rb
index 82a3ef44..0167762b 100644
--- a/app/models/node_action.rb
+++ b/app/models/node_action.rb
@@ -107,6 +107,36 @@ class NodeAction < ApplicationRecord
107 # participant differ: 107 # participant differ:
108 # "target_login" -- flat string, the affected account's login 108 # "target_login" -- flat string, the affected account's login
109 # 109 #
110 # "event_create" / "event_update" / "event_destroy" (calendar
111 # entries; participants: the Event and, when it has one, its Node,
112 # which also fills the node column. Deliberately not gated --
113 # events reach chapter pages and widgets, never the feeds, and
114 # protection here follows emission, not position).
115 #
116 # Events carry no revisions, so the log is their only history:
117 # every entry holds a full snapshot of the state it produced, and
118 # walking an event's entries reconstructs it. Times are ISO 8601
119 # and the rrule is raw, never humanised -- entries are read in both
120 # locales and event_schedule_text resolves that at render time.
121 # "event_title" -- flat string; the title, else the node's
122 # unique_name, else "#<id>"
123 # "start_time", "end_time" -- ISO 8601, when set
124 # "allday" -- boolean
125 # "rrule" -- raw RRULE, when set
126 # "location", "url" -- flat strings, when set
127 # "event_tags" -- array of names, sorted, always. Named apart
128 # from the node verbs' "tags", which is a pair,
129 # so one renderer cannot mistake the other.
130 # "path" -- the node's unique_name, when it has a node
131 #
132 # On "event_update" only, and only when something changed -- an
133 # update that changes nothing records no entry at all:
134 # "changes" -- {field => pair}, node_id resolved to paths
135 # under "node_path", tag_list under "tags",
136 # times as ISO 8601
137 # "description_changed" -- boolean; prose, flagged not quoted,
138 # as abstract_changed and body_changed are
139 #
110 # Reserved: "demote" (via "trash" | "depublish") for an explicit 140 # Reserved: "demote" (via "trash" | "depublish") for an explicit
111 # depublish workflow, if ever built. 141 # depublish workflow, if ever built.
112 # 142 #
diff --git a/app/views/node_actions/_change_details.html.erb b/app/views/node_actions/_change_details.html.erb
index 2da8bfd5..98e70d8f 100644
--- a/app/views/node_actions/_change_details.html.erb
+++ b/app/views/node_actions/_change_details.html.erb
@@ -17,6 +17,12 @@
17 </td> 17 </td>
18 </tr> 18 </tr>
19 <% end %> 19 <% end %>
20 <% if (event_items = event_changes_list(action_entry)).any? %>
21 <tr>
22 <th></th>
23 <td><%= safe_join(event_items, tag.br) %></td>
24 </tr>
25 <% end %>
20 <% (action_entry.metadata["translation_diff"] || {}).each do |locale, diff| %> 26 <% (action_entry.metadata["translation_diff"] || {}).each do |locale, diff| %>
21 <tr> 27 <tr>
22 <th><%= locale.upcase %></th> 28 <th><%= locale.upcase %></th>