card-mod-history 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
ADDED
@@ -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
|
data/lib/card/act.rb
ADDED
@@ -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
|