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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f748f6e48dcfe648aaa721a41135f026e064752ecf8cdb16f5a9bf2fdf7c845c
4
+ data.tar.gz: ab636c41792878cd5196ec6dd019d35b560d72c9ffe0d25e5a3e61b9d026229d
5
+ SHA512:
6
+ metadata.gz: 42167bcf664c4de96c4966872df4211f5fbbb601ceee045e406b5445fed4ee73223c12712ad9aadd131af4382792702db7567703476559ab878b4bf6af15d092
7
+ data.tar.gz: 49a5f66ce214ccd87a86ffa99898267974c859563cf08d1fddb81dfe72422bec73d64694e8babd91ee6b9ced7ff0f33cae84668049519f4ea2cf8d20d3d3173f
@@ -0,0 +1,137 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ class Card
4
+ # An "act" is a group of recorded {Card::Action actions} on {Card cards}.
5
+ # Together, {Act acts}, {Action actions}, and {Change changes} comprise a
6
+ # comprehensive {Card card} history tracking system.
7
+ #
8
+ # For example, if a given web form submissions updates the contents of three cards,
9
+ # then the submission will result in the recording of three {Action actions}, each
10
+ # of which is tied to one {Act act}.
11
+ #
12
+ # Each act records:
13
+ #
14
+ # - the _actor_id_ (an id associated with the account responsible)
15
+ # - the _card_id_ of the act's primary card
16
+ # - _acted_at_, a timestamp of the action
17
+ # - the _ip_address_ of the actor where applicable.
18
+ #
19
+ class Act < ApplicationRecord
20
+ before_save :assign_actor
21
+ has_many :ar_actions, -> { order :id }, foreign_key: :card_act_id,
22
+ inverse_of: :act,
23
+ class_name: "Card::Action"
24
+ class << self
25
+ # remove all acts that have no card. (janitorial)
26
+ #
27
+ # CAREFUL - could still have actions even if act card is gone...
28
+ def delete_cardless
29
+ left_join = "LEFT JOIN cards ON card_acts.card_id = cards.id"
30
+ joins(left_join).where("cards.id IS NULL").delete_all
31
+ end
32
+
33
+ # remove all acts that have no action. (janitorial)
34
+ def delete_actionless
35
+ joins(
36
+ "LEFT JOIN card_actions ON card_acts.id = card_act_id"
37
+ ).where(
38
+ "card_actions.id is null"
39
+ ).delete_all
40
+ end
41
+
42
+ # all acts with actions on a given list of cards
43
+ # @param card_ids [Array of Integers]
44
+ # @param with_drafts [true, false] (only shows drafts of current user)
45
+ # @return [Array of Acts]
46
+ def all_with_actions_on card_ids, with_drafts=false
47
+ sql = "card_actions.card_id IN (:card_ids) AND (draft is not true"
48
+ sql << (with_drafts ? " OR actor_id = :user_id)" : ")")
49
+ all_viewable([sql, { card_ids: card_ids, user_id: Card::Auth.current_id }])
50
+ end
51
+
52
+ # all acts with actions that current user has permission to view
53
+ # @return [ActiveRecord Relation]
54
+ def all_viewable action_where=nil
55
+ relation = joins(ar_actions: :ar_card)
56
+ relation = relation.where(action_where) if action_where
57
+ relation.where(Query::CardQuery.viewable_sql).where.not(card_id: nil).distinct
58
+ end
59
+
60
+ def cache
61
+ Card::Cache[Card::Act]
62
+ end
63
+
64
+ # used by rails time_ago
65
+ # timestamp is set by rails on create
66
+ def timestamp_attributes_for_create
67
+ super << "acted_at"
68
+ end
69
+ end
70
+
71
+ def actor
72
+ Card.fetch actor_id
73
+ end
74
+
75
+ # the act's primary card
76
+ # @return [Card]
77
+ def card
78
+ Card.fetch card_id, look_in_trash: true # , skip_modules: true
79
+
80
+ # FIXME: if the following is necessary, we need to document why.
81
+ # generally it's a very bad idea to have type-specific code here.
82
+
83
+ # return res unless res&.type_id&.in?([Card::FileID, Card::ImageID])
84
+ # res.include_set_modules
85
+ end
86
+
87
+ # list of all actions that are part of the act
88
+ # @return [Array]
89
+ def actions cached=true
90
+ return ar_actions unless cached
91
+
92
+ self.class.cache.fetch("#{id}-actions") { ar_actions.find_all.to_a }
93
+ end
94
+
95
+ # act's action on the card in question
96
+ # @param card_id [Integer]
97
+ # @return [Card::Action]
98
+ def action_on card_id
99
+ actions.find do |action|
100
+ action.card_id == card_id && !action.draft
101
+ end
102
+ end
103
+
104
+ # act's action on primary card if it exists. otherwise act's first action
105
+ # @return [Card::Action]
106
+ def main_action
107
+ action_on(card_id) || actions.first
108
+ end
109
+
110
+ def draft?
111
+ main_action.draft
112
+ end
113
+
114
+ # time (in words) since act took place
115
+ # @return [String]
116
+ def elapsed_time
117
+ DateTime.new(acted_at).distance_of_time_in_words_to_now
118
+ end
119
+
120
+ # act's actions on either the card itself or another card that includes it
121
+ # @param card [Card]
122
+ # @return [Array of Actions]
123
+ def actions_affecting card
124
+ actions.select do |action|
125
+ (card.id == action.card_id) ||
126
+ card.nestee_ids.include?(action.card_id)
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ # used by before filter
133
+ def assign_actor
134
+ self.actor_id ||= Auth.current_id
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,217 @@
1
+ class Card
2
+ class Act
3
+ class ActRenderer
4
+ def initialize format, act, args
5
+ @format = format
6
+ @act = act
7
+ @act_card = act.card
8
+ @args = args
9
+ @card = @format.card
10
+ @context = @args[:act_context]
11
+ end
12
+
13
+ include ::Bootstrapper
14
+
15
+ def method_missing method_name, *args, &block
16
+ if block_given?
17
+ @format.send(method_name, *args, &block)
18
+ else
19
+ @format.send(method_name, *args)
20
+ end
21
+ end
22
+
23
+ def respond_to_missing? method_name, _include_private=false
24
+ @format.respond_to? method_name
25
+ end
26
+
27
+ def render
28
+ return "" unless @act_card
29
+
30
+ act_accordion
31
+ end
32
+
33
+ def header
34
+ #::Bootstrap.new(self).render do
35
+ bs_layout do
36
+ row xs: [10, 2] do
37
+ column do
38
+ html title
39
+ tag(:span, "text-muted pl-1 badge") { summary }
40
+ end
41
+ column act_links, class: "text-right"
42
+ end
43
+ end
44
+ # end
45
+ end
46
+
47
+ def absolute_title
48
+ accordion_expand_link(@act_card.name)
49
+ end
50
+
51
+ def details
52
+ approved_actions[0..20].map do |action|
53
+ Action::ActionRenderer.new(@format, action, action_header?,
54
+ :summary).render
55
+ end.join
56
+ end
57
+
58
+ def summary
59
+ %i[create update delete draft].map do |type|
60
+ next unless count_types[type].positive?
61
+
62
+ "#{@format.action_icon type}<small> #{count_types[type]}</small>"
63
+ end.compact.join "<small class='text-muted'> | </small>"
64
+ end
65
+
66
+ def act_links
67
+ [
68
+ link_to_history,
69
+ (link_to_act_card unless @act_card.trash)
70
+ ].compact.join " "
71
+ end
72
+
73
+ def link_to_act_card
74
+ link_to_card @act_card, icon_tag(:new_window), class: "_stop_propagation"
75
+ end
76
+
77
+ def link_to_history
78
+ link_to_card @act_card, icon_tag(:history),
79
+ path: { view: :history, look_in_trash: true },
80
+ class: "_stop_propagation",
81
+ rel: "nofollow"
82
+ end
83
+
84
+ def approved_actions
85
+ @approved_actions ||= actions.select { |a| a.card&.ok?(:read) }
86
+ # FIXME: should not need to test for presence of card here.
87
+ end
88
+
89
+ def action_header?
90
+ true
91
+ # @action_header ||= approved_actions.size != 1 ||
92
+ # approved_actions[0].card_id != @format.card.id
93
+ end
94
+
95
+ def count_types
96
+ @count_types ||=
97
+ approved_actions.each_with_object(
98
+ Hash.new { |h, k| h[k] = 0 }
99
+ ) do |action, type_cnt|
100
+ type_cnt[action.action_type] += 1
101
+ end
102
+ end
103
+
104
+ def edited_ago
105
+ return "" unless @act.acted_at
106
+
107
+ "#{time_ago_in_words(@act.acted_at)} ago"
108
+ end
109
+
110
+ def collapse_id
111
+ "act-id-#{@act.id}"
112
+ end
113
+
114
+ def accordion_expand_link text
115
+ <<-HTML
116
+ <a>
117
+ #{text}
118
+ </a>
119
+ HTML
120
+ end
121
+
122
+ # TODO: change accordion API in bootstrap/helper.rb so that it can be used
123
+ # here. The problem is that here we have extra links in the title
124
+ # that are not supposed to expand the accordion
125
+ def act_accordion
126
+ context = @act.main_action.draft ? :warning : :default
127
+ <<-HTML
128
+ <div class="card card-#{context} nodblclick">
129
+ #{act_accordion_panel}
130
+ </div>
131
+ HTML
132
+ end
133
+
134
+ def accordion_expand_options
135
+ {
136
+ "data-toggle" => "collapse",
137
+ "data-target" => ".#{collapse_id}",
138
+ "aria-expanded" => true,
139
+ "aria-controls" => collapse_id
140
+ }
141
+ end
142
+
143
+ def act_panel_options
144
+ { class: "card-header", role: "tab", id: "heading-#{collapse_id}" }
145
+ end
146
+
147
+ def act_accordion_panel
148
+ act_accordion_heading + act_accordion_body
149
+ end
150
+
151
+ def act_accordion_heading
152
+ wrap_with :div, act_panel_options.merge(accordion_expand_options) do
153
+ wrap_with(:h5, header, class: "mb-0") + subtitle
154
+ end
155
+ end
156
+
157
+ def act_accordion_body
158
+ wrap_with :div, id: collapse_id,
159
+ class: "collapse #{collapse_id}",
160
+ "data-parent": ".act-accordion-group" do
161
+ wrap_with :div, details, class: "card-body"
162
+ end
163
+ end
164
+
165
+ # Revert:
166
+ # current update
167
+ # Restore:
168
+ # current deletion
169
+ # Revert and Restore:
170
+ # old deletions
171
+ # blank:
172
+ # current create
173
+ # save as current:
174
+ # not current, not deletion
175
+ def rollback_link
176
+ return unless card.ok? :update
177
+
178
+ wrap_with :div, class: "act-link collapse #{collapse_id} float-right" do
179
+ content_tag(:small, revert_link)
180
+
181
+ # link_to "Save as current",
182
+ # class: "slotter", remote: true,
183
+ # method: :post, rel: "nofollow",
184
+ # "data-slot-selector" => ".card-slot.history-view",
185
+ # path: { action: :update, action_ids: prior,
186
+ # view: :open, look_in_trash: true }
187
+ end
188
+ end
189
+
190
+ def deletion_act?
191
+ act_type == :delete
192
+ end
193
+
194
+ def act_type
195
+ @act.main_action.action_type
196
+ end
197
+
198
+ def show_or_hide_changes_link
199
+ wrap_with :div, class: "act-link" do
200
+ @format.link_to_view(
201
+ :act, "#{@args[:hide_diff] ? 'Show' : 'Hide'} changes",
202
+ path: { act_id: @args[:act].id, act_seq: @args[:act_seq],
203
+ hide_diff: !@args[:hide_diff], action_view: :expanded,
204
+ act_context: @args[:act_context], look_in_trash: true }
205
+ )
206
+ end
207
+ end
208
+
209
+ def autosaved_draft_link opts={}
210
+ text = opts.delete(:text) || "autosaved draft"
211
+ opts[:path] = { edit_draft: true }
212
+ add_class opts, "navbar-link"
213
+ link_to_view :edit, text, opts
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,34 @@
1
+ class Card
2
+ class Act
3
+ class ActRenderer
4
+ # Used for recent changes.
5
+ # It shows all actions of an act
6
+ class AbsoluteActRenderer < ActRenderer
7
+ def title
8
+ absolute_title
9
+ end
10
+
11
+ def subtitle
12
+ wrap_with :small do
13
+ [
14
+ @format.link_to_card(@act.actor, nil, class: "_stop_propagation"),
15
+ edited_ago,
16
+ rollback_link
17
+ ]
18
+ end
19
+ end
20
+
21
+ # FIXME: how do we know we need main here??
22
+ def revert_link
23
+ revert_actions_link "revert to previous",
24
+ { revert_to: :previous, revert_act: @act.id },
25
+ "data-slot-selector": "#main > .card-slot"
26
+ end
27
+
28
+ def actions
29
+ @act.actions
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,53 @@
1
+ class Card
2
+ class Act
3
+ class ActRenderer
4
+ # Used for the bridge
5
+ class BridgeActRenderer < RelativeActRenderer
6
+ def title
7
+ wrap_with(:div, left_title, class: "mr-2") +
8
+ wrap_with(:div, right_title, class: "ml-auto act-summary")
9
+ end
10
+
11
+ def left_title
12
+ ["##{@args[:act_seq]}", @act.actor.name, wrap_with(:small, edited_ago)].join " "
13
+ end
14
+
15
+ def right_title
16
+ summary
17
+ end
18
+
19
+ def render
20
+ return "" unless @act_card
21
+
22
+ details
23
+ end
24
+
25
+ def bridge_link
26
+ opts = @format.bridge_link_opts(
27
+ path: { act_id: @act.id, view: :bridge_act, act_seq: @args[:act_seq] },
28
+ "data-toggle": "pill"
29
+ )
30
+ add_class opts, "d-flex nav-link"
31
+ opts[:path].delete :layout
32
+ link_to_card @card, title, opts
33
+ end
34
+
35
+ def overlay_title
36
+ wrap_with :div do
37
+ [left_title, summary,
38
+ subtitle.present? ? subtitle : nil,
39
+ rollback_or_edit_link].compact.join " | "
40
+ end
41
+ end
42
+
43
+ def rollback_or_edit_link
44
+ if @act.draft?
45
+ autosaved_draft_link text: "continue editing"
46
+ elsif show_rollback_link?
47
+ revert_link
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end