gitlab-org--gitlab-foss/app/services/issuable_base_service.rb
Luke Duncalfe 37b17fa61a Add service classes for mutating AwardEmoji
Adding, destroying and toggling emoji previously lacked services and
instead were performed through methods called on Awardable models.

This led to inconsistencies where relevant todos would be marked as done
only when emoji were awarded through our controllers, but not through
the API. Todos could also be marked as done when an emoji was being
removed.

Behaviour changes

- Awarding emoji through the API will now mark a relevant Todo as done
- Toggling an emoji off (destroying it) through our controllers will no
  longer mark a relevant Todo as done

Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/63372
2019-08-21 11:39:41 +12:00

412 lines
13 KiB
Ruby

# frozen_string_literal: true
class IssuableBaseService < BaseService
private
attr_accessor :params, :skip_milestone_email
def initialize(project, user = nil, params = {})
super
@skip_milestone_email = @params.delete(:skip_milestone_email)
end
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
unless can?(current_user, ability_name, issuable)
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
params.delete(:remove_label_ids)
params.delete(:label_ids)
params.delete(:assignee_ids)
params.delete(:assignee_id)
params.delete(:due_date)
params.delete(:canonical_issue_id)
params.delete(:project)
params.delete(:discussion_locked)
end
filter_assignee(issuable)
filter_milestone
filter_labels
end
def filter_assignee(issuable)
return if params[:assignee_ids].blank?
unless issuable.allows_multiple_assignees?
params[:assignee_ids] = params[:assignee_ids].first(1)
end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
params[:assignee_ids] = []
elsif assignee_ids.any?
params[:assignee_ids] = assignee_ids
else
params.delete(:assignee_ids)
end
end
def assignee_can_read?(issuable, assignee_id)
new_assignee = User.find_by_id(assignee_id)
return false unless new_assignee
ability_name = :"read_#{issuable.to_ability_name}"
resource = issuable.persisted? ? issuable : project
can?(new_assignee, ability_name, resource)
end
def filter_milestone
milestone_id = params[:milestone_id]
return unless milestone_id
params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE
groups = project.group&.self_and_ancestors&.select(:id)
milestone =
Milestone.for_projects_and_groups([project.id], groups).find_by_id(milestone_id)
params[:milestone_id] = '' unless milestone
end
def filter_labels
params[:add_label_ids] = labels_service.filter_labels_ids_in_param(:add_label_ids) if params[:add_label_ids]
params[:remove_label_ids] = labels_service.filter_labels_ids_in_param(:remove_label_ids) if params[:remove_label_ids]
if params[:label_ids]
params[:label_ids] = labels_service.filter_labels_ids_in_param(:label_ids)
elsif params[:labels]
params[:label_ids] = labels_service.find_or_create_by_titles.map(&:id)
end
end
def filter_labels_in_param(key)
return if params[key].to_a.empty?
params[key] = available_labels.id_in(params[key]).pluck_primary_key
end
def find_or_create_label_ids
labels = params.delete(:labels)
return unless labels
params[:label_ids] = labels.map do |label_name|
label = Labels::FindOrCreateService.new(
current_user,
parent,
title: label_name.strip,
available_labels: available_labels
).execute
label.try(:id)
end.compact
end
def labels_service
@labels_service ||= ::Labels::AvailableLabelsService.new(current_user, parent, params)
end
def process_label_ids(attributes, existing_label_ids: nil, extra_label_ids: [])
label_ids = attributes.delete(:label_ids)
add_label_ids = attributes.delete(:add_label_ids)
remove_label_ids = attributes.delete(:remove_label_ids)
new_label_ids = existing_label_ids || label_ids || []
new_label_ids |= extra_label_ids
if add_label_ids.blank? && remove_label_ids.blank?
new_label_ids = label_ids if label_ids
else
new_label_ids |= add_label_ids if add_label_ids
new_label_ids -= remove_label_ids if remove_label_ids
end
new_label_ids.uniq
end
def handle_quick_actions_on_create(issuable)
merge_quick_actions_into_params!(issuable)
end
def merge_quick_actions_into_params!(issuable, only: nil)
original_description = params.fetch(:description, issuable.description)
description, command_params =
QuickActions::InterpretService.new(project, current_user)
.execute(original_description, issuable, only: only)
# Avoid a description already set on an issuable to be overwritten by a nil
params[:description] = description if description
params.merge!(command_params)
end
def create(issuable)
handle_quick_actions_on_create(issuable)
filter_params(issuable)
params.delete(:state_event)
params[:author] ||= current_user
params[:label_ids] = process_label_ids(params, extra_label_ids: issuable.label_ids.to_a)
issuable.assign_attributes(params)
before_create(issuable)
if issuable.save
ActiveRecord::Base.no_touching do
Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, is_update: false)
end
after_create(issuable)
execute_hooks(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees)
issuable.update_project_counter_caches
end
issuable
end
def before_create(issuable)
# To be overridden by subclasses
end
def after_create(issuable)
# To be overridden by subclasses
end
def before_update(issuable, skip_spam_check: false)
# To be overridden by subclasses
end
def after_update(issuable)
# To be overridden by subclasses
end
def update(issuable)
change_state(issuable)
change_subscription(issuable)
change_todo(issuable)
toggle_award(issuable)
filter_params(issuable)
old_associations = associations_before_update(issuable)
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
if labels_changing?(issuable.label_ids, label_ids)
params[:label_ids] = label_ids
issuable.touch
end
if issuable.changed? || params.present?
issuable.assign_attributes(params)
if has_title_or_description_changed?(issuable)
issuable.assign_attributes(last_edited_at: Time.now, last_edited_by: current_user)
end
before_update(issuable)
# Do not touch when saving the issuable if only changes position within a list. We should call
# this method at this point to capture all possible changes.
should_touch = update_timestamp?(issuable)
issuable.updated_by = current_user if should_touch
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
update_project_counters = issuable.project && update_project_counter_caches?(issuable)
if issuable.with_transaction_returning_status { issuable.save(touch: should_touch) }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: old_associations[:labels])
end
handle_changes(issuable, old_associations: old_associations)
new_assignees = issuable.assignees.to_a
affected_assignees = (old_associations[:assignees] + new_assignees) - (old_associations[:assignees] & new_assignees)
invalidate_cache_counts(issuable, users: affected_assignees.compact)
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(
issuable,
'update',
old_associations: old_associations
)
issuable.update_project_counter_caches if update_project_counters
end
end
issuable
end
def update_task(issuable)
filter_params(issuable)
if issuable.changed? || params.present?
issuable.assign_attributes(params.merge(updated_by: current_user,
last_edited_at: Time.now,
last_edited_by: current_user))
before_update(issuable, skip_spam_check: true)
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: nil)
end
handle_task_changes(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees.to_a)
after_update(issuable)
execute_hooks(issuable, 'update', old_associations: nil)
end
end
issuable
end
# Handle the `update_task` event sent from UI. Attempts to update a specific
# line in the markdown and cached html, bypassing any unnecessary updates or checks.
def update_task_event(issuable)
update_task_params = params.delete(:update_task)
return unless update_task_params
tasklist_toggler = TaskListToggleService.new(issuable.description, issuable.description_html,
line_source: update_task_params[:line_source],
line_number: update_task_params[:line_number].to_i,
toggle_as_checked: update_task_params[:checked])
unless tasklist_toggler.execute
# if we make it here, the data is much newer than we thought it was - fail fast
raise ActiveRecord::StaleObjectError
end
# by updating the description_html field at the same time,
# the markdown cache won't be considered invalid
params[:description] = tasklist_toggler.updated_markdown
params[:description_html] = tasklist_toggler.updated_markdown_html
# since we're updating a very specific line, we don't care whether
# the `lock_version` sent from the FE is the same or not. Just
# make sure the data hasn't changed since we queried it
params[:lock_version] = issuable.lock_version
update_task(issuable)
end
def labels_changing?(old_label_ids, new_label_ids)
old_label_ids.sort != new_label_ids.sort
end
def has_title_or_description_changed?(issuable)
issuable.title_changed? || issuable.description_changed?
end
def change_state(issuable)
case params.delete(:state_event)
when 'reopen'
reopen_service.new(project, current_user, {}).execute(issuable)
when 'close'
close_service.new(project, current_user, {}).execute(issuable)
end
end
def change_subscription(issuable)
case params.delete(:subscription_event)
when 'subscribe'
issuable.subscribe(current_user, project)
when 'unsubscribe'
issuable.unsubscribe(current_user, project)
end
end
# rubocop: disable CodeReuse/ActiveRecord
def change_todo(issuable)
case params.delete(:todo_event)
when 'add'
todo_service.mark_todo(issuable, current_user)
when 'done'
todo = TodosFinder.new(current_user).find_by(target: issuable)
todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo
end
end
# rubocop: enable CodeReuse/ActiveRecord
def toggle_award(issuable)
award = params.delete(:emoji_award)
AwardEmojis::ToggleService.new(issuable, award, current_user).execute if award
end
def associations_before_update(issuable)
associations =
{
labels: issuable.labels.to_a,
mentioned_users: issuable.mentioned_users.to_a,
assignees: issuable.assignees.to_a
}
associations[:total_time_spent] = issuable.total_time_spent if issuable.respond_to?(:total_time_spent)
associations[:description] = issuable.description
associations
end
def has_changes?(issuable, old_labels: [], old_assignees: [])
valid_attrs = [:title, :description, :assignee_ids, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s)
end
labels_changed = issuable.labels != old_labels
assignees_changed = issuable.assignees != old_assignees
attrs_changed || labels_changed || assignees_changed
end
def invalidate_cache_counts(issuable, users: [])
users.each do |user|
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end
end
# override if needed
def handle_changes(issuable, options)
end
# override if needed
def handle_task_changes(issuable)
end
# override if needed
def execute_hooks(issuable, action = 'open', params = {})
end
def update_project_counter_caches?(issuable)
issuable.state_changed?
end
def parent
project
end
# we need to check this because milestone from milestone_id param is displayed on "new" page
# where private project milestone could leak without this check
def ensure_milestone_available(issuable)
issuable.milestone_id = nil unless issuable.milestone_available?
end
def update_timestamp?(issuable)
issuable.changes.keys != ["relative_position"]
end
end