card-mod-follow 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,11 @@
1
+ #! no set module
2
+
3
+ class StartFollowLink < FollowLink
4
+ def initialize format
5
+ @rule_content = "*always"
6
+ @link_text = "follow"
7
+ @action = "send"
8
+ @css_class = ""
9
+ super
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ #! no set module
2
+
3
+ class StopFollowLink < FollowLink
4
+ def initialize format
5
+ @rule_content = "*never"
6
+ @link_text = "following"
7
+ @hover_text = "unfollow"
8
+ @action = "stop sending"
9
+ @css_class = "btn-item-delete"
10
+ super
11
+ end
12
+ end
@@ -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