From 48ef6c0898c7adcb48b778a3f32cfa0d90e12326 Mon Sep 17 00:00:00 2001 From: Frederico Camara Date: Thu, 7 Nov 2019 09:13:50 -0300 Subject: [PATCH 04/21] Add workspace functionality: to reuse roles and trackers in different projects with different workflows - Add new Workspace in Administration Workspace - In Admin Workflows set different workflows in different workspaces - Select in project settings which workflow to use - Add Expand to Roles/Trackers in Workflows view --- app/controllers/admin_controller.rb | 2 + app/controllers/workflows_controller.rb | 46 +++++++--- app/controllers/workspaces_controller.rb | 80 ++++++++++++++++++ app/helpers/projects_helper.rb | 8 ++ app/helpers/workflows_helper.rb | 10 +-- app/models/issue.rb | 3 +- app/models/issue_status.rb | 10 +-- app/models/project.rb | 2 + app/models/role.rb | 2 +- app/models/tracker.rb | 2 +- app/models/workflow_permission.rb | 21 +++-- app/models/workflow_rule.rb | 39 +++++---- app/models/workflow_transition.rb | 84 ++++++++++--------- app/models/workspace.rb | 46 ++++++++++ app/views/admin/projects.html.erb | 2 + app/views/projects/_form.html.erb | 4 + app/views/workflows/copy.html.erb | 25 ++++-- app/views/workflows/edit.html.erb | 11 ++- app/views/workflows/index.html.erb | 64 +++++++++++++-- app/views/workflows/permissions.html.erb | 12 ++- app/views/workspaces/_form.html.erb | 8 ++ app/views/workspaces/edit.html.erb | 6 ++ app/views/workspaces/index.api.rsb | 9 ++ app/views/workspaces/index.html.erb | 31 +++++++ app/views/workspaces/new.html.erb | 6 ++ config/locales/en.yml | 5 ++ config/locales/pt-BR.yml | 5 ++ config/routes.rb | 2 + ...0160314174310_add_workspace_to_projects.rb | 9 ++ ...160314174311_add_workspace_to_workflows.rb | 9 ++ .../20160314174312_create_workspaces.rb | 18 ++++ lib/redmine.rb | 2 + public/stylesheets/application.css | 3 + 33 files changed, 437 insertions(+), 107 deletions(-) create mode 100644 app/controllers/workspaces_controller.rb create mode 100644 app/models/workspace.rb create mode 100644 app/views/workspaces/_form.html.erb create mode 100644 app/views/workspaces/edit.html.erb create mode 100644 app/views/workspaces/index.api.rsb create mode 100644 app/views/workspaces/index.html.erb create mode 100644 app/views/workspaces/new.html.erb create mode 100644 db/migrate/20160314174310_add_workspace_to_projects.rb create mode 100644 db/migrate/20160314174311_add_workspace_to_workflows.rb create mode 100644 db/migrate/20160314174312_create_workspaces.rb diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 885c3a169..0b85ba53c 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -38,6 +38,8 @@ class AdminController < ApplicationController @project_pages = Paginator.new @project_count, per_page_option, params['page'] @projects = scope.limit(@project_pages.per_page).offset(@project_pages.offset).to_a + @workspaces = Hash[Workspace.pluck(:id, :name)] + render :action => "projects", :layout => false if request.xhr? end diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index c166fa1a2..2b3661292 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -24,28 +24,29 @@ class WorkflowsController < ApplicationController def index @roles = Role.sorted.select(&:consider_workflow?) @trackers = Tracker.sorted - @workflow_counts = WorkflowTransition.group(:tracker_id, :role_id).count + @workspaces = Workspace.sorted + @workflow_counts = WorkflowTransition.group(:tracker_id, :role_id, :workspace_id).count end def edit find_trackers_roles_and_statuses_for_edit - if request.post? && @roles && @trackers && params[:transitions] + if request.post? && @roles && @trackers && @workspaces && params[:transitions] transitions = params[:transitions].deep_dup transitions.each do |old_status_id, transitions_by_new_status| transitions_by_new_status.each do |new_status_id, transition_by_rule| transition_by_rule.reject! {|rule, transition| transition == 'no_change'} end end - WorkflowTransition.replace_transitions(@trackers, @roles, transitions) + WorkflowTransition.replace_transitions(@trackers, @roles, transitions, @workspaces) flash[:notice] = l(:notice_successful_update) redirect_to_referer_or workflows_edit_path return end - if @trackers && @roles && @statuses.any? + if @trackers && @roles && @workspaces && @statuses.any? workflows = WorkflowTransition. - where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id)). + where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id), :workspace_id => @workspaces.map(&:id)). preload(:old_status, :new_status) @workflows = {} @workflows['always'] = workflows.select {|w| !w.author && !w.assignee} @@ -57,21 +58,21 @@ class WorkflowsController < ApplicationController def permissions find_trackers_roles_and_statuses_for_edit - if request.post? && @roles && @trackers && params[:permissions] + if request.post? && @roles && @trackers && @workspaces && params[:permissions] permissions = params[:permissions].deep_dup permissions.each { |field, rule_by_status_id| rule_by_status_id.reject! {|status_id, rule| rule == 'no_change'} } - WorkflowPermission.replace_permissions(@trackers, @roles, permissions) + WorkflowPermission.replace_permissions(@trackers, @roles, permissions, @workspaces) flash[:notice] = l(:notice_successful_update) redirect_to_referer_or workflows_permissions_path return end - if @roles && @trackers + if @roles && @trackers && @workspaces @fields = (Tracker::CORE_FIELDS_ALL - @trackers.map(&:disabled_core_fields).reduce(:&)).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]} @custom_fields = @trackers.map(&:custom_fields).flatten.uniq.sort - @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles) + @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles, @workspaces) @statuses.each {|status| @permissions[status.id] ||= {}} end end @@ -79,6 +80,7 @@ class WorkflowsController < ApplicationController def copy @roles = Role.sorted.select(&:consider_workflow?) @trackers = Tracker.sorted + @workspaces = Workspace.sorted if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any' @source_tracker = nil @@ -90,19 +92,26 @@ class WorkflowsController < ApplicationController else @source_role = Role.find_by_id(params[:source_role_id].to_i) end + if params[:source_workspace_id].blank? || params[:source_workspace_id] == 'any' + @source_workspace = nil + else + @source_workspace = Workspace.find_by_id(params[:source_workspace_id].to_i) + end @target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.where(:id => params[:target_tracker_ids]).to_a @target_roles = params[:target_role_ids].blank? ? nil : Role.where(:id => params[:target_role_ids]).to_a + @target_workspaces = params[:target_workspace_ids].blank? ? + nil : Workspace.where(:id => params[:target_workspace_ids]).to_a if request.post? - if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?) + if params[:source_tracker_id].blank? || params[:source_role_id].blank? || params[:source_workspace_id].blank? || (@source_tracker.nil? && @source_role.nil? && @source_workspace.nil?) flash.now[:error] = l(:error_workflow_copy_source) - elsif @target_trackers.blank? || @target_roles.blank? + elsif @target_trackers.blank? || @target_roles.blank? || @target_workspaces.blank? flash.now[:error] = l(:error_workflow_copy_target) else - WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles) + WorkflowRule.copy(@source_tracker, @source_role, @source_workspace, @target_trackers, @target_roles, @target_workspaces) flash[:notice] = l(:notice_successful_update) - redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role) + redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role, :source_workspace_id => @source_workspace) end end end @@ -112,6 +121,7 @@ class WorkflowsController < ApplicationController def find_trackers_roles_and_statuses_for_edit find_roles find_trackers + find_workspaces find_statuses end @@ -135,6 +145,16 @@ class WorkflowsController < ApplicationController @trackers = nil if @trackers.blank? end + def find_workspaces + ids = Array.wrap(params[:workspace_id]) + if ids == ['all'] + @workspaces = Workspace.sorted.to_a + elsif ids.present? + @workspaces = Workspace.where(:id => ids).to_a + end + @workspaces = nil if @workspaces.blank? + end + def find_statuses @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true) if @trackers && @used_statuses_only diff --git a/app/controllers/workspaces_controller.rb b/app/controllers/workspaces_controller.rb new file mode 100644 index 000000000..604f22326 --- /dev/null +++ b/app/controllers/workspaces_controller.rb @@ -0,0 +1,80 @@ +# Redmine - project management software +# Copyright (C) 2006-2017 Jean-Philippe Lang +# +# 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. + +class WorkspacesController < ApplicationController + layout 'admin' + self.main_menu = false + + before_action :require_admin, :except => :index + before_action :require_admin_or_api_request, :only => :index + accept_api_auth :index + + def index + @workspaces = Workspace.sorted.to_a + respond_to do |format| + format.html { render :layout => false if request.xhr? } + format.api + end + end + + def new + @workspace = Workspace.new + end + + def create + @workspace = Workspace.new + @workspace.safe_attributes = params[:workspace] + if @workspace.save + flash[:notice] = l(:notice_successful_create) + redirect_to workspaces_path + else + render :action => 'new' + end + end + + def edit + @workspace = Workspace.find(params[:id]) + end + + def update + @workspace = Workspace.find(params[:id]) + @workspace.safe_attributes = params[:workspace] + if @workspace.save + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_update) + redirect_to workspaces_path(:page => params[:page]) + } + format.js { head 200 } + end + else + respond_to do |format| + format.html { render :action => 'edit' } + format.js { head 422 } + end + end + end + + def destroy + unless Project.where(:workspace_id => params[:id]).any? || params[:id] == "1" + Workspace.find(params[:id]).destroy + else + flash[:error] = l(:error_unable_delete_workspace) + end + redirect_to workspaces_path + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 794546163..1dd7a84b7 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -95,6 +95,14 @@ module ProjectsHelper principals_options_for_select(assignable_users, project.default_assigned_to) end + def project_workspace_options(project) + grouped = Hash.new {|h,k| h[k] = []} + Workspace.all.sorted.each do |workspace| + grouped[workspace.name] = workspace.id + end + options_for_select(grouped, project.workspace_id) + end + def format_version_sharing(sharing) sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing) l("label_version_sharing_#{sharing}") diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb index 8ac75aa98..f1e5e1f40 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -21,7 +21,7 @@ module WorkflowsHelper def options_for_workflow_select(name, objects, selected, options={}) option_tags = ''.html_safe multiple = false - if selected + if selected if selected.size == objects.size selected = 'all' else @@ -51,9 +51,9 @@ module WorkflowsHelper options = [["", ""], [l(:label_readonly), "readonly"]] options << [l(:label_required), "required"] unless field_required?(field) html_options = {} - + if perm = permissions[status.id][name] - if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size + if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size * @workspaces.size options << [l(:label_no_change_option), "no_change"] selected = 'no_change' else @@ -76,12 +76,12 @@ module WorkflowsHelper def transition_tag(workflows, old_status, new_status, name) w = workflows.select {|w| w.old_status == old_status && w.new_status == new_status}.size - + tag_name = "transitions[#{ old_status.try(:id) || 0 }][#{new_status.id}][#{name}]" if old_status == new_status check_box_tag(tag_name, "1", true, {:disabled => true, :class => "old-status-#{old_status.try(:id) || 0} new-status-#{new_status.id}"}) - elsif w == 0 || w == @roles.size * @trackers.size + elsif w == 0 || w == @roles.size * @trackers.size * @workspaces.size hidden_field_tag(tag_name, "0", :id => nil) + check_box_tag(tag_name, "1", w != 0, :class => "old-status-#{old_status.try(:id) || 0} new-status-#{new_status.id}") diff --git a/app/models/issue.rb b/app/models/issue.rb index 7481d2508..de2023b2b 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -655,7 +655,7 @@ class Issue < ActiveRecord::Base return {} if roles.empty? result = {} - workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a + workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id), :workspace_id => project.workspace_id).to_a if workflow_permissions.any? workflow_rules = workflow_permissions.inject({}) do |h, wp| h[wp.field_name] ||= {} @@ -995,6 +995,7 @@ class Issue < ActiveRecord::Base initial_status, user.admin ? Role.all.to_a : user.roles_for_project(project), tracker, + project.workspace_id, author == user, assignee_transitions_allowed ) diff --git a/app/models/issue_status.rb b/app/models/issue_status.rb index 9f139f2fc..cb915c745 100644 --- a/app/models/issue_status.rb +++ b/app/models/issue_status.rb @@ -51,18 +51,18 @@ class IssueStatus < ActiveRecord::Base end # Returns an array of all statuses the given role can switch to - def new_statuses_allowed_to(roles, tracker, author=false, assignee=false) - self.class.new_statuses_allowed(self, roles, tracker, author, assignee) + def new_statuses_allowed_to(roles, tracker, workspace_id, author=false, assignee=false) + self.class.new_statuses_allowed(self, roles, tracker, workspace_id, author, assignee) end alias :find_new_statuses_allowed_to :new_statuses_allowed_to - def self.new_statuses_allowed(status, roles, tracker, author=false, assignee=false) - if roles.present? && tracker + def self.new_statuses_allowed(status, roles, tracker, workspace_id, author=false, assignee=false) + if roles.present? && tracker && workspace_id status_id = status.try(:id) || 0 scope = IssueStatus. joins(:workflow_transitions_as_new_status). - where(:workflows => {:old_status_id => status_id, :role_id => roles.map(&:id), :tracker_id => tracker.id}) + where(:workflows => {:old_status_id => status_id, :role_id => roles.map(&:id), :tracker_id => tracker.id, :workspace_id => workspace_id}) unless author && assignee if author || assignee diff --git a/app/models/project.rb b/app/models/project.rb index 6d91d570a..64f3451f2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -40,6 +40,7 @@ class Project < ActiveRecord::Base has_many :versions, :dependent => :destroy belongs_to :default_version, :class_name => 'Version' belongs_to :default_assigned_to, :class_name => 'Principal' + belongs_to :workspace has_many :time_entries, :dependent => :destroy has_many :queries, :dependent => :delete_all has_many :documents, :dependent => :destroy @@ -753,6 +754,7 @@ class Project < ActiveRecord::Base 'issue_custom_field_ids', 'parent_id', 'default_version_id', + 'workspace_id', 'default_assigned_to_id' safe_attributes 'enabled_module_names', diff --git a/app/models/role.rb b/app/models/role.rb index 36ef4bc8b..c5d0c1c6e 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -257,7 +257,7 @@ class Role < ActiveRecord::Base end def copy_workflow_rules(source_role) - WorkflowRule.copy(nil, source_role, nil, self) + WorkflowRule.copy(nil, source_role, nil, nil, self, nil) end # Find all the roles that can be given to a project member diff --git a/app/models/tracker.rb b/app/models/tracker.rb index d90ab3e15..98ea71816 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -115,7 +115,7 @@ class Tracker < ActiveRecord::Base end def copy_workflow_rules(source_tracker) - WorkflowRule.copy(source_tracker, nil, self, nil) + WorkflowRule.copy(source_tracker, nil, nil, self, nil, nil) end # Returns the fields that are disabled for all the given trackers diff --git a/app/models/workflow_permission.rb b/app/models/workflow_permission.rb index a231978b6..a4ae3ba07 100644 --- a/app/models/workflow_permission.rb +++ b/app/models/workflow_permission.rb @@ -20,14 +20,14 @@ class WorkflowPermission < WorkflowRule validates_presence_of :old_status validate :validate_field_name - # Returns the workflow permissions for the given trackers and roles + # Returns the workflow permissions for the given trackers, roles and workspaces # grouped by status_id # # Example: - # WorkflowPermission.rules_by_status_id trackers, roles + # WorkflowPermission.rules_by_status_id trackers, roles, workspaces # # => {1 => {'start_date' => 'required', 'due_date' => 'readonly'}} - def self.rules_by_status_id(trackers, roles) - WorkflowPermission.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id)).inject({}) do |h, w| + def self.rules_by_status_id(trackers, roles, workspaces) + WorkflowPermission.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :workspace_id => workspaces.map(&:id)).inject({}) do |h, w| h[w.old_status_id] ||= {} h[w.old_status_id][w.field_name] ||= [] h[w.old_status_id][w.field_name] << w.rule @@ -35,22 +35,25 @@ class WorkflowPermission < WorkflowRule end end - # Replaces the workflow permissions for the given trackers and roles + # Replaces the workflow permissions for the given trackers, roles and workspaces # # Example: - # WorkflowPermission.replace_permissions trackers, roles, {'1' => {'start_date' => 'required', 'due_date' => 'readonly'}} - def self.replace_permissions(trackers, roles, permissions) + # WorkflowPermission.replace_permissions trackers, roles, {'1' => {'start_date' => 'required', 'due_date' => 'readonly'}}, workspaces + def self.replace_permissions(trackers, roles, permissions, workspaces) trackers = Array.wrap trackers roles = Array.wrap roles + workspaces = Array.wrap workspaces transaction do permissions.each { |status_id, rule_by_field| rule_by_field.each { |field, rule| - where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :old_status_id => status_id, :field_name => field).destroy_all + where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :old_status_id => status_id, :field_name => field, :workspace_id => workspaces.map(&:id)).destroy_all if rule.present? trackers.each do |tracker| roles.each do |role| - WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule) + workspaces.each do |workspace| + WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule, :workspace_id => workspace.id) + end end end end diff --git a/app/models/workflow_rule.rb b/app/models/workflow_rule.rb index c4cbf1508..68c521f71 100644 --- a/app/models/workflow_rule.rb +++ b/app/models/workflow_rule.rb @@ -22,50 +22,59 @@ class WorkflowRule < ActiveRecord::Base belongs_to :tracker belongs_to :old_status, :class_name => 'IssueStatus' belongs_to :new_status, :class_name => 'IssueStatus' + belongs_to :workspace - validates_presence_of :role, :tracker + validates_presence_of :role, :tracker, :workspace # Copies workflows from source to targets - def self.copy(source_tracker, source_role, target_trackers, target_roles) - unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role) - raise ArgumentError.new("source_tracker or source_role must be specified, given: #{source_tracker.class.name} and #{source_role.class.name}") + def self.copy(source_tracker, source_role, source_workspace, target_trackers, target_roles, target_workspaces) + unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role) || source_workspace.is_a?(Workspace) + raise ArgumentError.new("source_tracker, source_role or source_workspace must be specified, given: #{source_tracker.class.name}, #{source_role.class.name} and #{source_workspace.class.name}") end target_trackers = [target_trackers].flatten.compact target_roles = [target_roles].flatten.compact + target_workspaces = [target_workspaces].flatten.compact target_trackers = Tracker.sorted.to_a if target_trackers.empty? target_roles = Role.all.select(&:consider_workflow?) if target_roles.empty? + target_workspaces = Workspace.sorted.to_a if target_workspaces.empty? target_trackers.each do |target_tracker| target_roles.each do |target_role| - copy_one(source_tracker || target_tracker, - source_role || target_role, - target_tracker, - target_role) + target_workspaces.each do |target_workspace| + copy_one(source_tracker || target_tracker, + source_role || target_role, + source_workspace || target_workspace, + target_tracker, + target_role, + target_workspace) + end end end end # Copies a single set of workflows from source to target - def self.copy_one(source_tracker, source_role, target_tracker, target_role) + def self.copy_one(source_tracker, source_role, source_workspace, target_tracker, target_role, target_workspace) unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? && source_role.is_a?(Role) && !source_role.new_record? && + source_workspace.is_a?(Workspace) && !source_workspace.new_record? && target_tracker.is_a?(Tracker) && !target_tracker.new_record? && - target_role.is_a?(Role) && !target_role.new_record? + target_role.is_a?(Role) && !target_role.new_record? && + target_workspace.is_a?(Workspace) && !target_workspace.new_record? raise ArgumentError.new("arguments can not be nil or unsaved objects") end - if source_tracker == target_tracker && source_role == target_role + if source_tracker == target_tracker && source_role == target_role && source_workspace == target_workspace false else transaction do - where(:tracker_id => target_tracker.id, :role_id => target_role.id).delete_all - connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type)" + - " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type" + + where(:tracker_id => target_tracker.id, :role_id => target_role.id, :workspace_id => target_workspace.id).delete_all + connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type, workspace_id)" + + " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type, #{target_workspace.id}" + " FROM #{WorkflowRule.table_name}" + - " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}" + " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id} AND workspace_id = #{source_workspace.id}" end true end diff --git a/app/models/workflow_transition.rb b/app/models/workflow_transition.rb index 4f161df1f..439322580 100644 --- a/app/models/workflow_transition.rb +++ b/app/models/workflow_transition.rb @@ -18,60 +18,64 @@ class WorkflowTransition < WorkflowRule validates_presence_of :new_status - def self.replace_transitions(trackers, roles, transitions) + def self.replace_transitions(trackers, roles, transitions, workspaces) trackers = Array.wrap trackers roles = Array.wrap roles + workspaces = Array.wrap workspaces transaction do - records = WorkflowTransition.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id)).to_a + records = WorkflowTransition.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :workspace_id => workspaces.map(&:id)).to_a transitions.each do |old_status_id, transitions_by_new_status| transitions_by_new_status.each do |new_status_id, transition_by_rule| transition_by_rule.each do |rule, transition| trackers.each do |tracker| roles.each do |role| - w = records.select {|r| - r.old_status_id == old_status_id.to_i && - r.new_status_id == new_status_id.to_i && - r.tracker_id == tracker.id && - r.role_id == role.id && - !r.destroyed? - } + workspaces.each do |workspace| + w = records.select {|r| + r.old_status_id == old_status_id.to_i && + r.new_status_id == new_status_id.to_i && + r.tracker_id == tracker.id && + r.role_id == role.id && + r.workspace_id == workspace.id && + !r.destroyed? + } - if rule == 'always' - w = w.select {|r| !r.author && !r.assignee} - else - w = w.select {|r| r.author || r.assignee} - end - if w.size > 1 - w[1..-1].each(&:destroy) - end - w = w.first - - if transition == "1" || transition == true - unless w - w = WorkflowTransition.new(:old_status_id => old_status_id, :new_status_id => new_status_id, :tracker_id => tracker.id, :role_id => role.id) - records << w - end - w.author = true if rule == "author" - w.assignee = true if rule == "assignee" - w.save if w.changed? - elsif w if rule == 'always' - w.destroy - elsif rule == 'author' - if w.assignee - w.author = false - w.save if w.changed? - else - w.destroy + w = w.select {|r| !r.author && !r.assignee} + else + w = w.select {|r| r.author || r.assignee} + end + if w.size > 1 + w[1..-1].each(&:destroy) + end + w = w.first + + if transition == "1" || transition == true + unless w + w = WorkflowTransition.new(:old_status_id => old_status_id, :new_status_id => new_status_id, :tracker_id => tracker.id, :role_id => role.id, :workspace_id => workspace.id) + records << w end - elsif rule == 'assignee' - if w.author - w.assignee = false - w.save if w.changed? - else + w.author = true if rule == "author" + w.assignee = true if rule == "assignee" + w.save if w.changed? + elsif w + if rule == 'always' w.destroy + elsif rule == 'author' + if w.assignee + w.author = false + w.save if w.changed? + else + w.destroy + end + elsif rule == 'assignee' + if w.author + w.assignee = false + w.save if w.changed? + else + w.destroy + end end end end diff --git a/app/models/workspace.rb b/app/models/workspace.rb new file mode 100644 index 000000000..fd9f90c2d --- /dev/null +++ b/app/models/workspace.rb @@ -0,0 +1,46 @@ +# Redmine - project management software +# Copyright (C) 2006-2017 Jean-Philippe Lang +# +# 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. + +class Workspace < ActiveRecord::Base + include Redmine::SafeAttributes + + before_destroy :check_integrity + has_many :projects + has_many :workflow_rules, :dependent => :delete_all + acts_as_positioned + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + + scope :sorted, lambda { order(:position) } + + safe_attributes 'name', + 'description', + 'position' + + def <=>(workspace) + position <=> workspace.position + end + + def to_s; name end + +private + def check_integrity + raise Exception.new("Cannot delete workspace") if Project.where(:workspace_id => self.id).any? + end +end diff --git a/app/views/admin/projects.html.erb b/app/views/admin/projects.html.erb index aa462741d..19b4d1844 100644 --- a/app/views/admin/projects.html.erb +++ b/app/views/admin/projects.html.erb @@ -23,6 +23,7 @@ <%=l(:label_project)%> <%=l(:field_is_public)%> <%=l(:field_created_on)%> + <%=l(:field_workspace)%> @@ -31,6 +32,7 @@ <%= link_to_project_settings(project, {}, :title => project.short_description) %> <%= checked_image project.is_public? %> <%= format_date(project.created_on) %> + <%= @workspaces[project.workspace_id] %> <%= link_to(l(:button_archive), archive_project_path(project, :status => params[:status]), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock') unless project.archived? %> <%= link_to(l(:button_unarchive), unarchive_project_path(project, :status => params[:status]), :method => :post, :class => 'icon icon-unlock') if project.archived? %> diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb index fc758f4e9..30b2a98aa 100644 --- a/app/views/projects/_form.html.erb +++ b/app/views/projects/_form.html.erb @@ -19,6 +19,10 @@

<%= label(:project, :parent_id, l(:field_parent)) %><%= parent_project_select_tag(@project) %>

<% end %> +<% if @project.safe_attribute? 'workspace_id' %> +

<%= f.select :workspace_id, project_workspace_options(@project) %>

+<% end %> + <% if @project.safe_attribute? 'inherit_members' %>

<%= f.check_box :inherit_members %>

<% end %> diff --git a/app/views/workflows/copy.html.erb b/app/views/workflows/copy.html.erb index 78997caf5..7bb4b73bb 100644 --- a/app/views/workflows/copy.html.erb +++ b/app/views/workflows/copy.html.erb @@ -3,6 +3,13 @@ <%= form_tag({}, :id => 'workflow_copy_form') do %>
<%= l(:label_copy_source) %> +

+ + <%= select_tag('source_role_id', + content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '') + + content_tag('option', "--- #{ l(:label_copy_same_as_target) } ---", :value => 'any') + + options_from_collection_for_select(@roles, 'id', 'name', @source_role && @source_role.id)) %> +

<%= select_tag('source_tracker_id', @@ -11,16 +18,22 @@ options_from_collection_for_select(@trackers, 'id', 'name', @source_tracker && @source_tracker.id)) %>

- - <%= select_tag('source_role_id', + + <%= select_tag('source_workspace_id', content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '') + content_tag('option', "--- #{ l(:label_copy_same_as_target) } ---", :value => 'any') + - options_from_collection_for_select(@roles, 'id', 'name', @source_role && @source_role.id)) %> + options_from_collection_for_select(@workspaces, 'id', 'name', @source_workspace && @source_workspace.id)) %>

<%= l(:label_copy_target) %> +

+ + <%= select_tag 'target_role_ids', + content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '', :disabled => true) + + options_from_collection_for_select(@roles, 'id', 'name', @target_roles && @target_roles.map(&:id)), :multiple => true %> +

<%= select_tag 'target_tracker_ids', @@ -28,10 +41,10 @@ options_from_collection_for_select(@trackers, 'id', 'name', @target_trackers && @target_trackers.map(&:id)), :multiple => true %>

- - <%= select_tag 'target_role_ids', + + <%= select_tag 'target_workspace_ids', content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '', :disabled => true) + - options_from_collection_for_select(@roles, 'id', 'name', @target_roles && @target_roles.map(&:id)), :multiple => true %> + options_from_collection_for_select(@workspaces, 'id', 'name', @target_workspaces && @target_workspaces.map(&:id)), :multiple => true %>

<%= submit_tag l(:button_copy) %> diff --git a/app/views/workflows/edit.html.erb b/app/views/workflows/edit.html.erb index ef8cd3496..0d58b69c9 100644 --- a/app/views/workflows/edit.html.erb +++ b/app/views/workflows/edit.html.erb @@ -4,8 +4,8 @@
@@ -23,6 +23,10 @@ + + <%= submit_tag l(:button_edit), :name => nil %> <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %> @@ -31,10 +35,11 @@

<% end %> -<% if @trackers && @roles && @statuses.any? %> +<% if @trackers && @roles && @workspaces && @statuses.any? %> <%= form_tag({}, :id => 'workflow_form' ) do %> <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %> <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %> + <%= @workspaces.map {|workspace| hidden_field_tag 'workspace_id[]', workspace.id, :id => nil}.join.html_safe %> <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
<%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %> diff --git a/app/views/workflows/index.html.erb b/app/views/workflows/index.html.erb index 02653b347..fdbce995f 100644 --- a/app/views/workflows/index.html.erb +++ b/app/views/workflows/index.html.erb @@ -3,11 +3,14 @@ <% if @roles.empty? || @trackers.empty? %>

<%= l(:label_no_data) %>

<% else %> +

- +
- + <% @roles.each do |role| %> <% @roles.each do |role| -%> - <% count = @workflow_counts[[tracker.id, role.id]] || 0 %> <% end -%> @@ -32,4 +46,42 @@
+ <% end %> diff --git a/app/views/workflows/permissions.html.erb b/app/views/workflows/permissions.html.erb index c62ea19dc..d7fc6aab3 100644 --- a/app/views/workflows/permissions.html.erb +++ b/app/views/workflows/permissions.html.erb @@ -4,8 +4,8 @@
    -
  • <%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers) %>
  • -
  • <%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %>
  • +
  • <%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces, :used_statuses_only => @used_statuses_only ? 1 : 0) %>
  • +
  • <%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces, :used_statuses_only => @used_statuses_only ? 1 : 0), :class => 'selected' %>
@@ -22,6 +22,11 @@ <%= options_for_workflow_select 'tracker_id[]', Tracker.sorted, @trackers, :id => 'tracker_id', :class => 'expandable' %> + + + <%= submit_tag l(:button_edit), :name => nil %> @@ -30,10 +35,11 @@

<% end %> -<% if @trackers && @roles && @statuses.any? %> +<% if @trackers && @roles && @workspaces && @statuses.any? %> <%= form_tag({}, :id => 'workflow_form' ) do %> <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %> <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %> + <%= @workspaces.map {|workspace| hidden_field_tag 'workspace_id[]', workspace.id, :id => nil}.join.html_safe %> <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
diff --git a/app/views/workspaces/_form.html.erb b/app/views/workspaces/_form.html.erb new file mode 100644 index 000000000..dc26bb025 --- /dev/null +++ b/app/views/workspaces/_form.html.erb @@ -0,0 +1,8 @@ +<%= error_messages_for 'workspace' %> + +
+

<%= f.text_field :name, :required => true %>

+

<%= f.text_field :description %>

+ +<%= call_hook(:view_workspaces_form, :workspace => @workspace) %> +
diff --git a/app/views/workspaces/edit.html.erb b/app/views/workspaces/edit.html.erb new file mode 100644 index 000000000..266291444 --- /dev/null +++ b/app/views/workspaces/edit.html.erb @@ -0,0 +1,6 @@ +<%= title [l(:label_workspace_plural), workspaces_path], @workspace.name %> + +<%= labelled_form_for @workspace do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/workspaces/index.api.rsb b/app/views/workspaces/index.api.rsb new file mode 100644 index 000000000..74c93897d --- /dev/null +++ b/app/views/workspaces/index.api.rsb @@ -0,0 +1,9 @@ +api.array :workspaces do + @workspaces.each do |workspace| + api.workspace do + api.id status.id + api.name status.name + api.description status.description + end + end +end diff --git a/app/views/workspaces/index.html.erb b/app/views/workspaces/index.html.erb new file mode 100644 index 000000000..d4d222275 --- /dev/null +++ b/app/views/workspaces/index.html.erb @@ -0,0 +1,31 @@ +
+<%= link_to l(:label_workspace_new), new_workspace_path, :class => 'icon icon-add' %> +
+ +

<%=l(:label_workspace_plural)%>

+ +
+ + + + + + +<% for workspace in @workspaces %> + "> + + + + +<% end %> + +
<%=l(:field_name)%><%=l(:field_description)%>
<%= link_to workspace.name, edit_workspace_path(workspace) %><%= link_to workspace.description, edit_workspace_path(workspace) %> + <%= reorder_handle(workspace) unless workspace.id == 1 %> + <%= delete_link workspace_path(workspace) unless workspace.id == 1 %> +
+ +<% html_title(l(:label_workspace_plural)) -%> + +<%= javascript_tag do %> + $(function() { $("table.workspaces tbody").positionedItems({items: ".givable"}); }); +<% end %> diff --git a/app/views/workspaces/new.html.erb b/app/views/workspaces/new.html.erb new file mode 100644 index 000000000..9c460c3a0 --- /dev/null +++ b/app/views/workspaces/new.html.erb @@ -0,0 +1,6 @@ +<%= title [l(:label_workspace_plural), workspaces_path], l(:label_workspace_new) %> + +<%= labelled_form_for @workspace do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 510968bd9..fe7f48abc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -588,6 +588,11 @@ en: label_tracker_plural: Trackers label_tracker_all: All trackers label_tracker_new: New tracker + label_workspace: Workspace + label_workspace_plural: Workspaces + label_workspace_new: New workspace + error_unable_delete_workspace: Unable to delete workspace + field_workspace: Workspace label_workflow: Workflow label_issue_status: Issue status label_issue_status_plural: Issue statuses diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 60e5c7fa6..091b200bc 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -376,6 +376,11 @@ pt-BR: label_tracker: Tipo de tarefa label_tracker_plural: Tipos de tarefas label_tracker_new: Novo tipo + label_workspace: Espaço de trabalho + label_workspace_plural: Espaços de trabalho + label_workspace_new: Novo espaço de trabalho + error_unable_delete_workspace: Não foi possível excluir espaço de trabalho + field_workspace: Espaço de trabalho label_workflow: Fluxo de trabalho label_issue_status: Situação da tarefa label_issue_status_plural: Situação das tarefas diff --git a/config/routes.rb b/config/routes.rb index 044df6a87..851dd38fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -341,6 +341,8 @@ Rails.application.routes.draw do end end + resources :workspaces, :except => :show + match 'workflows', :controller => 'workflows', :action => 'index', :via => :get match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post] match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post] diff --git a/db/migrate/20160314174310_add_workspace_to_projects.rb b/db/migrate/20160314174310_add_workspace_to_projects.rb new file mode 100644 index 000000000..c8c37c054 --- /dev/null +++ b/db/migrate/20160314174310_add_workspace_to_projects.rb @@ -0,0 +1,9 @@ +class AddWorkspaceToProjects < ActiveRecord::Migration[4.2] + def self.up + add_column :projects, :workspace_id, :integer, :default => 1 + end + + def self.down + remove_column :projects, :workspace_id + end +end diff --git a/db/migrate/20160314174311_add_workspace_to_workflows.rb b/db/migrate/20160314174311_add_workspace_to_workflows.rb new file mode 100644 index 000000000..964c3f057 --- /dev/null +++ b/db/migrate/20160314174311_add_workspace_to_workflows.rb @@ -0,0 +1,9 @@ +class AddWorkspaceToWorkflows < ActiveRecord::Migration[4.2] + def self.up + add_column :workflows, :workspace_id, :integer, :default => 1 + end + + def self.down + remove_column :workflows, :workspace_id + end +end diff --git a/db/migrate/20160314174312_create_workspaces.rb b/db/migrate/20160314174312_create_workspaces.rb new file mode 100644 index 000000000..602cb1cc2 --- /dev/null +++ b/db/migrate/20160314174312_create_workspaces.rb @@ -0,0 +1,18 @@ +class CreateWorkspaces < ActiveRecord::Migration[4.2] + def self.up + create_table :workspaces do |t| + t.string :name + t.string :description + t.integer :position, :default => nil, :null => true + end + + # create default workspace + unless Workspace.exists?(1) + Workspace.create(:name => "Default", :description => "Default workspace", :position => 1) + end + end + + def self.down + drop_table :workspaces + end +end diff --git a/lib/redmine.rb b/lib/redmine.rb index ca09db056..f03b11db3 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -234,6 +234,8 @@ Redmine::MenuManager.map :admin_menu do |menu| :html => {:class => 'icon icon-issue'} menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural, :html => {:class => 'icon icon-issue-edit'} + menu.push :workspaces, {:controller => 'workspaces'}, :caption => :label_workspace_plural, + :html => {:class => 'icon icon-multiple'} menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow, :html => {:class => 'icon icon-workflows'} menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural, diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index a8230749f..087904f5d 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -555,6 +555,9 @@ div#roadmap .wiki h1 { font-size: 120%; } div#roadmap .wiki h2 { font-size: 110%; } body.controller-versions.action-show div#roadmap .related-issues {width:70%;} +span.tip_exp_on {font-weight:bold; position:relative; color:#fff; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;} +span.tip_exp_off {font-weight:bold; position:relative; color:#fff; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;} + div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; } div#version-summary fieldset { margin-bottom: 1em; } div#version-summary fieldset.time-tracking table { width:100%; } -- 2.17.1