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