Merge pull request #3297 from ulferts/fix/filter_allowed_projects_on_move

Fix/filter allowed projects on move
pull/3317/head
Jan Sandbrink 9 years ago
commit bbd119431c
  1. 11
      app/controllers/work_packages/moves_controller.rb
  2. 114
      app/models/work_package.rb
  3. 184
      app/services/move_work_package_service.rb
  4. 3
      spec/legacy/unit/issue_nested_set_spec.rb
  5. 361
      spec/models/work_package/work_package_copy_spec.rb
  6. 288
      spec/models/work_package_spec.rb
  7. 493
      spec/services/move_work_package_service_spec.rb

@ -60,9 +60,10 @@ class WorkPackages::MovesController < ApplicationController
:new_project_id,
ids: [])
if r = work_package.move_to_project(@target_project, new_type, copy: @copy,
attributes: permitted_params,
journal_note: @notes)
move_service = MoveWorkPackageService.new(work_package, current_user)
if r = move_service.call(@target_project, new_type, copy: @copy,
attributes: permitted_params,
journal_note: @notes)
moved_work_packages << r
else
unsaved_work_package_ids << work_package.id
@ -79,7 +80,7 @@ class WorkPackages::MovesController < ApplicationController
else
redirect_to project_work_packages_path(@project)
end
end
end
def set_flash_from_bulk_work_package_save(work_packages, unsaved_work_package_ids)
if unsaved_work_package_ids.empty? and not work_packages.empty?
@ -101,7 +102,7 @@ class WorkPackages::MovesController < ApplicationController
def prepare_for_work_package_move
@work_packages.sort!
@copy = params.has_key? :copy
@allowed_projects = WorkPackage.allowed_target_projects_on_move
@allowed_projects = WorkPackage.allowed_target_projects_on_move(current_user)
@target_project = @allowed_projects.detect { |p| p.id.to_s == params[:new_project_id].to_s } if params[:new_project_id]
@target_project ||= @project
@types = @target_project.types

@ -487,14 +487,6 @@ class WorkPackage < ActiveRecord::Base
@spent_hours ||= compute_spent_hours(usr)
end
# Moves/copies an work_package to a new project and type
# Returns the moved/copied work_package on success, false on failure
def move_to_project(*args)
WorkPackage.transaction do
move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
end || false
end
# >>> issues.rb >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Returns users that should be notified
def recipients
@ -674,77 +666,6 @@ class WorkPackage < ActiveRecord::Base
done_date <= Date.today
end
def move_to_project_without_transaction(new_project, new_type = nil, options = {})
options ||= {}
work_package = options[:copy] ? self.class.new.copy_from(self) : self
if new_project && work_package.project_id != new_project.id
delete_relations(work_package)
# work_package is moved to another project
# reassign to the category with same name if any
new_category = if work_package.category.nil?
nil
else
new_project.categories.find_by_name(work_package.category.name)
end
work_package.category = new_category
# Keep the fixed_version if it's still valid in the new_project
unless new_project.shared_versions.include?(work_package.fixed_version)
work_package.fixed_version = nil
end
work_package.project = new_project
enforce_cross_project_settings(work_package)
end
if new_type
work_package.type = new_type
work_package.reset_custom_values!
end
# Allow bulk setting of attributes on the work_package
if options[:attributes]
# before setting the attributes, we need to remove the move-related fields
work_package.attributes =
options[:attributes].except(:copy, :new_project_id, :new_type_id, :follow, :ids)
.reject { |_key, value| value.blank? }
end # FIXME this eliminates the case, where values shall be bulk-assigned to null,
# but this needs to work together with the permit
if options[:copy]
work_package.author = User.current
work_package.custom_field_values =
custom_field_values.inject({}) do |h, v|
h[v.custom_field_id] = v.value
h
end
work_package.status = if options[:attributes] && options[:attributes][:status_id].present?
Status.find_by_id(options[:attributes][:status_id])
else
status
end
else
work_package.add_journal User.current, options[:journal_note] if options[:journal_note]
end
if work_package.save
if options[:copy]
create_and_save_journal_note work_package, options[:journal_note]
else
# Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", work_package_id: id)
work_package.children.each do |child|
unless child.move_to_project_without_transaction(new_project)
# Move failed and transaction was rollback'd
return false
end
end
end
else
return false
end
work_package
end
# check if user is allowed to edit WorkPackage Journals.
# see Redmine::Acts::Journalized::Permissions#journal_editable_by
def editable_by?(user)
@ -849,22 +770,10 @@ class WorkPackage < ActiveRecord::Base
reload(select: [:lock_version, :created_at, :updated_at])
end
# Returns an array of projects that current user can move issues to
def self.allowed_target_projects_on_move
projects = []
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
projects = Project.visible.all
elsif User.current.logged?
if Role.non_member.allowed_to?(:move_work_packages)
projects = Project.visible.all
else
User.current.memberships.each do |m|
projects << m.project if m.roles.detect { |r| r.allowed_to?(:move_work_packages) }
end
end
end
projects
# Returns a scope for the projects
# the user is allowed to move a work package to
def self.allowed_target_projects_on_move(user)
Project.where(Project.allowed_to_condition(user, :move_work_packages))
end
# Do not redefine alias chain on reload (see #4838)
@ -1112,21 +1021,6 @@ class WorkPackage < ActiveRecord::Base
end
end
def create_and_save_journal_note(work_package, journal_note)
if work_package && journal_note
work_package.add_journal User.current, journal_note
work_package.save!
end
end
def enforce_cross_project_settings(work_package)
parent_in_project =
work_package.parent.nil? || work_package.parent.project == work_package.project
work_package.parent_id =
nil unless Setting.cross_project_work_package_relations? || parent_in_project
end
def compute_spent_hours(usr = User.current)
spent_time = TimeEntry.visible(usr)
.on_work_packages(self_and_descendants.visible(usr))

@ -0,0 +1,184 @@
# Moves/copies an work_package to a new project and type
# Returns the moved/copied work_package on success, false on failure
class MoveWorkPackageService
attr_accessor :work_package,
:user
def initialize(work_package, user)
self.work_package = work_package
self.user = user
end
def call(new_project, new_type = nil, options = {})
if options[:no_transaction]
move_without_transaction(new_project, new_type, options)
else
WorkPackage.transaction do
move_without_transaction(new_project, new_type, options) ||
raise(ActiveRecord::Rollback)
end || false
end
end
private
def move_without_transaction(new_project, new_type = nil, options = {})
attributes = options[:attributes] || {}
modified_work_package = copy_or_move(options[:copy], new_project, new_type, attributes)
if options[:copy]
return false unless copy(modified_work_package, attributes, options)
else
return false unless move(modified_work_package, new_project, options)
end
modified_work_package
end
def copy_or_move(make_copy, new_project, new_type, attributes)
modified_work_package = if make_copy
WorkPackage.new.copy_from(work_package)
else
work_package
end
move_to_project(modified_work_package, new_project)
move_to_type(modified_work_package, new_type)
bulk_assign_attributes(modified_work_package, attributes)
modified_work_package
end
def copy(modified_work_package, attributes, options)
set_default_values_on_copy(modified_work_package, attributes)
return false unless modified_work_package.save
create_and_save_journal_note modified_work_package, options[:journal_note]
true
end
def move(modified_work_package, new_project, options)
if options[:journal_note]
modified_work_package.add_journal user, options[:journal_note]
end
return false unless modified_work_package.save
move_time_entries(modified_work_package, new_project)
return false unless move_children(modified_work_package, new_project, options)
true
end
def move_to_project(work_package, new_project)
if new_project &&
work_package.project_id != new_project.id &&
allowed_to_move_to_project?(new_project)
work_package.delete_relations(work_package)
reassign_category(work_package, new_project)
# Keep the fixed_version if it's still valid in the new_project
unless new_project.shared_versions.include?(work_package.fixed_version)
work_package.fixed_version = nil
end
work_package.project = new_project
enforce_cross_project_settings(work_package)
end
end
def move_to_type(work_package, new_type)
if new_type
work_package.type = new_type
work_package.reset_custom_values!
end
end
def bulk_assign_attributes(work_package, attributes)
# Allow bulk setting of attributes on the work_package
if attributes
# before setting the attributes, we need to remove the move-related fields
work_package.attributes =
attributes.except(:copy, :new_project_id, :new_type_id, :follow, :ids)
.reject { |_key, value| value.blank? }
end # FIXME this eliminates the case, where values shall be bulk-assigned to null,
# but this needs to work together with the permit
end
def set_default_values_on_copy(work_package, attributes)
work_package.author = user
assign_status_or_default(work_package, attributes[:status_id])
end
def move_children(work_package, new_project, options)
work_package.children.each do |child|
child_service = self.class.new(child, user)
unless child_service.call(new_project, nil, options.merge(no_transaction: true))
# Move failed and transaction was rollback'd
return false
end
end
true
end
def move_time_entries(work_package, new_project)
# Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", work_package_id: work_package.id)
end
def enforce_cross_project_settings(work_package)
parent_in_project =
work_package.parent.nil? || work_package.parent.project == work_package.project
work_package.parent_id =
nil unless Setting.cross_project_work_package_relations? || parent_in_project
end
def create_and_save_journal_note(work_package, journal_note)
if journal_note
work_package.add_journal user, journal_note
work_package.save!
end
end
def allowed_to_move_to_project?(new_project)
WorkPackage
.allowed_target_projects_on_move(user)
.where(id: new_project.id)
.exists?
end
def reassign_category(work_package, new_project)
# work_package is moved to another project
# reassign to the category with same name if any
new_category = if work_package.category.nil?
nil
else
new_project.categories.find_by_name(work_package.category.name)
end
work_package.category = new_category
end
def assign_status_or_default(work_package, status_id)
status = if status_id.present?
Status.find_by_id(status_id)
else
self.work_package.status
end
work_package.status = status
end
end

@ -77,7 +77,8 @@ describe 'IssueNestedSet', type: :model do
assert_equal [1, parent1.id, 5], [parent1.project_id, parent1.root_id, parent1.nested_set_span]
# child can not be moved to Project 2 because its child is on a disabled type
assert_equal false, WorkPackage.find(child.id).move_to_project(Project.find(2))
service = MoveWorkPackageService.new(child, User.current)
assert_equal false, service.call(Project.find(2))
child.reload
grandchild.reload
parent1.reload

@ -1,361 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe WorkPackage, type: :model do
describe '#copy' do
let(:user) { FactoryGirl.create(:user) }
let(:custom_field) { FactoryGirl.create(:work_package_custom_field) }
let(:source_type) {
FactoryGirl.create(:type,
custom_fields: [custom_field])
}
let(:source_project) {
FactoryGirl.create(:project,
types: [source_type])
}
let(:work_package) {
FactoryGirl.create(:work_package,
project: source_project,
type: source_type,
author: user)
}
let(:custom_value) {
FactoryGirl.create(:work_package_custom_value,
custom_field: custom_field,
customized: work_package,
value: false)
}
shared_examples_for 'copied work package' do
subject { copy.id }
it { is_expected.not_to eq(work_package.id) }
end
describe 'to the same project' do
let(:copy) { work_package.move_to_project(source_project, nil, copy: true) }
it_behaves_like 'copied work package'
context 'project' do
subject { copy.project }
it { is_expected.to eq(source_project) }
end
end
describe 'to a different project' do
let(:target_type) { FactoryGirl.create(:type) }
let(:target_project) {
FactoryGirl.create(:project,
types: [target_type])
}
let(:copy) { work_package.move_to_project(target_project, target_type, copy: true) }
it_behaves_like 'copied work package'
context 'project' do
subject { copy.project_id }
it { is_expected.to eq(target_project.id) }
end
context 'type' do
subject { copy.type_id }
it { is_expected.to eq(target_type.id) }
end
context 'custom_fields' do
before { custom_value }
subject { copy.custom_value_for(custom_field.id) }
it { is_expected.to be_nil }
end
describe '#attributes' do
let(:copy) {
work_package.move_to_project(target_project,
target_type,
copy: true,
attributes: attributes)
}
context 'assigned_to' do
let(:target_user) { FactoryGirl.create(:user) }
let(:target_project_member) {
FactoryGirl.create(:member,
project: target_project,
principal: target_user,
roles: [FactoryGirl.create(:role)])
}
let(:attributes) { { assigned_to_id: target_user.id } }
before { target_project_member }
it_behaves_like 'copied work package'
subject { copy.assigned_to_id }
it { is_expected.to eq(target_user.id) }
end
context 'status' do
let(:target_status) { FactoryGirl.create(:status) }
let(:attributes) { { status_id: target_status.id } }
it_behaves_like 'copied work package'
subject { copy.status_id }
it { is_expected.to eq(target_status.id) }
end
context 'date' do
let(:target_date) { Date.today + 14 }
context 'start' do
let(:attributes) { { start_date: target_date } }
it_behaves_like 'copied work package'
subject { copy.start_date }
it { is_expected.to eq(target_date) }
end
context 'end' do
let(:attributes) { { due_date: target_date } }
it_behaves_like 'copied work package'
subject { copy.due_date }
it { is_expected.to eq(target_date) }
end
end
end
describe 'private project' do
let(:role) {
FactoryGirl.create(:role,
permissions: [:view_work_packages])
}
let(:target_project) {
FactoryGirl.create(:project,
is_public: false,
types: [target_type])
}
let(:source_project_member) {
FactoryGirl.create(:member,
project: source_project,
principal: user,
roles: [role])
}
before do
source_project_member
allow(User).to receive(:current).and_return user
end
it_behaves_like 'copied work package'
context 'pre-condition' do
subject { work_package.recipients }
it { is_expected.to include(work_package.author) }
end
subject { copy.recipients }
it { is_expected.not_to include(copy.author) }
end
describe 'with children' do
let(:target_project) { FactoryGirl.create(:project, types: [source_type]) }
let(:copy) { child.reload.move_to_project(target_project) }
let!(:child) {
FactoryGirl.create(:work_package, parent: work_package, project: source_project)
}
let!(:grandchild) {
FactoryGirl.create(:work_package, parent: child, project: source_project)
}
context 'cross project relations deactivated' do
before {
allow(Setting).to receive(:cross_project_work_package_relations?).and_return(false)
}
it { expect(copy).to be_falsy }
it { expect(child.reload.project).to eql(source_project) }
describe 'grandchild' do
before { copy }
it { expect(grandchild.reload.project).to eql(source_project) }
end
end
context 'cross project relations activated' do
before {
allow(Setting).to receive(:cross_project_work_package_relations?).and_return(true)
}
it { expect(copy).to be_truthy }
it { expect(copy.project).to eql(target_project) }
describe 'grandchild' do
before { copy }
it { expect(grandchild.reload.project).to eql(target_project) }
end
end
end
end
end
shared_context 'project with required custom field' do
before do
project.work_package_custom_fields << custom_field
type.custom_fields << custom_field
source.save
end
end
before do
def self.change_custom_field_value(work_package, value)
work_package.custom_field_values = { custom_field.id => value } unless value.nil?
work_package.save
end
end
let(:type) { FactoryGirl.create(:type_standard) }
let(:project) { FactoryGirl.create(:project, types: [type]) }
let(:custom_field) {
FactoryGirl.create(:work_package_custom_field,
name: 'Database',
field_format: 'list',
possible_values: ['MySQL', 'PostgreSQL', 'Oracle'],
is_required: true)
}
describe '#copy_from' do
include_context 'project with required custom field'
let(:source) { FactoryGirl.build(:work_package) }
let(:sink) { FactoryGirl.build(:work_package) }
before do
source.project_id = project.id
change_custom_field_value(source, 'MySQL')
end
shared_examples_for 'work package copy' do
context 'subject' do
subject { sink.subject }
it { is_expected.to eq(source.subject) }
end
context 'type' do
subject { sink.type }
it { is_expected.to eq(source.type) }
end
context 'status' do
subject { sink.status }
it { is_expected.to eq(source.status) }
end
context 'project' do
subject { sink.project_id }
it { is_expected.to eq(project_id) }
end
context 'watchers' do
subject { sink.watchers.map(&:user_id) }
it do
is_expected.to match_array(source.watchers.map(&:user_id))
sink.watchers.each { |w| expect(w).to be_valid }
end
end
end
shared_examples_for 'work package copy with custom field' do
it_behaves_like 'work package copy'
context 'custom_field' do
subject { sink.custom_value_for(custom_field.id).value }
it { is_expected.to eq('MySQL') }
end
end
context 'with project' do
let(:project_id) { source.project_id }
describe 'should copy project' do
before { sink.copy_from(source) }
it_behaves_like 'work package copy with custom field'
end
describe 'should not copy excluded project' do
let(:project_id) { sink.project_id }
before { sink.copy_from(source, exclude: [:project_id]) }
it_behaves_like 'work package copy'
end
describe 'should copy over watchers' do
let(:project_id) { sink.project_id }
let(:stub_user) { FactoryGirl.create(:user, member_in_project: project) }
before do
source.watchers.build(user: stub_user, watchable: source)
sink.copy_from(source)
end
it_behaves_like 'work package copy'
end
end
end
end

@ -481,171 +481,115 @@ describe WorkPackage, type: :model do
end
end
describe '#move' do
let(:work_package) {
FactoryGirl.create(:work_package,
project: project,
type: type)
describe '#copy_from' do
let(:type) { FactoryGirl.create(:type_standard) }
let(:project) { FactoryGirl.create(:project, types: [type]) }
let(:custom_field) {
FactoryGirl.create(:work_package_custom_field,
name: 'Database',
field_format: 'list',
possible_values: ['MySQL', 'PostgreSQL', 'Oracle'],
is_required: true)
}
let(:target_project) { FactoryGirl.create(:project) }
shared_examples_for 'moved work package' do
subject { work_package.project }
let(:source) { FactoryGirl.build(:work_package) }
let(:sink) { FactoryGirl.build(:work_package) }
it { is_expected.to eq(target_project) }
before do
def self.change_custom_field_value(work_package, value)
work_package.custom_field_values = { custom_field.id => value } unless value.nil?
work_package.save
end
end
describe '#time_entries' do
let(:time_entry_1) {
FactoryGirl.create(:time_entry,
project: project,
work_package: work_package)
}
let(:time_entry_2) {
FactoryGirl.create(:time_entry,
project: project,
work_package: work_package)
}
before do
time_entry_1
time_entry_2
before do
project.work_package_custom_fields << custom_field
type.custom_fields << custom_field
work_package.reload
work_package.move_to_project(target_project)
source.save
end
time_entry_1.reload
time_entry_2.reload
end
before do
source.project_id = project.id
change_custom_field_value(source, 'MySQL')
end
context 'time entry 1' do
subject { work_package.time_entries }
shared_examples_for 'work package copy' do
context 'subject' do
subject { sink.subject }
it { is_expected.to include(time_entry_1) }
it { is_expected.to eq(source.subject) }
end
context 'time entry 2' do
subject { work_package.time_entries }
context 'type' do
subject { sink.type }
it { is_expected.to include(time_entry_2) }
it { is_expected.to eq(source.type) }
end
it_behaves_like 'moved work package'
end
context 'status' do
subject { sink.status }
describe '#category' do
let(:category) {
FactoryGirl.create(:category,
project: project)
}
before do
work_package.category = category
work_package.save!
work_package.reload
it { is_expected.to eq(source.status) }
end
context 'with same category' do
let(:target_category) {
FactoryGirl.create(:category,
name: category.name,
project: target_project)
}
before do
target_category
context 'project' do
subject { sink.project_id }
work_package.move_to_project(target_project)
end
it { is_expected.to eq(project_id) }
end
describe 'category moved' do
subject { work_package.category_id }
context 'watchers' do
subject { sink.watchers.map(&:user_id) }
it { is_expected.to eq(target_category.id) }
it do
is_expected.to match_array(source.watchers.map(&:user_id))
sink.watchers.each { |w| expect(w).to be_valid }
end
it_behaves_like 'moved work package'
end
end
context 'w/o target category' do
before { work_package.move_to_project(target_project) }
describe 'category discarded' do
subject { work_package.category_id }
shared_examples_for 'work package copy with custom field' do
it_behaves_like 'work package copy'
it { is_expected.to be_nil }
end
context 'custom_field' do
subject { sink.custom_value_for(custom_field.id).value }
it_behaves_like 'moved work package'
it { is_expected.to eq('MySQL') }
end
end
describe '#version' do
let(:sharing) { 'none' }
let(:version) {
FactoryGirl.create(:version,
status: 'open',
project: project,
sharing: sharing)
}
let(:work_package) {
FactoryGirl.create(:work_package,
fixed_version: version,
project: project)
}
before { work_package.move_to_project(target_project) }
context 'with project' do
let(:project_id) { source.project_id }
it_behaves_like 'moved work package'
describe 'should copy project' do
context 'unshared version' do
subject { work_package.fixed_version }
before { sink.copy_from(source) }
it { is_expected.to be_nil }
it_behaves_like 'work package copy with custom field'
end
context 'system wide shared version' do
let(:sharing) { 'system' }
describe 'should not copy excluded project' do
let(:project_id) { sink.project_id }
subject { work_package.fixed_version }
before { sink.copy_from(source, exclude: [:project_id]) }
it { is_expected.to eq(version) }
it_behaves_like 'work package copy'
end
context 'move work package in project hierarchy' do
let(:target_project) {
FactoryGirl.create(:project,
parent: project)
}
describe 'should copy over watchers' do
let(:project_id) { sink.project_id }
let(:stub_user) { FactoryGirl.create(:user, member_in_project: project) }
context 'unshared version' do
subject { work_package.fixed_version }
before do
source.watchers.build(user: stub_user, watchable: source)
it { is_expected.to be_nil }
sink.copy_from(source)
end
context 'shared version' do
let(:sharing) { 'tree' }
subject { work_package.fixed_version }
it { is_expected.to eq(version) }
end
it_behaves_like 'work package copy'
end
end
describe '#type' do
let(:target_type) { FactoryGirl.create(:type) }
let(:target_project) {
FactoryGirl.create(:project,
types: [target_type])
}
subject { work_package.move_to_project(target_project) }
it { is_expected.to be_falsey }
end
end
describe '#destroy' do
@ -1333,36 +1277,104 @@ describe WorkPackage, type: :model do
end
describe '#allowed_target_projects_on_move' do
let(:admin_user) { FactoryGirl.create :admin }
let(:valid_user) { FactoryGirl.create :user }
let(:project) { FactoryGirl.create :project }
context 'admin user' do
before do
allow(User).to receive(:current).and_return admin_user
project
subject { WorkPackage.allowed_target_projects_on_move(user) }
before do
allow(User).to receive(:current).and_return user
project
end
shared_examples_for 'has the permission to see projects' do
it 'sees the project' do
is_expected.to match_array [project]
end
it 'does not see the archived project' do
project.update_attribute(:status, Project::STATUS_ARCHIVED)
is_expected.to match_array []
end
subject { WorkPackage.allowed_target_projects_on_move.count }
it 'does not see the project having the work package module disabled' do
enabled_modules = project.enabled_module_names.delete(:work_package_tracking)
project.enabled_module_names = enabled_modules
project.save!
is_expected.to match_array []
end
end
it 'sees all active projects' do
is_expected.to eq Project.active.count
shared_examples_for 'lacks the permission to see projects' do
it 'does not see the project' do
is_expected.to match_array []
end
end
context 'admin user' do
let(:admin_user) { FactoryGirl.create :admin }
it_behaves_like 'has the permission to see projects' do
let(:user) { admin_user }
end
end
context 'non admin user' do
before do
allow(User).to receive(:current).and_return valid_user
let(:role) { FactoryGirl.build(:role, permissions: user_in_project_permissions) }
let(:user_in_project_permissions) { [:move_work_packages] }
let(:user_in_project) {
FactoryGirl.build :user,
member_in_project: project,
member_through_role: role
}
role = FactoryGirl.create :role, permissions: [:move_work_packages]
it_behaves_like 'has the permission to see projects' do
before do
user_in_project.save!
end
let(:user) { user_in_project }
end
it_behaves_like 'lacks the permission to see projects' do
let(:user_in_project_permissions) { [] }
before do
user_in_project.save!
end
let(:user) { user_in_project }
end
end
FactoryGirl.create(:member, user: valid_user, project: project, roles: [role])
context 'non member user' do
it_behaves_like 'lacks the permission to see projects' do
before do
project.update_attribute(:is_public, true)
FactoryGirl.create(:non_member, permissions: [])
end
let(:user) { FactoryGirl.create(:user) }
end
subject { WorkPackage.allowed_target_projects_on_move.count }
it_behaves_like 'has the permission to see projects' do
before do
project.update_attribute(:is_public, true)
FactoryGirl.create(:non_member, permissions: [:move_work_packages])
end
let(:user) { FactoryGirl.create(:user) }
end
end
context 'anonymous user' do
it_behaves_like 'lacks the permission to see projects' do
before do
project.update_attribute(:is_public, true)
end
it 'sees all active projects' do
is_expected.to eq Project.active.count
let(:user) { FactoryGirl.create(:anonymous) }
end
end
end

@ -0,0 +1,493 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe MoveWorkPackageService, type: :model do
let(:user) { FactoryGirl.create(:user) }
let(:type) { FactoryGirl.create(:type_standard) }
let(:project) { FactoryGirl.create(:project, types: [type]) }
let(:work_package) {
FactoryGirl.create(:work_package,
project: project,
type: type)
}
let(:instance) { MoveWorkPackageService.new(work_package, user) }
before do
allow(User).to receive(:current).and_return(user)
end
def mock_allowed_to_move_to_project(project, is_allowed = true)
allowed_scope = double('allowed_scope', :'exists?' => is_allowed)
allow(WorkPackage)
.to receive(:allowed_target_projects_on_move)
.with(user)
.and_return(allowed_scope)
allow(allowed_scope)
.to receive(:where)
.with(id: project.id)
.and_return(allowed_scope)
end
describe '#call' do
context 'when moving' do
let(:target_project) { FactoryGirl.create(:project) }
before do
work_package
mock_allowed_to_move_to_project(target_project, true)
end
shared_examples_for 'moved work package' do
subject { work_package.project }
it { is_expected.to eq(target_project) }
end
context 'the project the work package is moved to' do
it_behaves_like 'moved work package' do
before do
instance.call(target_project)
end
end
it 'will not move if the user does not have the permission' do
mock_allowed_to_move_to_project(target_project, false)
instance.call(target_project)
expect(work_package.project).to eql(project)
end
end
describe '#time_entries' do
let(:time_entry_1) {
FactoryGirl.create(:time_entry,
project: project,
work_package: work_package)
}
let(:time_entry_2) {
FactoryGirl.create(:time_entry,
project: project,
work_package: work_package)
}
before do
time_entry_1
time_entry_2
work_package.reload
instance.call(target_project)
time_entry_1.reload
time_entry_2.reload
end
context 'time entry 1' do
subject { work_package.time_entries }
it { is_expected.to include(time_entry_1) }
end
context 'time entry 2' do
subject { work_package.time_entries }
it { is_expected.to include(time_entry_2) }
end
it_behaves_like 'moved work package'
end
describe '#category' do
let(:category) {
FactoryGirl.create(:category,
project: project)
}
before do
work_package.category = category
work_package.save!
work_package.reload
end
context 'with same category' do
let(:target_category) {
FactoryGirl.create(:category,
name: category.name,
project: target_project)
}
before do
target_category
instance.call(target_project)
end
describe 'category moved' do
subject { work_package.category_id }
it { is_expected.to eq(target_category.id) }
end
it_behaves_like 'moved work package'
end
context 'w/o target category' do
before do
instance.call(target_project)
end
describe 'category discarded' do
subject { work_package.category_id }
it { is_expected.to be_nil }
end
it_behaves_like 'moved work package'
end
end
describe '#version' do
let(:sharing) { 'none' }
let(:version) {
FactoryGirl.create(:version,
status: 'open',
project: project,
sharing: sharing)
}
let(:work_package) {
FactoryGirl.create(:work_package,
fixed_version: version,
project: project)
}
before do
instance.call(target_project)
end
it_behaves_like 'moved work package'
context 'unshared version' do
subject { work_package.fixed_version }
it { is_expected.to be_nil }
end
context 'system wide shared version' do
let(:sharing) { 'system' }
subject { work_package.fixed_version }
it { is_expected.to eq(version) }
end
context 'move work package in project hierarchy' do
let(:target_project) {
FactoryGirl.create(:project,
parent: project)
}
context 'unshared version' do
subject { work_package.fixed_version }
it { is_expected.to be_nil }
end
context 'shared version' do
let(:sharing) { 'tree' }
subject { work_package.fixed_version }
it { is_expected.to eq(version) }
end
end
end
describe '#type' do
let(:target_type) { FactoryGirl.create(:type) }
let(:target_project) {
FactoryGirl.create(:project,
types: [target_type])
}
it 'is false if the current type is not defined for the new project' do
expect(instance.call(target_project)).to be_falsey
end
end
end
describe 'when copying' do
let(:custom_field) { FactoryGirl.create(:work_package_custom_field) }
let(:source_type) {
FactoryGirl.create(:type,
custom_fields: [custom_field])
}
let(:source_project) {
FactoryGirl.create(:project,
types: [source_type])
}
let(:work_package) {
FactoryGirl.create(:work_package,
project: source_project,
type: source_type,
author: user)
}
let(:custom_value) {
FactoryGirl.create(:work_package_custom_value,
custom_field: custom_field,
customized: work_package,
value: false)
}
shared_examples_for 'copied work package' do
subject { copy.id }
it { is_expected.not_to eq(work_package.id) }
end
describe 'to the same project' do
let(:copy) {
mock_allowed_to_move_to_project(source_project)
instance.call(source_project, nil, copy: true)
}
it_behaves_like 'copied work package'
context 'project' do
subject { copy.project }
it { is_expected.to eq(source_project) }
end
end
describe 'to a different project' do
let(:target_type) { FactoryGirl.create(:type) }
let(:target_project) {
FactoryGirl.create(:project,
types: [target_type])
}
let(:copy) do
mock_allowed_to_move_to_project(target_project)
instance.call(target_project, target_type, copy: true)
end
it_behaves_like 'copied work package'
context 'project' do
subject { copy.project_id }
it { is_expected.to eq(target_project.id) }
end
context 'type' do
subject { copy.type_id }
it { is_expected.to eq(target_type.id) }
end
context 'custom_fields' do
before do
custom_value
end
subject { copy.custom_value_for(custom_field.id) }
it { is_expected.to be_nil }
end
describe '#attributes' do
let(:copy) {
mock_allowed_to_move_to_project(target_project)
instance.call(target_project,
target_type,
copy: true,
attributes: attributes)
}
context 'assigned_to' do
let(:target_user) { FactoryGirl.create(:user) }
let(:target_project_member) {
FactoryGirl.create(:member,
project: target_project,
principal: target_user,
roles: [FactoryGirl.create(:role)])
}
let(:attributes) { { assigned_to_id: target_user.id } }
before do
target_project_member
end
it_behaves_like 'copied work package'
subject { copy.assigned_to_id }
it { is_expected.to eq(target_user.id) }
end
context 'status' do
let(:target_status) { FactoryGirl.create(:status) }
let(:attributes) { { status_id: target_status.id } }
it_behaves_like 'copied work package'
subject { copy.status_id }
it { is_expected.to eq(target_status.id) }
end
context 'date' do
let(:target_date) { Date.today + 14 }
context 'start' do
let(:attributes) { { start_date: target_date } }
it_behaves_like 'copied work package'
subject { copy.start_date }
it { is_expected.to eq(target_date) }
end
context 'end' do
let(:attributes) { { due_date: target_date } }
it_behaves_like 'copied work package'
subject { copy.due_date }
it { is_expected.to eq(target_date) }
end
end
end
describe 'private project' do
let(:role) {
FactoryGirl.create(:role,
permissions: [:view_work_packages])
}
let(:target_project) {
FactoryGirl.create(:project,
is_public: false,
types: [target_type])
}
let(:source_project_member) {
FactoryGirl.create(:member,
project: source_project,
principal: user,
roles: [role])
}
before do
source_project_member
allow(User).to receive(:current).and_return user
end
it_behaves_like 'copied work package'
context 'pre-condition' do
subject { work_package.recipients }
it { is_expected.to include(work_package.author) }
end
subject { copy.recipients }
it { is_expected.not_to include(copy.author) }
end
describe 'with children' do
let(:target_project) { FactoryGirl.create(:project, types: [source_type]) }
let(:instance) { MoveWorkPackageService.new(child, user) }
let(:copy) do
mock_allowed_to_move_to_project(target_project)
child.reload
instance.call(target_project)
end
let!(:child) {
FactoryGirl.create(:work_package, parent: work_package, project: source_project)
}
let!(:grandchild) {
FactoryGirl.create(:work_package, parent: child, project: source_project)
}
context 'cross project relations deactivated' do
before do
allow(Setting).to receive(:cross_project_work_package_relations?).and_return(false)
end
it do
expect(copy).to be_falsy
end
it do
expect(child.reload.project).to eql(source_project)
end
describe 'grandchild' do
before do
copy
end
it { expect(grandchild.reload.project).to eql(source_project) }
end
end
context 'cross project relations activated' do
before do
allow(Setting).to receive(:cross_project_work_package_relations?).and_return(true)
end
it do
expect(copy).to be_truthy
end
it do
expect(copy.project).to eql(target_project)
end
describe 'grandchild' do
before do
copy
end
it { expect(grandchild.reload.project).to eql(target_project) }
end
end
end
end
end
end
end
Loading…
Cancel
Save