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,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