todo.rb 7.42 KB
Newer Older
1 2
# frozen_string_literal: true

3
class Todo < ApplicationRecord
Felipe Artur's avatar
Felipe Artur committed
4
  include Sortable
5
  include FromUnion
6
  include EachBatch
Felipe Artur's avatar
Felipe Artur committed
7

8 9 10
  # Time to wait for todos being removed when not visible for user anymore.
  # Prevents TODOs being removed by mistake, for example, removing access from a user
  # and giving it back again.
11
  WAIT_FOR_DELETE = 1.hour
12

13 14 15 16 17 18 19 20
  ASSIGNED            = 1
  MENTIONED           = 2
  BUILD_FAILED        = 3
  MARKED              = 4
  APPROVAL_REQUIRED   = 5 # This is an EE-only feature
  UNMERGEABLE         = 6
  DIRECTLY_ADDRESSED  = 7
  MERGE_TRAIN_REMOVED = 8 # This is an EE-only feature
21
  REVIEW_REQUESTED    = 9
22
  ATTENTION_REQUESTED = 10
23

Robert Schilling's avatar
Robert Schilling committed
24 25
  ACTION_NAMES = {
    ASSIGNED => :assigned,
26
    REVIEW_REQUESTED => :review_requested,
Robert Schilling's avatar
Robert Schilling committed
27 28
    MENTIONED => :mentioned,
    BUILD_FAILED => :build_failed,
29
    MARKED => :marked,
30
    APPROVAL_REQUIRED => :approval_required,
31
    UNMERGEABLE => :unmergeable,
32
    DIRECTLY_ADDRESSED => :directly_addressed,
33
    MERGE_TRAIN_REMOVED => :merge_train_removed,
34
    ATTENTION_REQUESTED => :attention_requested
Douwe Maan's avatar
Douwe Maan committed
35
  }.freeze
Robert Schilling's avatar
Robert Schilling committed
36

Douglas Barbosa Alexandre's avatar
Douglas Barbosa Alexandre committed
37
  belongs_to :author, class_name: "User"
38
  belongs_to :note
Douglas Barbosa Alexandre's avatar
Douglas Barbosa Alexandre committed
39
  belongs_to :project
40
  belongs_to :group
41 42 43 44 45 46 47
  belongs_to :target, -> {
    if self.klass.respond_to?(:with_api_entity_associations)
      self.with_api_entity_associations
    else
      self
    end
  }, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
48

Douglas Barbosa Alexandre's avatar
Douglas Barbosa Alexandre committed
49
  belongs_to :user
50
  belongs_to :issue, -> { where("target_type = 'Issue'") }, foreign_key: :target_id
Douglas Barbosa Alexandre's avatar
Douglas Barbosa Alexandre committed
51

52 53
  delegate :name, :email, to: :author, prefix: true, allow_nil: true

54
  validates :action, :target_type, :user, presence: true
55
  validates :author, presence: true
56 57
  validates :target_id, presence: true, unless: :for_commit?
  validates :commit_id, presence: true, if: :for_commit?
58 59
  validates :project, presence: true, unless: :group_id
  validates :group, presence: true, unless: :project_id
Douglas Barbosa Alexandre's avatar
Douglas Barbosa Alexandre committed
60

61 62
  scope :pending, -> { with_state(:pending) }
  scope :done, -> { with_state(:done) }
63 64
  scope :for_action, -> (action) { where(action: action) }
  scope :for_author, -> (author) { where(author: author) }
65
  scope :for_user, -> (user) { where(user: user) }
66
  scope :for_project, -> (projects) { where(project: projects) }
67
  scope :for_note, -> (notes) { where(note: notes) }
68
  scope :for_undeleted_projects, -> { joins(:project).merge(Project.without_deleted) }
69 70
  scope :for_group, -> (group) { where(group: group) }
  scope :for_type, -> (type) { where(target_type: type) }
71 72
  scope :for_target, -> (id) { where(target_id: id) }
  scope :for_commit, -> (id) { where(commit_id: id) }
73
  scope :with_entity_associations, -> { preload(:target, :author, :note, group: :route, project: [:route, { namespace: :route }]) }
74
  scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) }
75

76 77
  enum resolved_by_action: { system_done: 0, api_all_done: 1, api_done: 2, mark_all_done: 3, mark_done: 4 }, _prefix: :resolved_by

Douglas Barbosa Alexandre's avatar
Douglas Barbosa Alexandre committed
78
  state_machine :state, initial: :pending do
79
    event :done do
80
      transition [:pending] => :done
81 82
    end

Douglas Barbosa Alexandre's avatar
Douglas Barbosa Alexandre committed
83 84 85
    state :pending
    state :done
  end
86

87
  after_save :keep_around_commit, if: :commit_id
88

Felipe Artur's avatar
Felipe Artur committed
89
  class << self
90
    # Returns all todos for the given group ids and their descendants.
91
    #
92
    # group_ids - Group Ids to retrieve todos for.
93 94
    #
    # Returns an `ActiveRecord::Relation`.
95 96
    def for_group_ids_and_descendants(group_ids)
      groups = Group.groups_including_descendants_by(group_ids)
97 98 99 100 101 102 103

      from_union([
        for_project(Project.for_group(groups)),
        for_group(groups)
      ])
    end

104
    # Returns `true` if the current user has any todos for the given target with the optional given state.
105 106
    #
    # target - The value of the `target_type` column, such as `Issue`.
107
    # state - The value of the `state` column, such as `pending` or `done`.
108 109
    def any_for_target?(target, state = nil)
      state.nil? ? exists?(target: target) : exists?(target: target, state: state)
110 111
    end

112
    # Updates attributes of a relation of todos to the new state.
113
    #
114
    # new_attributes - The new attributes of the todos.
115 116
    #
    # Returns an `Array` containing the IDs of the updated todos.
117 118 119
    def batch_update(**new_attributes)
      # Only update those that have different state
      base = where.not(state: new_attributes[:state]).except(:order)
120 121
      ids = base.pluck(:id)

122
      base.update_all(new_attributes.merge(updated_at: Time.current))
123 124 125 126

      ids
    end

127 128 129
    # Priority sorting isn't displayed in the dropdown, because we don't show
    # milestones, but still show something if the user has a URL with that
    # selected.
130
    def sort_by_attribute(method)
131 132 133 134 135 136 137 138
      sorted =
        case method.to_s
        when 'priority', 'label_priority' then order_by_labels_priority
        else order_by(method)
        end

      # Break ties with the ID column for pagination
      sorted.order(id: :desc)
Felipe Artur's avatar
Felipe Artur committed
139 140 141 142 143 144
    end

    # Order by priority depending on which issue/merge request the Todo belongs to
    # Todos with highest priority first then oldest todos
    # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
    def order_by_labels_priority
145
      highest_priority = highest_label_priority(
146
        target_type_column: "todos.target_type",
147 148
        target_column: "todos.target_id",
        project_column: "todos.project_id"
149
      ).to_sql
Felipe Artur's avatar
Felipe Artur committed
150

151 152 153
      select("#{table_name}.*, (#{highest_priority}) AS highest_priority")
        .order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
        .order('todos.created_at')
Felipe Artur's avatar
Felipe Artur committed
154
    end
155

156 157
    def distinct_user_ids
      distinct.pluck(:user_id)
158
    end
159 160 161 162 163 164 165 166 167 168 169 170 171 172

    # Count todos grouped by user_id and state, using an UNION query
    # so we can utilize the partial indexes for each state.
    def count_grouped_by_user_id_and_state
      grouped_count = select(:user_id, 'count(id) AS count').group(:user_id)

      done = grouped_count.where(state: :done).select("'done' AS state")
      pending = grouped_count.where(state: :pending).select("'pending' AS state")
      union = unscoped.from_union([done, pending], remove_duplicates: false)

      connection.select_all(union).each_with_object({}) do |row, counts|
        counts[[row['user_id'], row['state']]] = row['count']
      end
    end
Felipe Artur's avatar
Felipe Artur committed
173 174
  end

175
  def resource_parent
176 177 178
    project
  end

179 180 181 182
  def unmergeable?
    action == UNMERGEABLE
  end

183 184 185 186
  def build_failed?
    action == BUILD_FAILED
  end

187 188 189 190
  def assigned?
    action == ASSIGNED
  end

191 192 193 194
  def review_requested?
    action == REVIEW_REQUESTED
  end

195 196
  def attention_requested?
    action == ATTENTION_REQUESTED
197 198
  end

199 200 201 202
  def merge_train_removed?
    action == MERGE_TRAIN_REMOVED
  end

203 204 205 206
  def done?
    state == 'done'
  end

Robert Schilling's avatar
Robert Schilling committed
207 208 209 210
  def action_name
    ACTION_NAMES[action]
  end

211 212 213 214 215 216
  def body
    if note.present?
      note.note
    else
      target.title
    end
217
  end
218 219 220 221 222

  def for_commit?
    target_type == "Commit"
  end

223 224 225 226
  def for_design?
    target_type == DesignManagement::Design.name
  end

227 228 229 230
  def for_alert?
    target_type == AlertManagement::Alert.name
  end

231 232 233
  # override to return commits, which are not active record
  def target
    if for_commit?
234
      project.commit(commit_id) rescue nil
235 236 237 238 239
    else
      super
    end
  end

240
  def target_reference
241
    if for_commit?
242
      target.reference_link_text
243
    else
244
      target.to_reference
245 246
    end
  end
247

248 249 250 251 252
  def self_added?
    author == user
  end

  def self_assigned?
253
    self_added? && (assigned? || review_requested?)
254 255
  end

256 257 258 259 260
  private

  def keep_around_commit
    project.repository.keep_around(self.commit_id)
  end
Douglas Barbosa Alexandre's avatar
Douglas Barbosa Alexandre committed
261
end
262

263
Todo.prepend_mod_with('Todo')