summaryrefslogtreecommitdiff
path: root/app/models/concerns/subscribable.rb
blob: fe47393c554df05245a5a3fe524a32bcf3617483 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# frozen_string_literal: true

# == Subscribable concern
#
# Users can subscribe to these models.
#
# Used by Issue, MergeRequest, Label
#

module Subscribable
  extend ActiveSupport::Concern

  included do
    has_many :subscriptions, dependent: :destroy, as: :subscribable # rubocop:disable Cop/ActiveRecordDependent
  end

  def subscribed?(user, project = nil)
    return false unless user

    if (subscription = lazy_subscription(user, project)&.itself)
      subscription.subscribed
    else
      subscribed_without_subscriptions?(user, project)
    end
  end

  def lazy_subscription(user, project = nil)
    return unless user

    BatchLoader.for(id: id, subscribable_type: subscribable_type, project_id: project&.id).batch do |items, loader|
      values = items.each_with_object({ ids: Set.new, subscribable_types: Set.new, project_ids: Set.new }) do |item, result|
        result[:ids] << item[:id]
        result[:subscribable_types] << item[:subscribable_type]
        result[:project_ids] << item[:project_id]
      end

      subscriptions = Subscription.where(subscribable_id: values[:ids], subscribable_type: values[:subscribable_types], project_id: values[:project_ids], user: user)

      subscriptions.each do |subscription|
        loader.call({
          id: subscription.subscribable_id,
          subscribable_type: subscription.subscribable_type,
          project_id: subscription.project_id
          }, subscription)
      end
    end
  end

  # Override this method to define custom logic to consider a subscribable as
  # subscribed without an explicit subscription record.
  def subscribed_without_subscriptions?(user, project)
    false
  end

  def subscribers(project)
    relation = subscriptions_available(project)
                 .where(subscribed: true)
                 .select(:user_id)

    User.where(id: relation)
  end

  def toggle_subscription(user, project = nil)
    unsubscribe_from_other_levels(user, project)

    new_value = !subscribed?(user, project)

    find_or_initialize_subscription(user, project)
      .update(subscribed: new_value)
  end

  def subscribe(user, project = nil)
    unsubscribe_from_other_levels(user, project)

    find_or_initialize_subscription(user, project)
      .update(subscribed: true)
  end

  def unsubscribe(user, project = nil)
    unsubscribe_from_other_levels(user, project)

    find_or_initialize_subscription(user, project)
      .update(subscribed: false)
  end

  def set_subscription(user, desired_state, project = nil)
    if desired_state
      subscribe(user, project)
    else
      unsubscribe(user, project)
    end
  end

  private

  def unsubscribe_from_other_levels(user, project)
    other_subscriptions = subscriptions.where(user: user)

    other_subscriptions =
      if project.blank?
        other_subscriptions.where.not(project: nil)
      else
        other_subscriptions.where(project: nil)
      end

    other_subscriptions.update_all(subscribed: false)
  end

  def find_or_initialize_subscription(user, project)
    BatchLoader::Executor.clear_current

    subscriptions
      .find_or_initialize_by(user_id: user.id, project_id: project.try(:id))
  end

  def subscriptions_available(project)
    t = Subscription.arel_table

    subscriptions
      .where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id))))
  end

  def subscribable_type
    # handle project and group labels as well as issuable subscriptions
    if self.class.ancestors.include?(Label)
      'Label'
    elsif self.class.ancestors.include?(Issue)
      'Issue'
    else
      self.class.name
    end
  end
end