card-mod-follow 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/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
|