card-mod-history 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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