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 =
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
DUO_PRO_ACCESS_GRANTED =

This is an EE-only feature

16
DUO_ENTERPRISE_ACCESS_GRANTED =

This is an EE-only feature

17
DUO_CORE_ACCESS_GRANTED =

This is an EE-only feature

18
ACTION_NAMES =

EE Action names should be defined in EE_ACTION_NAMES in ee/app/models/ee/todo.rb

{
  ASSIGNED => :assigned,
  REVIEW_REQUESTED => :review_requested,
  MENTIONED => :mentioned,
  BUILD_FAILED => :build_failed,
  MARKED => :marked,
  APPROVAL_REQUIRED => :approval_required,
  UNMERGEABLE => :unmergeable,
  DIRECTLY_ADDRESSED => :directly_addressed,
  MEMBER_ACCESS_REQUESTED => :member_access_requested,
   => :review_submitted,
  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
PARENTLESS_ACTION_TYPES =

EE parentless action types should be defined in EE_PARENTLESS_ACTION_TYPES in ee/app/models/ee/todo.rb

[
  SSH_KEY_EXPIRED,
  SSH_KEY_EXPIRING_SOON
].freeze
BATCH_DELETE_SIZE =
100

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, current_transaction, declarative_enum, default_select_columns, delete_all_returning, #deleted_from_database?, id_in, id_not_in, iid_in, nullable_column?, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, sharding_keys, #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

.action_namesObject



269
270
271
# File 'app/models/todo.rb', line 269

def action_names
  ACTION_NAMES
end

.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)


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

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.



181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'app/models/todo.rb', line 181

def batch_update(**new_attributes)
  # We force a `WHERE todo.id IN ()` SQL clause to circumvent issues where resolving todos
  # associated to a specific group would also resolve other todos due to limitations of the
  # `update_all` which doesn't handle UNIONs well.
  ids_to_update = select(:id)
  todos_to_update = where(id: ids_to_update)
  # Only update those that have different state
  base = todos_to_update.where.not(state: new_attributes[:state]).except(:order)
  ids = base.pluck(:id)

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

  ids
end

.distinct_user_idsObject



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

def distinct_user_ids
  # When used from the todos finder that applies a default order, we need to reset it.
  reorder(nil).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`.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/models/todo.rb', line 133

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”



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'app/models/todo.rb', line 236

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

.parentless_action_typesObject



273
274
275
# File 'app/models/todo.rb', line 273

def parentless_action_types
  PARENTLESS_ACTION_TYPES
end

.pending_count_by_user_idObject

Count pending todos grouped by user_id and state so we can utilize the index on state / user id.



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

def pending_count_by_user_id
  where(state: :pending)
    .group(:user_id)
    .count(:id)
end

.pending_for_expiring_ssh_keys(ssh_key_ids) ⇒ Object



148
149
150
151
152
153
154
155
# File 'app/models/todo.rb', line 148

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.



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'app/models/todo.rb', line 199

def sort_by_attribute(method)
  sorted =
    case method.to_s
    when 'snoozed_and_creation_dates_asc' then sort_by_snoozed_and_creation_dates(direction: :asc)
    when 'snoozed_and_creation_dates_desc' then sort_by_snoozed_and_creation_dates
    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

.sort_by_snoozed_and_creation_dates(direction: :desc) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'app/models/todo.rb', line 215

def sort_by_snoozed_and_creation_dates(direction: :desc)
  coalesced_arel = Arel.sql('timestamp_coalesce(todos.snoozed_until, todos.created_at)')
  attribute_name = 'coalesced_date'

  order = Gitlab::Pagination::Keyset::Order.build([
    Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
      attribute_name: attribute_name,
      column_expression: coalesced_arel,
      order_expression: direction == :asc ? coalesced_arel.asc : coalesced_arel.desc,
      reversed_order_expression: direction == :asc ? coalesced_arel.desc : coalesced_arel.asc,
      nullable: :not_nullable,
      order_direction: direction
    )
  ])

  select("todos.*, #{coalesced_arel} AS #{attribute_name}").order(order)
end

Instance Method Details

#access_request_url(only_path: false) ⇒ Object



327
328
329
330
331
332
333
334
335
# File 'app/models/todo.rb', line 327

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



341
342
343
# File 'app/models/todo.rb', line 341

def action_name
  self.class.action_names[action]
end

#assigned?Boolean

Returns:

  • (Boolean)


303
304
305
# File 'app/models/todo.rb', line 303

def assigned?
  action == ASSIGNED
end

#bodyObject



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

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

#build_failed?Boolean

Returns:

  • (Boolean)


299
300
301
# File 'app/models/todo.rb', line 299

def build_failed?
  action == BUILD_FAILED
end

#done?Boolean

Returns:

  • (Boolean)


337
338
339
# File 'app/models/todo.rb', line 337

def done?
  state == 'done'
end

#for_alert?Boolean

Returns:

  • (Boolean)


363
364
365
# File 'app/models/todo.rb', line 363

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

#for_commit?Boolean

Returns:

  • (Boolean)


355
356
357
# File 'app/models/todo.rb', line 355

def for_commit?
  target_type == "Commit"
end

#for_design?Boolean

Returns:

  • (Boolean)


359
360
361
# File 'app/models/todo.rb', line 359

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

#for_issue_or_work_item?Boolean

Returns:

  • (Boolean)


367
368
369
# File 'app/models/todo.rb', line 367

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

#for_ssh_key?Boolean

Returns:

  • (Boolean)


371
372
373
# File 'app/models/todo.rb', line 371

def for_ssh_key?
  target_type == Key.name
end

#member_access_requested?Boolean

Returns:

  • (Boolean)


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

def member_access_requested?
  action == MEMBER_ACCESS_REQUESTED
end

#member_access_typeObject



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

def member_access_type
  target.class.name.downcase
end

#merge_train_removed?Boolean

Returns:

  • (Boolean)


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

def merge_train_removed?
  action == MERGE_TRAIN_REMOVED
end

#parentless_type?Boolean

Returns:

  • (Boolean)


375
376
377
# File 'app/models/todo.rb', line 375

def parentless_type?
  self.class.parentless_action_types.include?(action)
end

#resource_parentObject



291
292
293
# File 'app/models/todo.rb', line 291

def resource_parent
  project || group
end

#review_requested?Boolean

Returns:

  • (Boolean)


307
308
309
# File 'app/models/todo.rb', line 307

def review_requested?
  action == REVIEW_REQUESTED
end

#review_submitted?Boolean

Returns:

  • (Boolean)


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

def 
  action == 
end

#self_added?Boolean

Returns:

  • (Boolean)


429
430
431
# File 'app/models/todo.rb', line 429

def self_added?
  author == user
end

#self_assigned?Boolean

Returns:

  • (Boolean)


433
434
435
# File 'app/models/todo.rb', line 433

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

#targetObject

override to return commits, which are not active record



380
381
382
383
384
385
386
387
388
389
390
# File 'app/models/todo.rb', line 380

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

#target_referenceObject



392
393
394
395
396
397
398
399
400
# File 'app/models/todo.rb', line 392

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



402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'app/models/todo.rb', line 402

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
  when WikiPage::Meta
    build_wiki_page_target_url
  end
end

#unmergeable?Boolean

Returns:

  • (Boolean)


295
296
297
# File 'app/models/todo.rb', line 295

def unmergeable?
  action == UNMERGEABLE
end