card-mod-follow 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/README.md +134 -0
- data/lib/card/follow_option.rb +33 -0
- data/lib/card/follower_stash.rb +88 -0
- data/set/abstract/follow_option.rb +56 -0
- data/set/all/follow.rb +33 -0
- data/set/all/follow/follow_link.rb +61 -0
- data/set/all/follow/follow_link_views.rb +29 -0
- data/set/all/follow/followed_by.rb +70 -0
- data/set/all/follow/follower_ids.rb +121 -0
- data/set/all/follow/start_follow_link.rb +11 -0
- data/set/all/follow/stop_follow_link.rb +12 -0
- data/set/all/notify.rb +81 -0
- data/set/all/notify/base_views.rb +127 -0
- data/set/all/notify/html_views.rb +17 -0
- data/set/right/account.rb +15 -0
- data/set/right/follow.rb +118 -0
- data/set/right/follow/follow_status.haml +9 -0
- data/set/right/follow_fields.rb +3 -0
- data/set/right/followers.rb +23 -0
- data/set/right/following.rb +50 -0
- data/set/self/always.rb +13 -0
- data/set/self/created.rb +19 -0
- data/set/self/edited.rb +20 -0
- data/set/self/follow.rb +1 -0
- data/set/self/follow_defaults.rb +88 -0
- data/set/self/follow_fields.rb +1 -0
- data/set/self/never.rb +13 -0
- data/set/type/cardtype.rb +21 -0
- data/set/type/notification_template.rb +33 -0
- data/set/type/set.rb +53 -0
- data/set/type/user.rb +7 -0
- data/set/type_plus_right/user/follow.rb +77 -0
- data/set/type_plus_right/user/follow/follow_editor.haml +8 -0
- data/set/type_plus_right/user/follow/follow_editor_helper.rb +121 -0
- metadata +108 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
# used by +:followers overwritten in type/set.rb and type/cardtype.rb
|
2
|
+
def followed?
|
3
|
+
followed_by? Auth.current_id
|
4
|
+
end
|
5
|
+
|
6
|
+
# for sets and cardtypes it doesn't check whether the users is following the
|
7
|
+
# card itself instead it checks whether he is following the complete set
|
8
|
+
def followed_by? user_id
|
9
|
+
follow_rule_applies?(user_id) || left&.followed_by_as_field?(self, user_id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def followed_by_as_field? field, user_id
|
13
|
+
followed_field?(field) && followed_by?(user_id)
|
14
|
+
end
|
15
|
+
|
16
|
+
# returns true if according to the follow_field_rule followers of self also
|
17
|
+
# follow changes of field_card
|
18
|
+
def followed_field? field_card
|
19
|
+
return unless (follow_field_rule = rule_card(:follow_fields))
|
20
|
+
|
21
|
+
follow_field_rule.item_names(context: self).find do |item|
|
22
|
+
case item.to_name.key
|
23
|
+
when field_card.key then true
|
24
|
+
when :nests.cardname.key then nested_card?(field_card)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def nested_card? card
|
30
|
+
nestee_ids.include? card.id
|
31
|
+
end
|
32
|
+
|
33
|
+
## the following methods all handle _explicit_ (direct) follow rules (not fields)
|
34
|
+
|
35
|
+
def follow_rule_applies? follower_id
|
36
|
+
!follow_rule_option(follower_id).nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def follow_rule_option follower_id
|
40
|
+
all_follow_rule_options(follower_id).find do |option|
|
41
|
+
follow_rule_option_applies? follower_id, option
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def all_follow_rule_options follower_id
|
46
|
+
follow_rule = preference :follow, follower_id
|
47
|
+
return [] unless follow_rule.present?
|
48
|
+
|
49
|
+
follow_rule.split("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def follow_rule_option_applies? follower_id, option
|
53
|
+
option_code = option.to_name.code
|
54
|
+
candidate_ids = follower_candidate_ids_for_option option_code
|
55
|
+
follow_rule_option_applies_to_candidates? follower_id, option_code, candidate_ids
|
56
|
+
end
|
57
|
+
|
58
|
+
def follow_rule_option_applies_to_candidates? follower_id, option_code, candidate_ids
|
59
|
+
if (test = FollowOption.test[option_code])
|
60
|
+
test.call follower_id, candidate_ids
|
61
|
+
else
|
62
|
+
candidate_ids.include? follower_id
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def follower_candidate_ids_for_option option_code
|
67
|
+
return [] unless (block = FollowOption.follower_candidate_ids[option_code])
|
68
|
+
|
69
|
+
block.call self
|
70
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
FOLLOWER_IDS_CACHE_KEY = "FOLLOWER_IDS".freeze
|
2
|
+
|
3
|
+
card_accessor :followers
|
4
|
+
|
5
|
+
event :cache_expired_for_type_change, :store, on: :update, changed: %i[type_id name] do
|
6
|
+
act_card&.schedule_preference_expiration
|
7
|
+
# FIXME: expire (also?) after save
|
8
|
+
Card.follow_caches_expired
|
9
|
+
end
|
10
|
+
|
11
|
+
def schedule_preference_expiration
|
12
|
+
@expire_preferences_scheduled = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def expire_preferences?
|
16
|
+
@expire_preferences_scheduled
|
17
|
+
end
|
18
|
+
|
19
|
+
event :expire_preferences_cache, :finalize, when: :expire_preferences? do
|
20
|
+
Card::Rule.clear_preference_cache
|
21
|
+
end
|
22
|
+
|
23
|
+
# follow cache methods on Card class
|
24
|
+
module ClassMethods
|
25
|
+
def follow_caches_expired
|
26
|
+
Card.clear_follower_ids_cache
|
27
|
+
Card::Rule.clear_preference_cache
|
28
|
+
end
|
29
|
+
|
30
|
+
def follower_ids_cache
|
31
|
+
Card.cache.read(FOLLOWER_IDS_CACHE_KEY) || {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_follower_ids_cache hash
|
35
|
+
Card.cache.write FOLLOWER_IDS_CACHE_KEY, hash
|
36
|
+
end
|
37
|
+
|
38
|
+
def clear_follower_ids_cache
|
39
|
+
Card.cache.write FOLLOWER_IDS_CACHE_KEY, nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def write_follower_ids_cache user_ids
|
44
|
+
hash = Card.follower_ids_cache
|
45
|
+
hash[id] = user_ids
|
46
|
+
Card.write_follower_ids_cache hash
|
47
|
+
end
|
48
|
+
|
49
|
+
def read_follower_ids_cache
|
50
|
+
Card.follower_ids_cache[id]
|
51
|
+
end
|
52
|
+
|
53
|
+
def follower_names
|
54
|
+
followers.map(&:name)
|
55
|
+
end
|
56
|
+
|
57
|
+
def followers
|
58
|
+
follower_ids.map do |id|
|
59
|
+
Card.fetch(id)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def follower_ids
|
64
|
+
@follower_ids = read_follower_ids_cache || begin
|
65
|
+
result = direct_follower_ids + indirect_follower_ids
|
66
|
+
write_follower_ids_cache result
|
67
|
+
result
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def followers_count
|
72
|
+
follower_ids.size
|
73
|
+
end
|
74
|
+
|
75
|
+
def indirect_follower_ids
|
76
|
+
result = ::Set.new
|
77
|
+
left_card = left
|
78
|
+
while left_card
|
79
|
+
result += left_card.direct_follower_ids if left_card.followed_field? self
|
80
|
+
left_card = left_card.left
|
81
|
+
end
|
82
|
+
result
|
83
|
+
end
|
84
|
+
|
85
|
+
# all users (cards) that "directly" follow this card
|
86
|
+
# "direct" means there is a follow rule that applies explicitly to this card.
|
87
|
+
# one can also "indirectly" follow cards by following parent cards or other
|
88
|
+
# cards that nest this one.
|
89
|
+
def direct_followers
|
90
|
+
direct_follower_ids.map do |id|
|
91
|
+
Card.fetch(id)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def direct_follower_ids &block
|
96
|
+
ids = ::Set.new
|
97
|
+
set_names.each do |set_name|
|
98
|
+
direct_follower_ids_for_set setcard_from_name(set_name), ids, &block
|
99
|
+
end
|
100
|
+
ids
|
101
|
+
end
|
102
|
+
|
103
|
+
def setcard_from_name set_name
|
104
|
+
Card.fetch set_name, new: { type_id: SetID }
|
105
|
+
end
|
106
|
+
|
107
|
+
def direct_follower_ids_for_set set_card, ids
|
108
|
+
set_card.all_user_ids_with_rule_for(:follow).each do |user_id|
|
109
|
+
next if ids.include?(user_id) || !(option = follow_rule_option user_id)
|
110
|
+
|
111
|
+
yield user_id, set_card, option if block_given?
|
112
|
+
ids << user_id
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def each_direct_follower_id_with_reason
|
117
|
+
direct_follower_ids do |user_id, set_card, follow_option|
|
118
|
+
reason = follow_option.gsub(/[\[\]]/, "")
|
119
|
+
yield user_id, set_card: set_card, option: reason
|
120
|
+
end
|
121
|
+
end
|
data/set/all/notify.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
attr_accessor :follower_stash
|
2
|
+
mattr_accessor :force_notifications
|
3
|
+
|
4
|
+
event :silence_notifications, :initialize, when: :silence_notifications? do
|
5
|
+
@silent_change = true
|
6
|
+
end
|
7
|
+
|
8
|
+
def silence_notifications?
|
9
|
+
!(Card::Env[:controller] || force_notifications)
|
10
|
+
end
|
11
|
+
|
12
|
+
event :notify_followers_after_save,
|
13
|
+
:integrate_with_delay, on: :save, when: :notable_change? do
|
14
|
+
notify_followers
|
15
|
+
end
|
16
|
+
|
17
|
+
# in the delete case we have to calculate the follower_stash beforehand
|
18
|
+
# but we can't pass the follower_stash through the ActiveJob queue.
|
19
|
+
# We have to deal with the notifications in the integrate phase instead of the
|
20
|
+
# integrate_with_delay phase
|
21
|
+
event :stash_followers, :store, on: :delete, when: :notable_change? do
|
22
|
+
act_card.follower_stash ||= FollowerStash.new
|
23
|
+
act_card.follower_stash.check_card self
|
24
|
+
end
|
25
|
+
|
26
|
+
event :notify_followers_after_delete, :integrate, on: :delete, when: :notable_change? do
|
27
|
+
notify_followers
|
28
|
+
end
|
29
|
+
|
30
|
+
def notify_followers
|
31
|
+
return unless (act = Card::Director.act)
|
32
|
+
|
33
|
+
act.reload
|
34
|
+
notify_followers_of act
|
35
|
+
end
|
36
|
+
|
37
|
+
def notable_change?
|
38
|
+
!silent_change? && current_act_card? &&
|
39
|
+
(Card::Auth.current_id != WagnBotID) && followable?
|
40
|
+
end
|
41
|
+
|
42
|
+
def silent_change?
|
43
|
+
silent_change
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def notify_followers_of act
|
49
|
+
act_followers(act).each_follower_with_reason do |follower, reason|
|
50
|
+
next if !follower.account || (follower == act.actor)
|
51
|
+
|
52
|
+
notify_follower follower, act, reason
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def notify_follower follower, act, reason
|
57
|
+
follower.account.send_change_notice act, reason[:set_card].name, reason[:option]
|
58
|
+
end
|
59
|
+
|
60
|
+
def act_followers act
|
61
|
+
@follower_stash ||= FollowerStash.new
|
62
|
+
act.actions(false).each do |a|
|
63
|
+
next if !a.card || a.card.silent_change?
|
64
|
+
|
65
|
+
@follower_stash.check_card a.card
|
66
|
+
end
|
67
|
+
@follower_stash
|
68
|
+
end
|
69
|
+
|
70
|
+
def silent_change
|
71
|
+
@silent_change || @supercard&.silent_change
|
72
|
+
end
|
73
|
+
|
74
|
+
def current_act_card?
|
75
|
+
return false unless act_card
|
76
|
+
|
77
|
+
act_card.id.nil? || act_card.id == id
|
78
|
+
# FIXME: currently card_id is nil for deleted acts (at least
|
79
|
+
# in the store phase when it's tested). The nil test was needed
|
80
|
+
# to make this work.
|
81
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
format do
|
2
|
+
view :list_of_changes, denial: :blank, cache: :never do
|
3
|
+
action = notification_action voo.action_id
|
4
|
+
relevant_fields(action).map do |type|
|
5
|
+
edit_info_for(type, action)
|
6
|
+
end.compact.join
|
7
|
+
end
|
8
|
+
|
9
|
+
view :subedits, perms: :none, cache: :never do
|
10
|
+
return unless notification_act
|
11
|
+
|
12
|
+
wrap_subedits do
|
13
|
+
notification_act.actions_affecting(card).map do |action|
|
14
|
+
next if action.card_id == card.id
|
15
|
+
|
16
|
+
action.card.format(format: @format).render_subedit_notice action_id: action.id
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
view :subedit_notice, cache: :never do
|
22
|
+
action = notification_action voo.action_id
|
23
|
+
wrap_subedit_item do
|
24
|
+
%(#{name_before_action action} #{action.action_type}d\n) +
|
25
|
+
render_list_of_changes(action_id: action.id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
view :followed, perms: :none, compact: true do
|
30
|
+
if (set_card = followed_set_card) && (option_card = follow_option_card)
|
31
|
+
option_card.description set_card
|
32
|
+
else
|
33
|
+
"*followed set of cards*"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
view :follower, perms: :none, compact: true do
|
38
|
+
active_notice(:follower) || "follower"
|
39
|
+
end
|
40
|
+
|
41
|
+
view :last_action_verb, cache: :never do
|
42
|
+
"#{notification_act&.main_action&.action_type || 'edite'}d"
|
43
|
+
end
|
44
|
+
|
45
|
+
view :unfollow_url, perms: :none, compact: true, cache: :never do
|
46
|
+
return "" unless (rule_name = live_follow_rule_name)
|
47
|
+
|
48
|
+
card_url path(mark: "#{active_notice(:follower)}+#{:follow.cardname}",
|
49
|
+
action: :update,
|
50
|
+
card: { subcards: { rule_name => Card[:never].name } })
|
51
|
+
end
|
52
|
+
|
53
|
+
def relevant_fields action
|
54
|
+
case action.action_type
|
55
|
+
when :create then %i[cardtype content]
|
56
|
+
when :update then %i[name cardtype content]
|
57
|
+
when :delete then %i[content]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def name_before_action action
|
62
|
+
(action.value(:name) && action.previous_value(:name)) || card.name
|
63
|
+
end
|
64
|
+
|
65
|
+
def followed_set_card
|
66
|
+
(set_name = active_notice(:followed_set)) && Card.fetch(set_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def follow_option_card
|
70
|
+
return unless (option_name = active_notice(:follow_option))
|
71
|
+
|
72
|
+
Card.fetch option_name
|
73
|
+
end
|
74
|
+
|
75
|
+
def active_notice key
|
76
|
+
@active_notice ||= inherit :active_notice
|
77
|
+
return unless @active_notice
|
78
|
+
|
79
|
+
@active_notice[key]
|
80
|
+
end
|
81
|
+
|
82
|
+
def live_follow_rule_name
|
83
|
+
return unless (set_card = followed_set_card) && (follower = active_notice(:follower))
|
84
|
+
|
85
|
+
set_card.follow_rule_name follower
|
86
|
+
end
|
87
|
+
|
88
|
+
def edit_info_for field, action
|
89
|
+
return nil unless (value = action.value field)
|
90
|
+
value = action.previous_value if action.action_type == :delete
|
91
|
+
wrap_list_item " #{notification_action_label action} #{field}: #{value}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def notification_action_label action
|
95
|
+
case action.action_type
|
96
|
+
when :update then "new"
|
97
|
+
when :delete then "deleted"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def wrap_subedits
|
102
|
+
subedits = yield.compact.join
|
103
|
+
return "" if subedits.blank?
|
104
|
+
|
105
|
+
"\nThis update included the following changes:\n#{wrap_list subedits}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def wrap_list list
|
109
|
+
"\n#{list}\n"
|
110
|
+
end
|
111
|
+
|
112
|
+
def wrap_list_item item
|
113
|
+
"#{item}\n"
|
114
|
+
end
|
115
|
+
|
116
|
+
def wrap_subedit_item
|
117
|
+
"\n#{yield}\n"
|
118
|
+
end
|
119
|
+
|
120
|
+
def notification_act act=nil
|
121
|
+
@notification_act ||= act || card.acts.last
|
122
|
+
end
|
123
|
+
|
124
|
+
def notification_action action_id
|
125
|
+
action_id ? Action.fetch(action_id) : card.last_action
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
format :html do
|
2
|
+
# view :last_action, perms: :none, cache: :never do
|
3
|
+
# _render_last_action_verb
|
4
|
+
# end
|
5
|
+
|
6
|
+
def wrap_list list
|
7
|
+
"<ul>#{list}</ul>\n"
|
8
|
+
end
|
9
|
+
|
10
|
+
def wrap_list_item item
|
11
|
+
"<li>#{item}</li>\n"
|
12
|
+
end
|
13
|
+
|
14
|
+
def wrap_subedit_item
|
15
|
+
"<li>#{yield}</li>\n"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
def send_change_notice act, followed_set, follow_option
|
2
|
+
return unless email.present? && changes_visible?(act)
|
3
|
+
|
4
|
+
notify_of_act act do
|
5
|
+
{ follower: left.name, followed_set: followed_set, follow_option: follow_option }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def notify_of_act act
|
10
|
+
Auth.as(left.id) do
|
11
|
+
Card[:follower_notification_email].deliver(
|
12
|
+
act.card, { to: email }, auth: left, active_notice: yield
|
13
|
+
)
|
14
|
+
end
|
15
|
+
end
|