diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/controllers/events_controller.rb | 6 | ||||
| -rw-r--r-- | app/helpers/node_actions_helper.rb | 87 | ||||
| -rw-r--r-- | app/models/event.rb | 77 | ||||
| -rw-r--r-- | app/models/node_action.rb | 30 | ||||
| -rw-r--r-- | app/views/node_actions/_change_details.html.erb | 6 |
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 | ||
| 304 | end | 389 | end |
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 | ||
| 27 | end | 104 | end |
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> |
