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