diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 944e60c..bd498cd 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -36,6 +36,8 @@ class AdminController < ApplicationController scope = scope.like(params[:name]) if params[:name].present? @projects = scope.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 7f92f25..511fb11 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -23,28 +23,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} @@ -56,21 +57,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 @@ -78,6 +79,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 @@ -89,19 +91,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 @@ -111,6 +120,7 @@ class WorkflowsController < ApplicationController def find_trackers_roles_and_statuses_for_edit find_roles find_trackers + find_workspaces find_statuses end @@ -134,6 +144,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 0000000..9161f9c --- /dev/null +++ b/app/controllers/workspaces_controller.rb @@ -0,0 +1,74 @@ +# Redmine - project management software +# Copyright (C) 2006-2015 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' + + before_filter :require_admin, :except => :index + before_filter :require_admin_or_api_request, :only => :index + accept_api_auth :index + + def index + respond_to do |format| + format.html { + @workspace_pages, @workspaces = paginate Workspace.sorted, :per_page => 25 + render :action => "index", :layout => false if request.xhr? + } + format.api { + @workspaces = Workspace.order('position').to_a + } + end + end + + def new + @workspace = Workspace.new + end + + def create + @workspace = Workspace.new(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]) + if @workspace.update_attributes(params[:workspace]) + flash[:notice] = l(:notice_successful_update) + redirect_to workspaces_path(:page => params[:page]) + else + render :action => 'edit' + end + end + + def destroy + unless Project.where(:workspace_id => params[:id]).any? || params[:id] == "1" + Workspace.find(params[:id]).destroy + redirect_to workspaces_path + else + flash[:error] = l(:error_unable_delete_workspace) + redirect_to workspaces_path + end + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 6a47b1e..b10fb66 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -95,6 +95,18 @@ module ProjectsHelper version_options_for_select(versions, project.default_version) 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 used_workspaces_by_tracker(tracker) + WorkflowTransition.where(:tracker_id => tracker).map{|t| "ws-" + t.workspace_id.to_s}.uniq.join(" ") + 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 eda4043..d35cadd 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -53,7 +53,7 @@ module WorkflowsHelper 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 @@ -78,7 +78,7 @@ module WorkflowsHelper 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 w == 0 || w == @roles.size * @trackers.size + if 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, diff --git a/app/models/issue.rb b/app/models/issue.rb index f569ed0..2e48dc1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -623,10 +623,11 @@ class Issue < ActiveRecord::Base user_real = user || User.current roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project) roles = roles.select(&:consider_workflow?) + workspace = Project.where(:id => project).pluck(:workspace_id) 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 => workspace).to_a if workflow_permissions.any? workflow_rules = workflow_permissions.inject({}) do |h, wp| h[wp.field_name] ||= {} @@ -925,6 +926,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 31c0f03..53b7bd6 100644 --- a/app/models/issue_status.rb +++ b/app/models/issue_status.rb @@ -45,18 +45,17 @@ 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 4241392..d4f5fb7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -39,6 +39,7 @@ class Project < ActiveRecord::Base has_many :issue_changes, :through => :issues, :source => :journals has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy belongs_to :default_version, :class_name => 'Version' + belongs_to :workspace has_many :time_entries, :dependent => :destroy has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all has_many :documents, :dependent => :destroy @@ -721,7 +722,8 @@ class Project < ActiveRecord::Base 'tracker_ids', 'issue_custom_field_ids', 'parent_id', - 'default_version_id' + 'default_version_id', + 'workspace_id' safe_attributes 'enabled_module_names', :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) } diff --git a/app/models/role.rb b/app/models/role.rb index acf71c5..24d491c 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -59,7 +59,7 @@ class Role < ActiveRecord::Base before_destroy :check_deletable has_many :workflow_rules, :dependent => :delete_all do def copy(source_role) - WorkflowRule.copy(nil, source_role, nil, proxy_association.owner) + WorkflowRule.copy(nil, source_role, nil, nil, proxy_association.owner, nil) end end has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id" diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 5e4a24b..2564bc9 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -28,7 +28,7 @@ class Tracker < ActiveRecord::Base has_many :issues has_many :workflow_rules, :dependent => :delete_all do def copy(source_tracker) - WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil) + WorkflowRule.copy(source_tracker, nil, nil, proxy_association.owner, nil, nil) end end diff --git a/app/models/workflow_permission.rb b/app/models/workflow_permission.rb index 1990b82..85ffc76 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| - destroy_all(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :old_status_id => status_id, :field_name => field) + destroy_all(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :old_status_id => status_id, :field_name => field, :workspace_id => workspaces.map(&:id)) 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 6fd4b05..dffd037 100644 --- a/app/models/workflow_rule.rb +++ b/app/models/workflow_rule.rb @@ -22,51 +22,60 @@ 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 attr_protected :id # 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") + 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") 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_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 - delete_all :tracker_id => target_tracker.id, :role_id => target_role.id - 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" + + delete_all :tracker_id => target_tracker.id, :role_id => target_role.id, :workspace_id => target_workspace.id + 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 ff75254..9b4be85 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 0000000..65c489e --- /dev/null +++ b/app/models/workspace.rb @@ -0,0 +1,63 @@ +# Redmine - project management software +# Copyright (C) 2006-2015 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 + + before_destroy :check_integrity + has_many :projects + has_many :workflow_rules, :dependent => :delete_all do + def copy(source_workflow) + WorkflowRule.copy(nil, nil, source_tracker, nil, nil, proxy_association.owner) + end + end + acts_as_list + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + attr_protected :id + + scope :sorted, lambda { order(:position) } + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} + + # Returns an array of IssueStatus that are used + # in the tracker's workflows + def issue_statuses + if @issue_statuses + return @issue_statuses + elsif new_record? + return [] + end + + ids = WorkflowTransition. + connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE workspace_id = #{id} AND type = 'WorkflowTransition'"). + flatten. + uniq + @issue_statuses = IssueStatus.where(:id => ids).all.sort + end + + def <=>(status) + position <=> status.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 0437f9e..c57d3e5 100644 --- a/app/views/admin/projects.html.erb +++ b/app/views/admin/projects.html.erb @@ -22,6 +22,7 @@ <%=l(:label_project)%> <%=l(:field_is_public)%> <%=l(:field_created_on)%> + <%=l(:field_workspace)%> @@ -30,6 +31,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? && (project.parent.nil? || !project.parent.archived?) %> diff --git a/app/views/members/_new_form.html.erb b/app/views/members/_new_form.html.erb index 3fe9187..02cf178 100644 --- a/app/views/members/_new_form.html.erb +++ b/app/views/members/_new_form.html.erb @@ -7,10 +7,10 @@
- <%= l(:label_role_plural) %> <%= toggle_checkboxes_link('.roles-selection input') %> + <%= l(:label_role_plural) %> (<%= l(:label_all) %>) <%= toggle_checkboxes_link('.roles-selection input') %>
<% User.current.managed_roles(@project).each do |role| %> - + role.id, :workspace_id => @project.workspace_id).any? || ! role.assignable %>><%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %> <%= role %> <% end %>
diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb index b75ce82..73d2c95 100644 --- a/app/views/projects/_form.html.erb +++ b/app/views/projects/_form.html.erb @@ -16,6 +16,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), {}, {:onChange=>"displaytrackers(this);"} %>

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

<%= f.check_box :inherit_members %>

<% end %> @@ -46,9 +50,9 @@ <% if @project.new_record? || @project.module_enabled?('issue_tracking') %> <% unless @trackers.empty? %> -
<%=l(:label_tracker_plural)%> +
<%=l(:label_tracker_plural)%> (<%= l(:label_all) %>) <% @trackers.each do |tracker| %> -
@@ -33,6 +40,12 @@ 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_workspace_ids', + content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '', :disabled => true) + + options_from_collection_for_select(@workspaces, 'id', 'name', @target_workspaces && @target_workspaces.map(&:id)), :multiple => true %> +

<%= submit_tag l(:button_copy) %> <% end %> diff --git a/app/views/workflows/edit.html.erb b/app/views/workflows/edit.html.erb index 3eac0a3..3e8ef7b 100644 --- a/app/views/workflows/edit.html.erb +++ b/app/views/workflows/edit.html.erb @@ -4,8 +4,8 @@
@@ -23,6 +23,11 @@ + + + <%= submit_tag l(:button_edit), :name => nil %> <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %> @@ -31,10 +36,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 1a07805..eff8831 100644 --- a/app/views/workflows/index.html.erb +++ b/app/views/workflows/index.html.erb @@ -3,6 +3,11 @@ <% if @roles.empty? || @trackers.empty? %>

<%= l(:label_no_data) %>

<% else %> + +

+
@@ -20,11 +25,22 @@ <% @roles.each do |role| -%> - <% count = @workflow_counts[[tracker.id, role.id]] || 0 %> <% end -%> @@ -32,4 +48,11 @@
<%= tracker.name %> - <%= link_to((count > 0 ? count : content_tag(:span, nil, :class => 'icon-only icon-not-ok')), - {:action => 'edit', :role_id => role, :tracker_id => tracker}, + <% countall = 0 %> + <% @workspaces.each do |workspace| %> + <% count = @workflow_counts[[tracker.id, role.id, workspace.id]] || 0 %> + <% countall += count %> + + <% end %> +
+ <% end %> diff --git a/app/views/workflows/permissions.html.erb b/app/views/workflows/permissions.html.erb index 0fb4c8b..3671863 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) %>
  • +
  • <%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces), :class => 'selected' %>
@@ -23,6 +23,11 @@ + + <%= image_tag 'bullet_toggle_plus.png' %> + <%= submit_tag l(:button_edit), :name => nil %> <%= hidden_field_tag 'used_statuses_only', '0', :id => 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 0000000..dc26bb0 --- /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 0000000..2662914 --- /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 0000000..74c9389 --- /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 0000000..66d0148 --- /dev/null +++ b/app/views/workspaces/index.html.erb @@ -0,0 +1,30 @@ +
+<%= link_to l(:label_workspace_new), new_workspace_path, :class => 'icon icon-add' %> +
+ +

<%=l(:label_workspace_plural)%>

+ +
+ + + + + + + +<% for status in @workspaces %> + "> + + + + + +<% end %> + +
<%=l(:field_name)%><%=l(:field_description)%><%=l(:button_sort)%>
<%= link_to status.name, edit_workspace_path(status) %><%= link_to status.description, edit_workspace_path(status) %><%= reorder_links('workspace', {:action => 'update', :id => status, :page => params[:page]}, :put) %> + <%= delete_link workspace_path(status) unless status.id == 1 %> +
+ +<%= pagination_links_full @workspace_pages %> + +<% html_title(l(:label_workspace_plural)) -%> diff --git a/app/views/workspaces/new.html.erb b/app/views/workspaces/new.html.erb new file mode 100644 index 0000000..9c460c3 --- /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 399c2e8..2e5b5a9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -566,6 +566,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 b7e0ab1..b1398cb 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -375,6 +375,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 06b5001..1cce05d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -310,6 +310,7 @@ Rails.application.routes.draw do match 'fields', :via => [:get, :post] end end + resources :workspaces, :except => :show resources :issue_statuses, :except => :show do collection do post 'update_issue_done_ratio' 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 0000000..3c7265b --- /dev/null +++ b/db/migrate/20160314174310_add_workspace_to_projects.rb @@ -0,0 +1,7 @@ +class AddWorkspaceToProjects < ActiveRecord::Migration + def self.up + unless ActiveRecord::Base.connection.column_exists?(:projects, :workspace_id) + add_column :projects, :workspace_id, :integer, :default => 1 + end + 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 0000000..44f0dc3 --- /dev/null +++ b/db/migrate/20160314174311_add_workspace_to_workflows.rb @@ -0,0 +1,7 @@ +class AddWorkspaceToWorkflows < ActiveRecord::Migration + def self.up + unless ActiveRecord::Base.connection.column_exists?(:workflows, :workspace_id) + add_column :workflows, :workspace_id, :integer, :default => 1 + end + end +end diff --git a/db/migrate/20160314174312_create_workspaces.rb b/db/migrate/20160314174312_create_workspaces.rb new file mode 100644 index 0000000..20faba6 --- /dev/null +++ b/db/migrate/20160314174312_create_workspaces.rb @@ -0,0 +1,19 @@ +class CreateWorkspaces < ActiveRecord::Migration + class Workspace < ActiveRecord::Base; end + + def self.up + unless ActiveRecord::Base.connection.table_exists?('workspaces') + create_table :workspaces do |t| + t.string :name + t.string :description + t.integer :position + end + + # create default workspace + workspace = Workspace.new :name => "Default", + :description => "Default workspace", + :position => "1" + workspace.save + end + end +end diff --git a/db/migrate/20161221111437_fix_workspaces_allow_null_position.rb b/db/migrate/20161221111437_fix_workspaces_allow_null_position.rb new file mode 100644 index 0000000..6832c68 --- /dev/null +++ b/db/migrate/20161221111437_fix_workspaces_allow_null_position.rb @@ -0,0 +1,10 @@ +class FixWorkspacesAllowNullPosition < ActiveRecord::Migration + def self.up + # removes the 'not null' constraint on position fields + change_column :workspaces, :position, :integer, :default => 1, :null => true + end + + def self.down + # nothing to do + end +end diff --git a/lib/redmine.rb b/lib/redmine.rb index 71722cc..1fb157a 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -214,6 +214,7 @@ Redmine::MenuManager.map :admin_menu do |menu| menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural, :html => {:class => 'issue_statuses'} + menu.push :workspaces, {:controller => 'workspaces'}, :caption => :label_workspace_plural menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural, :html => {:class => 'custom_fields'} diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 99aa29b..e68534f 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -85,6 +85,7 @@ pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;} #admin-menu a.roles { background-image: url(../images/database_key.png); } #admin-menu a.trackers { background-image: url(../images/ticket.png); } #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); } +#admin-menu a.workspaces { background-image: url(../images/table_multiple.png); } #admin-menu a.workflows { background-image: url(../images/ticket_go.png); } #admin-menu a.custom_fields { background-image: url(../images/textfield.png); } #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); } @@ -719,6 +720,9 @@ table.members td.group, table.members td.groupnonmember, table.members td.groupa input#principal_search, input#user_search {width:90%} .roles-selection label {display:inline-block; width:210px;} +.roles-selection .unused {display:none;} +.roles-selection .unused.show {display:inline-block;} +.unused {display:none;} input.autocomplete { background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;