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.
@@ -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
@@ -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