card-mod-history 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/card/act.rb +137 -0
- data/lib/card/act/act_renderer.rb +217 -0
- data/lib/card/act/act_renderer/absolute_act_renderer.rb +34 -0
- data/lib/card/act/act_renderer/bridge_act_renderer.rb +53 -0
- data/lib/card/act/act_renderer/relative_act_renderer.rb +59 -0
- data/lib/card/action.rb +230 -0
- data/lib/card/action/action_renderer.rb +94 -0
- data/lib/card/action/admin.rb +36 -0
- data/lib/card/action/differ.rb +89 -0
- data/lib/card/change.rb +70 -0
- data/set/all/history.rb +109 -0
- data/set/all/history/act_listing.rb +124 -0
- data/set/all/history/actions.rb +124 -0
- data/set/all/history/acts.rb +8 -0
- data/set/all/history/events.rb +100 -0
- data/set/all/history/last.rb +98 -0
- data/set/all/history/revision.rb +65 -0
- data/set/all/history/selected.rb +64 -0
- data/set/all/history/views.rb +35 -0
- data/set/all/history_bridge.rb +65 -0
- metadata +80 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
class Card
|
2
|
+
class Act
|
3
|
+
class ActRenderer
|
4
|
+
# Use for the history for one specific card
|
5
|
+
# It shows only the actions of an act that are relevant
|
6
|
+
# for the card of the format that renders the act.
|
7
|
+
class RelativeActRenderer < ActRenderer
|
8
|
+
def title
|
9
|
+
"<span class=\"nr\">##{@args[:act_seq]}</span>" +
|
10
|
+
accordion_expand_link(@act.actor.name) +
|
11
|
+
" " +
|
12
|
+
wrap_with(:small, edited_ago)
|
13
|
+
end
|
14
|
+
|
15
|
+
def subtitle
|
16
|
+
return "" unless @act.card_id != @format.card.id
|
17
|
+
|
18
|
+
wrap_with :small, "act on #{absolute_title}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def act_links
|
22
|
+
return unless (content = rollback_or_edit_link)
|
23
|
+
|
24
|
+
wrap_with :small, content
|
25
|
+
end
|
26
|
+
|
27
|
+
def rollback_or_edit_link
|
28
|
+
if @act.draft?
|
29
|
+
autosaved_draft_link text: "continue editing",
|
30
|
+
class: "collapse #{collapse_id}"
|
31
|
+
elsif show_rollback_link?
|
32
|
+
rollback_link
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def show_rollback_link?
|
37
|
+
!current_act?
|
38
|
+
end
|
39
|
+
|
40
|
+
def current_act?
|
41
|
+
return unless @format.card.last_act && @act
|
42
|
+
|
43
|
+
@act.id == @format.card.last_act.id
|
44
|
+
end
|
45
|
+
|
46
|
+
def actions
|
47
|
+
@actions ||= @act.actions_affecting(@card)
|
48
|
+
end
|
49
|
+
|
50
|
+
def revert_link
|
51
|
+
revert_actions_link "revert to this",
|
52
|
+
{ revert_actions: actions.map(&:id) },
|
53
|
+
class: "_close-modal",
|
54
|
+
"data-slotter-mode": "update-modal-origin"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/card/action.rb
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
class Card
|
4
|
+
# An _action_ is a group of {Card::Change changes} to a single {Card card}
|
5
|
+
# that is recorded during an {Card::Act act}.
|
6
|
+
# Together, {Act acts}, {Action actions}, and {Change changes} comprise a
|
7
|
+
# comprehensive {Card card} history tracking system.
|
8
|
+
#
|
9
|
+
# For example, if a given web submission changes both the name and type of
|
10
|
+
# a given card, that would be recorded as one {Action action} with two
|
11
|
+
# {Change changes}. If there are multiple cards changed, each card would
|
12
|
+
# have its own {Action action}, but the whole submission would still comprise
|
13
|
+
# just one single {Act act}.
|
14
|
+
#
|
15
|
+
# An {Action} records:
|
16
|
+
#
|
17
|
+
# * the _card_id_ of the {Card card} acted upon
|
18
|
+
# * the _card_act_id_ of the {Card::Act act} of which the action is part
|
19
|
+
# * the _action_type_ (create, update, or delete)
|
20
|
+
# * a boolean indicated whether the action is a _draft_
|
21
|
+
# * a _comment_ (where applicable)
|
22
|
+
#
|
23
|
+
class Action < ApplicationRecord
|
24
|
+
include Differ
|
25
|
+
extend Admin
|
26
|
+
|
27
|
+
belongs_to :act, foreign_key: :card_act_id, inverse_of: :ar_actions
|
28
|
+
belongs_to :ar_card, foreign_key: :card_id, inverse_of: :actions, class_name: "Card"
|
29
|
+
has_many :card_changes, foreign_key: :card_action_id,
|
30
|
+
inverse_of: :action,
|
31
|
+
dependent: :delete_all,
|
32
|
+
class_name: "Card::Change"
|
33
|
+
belongs_to :super_action, class_name: "Action", inverse_of: :sub_actions
|
34
|
+
has_many :sub_actions, class_name: "Action", inverse_of: :super_action
|
35
|
+
|
36
|
+
scope :created_by, lambda { |actor_id|
|
37
|
+
joins(:act).where "card_acts.actor_id = ?", actor_id
|
38
|
+
}
|
39
|
+
|
40
|
+
# these are the three possible values for action_type
|
41
|
+
TYPE_OPTIONS = %i[create update delete].freeze
|
42
|
+
|
43
|
+
after_save :expire
|
44
|
+
|
45
|
+
class << self
|
46
|
+
# retrieve action from cache if available
|
47
|
+
# @param id [id of Action]
|
48
|
+
# @return [Action, nil]
|
49
|
+
def fetch id
|
50
|
+
cache.fetch id.to_s do
|
51
|
+
find id.to_i
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# cache object for actions
|
56
|
+
# @return [Card::Cache]
|
57
|
+
def cache
|
58
|
+
Card::Cache[Action]
|
59
|
+
end
|
60
|
+
|
61
|
+
def all_with_cards
|
62
|
+
joins :ar_card
|
63
|
+
end
|
64
|
+
|
65
|
+
def all_viewable
|
66
|
+
all_with_cards.where Query::CardQuery.viewable_sql
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# each action is associated with on and only one card
|
71
|
+
# @return [Card]
|
72
|
+
def card
|
73
|
+
Card.fetch card_id, look_in_trash: true
|
74
|
+
|
75
|
+
# I'm not sure what the rationale for the following was/is, but it was causing
|
76
|
+
# problems in cases where slot attributes are overridden (eg see #wrap_data in
|
77
|
+
# sources on wikirate). The problem is the format object had the set modules but
|
78
|
+
# the card didn't.
|
79
|
+
#
|
80
|
+
# My guess is that the need for the following had something to do with errors
|
81
|
+
# associated with changed types. If so, the solution probably needs to handle
|
82
|
+
# including the set modules associated with the type at the time of the action
|
83
|
+
# rather than including no set modules at all.
|
84
|
+
#
|
85
|
+
# What's more, we _definitely_ don't want to hard code special behavior for
|
86
|
+
# specific types in here!
|
87
|
+
|
88
|
+
# , skip_modules: true
|
89
|
+
# return res unless res && res.type_id.in?([Card::FileID, Card::ImageID])
|
90
|
+
# res.include_set_modules
|
91
|
+
end
|
92
|
+
|
93
|
+
# remove action from action cache
|
94
|
+
def expire
|
95
|
+
self.class.cache.delete id.to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
# assign action_type (create, update, or delete)
|
99
|
+
# @param value [Symbol]
|
100
|
+
# @return [Integer]
|
101
|
+
def action_type= value
|
102
|
+
write_attribute :action_type, TYPE_OPTIONS.index(value)
|
103
|
+
end
|
104
|
+
|
105
|
+
# retrieve action_type (create, update, or delete)
|
106
|
+
# @return [Symbol]
|
107
|
+
def action_type
|
108
|
+
return :draft if draft
|
109
|
+
|
110
|
+
TYPE_OPTIONS[read_attribute(:action_type)]
|
111
|
+
end
|
112
|
+
|
113
|
+
def previous_action
|
114
|
+
Card::Action.where("id < ? AND card_id = ?", id, card_id).last
|
115
|
+
end
|
116
|
+
|
117
|
+
# value set by action's {Change} to given field
|
118
|
+
# @see #interpret_field #interpret_field for field param
|
119
|
+
# @see #interpret_value #interpret_value for return values
|
120
|
+
def value field
|
121
|
+
return unless (change = change field)
|
122
|
+
|
123
|
+
interpret_value field, change.value
|
124
|
+
end
|
125
|
+
|
126
|
+
# value of field set by most recent {Change} before this one
|
127
|
+
# @see #interpret_field #interpret_field for field param
|
128
|
+
# @see #interpret_field #interpret_field for field param
|
129
|
+
def previous_value field
|
130
|
+
return if action_type == :create
|
131
|
+
return unless (previous_change = previous_change field)
|
132
|
+
|
133
|
+
interpret_value field, previous_change.value
|
134
|
+
end
|
135
|
+
|
136
|
+
# action's {Change} object for given field
|
137
|
+
# @see #interpret_field #interpret_field for field param
|
138
|
+
# @return [Change]
|
139
|
+
def change field
|
140
|
+
changes[interpret_field field]
|
141
|
+
end
|
142
|
+
|
143
|
+
# most recent change to given field before this one
|
144
|
+
# @see #interpret_field #interpret_field for field param
|
145
|
+
# @return [Change]
|
146
|
+
def previous_change field
|
147
|
+
return nil if action_type == :create
|
148
|
+
|
149
|
+
field = interpret_field field
|
150
|
+
if @previous_changes&.key?(field)
|
151
|
+
@previous_changes[field]
|
152
|
+
else
|
153
|
+
@previous_changes ||= {}
|
154
|
+
@previous_changes[field] = card.last_change_on field, before: self
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def all_changes
|
159
|
+
self.class.cache.fetch("#{id}-changes") do
|
160
|
+
# using card_changes causes caching problem
|
161
|
+
Card::Change.where(card_action_id: id).to_a
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# all action {Change changes} in hash form. { field1: Change1 }
|
166
|
+
# @return [Hash]
|
167
|
+
def changes
|
168
|
+
@changes ||=
|
169
|
+
if sole?
|
170
|
+
current_changes
|
171
|
+
else
|
172
|
+
all_changes.each_with_object({}) do |change, hash|
|
173
|
+
hash[change.field.to_sym] = change
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# all changed values in hash form. { field1: new_value }
|
179
|
+
def changed_values
|
180
|
+
@changed_values ||= changes.each_with_object({}) do |(key, change), h|
|
181
|
+
h[key] = change.value
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# @return [Hash]
|
186
|
+
def current_changes
|
187
|
+
return {} unless card
|
188
|
+
|
189
|
+
@current_changes ||=
|
190
|
+
Card::Change::TRACKED_FIELDS.each_with_object({}) do |field, hash|
|
191
|
+
hash[field.to_sym] = Card::Change.new field: field,
|
192
|
+
value: card.send(field),
|
193
|
+
card_action_id: id
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# translate field into fieldname as referred to in database
|
198
|
+
# @see Change::TRACKED_FIELDS
|
199
|
+
# @param field [Symbol] can be :type_id, :cardtype, :db_content, :content,
|
200
|
+
# :name, :trash
|
201
|
+
# @return [Symbol]
|
202
|
+
def interpret_field field
|
203
|
+
case field
|
204
|
+
when :content then :db_content
|
205
|
+
when :cardtype then :type_id
|
206
|
+
else field.to_sym
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# value in form prescribed for specific field name
|
211
|
+
# @param value [value of {Change}]
|
212
|
+
# @return [Integer] for :type_id
|
213
|
+
# @return [String] for :name, :db_content, :content, :cardtype
|
214
|
+
# @return [True/False] for :trash
|
215
|
+
def interpret_value field, value
|
216
|
+
case field.to_sym
|
217
|
+
when :type_id
|
218
|
+
value&.to_i
|
219
|
+
when :cardtype
|
220
|
+
Card.fetch_name(value&.to_i)
|
221
|
+
else value
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def sole?
|
226
|
+
all_changes.empty? &&
|
227
|
+
(action_type == :create || Card::Action.where(card_id: card_id).count == 1)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
class Card
|
2
|
+
class Action
|
3
|
+
class ActionRenderer
|
4
|
+
attr_reader :action, :header
|
5
|
+
def initialize format, action, header=true, action_view=:summary, hide_diff=false
|
6
|
+
@format = format
|
7
|
+
@action = action
|
8
|
+
@header = header
|
9
|
+
@action_view = action_view
|
10
|
+
@hide_diff = hide_diff
|
11
|
+
end
|
12
|
+
|
13
|
+
include ::Bootstrapper
|
14
|
+
def method_missing method_name, *args, &block
|
15
|
+
if block_given?
|
16
|
+
@format.send(method_name, *args, &block)
|
17
|
+
else
|
18
|
+
@format.send(method_name, *args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def respond_to_missing? method_name, _include_private=false
|
23
|
+
@format.respond_to? method_name
|
24
|
+
end
|
25
|
+
|
26
|
+
def render
|
27
|
+
classes = @format.classy("action-list")
|
28
|
+
bs_layout container: true, fluid: true do
|
29
|
+
row do
|
30
|
+
html <<-HTML
|
31
|
+
<ul class="#{classes} w-100">
|
32
|
+
<li class="#{action.action_type}">
|
33
|
+
#{action_panel}
|
34
|
+
</li>
|
35
|
+
</ul>
|
36
|
+
HTML
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def action_panel
|
42
|
+
bs_panel do
|
43
|
+
if header
|
44
|
+
heading do
|
45
|
+
div type_diff, class: "float-right"
|
46
|
+
div name_diff
|
47
|
+
end
|
48
|
+
end
|
49
|
+
body do
|
50
|
+
content_diff
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def name_diff
|
56
|
+
if @action.card == @format.card
|
57
|
+
name_changes
|
58
|
+
else
|
59
|
+
link_to_view(
|
60
|
+
:related, name_changes,
|
61
|
+
path: { slot: { items: { view: "history", nest_name: @action.card.name } } },
|
62
|
+
# "data-slot-selector" => ".card-slot.history-view"
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def content_diff
|
68
|
+
return @action.raw_view if @action.action_type == :delete
|
69
|
+
|
70
|
+
@format.subformat(@action.card).render_action_summary action_id: @action.id
|
71
|
+
end
|
72
|
+
|
73
|
+
def type_diff
|
74
|
+
return "" unless @action.new_type?
|
75
|
+
|
76
|
+
@hide_diff ? @action.value(:cardtype) : @action.cardtype_diff
|
77
|
+
end
|
78
|
+
|
79
|
+
def name_changes
|
80
|
+
return old_name unless @action.new_name?
|
81
|
+
|
82
|
+
@hide_diff ? new_name : Card::Content::Diff.complete(old_name, new_name)
|
83
|
+
end
|
84
|
+
|
85
|
+
def old_name
|
86
|
+
(name = @action.previous_value :name) && title_in_context(name)
|
87
|
+
end
|
88
|
+
|
89
|
+
def new_name
|
90
|
+
title_in_context @action.value(:name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Card
|
2
|
+
class Action
|
3
|
+
# methods for administering card actions
|
4
|
+
module Admin
|
5
|
+
# permanently delete all {Action actions} not associated with a {Card}
|
6
|
+
def delete_cardless
|
7
|
+
left_join = "LEFT JOIN cards ON card_actions.card_id = cards.id"
|
8
|
+
joins(left_join).where("cards.id IS NULL").delete_all
|
9
|
+
end
|
10
|
+
|
11
|
+
# permanently delete all {Action actions} associate with non-current
|
12
|
+
# {Change changes}
|
13
|
+
def delete_old
|
14
|
+
Card::Change.delete_all
|
15
|
+
Card.find_each(&:delete_old_actions)
|
16
|
+
Card::Act.delete_actionless
|
17
|
+
end
|
18
|
+
|
19
|
+
# If an act is given then all remaining actions will be attached to that act.
|
20
|
+
# Otherwise the actions keep their acts.
|
21
|
+
def make_current_state_the_initial_state act=nil
|
22
|
+
Card::Change.delete_all
|
23
|
+
Card.find_each(&:delete_old_actions)
|
24
|
+
action_update = { action_type: Card::Action::TYPE_OPTIONS.index(:create) }
|
25
|
+
action_update[:card_act_id] = act.id if act
|
26
|
+
Card::Action.update_all action_update
|
27
|
+
|
28
|
+
if act
|
29
|
+
Card::Act.where("id != :id", id: act.id).delete_all
|
30
|
+
else
|
31
|
+
Card::Act.delete_actionless
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
class Card
|
2
|
+
class Action
|
3
|
+
# a collection of methods for comparing actions
|
4
|
+
module Differ
|
5
|
+
# compare action's name value with previous name value
|
6
|
+
# @return [rendered diff]
|
7
|
+
def name_diff opts={}
|
8
|
+
return unless new_name?
|
9
|
+
|
10
|
+
diff_object(:name, opts).complete
|
11
|
+
end
|
12
|
+
|
13
|
+
# does action change card's name?
|
14
|
+
# @return [true/false]
|
15
|
+
def new_name?
|
16
|
+
!value(:name).nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [rendered diff]
|
20
|
+
# compare action's cardtype value with previous cardtype value
|
21
|
+
def cardtype_diff opts={}
|
22
|
+
return unless new_type?
|
23
|
+
|
24
|
+
diff_object(:cardtype, opts).complete
|
25
|
+
end
|
26
|
+
|
27
|
+
# does action change card's type?
|
28
|
+
# @return [true/false]
|
29
|
+
def new_type?
|
30
|
+
!value(:type_id).nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [rendered diff]
|
34
|
+
# compare action's content value with previous content value
|
35
|
+
def content_diff diff_type=:expanded, opts=nil
|
36
|
+
return unless new_content?
|
37
|
+
|
38
|
+
dobj = content_diff_object(opts)
|
39
|
+
diff_type == :summary ? dobj.summary : dobj.complete
|
40
|
+
end
|
41
|
+
|
42
|
+
# does action change card's content?
|
43
|
+
# @return [true/false]
|
44
|
+
def new_content?
|
45
|
+
!value(:db_content).nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
# test whether content was visibly removed
|
49
|
+
# @return [true/false]
|
50
|
+
def red?
|
51
|
+
content_diff_object.red?
|
52
|
+
end
|
53
|
+
|
54
|
+
# test whether content was visibly added
|
55
|
+
# @return [true/false]
|
56
|
+
def green?
|
57
|
+
content_diff_object.green?
|
58
|
+
end
|
59
|
+
|
60
|
+
def raw_view content=nil
|
61
|
+
original_content = card.db_content
|
62
|
+
card.db_content = content || value(:db_content)
|
63
|
+
card.format.render_raw
|
64
|
+
ensure
|
65
|
+
card.db_content = original_content
|
66
|
+
end
|
67
|
+
|
68
|
+
def summary_diff_omits_content?
|
69
|
+
content_diff_object.summary_omits_content?
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def diff_object field, opts
|
75
|
+
Card::Content::Diff.new previous_value(field), value(field), opts
|
76
|
+
end
|
77
|
+
|
78
|
+
def content_diff_object opts=nil
|
79
|
+
@diff ||= begin
|
80
|
+
diff_args = opts || card.include_set_modules.diff_args
|
81
|
+
previous_value = previous_value(:content)
|
82
|
+
previous = previous_value ? raw_view(previous_value) : ""
|
83
|
+
current = raw_view
|
84
|
+
Card::Content::Diff.new previous, current, diff_args
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|