summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2016-03-21 14:01:19 +0000
committerKamil Trzciński <ayufan@ayufan.eu>2016-03-21 14:01:19 +0000
commit3fca30d27f90a52bdbca746b1244c95e6c7db2d2 (patch)
treecfc9e2ac318ca4c8513c1d8530abc0ea4270c236 /app
parentbfcdb47c403ae5054d1feb2b57e1e255ba01a9cb (diff)
parentdb8f70d5088cc1e0172fd6b94fc4628bd83aa4a4 (diff)
downloadgitlab-ce-3fca30d27f90a52bdbca746b1244c95e6c7db2d2.tar.gz
Merge branch 'feature/issue-move' into 'master'
Ability to move issue to another project Tasks: - [x] Create scaffold of service that will move issue to another project. - [x] Close old issue, add system note about moving issue to a new project. - [x] Create a new issue, add system note about issue being moved from old project. - [x] Check if issue can be moved to another project before executing service - [x] Check permissions when moving an issue (`:admin_issue` ability) - [x] Display select box for a new project when editing an issue - [x] Show only projects that issue can be moved into in that select box - [x] Add project select handler, helper and some permission filters to it - [x] Preserve as much information as possible, including author - [x] Prepare mechanisms that unfolds local references in issue description - [x] Rewrite issue description with references unfolding and add some specs for it - [x] Rewrite all system notes and comments attached to issue that is being moved - [x] Update `Label` so that is was able to create cross reference labels (separate MR) - [x] Add notifications about moving issue to another project - [x] Display confirmation alert/message when issue move has been requested - [x] Make it possible to undo selecting project where issue will be moved to - [x] Add column to issue, that will indicate if it has been moved to another project - [x] Do not allow to move issue that has been already moved - [x] Write top-to-bottom feature spec in RSpec instead of Spinach UI: ![issue_move_ui](/uploads/b3c6b563362c1fded9082cc0f51e5a74/issue_move_ui.png) ![issue_move_tooltip](/uploads/2ab913b06f52df1cafde9abe89bd9cb8/issue_move_tooltip.png) Closes #3024 See merge request !2831
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/issuable_form.js.coffee11
-rw-r--r--app/controllers/projects/issues_controller.rb6
-rw-r--r--app/helpers/issues_helper.rb13
-rw-r--r--app/mailers/emails/issues.rb8
-rw-r--r--app/models/concerns/issuable.rb9
-rw-r--r--app/models/issue.rb15
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/git_push_service.rb2
-rw-r--r--app/services/issues/close_service.rb6
-rw-r--r--app/services/issues/create_service.rb2
-rw-r--r--app/services/issues/move_service.rb94
-rw-r--r--app/services/merge_requests/post_merge_service.rb2
-rw-r--r--app/services/notification_service.rb10
-rw-r--r--app/services/system_note_service.rb22
-rw-r--r--app/views/notify/issue_moved_email.html.haml6
-rw-r--r--app/views/notify/issue_moved_email.text.erb4
-rw-r--r--app/views/shared/issuable/_form.html.haml23
17 files changed, 222 insertions, 13 deletions
diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee
index 6c1699c178c..7a788f761b7 100644
--- a/app/assets/javascripts/issuable_form.js.coffee
+++ b/app/assets/javascripts/issuable_form.js.coffee
@@ -1,5 +1,7 @@
class @IssuableForm
+ issueMoveConfirmMsg: 'Are you sure you want to move this issue to another project?'
wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i
+
constructor: (@form) ->
GitLab.GfmAutoComplete.setup()
new UsersSelect()
@@ -7,12 +9,13 @@ class @IssuableForm
@titleField = @form.find("input[name*='[title]']")
@descriptionField = @form.find("textarea[name*='[description]']")
+ @issueMoveField = @form.find("#move_to_project_id")
return unless @titleField.length && @descriptionField.length
@initAutosave()
- @form.on "submit", @resetAutosave
+ @form.on "submit", @handleSubmit
@form.on "click", ".btn-cancel", @resetAutosave
@initWip()
@@ -30,6 +33,12 @@ class @IssuableForm
"description"
]
+ handleSubmit: =>
+ if (parseInt(@issueMoveField?.val()) ? 0) > 0
+ return false unless confirm(@issueMoveConfirmMsg)
+
+ @resetAutosave()
+
resetAutosave: =>
@titleField.data("autosave").reset()
@descriptionField.data("autosave").reset()
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index fb8375116c6..a3c28a82d26 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -90,6 +90,12 @@ class Projects::IssuesController < Projects::ApplicationController
def update
@issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue)
+ if params[:move_to_project_id].to_i > 0
+ new_project = Project.find(params[:move_to_project_id])
+ move_service = Issues::MoveService.new(project, current_user)
+ @issue = move_service.execute(@issue, new_project)
+ end
+
respond_to do |format|
format.js
format.html do
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index e00d3204027..24b90fef4fe 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -57,6 +57,19 @@ module IssuesHelper
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end
+ def project_options(issuable, current_user, ability: :read_project)
+ projects = current_user.authorized_projects
+ projects = projects.select do |project|
+ current_user.can?(ability, project)
+ end
+
+ no_project = OpenStruct.new(id: 0, name_with_namespace: 'No project')
+ projects.unshift(no_project)
+ projects.delete(issuable.project)
+
+ options_from_collection_for_select(projects, :id, :name_with_namespace)
+ end
+
def status_box_class(item)
if item.respond_to?(:expired?) && item.expired?
'status-box-expired'
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 5f9adb32e00..6f54c42146c 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -36,6 +36,14 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
+ def issue_moved_email(recipient, issue, new_issue, updated_by_user)
+ setup_issue_mail(issue.id, recipient.id)
+
+ @new_issue = new_issue
+ @new_project = new_issue.project
+ mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id))
+ end
+
private
def setup_issue_mail(issue_id, recipient_id)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 86ab84615ba..9ab72652190 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -209,4 +209,13 @@ module Issuable
Taskable.get_updated_tasks(old_content: previous_changes['description'].first,
new_content: description)
end
+
+ ##
+ # Method that checks if issuable can be moved to another project.
+ #
+ # Should be overridden if issuable can be moved.
+ #
+ def can_move?(*)
+ false
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5347d4fa1be..35e08fa915b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -16,6 +16,7 @@
# state :string(255)
# iid :integer
# updated_by_id :integer
+# moved_to_id :integer
#
require 'carrierwave/orm/activerecord'
@@ -31,6 +32,8 @@ class Issue < ActiveRecord::Base
ActsAsTaggableOn.strict_case_match = true
belongs_to :project
+ belongs_to :moved_to, class_name: 'Issue'
+
validates :project, presence: true
scope :of_group,
@@ -137,6 +140,18 @@ class Issue < ActiveRecord::Base
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
end
+ def moved?
+ !moved_to.nil?
+ end
+
+ def can_move?(user, to_project = nil)
+ if to_project
+ return false unless user.can?(:admin_issue, to_project)
+ end
+
+ !moved? && user.can?(:admin_issue, self.project)
+ end
+
def to_branch_name
"#{title.parameterize}-#{iid}"
end
diff --git a/app/models/user.rb b/app/models/user.rb
index c011af03591..9c315cfe966 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -435,7 +435,7 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
- # Returns the groups a user is authorized to access.
+ # Returns projects user is authorized to access.
def authorized_projects
Project.where("projects.id IN (#{projects_union.to_sql})")
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 14e2a2c0699..c007d648dd6 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -120,7 +120,7 @@ class GitPushService < BaseService
closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
- Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
+ Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit: commit)
end
end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 78254b49af3..859c934ea3b 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -1,6 +1,6 @@
module Issues
class CloseService < Issues::BaseService
- def execute(issue, commit = nil)
+ def execute(issue, commit: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
todo_service.close_issue(issue, current_user)
@@ -9,8 +9,8 @@ module Issues
if project.default_issues_tracker? && issue.close
event_service.close_issue(issue, current_user)
- create_note(issue, commit)
- notification_service.close_issue(issue, current_user)
+ create_note(issue, commit) if system_note
+ notification_service.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 10787e8873c..e63e1af8766 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -4,7 +4,7 @@ module Issues
filter_params
label_params = params[:label_ids]
issue = project.issues.new(params.except(:label_ids))
- issue.author = current_user
+ issue.author = params[:author] || current_user
if issue.save
issue.update_attributes(label_ids: label_params)
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
new file mode 100644
index 00000000000..3cfbafe1576
--- /dev/null
+++ b/app/services/issues/move_service.rb
@@ -0,0 +1,94 @@
+module Issues
+ class MoveService < Issues::BaseService
+ class MoveError < StandardError; end
+
+ def execute(issue, new_project)
+ @old_issue = issue
+ @old_project = @project
+ @new_project = new_project
+
+ unless issue.can_move?(current_user, new_project)
+ raise MoveError, 'Cannot move issue due to insufficient permissions!'
+ end
+
+ if @project == new_project
+ raise MoveError, 'Cannot move issue to project it originates from!'
+ end
+
+ # Using transaction because of a high resources footprint
+ # on rewriting notes (unfolding references)
+ #
+ ActiveRecord::Base.transaction do
+ # New issue tasks
+ #
+ @new_issue = create_new_issue
+
+ rewrite_notes
+ add_note_moved_from
+
+ # Old issue tasks
+ #
+ add_note_moved_to
+ close_issue
+ mark_as_moved
+ end
+
+ notify_participants
+
+ @new_issue
+ end
+
+ private
+
+ def create_new_issue
+ new_params = { id: nil, iid: nil, label_ids: [], milestone: nil,
+ project: @new_project, author: @old_issue.author,
+ description: unfold_references(@old_issue.description) }
+
+ new_params = @old_issue.serializable_hash.merge(new_params)
+ CreateService.new(@new_project, @current_user, new_params).execute
+ end
+
+ def rewrite_notes
+ @old_issue.notes.find_each do |note|
+ new_note = note.dup
+ new_params = { project: @new_project, noteable: @new_issue,
+ note: unfold_references(new_note.note),
+ created_at: note.created_at }
+
+ new_note.update(new_params)
+ end
+ end
+
+ def close_issue
+ close_service = CloseService.new(@old_project, @current_user)
+ close_service.execute(@old_issue, notifications: false, system_note: false)
+ end
+
+ def add_note_moved_from
+ SystemNoteService.noteable_moved(@new_issue, @new_project,
+ @old_issue, @current_user,
+ direction: :from)
+ end
+
+ def add_note_moved_to
+ SystemNoteService.noteable_moved(@old_issue, @old_project,
+ @new_issue, @current_user,
+ direction: :to)
+ end
+
+ def unfold_references(content)
+ rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project,
+ @current_user)
+ rewriter.rewrite(@new_project)
+ end
+
+ def notify_participants
+ notification_service.issue_moved(@old_issue, @new_issue, @current_user)
+ end
+
+ def mark_as_moved
+ @old_issue.update(moved_to: @new_issue)
+ end
+ end
+end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index ebb67c7db65..064910f81f7 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -22,7 +22,7 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
- Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request)
+ Issues::CloseService.new(project, current_user, {}).execute(issue, commit: merge_request)
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 19a6779dea9..3bdf00a8291 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -236,6 +236,16 @@ class NotificationService
end
end
+ def issue_moved(issue, new_issue, current_user)
+ recipients = build_recipients(issue, issue.project, current_user)
+
+ recipients.map do |recipient|
+ email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
+ email.deliver_later
+ email
+ end
+ end
+
protected
# Get project users with WATCH notification level
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index c644cd0b951..e022a046c48 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -411,4 +411,26 @@ class SystemNoteService
body = "Marked the task **#{new_task.source}** as #{status_label}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
+
+ # Called when noteable has been moved to another project
+ #
+ # direction - symbol, :to or :from
+ # noteable - Noteable object
+ # noteable_ref - Referenced noteable
+ # author - User performing the move
+ #
+ # Example Note text:
+ #
+ # "Moved to some_namespace/project_new#11"
+ #
+ # Returns the created Note object
+ def self.noteable_moved(noteable, project, noteable_ref, author, direction:)
+ unless [:to, :from].include?(direction)
+ raise ArgumentError, "Invalid direction `#{direction}`"
+ end
+
+ cross_reference = noteable_ref.to_reference(project)
+ body = "Moved #{direction} #{cross_reference}"
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
end
diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml
new file mode 100644
index 00000000000..40f7d61fe19
--- /dev/null
+++ b/app/views/notify/issue_moved_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ Issue was moved to another project.
+%p
+ New issue:
+ = link_to namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) do
+ = @new_issue.title
diff --git a/app/views/notify/issue_moved_email.text.erb b/app/views/notify/issue_moved_email.text.erb
new file mode 100644
index 00000000000..b3bd43c2055
--- /dev/null
+++ b/app/views/notify/issue_moved_email.text.erb
@@ -0,0 +1,4 @@
+Issue was moved to another project.
+
+New issue location:
+<%= namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) %>
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 80418e69d9c..1740b128ee4 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -85,13 +85,26 @@
- if can? current_user, :admin_label, issuable.project
= link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank
+- if issuable.can_move?(current_user)
+ %hr
+ .form-group
+ = label_tag :move_to_project_id, 'Move', class: 'control-label'
+ .col-sm-10
+ - projects = project_options(issuable, current_user, ability: :admin_issue)
+ = select_tag(:move_to_project_id, projects, include_blank: true,
+ class: 'select2', data: { placeholder: 'Select project' })
+ &nbsp;
+ %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
+ title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
+ = icon('question-circle')
+
- if issuable.is_a?(MergeRequest)
%hr
- - if @merge_request.new_record?
- .form-group
- = f.label :source_branch, class: 'control-label'
- .col-sm-10
- = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
+ - if @merge_request.new_record?
+ .form-group
+ = f.label :source_branch, class: 'control-label'
+ .col-sm-10
+ = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
.form-group
= f.label :target_branch, class: 'control-label'
.col-sm-10