OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openproject/app/models/projects/copy.rb

387 lines
14 KiB

#-- encoding: UTF-8
5 years ago
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
module Projects::Copy
def self.included(base)
base.send :include, CopyModel
base.send :include, self::CopyMethods
# things that are explicitly excluded when copying a project
base.not_to_copy ['id', 'created_at', 'updated_at', 'name', 'identifier', 'active', 'lft', 'rgt']
# specify the order of associations to copy
base.copy_precedence ['members', 'versions', 'categories', 'work_packages', 'wiki', 'custom_values', 'queries']
end
module CopyMethods
def copy_attributes(project)
super
5 years ago
with_model(project) do |project_instance|
# Clear enabled modules
self.enabled_modules = []
5 years ago
self.enabled_module_names = project_instance.enabled_module_names - %w[repository]
self.types = project_instance.types
self.work_package_custom_fields = project_instance.work_package_custom_fields
self.custom_field_values = project_instance.custom_value_attributes
end
self
rescue ActiveRecord::RecordNotFound
nil
end
def copy_associations(from_model, options = {})
super(from_model, options) if save
end
private
# Copies custom values from +project+
5 years ago
def copy_custom_values(project, _selected_copies = [])
self.custom_values = project.custom_values.map(&:dup)
end
# Copies wiki from +project+
def copy_wiki(project, selected_copies = [])
# Check that the source project has a wiki first
unless project.wiki.nil?
self.wiki = build_wiki(project.wiki.attributes.dup.except('id', 'project_id'))
5 years ago
wiki.wiki_menu_items.delete_all
copy_wiki_pages(project, selected_copies)
copy_wiki_menu_items(project, selected_copies)
end
end
# Copies wiki pages from +project+, requires a wiki to be already set
def copy_wiki_pages(project, selected_copies = [])
wiki_pages_map = {}
project.wiki.pages.each do |page|
# Skip pages without content
next if page.content.nil?
new_wiki_content = WikiContent.new(page.content.attributes.dup.except('id', 'page_id', 'updated_at'))
new_wiki_page = WikiPage.new(page.attributes.dup.except('id', 'wiki_id', 'created_on', 'parent_id'))
new_wiki_page.content = new_wiki_content
wiki.pages << new_wiki_page
wiki_pages_map[page] = new_wiki_page
end
wiki.save
# Reproduce page hierarchy
project.wiki.pages.each do |page|
if page.parent_id && wiki_pages_map[page]
wiki_pages_map[page].parent = wiki_pages_map[page.parent]
wiki_pages_map[page].save
end
end
# Copy attachments
if selected_copies.include? :wiki_page_attachments
wiki_pages_map.each do |old_page, new_page|
copy_attachments(old_page, new_page)
end
end
end
# Copies wiki_menu_items from +project+, requires a wiki to be already set
5 years ago
def copy_wiki_menu_items(project, _selected_copies = [])
wiki_menu_items_map = {}
project.wiki.wiki_menu_items.each do |item|
new_item = MenuItems::WikiMenuItem.new
new_item.attributes = item.attributes.dup.except('id', 'wiki_id', 'parent_id')
new_item.wiki = wiki
(wiki_menu_items_map[item.id] = new_item.reload) if new_item.save
end
project.wiki.wiki_menu_items.each do |item|
if item.parent_id && (copy = wiki_menu_items_map[item.id])
copy.parent = wiki_menu_items_map[item.parent_id]
copy.save
end
end
end
# Copies versions from +project+
def copy_versions(project, _selected_copies = [])
project.versions.each do |version|
new_version = Version.new
new_version.attributes = version.attributes.dup.except('id', 'project_id', 'created_on', 'updated_at')
versions << new_version
end
end
# Copies issue categories from +project+
def copy_categories(project, _selected_copies = [])
project.categories.each do |category|
new_category = Category.new
new_category.send(:assign_attributes, category.attributes.dup.except('id', 'project_id'))
categories << new_category
end
end
# Copies work_packages from +project+
def copy_work_packages(project, selected_copies = [])
# Stores the source work_package id as a key and the copied work_packages as the
# value. Used to map the two together for work_package relations.
work_packages_map = {}
# Get work_packages sorted by their depth in the hierarchy tree
# so that parents get copied before their children.
to_copy = project
.work_packages
.includes(:custom_values, :version, :assigned_to, :responsible)
.order_by_ancestors('asc')
.order('id ASC')
user_cf_ids = WorkPackageCustomField.where(field_format: 'user').pluck(:id)
to_copy.each do |wp|
5 years ago
parent_id = work_packages_map[wp.parent_id]&.id || wp.parent_id
new_wp = copy_work_package(wp, parent_id, user_cf_ids)
work_packages_map[wp.id] = new_wp if new_wp
end
# reload all work_packages in our map, they might be modified by movement in their tree
work_packages_map.each_value(&:reload)
# Relations and attachments after in case work_packages related each other
to_copy.each do |wp|
new_wp = work_packages_map[wp.id]
unless new_wp
# work_package was not copied
next
end
# Attachments
if selected_copies.include? :work_package_attachments
copy_attachments(wp, new_wp)
end
# Relations
wp.relations_to.non_hierarchy.direct.each do |source_relation|
new_relation = Relation.new
new_relation.attributes = source_relation.attributes.dup.except('id', 'from_id', 'to_id', 'relation_type')
new_relation.to = work_packages_map[source_relation.to_id]
if new_relation.to.nil? && Setting.cross_project_work_package_relations?
new_relation.to = source_relation.to
end
new_relation.from = new_wp
new_relation.save
end
wp.relations_from.non_hierarchy.direct.each do |source_relation|
new_relation = Relation.new
new_relation.attributes = source_relation.attributes.dup.except('id', 'from_id', 'to_id', 'relation_type')
new_relation.from = work_packages_map[source_relation.from_id]
if new_relation.from.nil? && Setting.cross_project_work_package_relations?
new_relation.from = source_relation.from
end
new_relation.to = new_wp
new_relation.save
end
end
end
# Copies members from +project+
5 years ago
def copy_members(project, _selected_copies = [])
# Copy users first, then groups to handle members with inherited and given roles
members_to_copy = []
members_to_copy += project.memberships.select { |m| m.principal.is_a?(User) }
members_to_copy += project.memberships.reject { |m| m.principal.is_a?(User) }
members_to_copy.each do |member|
new_member = Member.new
new_member.send(:assign_attributes, member.attributes.dup.except('id', 'project_id', 'created_on'))
# only copy non inherited roles
# inherited roles will be added when copying the group membership
role_ids = member.member_roles.reject(&:inherited?).map(&:role_id)
next if role_ids.empty?
5 years ago
new_member.role_ids = role_ids
new_member.project = self
memberships << new_member
end
# Update the omitted attributes for the copied memberships
memberships.each do |new_member|
member = project.memberships.find_by(user_id: new_member.user_id)
Redmine::Hook.call_hook(:copy_project_add_member, new_member: new_member, member: member)
new_member.save
end
end
# Copies queries from +project+
# Only includes the queries visible in the wp table view.
def copy_queries(project, _selected_copies = [])
project.queries.non_hidden.includes(:query_menu_item).each do |query|
new_query = duplicate_query(query)
duplicate_query_menu_item(query, new_query)
end
# Update the context in the new project, otherwise, the filters will be invalid
queries.map(&:set_context)
end
# Copies forums from +project+
5 years ago
def copy_forums(project, _selected_copies = [])
project.forums.each do |forum|
new_forum = Forum.new
new_forum.attributes = forum.attributes.dup.except('id',
'project_id',
'topics_count',
'messages_count',
'last_message_id')
copy_topics(forum, new_forum)
new_forum.project = self
forums << new_forum
end
end
def copy_topics(board, new_forum)
topics = board.topics.where('parent_id is NULL')
topics.each do |topic|
new_topic = Message.new
new_topic.attributes = topic.attributes.dup.except('id',
'forum_id',
'author_id',
'replies_count',
'last_reply_id',
'created_on',
'updated_on')
new_topic.forum = new_forum
new_topic.author_id = topic.author_id
new_forum.topics << new_topic
end
end
def copy_attachments(from_container, to_container)
from_container.attachments.each do |old_attachment|
5 years ago
copied = old_attachment.dup
old_attachment.file.copy_to(copied)
to_container.attachments << copied
if copied.new_record?
log_error <<~MSG
Project#copy_attachments: Attachments ##{old_attachment.id} could not be copied: #{copied.errors.full_messages}
MSG
end
5 years ago
rescue StandardError => e
log_error("Failed to copy attachments from #{from_container} to #{to_container}: #{e}")
end
end
def duplicate_query(query)
new_query = ::Query.new name: '_'
new_query.attributes = query.attributes.dup.except('id', 'project_id', 'sort_criteria')
new_query.sort_criteria = query.sort_criteria if query.sort_criteria
new_query.set_context
new_query.project = self
queries << new_query
new_query
end
def duplicate_query_menu_item(source, sink)
if source.query_menu_item && sink.persisted?
::MenuItems::QueryMenuItem.create(
navigatable_id: sink.id,
name: SecureRandom.uuid,
title: source.query_menu_item.title
)
end
end
5 years ago
def copy_work_package(source_work_package, parent_id, user_cf_ids)
overrides = copy_work_package_attribute_overrides(source_work_package, parent_id, user_cf_ids)
5 years ago
service_call = WorkPackages::CopyService
.new(user: User.current,
work_package: source_work_package,
contract_class: WorkPackages::CopyProjectContract)
.call(overrides)
5 years ago
if service_call.success?
service_call.result
elsif logger&.info
log_work_package_copy_error(source_work_package, service_call.errors)
5 years ago
end
end
def copy_work_package_attribute_overrides(source_work_package, parent_id, user_cf_ids)
custom_value_attributes = source_work_package.custom_value_attributes.map do |id, value|
if user_cf_ids.include?(id) && !users.detect { |u| u.id.to_s == value }
[id, nil]
else
[id, value]
end
end.to_h
{
project: self,
parent_id: parent_id,
version: work_package_version(source_work_package),
5 years ago
assigned_to: work_package_assigned_to(source_work_package),
responsible: work_package_responsible(source_work_package),
custom_field_values: custom_value_attributes,
# We fetch the value from the global registry to persist it in the job which
# will trigger a delayed job for potentially sending the journal notifications.
send_notifications: ActionMailer::Base.perform_deliveries
}
end
def work_package_version(source_work_package)
source_work_package.version && versions.detect { |v| v.name == source_work_package.version.name }
5 years ago
end
def work_package_assigned_to(source_work_package)
source_work_package.assigned_to && possible_assignees.detect { |u| u.id == source_work_package.assigned_to_id }
end
def work_package_responsible(source_work_package)
source_work_package.responsible && possible_responsibles.detect { |u| u.id == source_work_package.responsible_id }
end
def log_work_package_copy_error(source_work_package, errors)
compiled_errors << errors
5 years ago
message = <<-MSG
5 years ago
Project#copy_work_packages: work package ##{source_work_package.id} could not be copied: #{errors.full_messages}
MSG
5 years ago
log_error(message, :info)
end
def log_error(message, level = :error)
Rails.logger.send(level, message)
5 years ago
end
end
end