Class: Todo

Inherits:
ApplicationRecord show all
Includes:
EachBatch, FromUnion, Sortable
Defined in:
app/models/todo.rb

Constant Summary collapse

WAIT_FOR_DELETE =

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.

1.hour
ASSIGNED =

Actions

1
MENTIONED =
2
BUILD_FAILED =
3
MARKED =
4
APPROVAL_REQUIRED =

This is an EE-only feature

5
UNMERGEABLE =
6
DIRECTLY_ADDRESSED =
7
MERGE_TRAIN_REMOVED =

This is an EE-only feature

8
REVIEW_REQUESTED =
9
MEMBER_ACCESS_REQUESTED =
10
REVIEW_SUBMITTED =

This is an EE-only feature

11
OKR_CHECKIN_REQUESTED =

This is an EE-only feature

12
ADDED_APPROVER =

This is an EE-only feature,

13
SSH_KEY_EXPIRED =
14
SSH_KEY_EXPIRING_SOON =
15
ACTION_NAMES =
{
  ASSIGNED => :assigned,
  REVIEW_REQUESTED => :review_requested,
  MENTIONED => :mentioned,
  BUILD_FAILED => :build_failed,
  MARKED => :marked,
  APPROVAL_REQUIRED => :approval_required,
  UNMERGEABLE => :unmergeable,
  DIRECTLY_ADDRESSED => :directly_addressed,
  MERGE_TRAIN_REMOVED => :merge_train_removed,
  MEMBER_ACCESS_REQUESTED => :member_access_requested,
  REVIEW_SUBMITTED => :review_submitted,
  OKR_CHECKIN_REQUESTED => :okr_checkin_requested,
  ADDED_APPROVER => :added_approver,
  SSH_KEY_EXPIRED => :ssh_key_expired,
  SSH_KEY_EXPIRING_SOON => :ssh_key_expiring_soon
}.freeze
ACTIONS_MULTIPLE_ALLOWED =
[Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED, Todo::MEMBER_ACCESS_REQUESTED].freeze

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Constants included from HasCheckConstraints

HasCheckConstraints::NOT_NULL_CHECK_PATTERN

Constants included from ResetOnColumnErrors

ResetOnColumnErrors::MAX_RESET_PERIOD

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

===, cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, nullable_column?, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from ResetOnColumnErrors

#reset_on_union_error, #reset_on_unknown_attribute_error

Methods included from Gitlab::SensitiveSerializableHash

#serializable_hash

Class Method Details

.any_for_target?(target, state = nil) ⇒ Boolean

Returns ‘true` if the current user has any todos for the given target with the optional given state.

target - The value of the ‘target_type` column, such as `Issue`. state - The value of the `state` column, such as `pending` or `done`.

Returns:

  • (Boolean)


144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'app/models/todo.rb', line 144

def any_for_target?(target, state = nil)
  conditions = {}

  if target.respond_to?(:todoable_target_type_name)
    conditions[:target_type] = target.todoable_target_type_name
    conditions[:target_id] = target.id
  else
    conditions[:target] = target
  end

  conditions[:state] = state unless state.nil?

  exists?(conditions)
end

.batch_update(**new_attributes) ⇒ Object

Updates attributes of a relation of todos to the new state.

new_attributes - The new attributes of the todos.

Returns an ‘Array` containing the IDs of the updated todos.



164
165
166
167
168
169
170
171
172
# File 'app/models/todo.rb', line 164

def batch_update(**new_attributes)
  # Only update those that have different state
  base = where.not(state: new_attributes[:state]).except(:order)
  ids = base.pluck(:id)

  base.update_all(new_attributes.merge(updated_at: Time.current))

  ids
end

.count_grouped_by_user_id_and_stateObject

Count todos grouped by user_id and state, using an UNION query so we can utilize the partial indexes for each state.



233
234
235
236
237
238
239
240
241
242
243
244
# File 'app/models/todo.rb', line 233

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)
    .select(:user_id, :count, :state)

  connection.select_all(union).each_with_object({}) do |row, counts|
    counts[[row['user_id'], row['state']]] = row['count']
  end
end

.distinct_user_idsObject



227
228
229
# File 'app/models/todo.rb', line 227

def distinct_user_ids
  distinct.pluck(:user_id)
end

.for_group_ids_and_descendants(group_ids) ⇒ Object

Returns all todos for the given group ids and their descendants.

group_ids - Group Ids to retrieve todos for.

Returns an ‘ActiveRecord::Relation`.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'app/models/todo.rb', line 116

def for_group_ids_and_descendants(group_ids)
  groups_and_descendants_cte = Gitlab::SQL::CTE.new(
    :groups_and_descendants_ids,
    Group.where(id: group_ids).self_and_descendant_ids
  )

  groups_and_descendants = Namespace.from(groups_and_descendants_cte.table)

  with(groups_and_descendants_cte.to_arel)
    .from_union([
      for_project(Project.for_group(groups_and_descendants)),
      for_group(groups_and_descendants)
    ], remove_duplicates: false)
end

.order_by_labels_priority(asc: true) ⇒ Object

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”



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'app/models/todo.rb', line 194

def order_by_labels_priority(asc: true)
  highest_priority = highest_label_priority(
    target_type_column: "todos.target_type",
    target_column: "todos.target_id",
    project_column: "todos.project_id"
  ).arel.as('highest_priority')

  highest_priority_arel = Arel.sql('highest_priority')

  order = Gitlab::Pagination::Keyset::Order.build([
    Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
      attribute_name: 'highest_priority',
      column_expression: highest_priority_arel,
      order_expression: asc ? highest_priority_arel.asc.nulls_last : highest_priority_arel.desc.nulls_first,
      reversed_order_expression: asc ? highest_priority_arel.desc.nulls_first : highest_priority_arel.asc.nulls_last,
      nullable: asc ? :nulls_last : :nulls_first,
      order_direction: asc ? :asc : :desc
    ),
    Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
      attribute_name: 'created_at',
      order_expression: asc ? Todo.arel_table[:created_at].asc : Todo.arel_table[:created_at].desc,
      nullable: :not_nullable
    ),
    Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
      attribute_name: 'id',
      order_expression: asc ? Todo.arel_table[:id].asc : Todo.arel_table[:id].desc,
      nullable: :not_nullable
    )
  ])

  select(arel_table[Arel.star], highest_priority).order(order)
end

.pending_for_expiring_ssh_keys(ssh_key_ids) ⇒ Object



131
132
133
134
135
136
137
138
# File 'app/models/todo.rb', line 131

def pending_for_expiring_ssh_keys(ssh_key_ids)
  where(
    target_type: Key,
    target_id: ssh_key_ids,
    action: ::Todo::SSH_KEY_EXPIRING_SOON,
    state: :pending
  )
end

.sort_by_attribute(method) ⇒ Object

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.



177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'app/models/todo.rb', line 177

def sort_by_attribute(method)
  sorted =
    case method.to_s
    when 'priority', 'label_priority', 'label_priority_asc' then order_by_labels_priority(asc: true)
    when 'label_priority_desc' then order_by_labels_priority(asc: false)
    else order_by(method)
    end

  return sorted if Gitlab::Pagination::Keyset::Order.keyset_aware?(sorted)

  # Break ties with the ID column for pagination
  sorted.order(id: :desc)
end

Instance Method Details

#access_request_url(only_path: false) ⇒ Object



283
284
285
286
287
288
289
290
291
# File 'app/models/todo.rb', line 283

def access_request_url(only_path: false)
  if target.instance_of? Group
    Gitlab::Routing.url_helpers.group_group_members_url(self.target, tab: 'access_requests', only_path: only_path)
  elsif target.instance_of? Project
    Gitlab::Routing.url_helpers.project_project_members_url(self.target, tab: 'access_requests', only_path: only_path)
  else
    ""
  end
end

#action_nameObject



297
298
299
# File 'app/models/todo.rb', line 297

def action_name
  ACTION_NAMES[action]
end

#assigned?Boolean

Returns:

  • (Boolean)


259
260
261
# File 'app/models/todo.rb', line 259

def assigned?
  action == ASSIGNED
end

#bodyObject



301
302
303
304
305
306
307
308
309
# File 'app/models/todo.rb', line 301

def body
  if note.present?
    note.note
  elsif member_access_requested?
    target.full_path
  else
    target.title
  end
end

#build_failed?Boolean

Returns:

  • (Boolean)


255
256
257
# File 'app/models/todo.rb', line 255

def build_failed?
  action == BUILD_FAILED
end

#done?Boolean

Returns:

  • (Boolean)


293
294
295
# File 'app/models/todo.rb', line 293

def done?
  state == 'done'
end

#for_alert?Boolean

Returns:

  • (Boolean)


319
320
321
# File 'app/models/todo.rb', line 319

def for_alert?
  target_type == AlertManagement::Alert.name
end

#for_commit?Boolean

Returns:

  • (Boolean)


311
312
313
# File 'app/models/todo.rb', line 311

def for_commit?
  target_type == "Commit"
end

#for_design?Boolean

Returns:

  • (Boolean)


315
316
317
# File 'app/models/todo.rb', line 315

def for_design?
  target_type == DesignManagement::Design.name
end

#for_issue_or_work_item?Boolean

Returns:

  • (Boolean)


323
324
325
# File 'app/models/todo.rb', line 323

def for_issue_or_work_item?
  [Issue.name, WorkItem.name].any?(target_type)
end

#for_ssh_key?Boolean

Returns:

  • (Boolean)


327
328
329
# File 'app/models/todo.rb', line 327

def for_ssh_key?
  target_type == Key.name
end

#keep_around_commitObject



387
388
389
# File 'app/models/todo.rb', line 387

def keep_around_commit
  project.repository.keep_around(self.commit_id, source: self.class.name)
end

#member_access_requested?Boolean

Returns:

  • (Boolean)


271
272
273
# File 'app/models/todo.rb', line 271

def member_access_requested?
  action == MEMBER_ACCESS_REQUESTED
end

#member_access_typeObject



279
280
281
# File 'app/models/todo.rb', line 279

def member_access_type
  target.class.name.downcase
end

#merge_train_removed?Boolean

Returns:

  • (Boolean)


267
268
269
# File 'app/models/todo.rb', line 267

def merge_train_removed?
  action == MERGE_TRAIN_REMOVED
end

#resource_parentObject



247
248
249
# File 'app/models/todo.rb', line 247

def resource_parent
  project || group
end

#review_requested?Boolean

Returns:

  • (Boolean)


263
264
265
# File 'app/models/todo.rb', line 263

def review_requested?
  action == REVIEW_REQUESTED
end

#review_submitted?Boolean

Returns:

  • (Boolean)


275
276
277
# File 'app/models/todo.rb', line 275

def 
  action == REVIEW_SUBMITTED
end

#self_added?Boolean

Returns:

  • (Boolean)


379
380
381
# File 'app/models/todo.rb', line 379

def self_added?
  author == user
end

#self_assigned?Boolean

Returns:

  • (Boolean)


383
384
385
# File 'app/models/todo.rb', line 383

def self_assigned?
  self_added? && (assigned? || review_requested?)
end

#targetObject

override to return commits, which are not active record



332
333
334
335
336
337
338
339
340
341
342
# File 'app/models/todo.rb', line 332

def target
  if for_commit?
    begin
      project.commit(commit_id)
    rescue StandardError
      nil
    end
  else
    super
  end
end

#target_referenceObject



344
345
346
347
348
349
350
351
352
# File 'app/models/todo.rb', line 344

def target_reference
  if for_commit?
    target.reference_link_text
  elsif member_access_requested?
    target.full_path
  else
    target.to_reference
  end
end

#target_urlObject



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'app/models/todo.rb', line 354

def target_url
  return if target.nil?

  case target
  when WorkItem
    build_work_item_target_url
  when Issue
    build_issue_target_url
  when MergeRequest
    build_merge_request_target_url
  when ::DesignManagement::Design
    build_design_target_url
  when ::AlertManagement::Alert
    build_alert_target_url
  when Commit
    build_commit_target_url
  when Project
    build_project_target_url
  when Group
    build_group_target_url
  when Key
    build_ssh_key_target_url
  end
end

#unmergeable?Boolean

Returns:

  • (Boolean)


251
252
253
# File 'app/models/todo.rb', line 251

def unmergeable?
  action == UNMERGEABLE
end