1# frozen_string_literal: true
2
3# TodoService class
4#
5# Used for creating/updating todos after certain user actions
6#
7# Ex.
8#   TodoService.new.new_issue(issue, current_user)
9#
10class TodoService
11  include Gitlab::Utils::UsageData
12  # When create an issue we should:
13  #
14  #  * create a todo for assignee if issue is assigned
15  #  * create a todo for each mentioned user on issue
16  #
17  def new_issue(issue, current_user)
18    new_issuable(issue, current_user)
19  end
20
21  # When update an issue we should:
22  #
23  #  * mark all pending todos related to the issue for the current user as done
24  #
25  def update_issue(issue, current_user, skip_users = [])
26    update_issuable(issue, current_user, skip_users)
27  end
28
29  # When close an issue we should:
30  #
31  #  * mark all pending todos related to the target for the current user as done
32  #
33  def close_issue(issue, current_user)
34    resolve_todos_for_target(issue, current_user)
35  end
36
37  # When we destroy a todo target we should:
38  #
39  #  * refresh the todos count cache for all users with todos on the target
40  #
41  # This needs to yield back to the caller to destroy the target, because it
42  # collects the todo users before the todos themselves are deleted, then
43  # updates the todo counts for those users.
44  #
45  def destroy_target(target)
46    todo_user_ids = target.todos.distinct_user_ids
47
48    yield target
49
50    Users::UpdateTodoCountCacheService.new(todo_user_ids).execute if todo_user_ids.present?
51  end
52
53  # When we reassign an assignable object (issuable, alert) we should:
54  #
55  #  * create a pending todo for new assignee if object is assigned
56  #
57  def reassigned_assignable(issuable, current_user, old_assignees = [])
58    create_assignment_todo(issuable, current_user, old_assignees)
59  end
60
61  # When we reassign an reviewable object (merge request) we should:
62  #
63  #  * create a pending todo for new reviewer if object is assigned
64  #
65  def reassigned_reviewable(issuable, current_user, old_reviewers = [])
66    create_reviewer_todo(issuable, current_user, old_reviewers)
67  end
68
69  # When create a merge request we should:
70  #
71  #  * creates a pending todo for assignee if merge request is assigned
72  #  * create a todo for each mentioned user on merge request
73  #
74  def new_merge_request(merge_request, current_user)
75    new_issuable(merge_request, current_user)
76  end
77
78  # When update a merge request we should:
79  #
80  #  * create a todo for each mentioned user on merge request
81  #
82  def update_merge_request(merge_request, current_user, skip_users = [])
83    update_issuable(merge_request, current_user, skip_users)
84  end
85
86  # When close a merge request we should:
87  #
88  #  * mark all pending todos related to the target for the current user as done
89  #
90  def close_merge_request(merge_request, current_user)
91    resolve_todos_for_target(merge_request, current_user)
92  end
93
94  # When merge a merge request we should:
95  #
96  #  * mark all pending todos related to the target for the current user as done
97  #
98  def merge_merge_request(merge_request, current_user)
99    resolve_todos_for_target(merge_request, current_user)
100  end
101
102  # When a build fails on the HEAD of a merge request we should:
103  #
104  #  * create a todo for each merge participant
105  #
106  def merge_request_build_failed(merge_request)
107    merge_request.merge_participants.each do |user|
108      create_build_failed_todo(merge_request, user)
109    end
110  end
111
112  # When a new commit is pushed to a merge request we should:
113  #
114  #  * mark all pending todos related to the merge request for that user as done
115  #
116  def merge_request_push(merge_request, current_user)
117    resolve_todos_for_target(merge_request, current_user)
118  end
119
120  # When a build is retried to a merge request we should:
121  #
122  #  * mark all pending todos related to the merge request as done for each merge participant
123  #
124  def merge_request_build_retried(merge_request)
125    merge_request.merge_participants.each do |user|
126      resolve_todos_for_target(merge_request, user)
127    end
128  end
129
130  # When a merge request could not be merged due to its unmergeable state we should:
131  #
132  #  * create a todo for each merge participant
133  #
134  def merge_request_became_unmergeable(merge_request)
135    merge_request.merge_participants.each do |user|
136      create_unmergeable_todo(merge_request, user)
137    end
138  end
139
140  # When create a note we should:
141  #
142  #  * mark all pending todos related to the noteable for the note author as done
143  #  * create a todo for each mentioned user on note
144  #
145  def new_note(note, current_user)
146    handle_note(note, current_user)
147  end
148
149  # When update a note we should:
150  #
151  #  * mark all pending todos related to the noteable for the current user as done
152  #  * create a todo for each new user mentioned on note
153  #
154  def update_note(note, current_user, skip_users = [])
155    handle_note(note, current_user, skip_users)
156  end
157
158  # When an emoji is awarded we should:
159  #
160  #  * mark all pending todos related to the awardable for the current user as done
161  #
162  def new_award_emoji(awardable, current_user)
163    resolve_todos_for_target(awardable, current_user)
164  end
165
166  # When user marks a target as todo
167  def mark_todo(target, current_user)
168    attributes = attributes_for_todo(target.project, target, current_user, Todo::MARKED)
169    create_todos(current_user, attributes)
170  end
171
172  def todo_exist?(issuable, current_user)
173    TodosFinder.new(current_user).any_for_target?(issuable, :pending)
174  end
175
176  # Resolves all todos related to target
177  def resolve_todos_for_target(target, current_user)
178    attributes = attributes_for_target(target)
179
180    resolve_todos(pending_todos([current_user], attributes), current_user)
181  end
182
183  def resolve_todos(todos, current_user, resolution: :done, resolved_by_action: :system_done)
184    todos_ids = todos.batch_update(state: resolution, resolved_by_action: resolved_by_action)
185
186    current_user.update_todos_count_cache
187
188    todos_ids
189  end
190
191  def resolve_todo(todo, current_user, resolution: :done, resolved_by_action: :system_done)
192    return if todo.done?
193
194    todo.update(state: resolution, resolved_by_action: resolved_by_action)
195
196    current_user.update_todos_count_cache
197  end
198
199  def restore_todos(todos, current_user)
200    todos_ids = todos.batch_update(state: :pending)
201
202    current_user.update_todos_count_cache
203
204    todos_ids
205  end
206
207  def restore_todo(todo, current_user)
208    return if todo.pending?
209
210    todo.update(state: :pending)
211
212    current_user.update_todos_count_cache
213  end
214
215  def create_request_review_todo(target, author, reviewers)
216    attributes = attributes_for_todo(target.project, target, author, Todo::REVIEW_REQUESTED)
217    create_todos(reviewers, attributes)
218  end
219
220  def create_attention_requested_todo(target, author, users)
221    attributes = attributes_for_todo(target.project, target, author, Todo::ATTENTION_REQUESTED)
222    create_todos(users, attributes)
223  end
224
225  private
226
227  def create_todos(users, attributes)
228    users = Array(users)
229
230    return if users.empty?
231
232    users_with_pending_todos = pending_todos(users, attributes).distinct_user_ids
233    users.reject! { |user| users_with_pending_todos.include?(user.id) && Feature.disabled?(:multiple_todos, user) }
234
235    todos = users.map do |user|
236      issue_type = attributes.delete(:issue_type)
237      track_todo_creation(user, issue_type)
238
239      Todo.create(attributes.merge(user_id: user.id))
240    end
241
242    Users::UpdateTodoCountCacheService.new(users.map(&:id)).execute
243
244    todos
245  end
246
247  def new_issuable(issuable, author)
248    create_assignment_todo(issuable, author)
249    create_reviewer_todo(issuable, author) if issuable.allows_reviewers?
250    create_mention_todos(issuable.project, issuable, author)
251  end
252
253  def update_issuable(issuable, author, skip_users = [])
254    # Skip toggling a task list item in a description
255    return if toggling_tasks?(issuable)
256
257    create_mention_todos(issuable.project, issuable, author, nil, skip_users)
258  end
259
260  def toggling_tasks?(issuable)
261    issuable.previous_changes.include?('description') &&
262      issuable.tasks? && issuable.updated_tasks.any?
263  end
264
265  def handle_note(note, author, skip_users = [])
266    return unless note.can_create_todo?
267
268    project = note.project
269    target = note.noteable
270
271    resolve_todos_for_target(target, author)
272    create_mention_todos(project, target, author, note, skip_users)
273  end
274
275  def create_assignment_todo(target, author, old_assignees = [])
276    if target.assignees.any?
277      assignees = target.assignees - old_assignees
278      attributes = attributes_for_todo(target.project, target, author, Todo::ASSIGNED)
279      create_todos(assignees, attributes)
280    end
281  end
282
283  def create_reviewer_todo(target, author, old_reviewers = [])
284    if target.reviewers.any?
285      reviewers = target.reviewers - old_reviewers
286      create_request_review_todo(target, author, reviewers)
287    end
288  end
289
290  def create_mention_todos(parent, target, author, note = nil, skip_users = [])
291    # Create Todos for directly addressed users
292    directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
293    attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note)
294    create_todos(directly_addressed_users, attributes)
295
296    # Create Todos for mentioned users
297    mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users + directly_addressed_users)
298    attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note)
299    create_todos(mentioned_users, attributes)
300  end
301
302  def create_build_failed_todo(merge_request, todo_author)
303    attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::BUILD_FAILED)
304    create_todos(todo_author, attributes)
305  end
306
307  def create_unmergeable_todo(merge_request, todo_author)
308    attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::UNMERGEABLE)
309    create_todos(todo_author, attributes)
310  end
311
312  def attributes_for_target(target)
313    attributes = {
314      project_id: target&.project&.id,
315      target_id: target.id,
316      target_type: target.class.name,
317      commit_id: nil
318    }
319
320    if target.is_a?(Commit)
321      attributes.merge!(target_id: nil, commit_id: target.id)
322    elsif target.is_a?(Issue)
323      attributes[:issue_type] = target.issue_type
324    elsif target.is_a?(Discussion)
325      attributes.merge!(target_type: nil, target_id: nil, discussion: target)
326    end
327
328    attributes
329  end
330
331  def attributes_for_todo(project, target, author, action, note = nil)
332    attributes_for_target(target).merge!(
333      project_id: project&.id,
334      author_id: author.id,
335      action: action,
336      note: note
337    )
338  end
339
340  def filter_todo_users(users, parent, target)
341    reject_users_without_access(users, parent, target).uniq
342  end
343
344  def filter_mentioned_users(parent, target, author, skip_users = [])
345    mentioned_users = target.mentioned_users(author) - skip_users
346    filter_todo_users(mentioned_users, parent, target)
347  end
348
349  def filter_directly_addressed_users(parent, target, author, skip_users = [])
350    directly_addressed_users = target.directly_addressed_users(author) - skip_users
351    filter_todo_users(directly_addressed_users, parent, target)
352  end
353
354  def reject_users_without_access(users, parent, target)
355    target = target.noteable if target.is_a?(Note)
356
357    if target.respond_to?(:to_ability_name)
358      select_users(users, :"read_#{target.to_ability_name}", target)
359    else
360      select_users(users, :read_project, parent)
361    end
362  end
363
364  def select_users(users, ability, subject)
365    users.select do |user|
366      user.can?(ability.to_sym, subject)
367    end
368  end
369
370  def pending_todos(users, criteria = {})
371    PendingTodosFinder.new(users, criteria).execute
372  end
373
374  def track_todo_creation(user, issue_type)
375    return unless issue_type == 'incident'
376
377    track_usage_event(:incident_management_incident_todo, user.id)
378  end
379end
380
381TodoService.prepend_mod_with('TodoService')
382