Project

General

Profile

Patch #20384 » 0001-Implements-Workspaces-in-redmine-5.1.patch

Frederico Camara, 2024-01-25 19:17

View differences:

app/controllers/admin_controller.rb
44 44
    @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
45 45
    @projects = scope.limit(@entry_pages.per_page).offset(@entry_pages.offset).to_a
46 46

  
47
    @workspaces = Hash[Workspace.pluck(:id, :name)]
48

  
47 49
    render :action => "projects", :layout => false if request.xhr?
48 50
  end
49 51

  
app/controllers/workflows_controller.rb
25 25
  before_action :require_admin
26 26

  
27 27
  def index
28
    @roles = Role.sorted.select(&:consider_workflow?)
29
    @trackers = Tracker.sorted
30
    @workflow_counts = WorkflowTransition.group(:tracker_id, :role_id).count
28
    @workspaces = Workspace.sorted
29
    @workspace = Workspace.find_by_id(params[:workspace_id]) || Workspace.first
30
    @roles = Role.where(id: WorkflowTransition.where(workspace_id: @workspace.id).pluck(:role_id)).sorted.select(&:consider_workflow?)
31
    @trackers = Tracker.where(id: WorkflowTransition.where(workspace_id: @workspace.id).pluck(:tracker_id)).sorted
32
    @workflow_counts = WorkflowTransition.group(:tracker_id, :role_id, :workspace_id).count
31 33
  end
32 34

  
33 35
  def edit
34
    if @trackers && @roles && @statuses.any?
36
    if @trackers && @roles && @workspaces && @statuses.any?
35 37
      workflows = WorkflowTransition.
36
        where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id)).
38
        where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id), :workspace_id => @workspaces.map(&:id)).
37 39
        preload(:old_status, :new_status)
38 40
      @workflows = {}
39 41
      @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
......
43 45
  end
44 46

  
45 47
  def update
46
    if @roles && @trackers && params[:transitions]
48
    if @roles && @trackers && @workspaces && params[:transitions]
47 49
      transitions = params[:transitions].deep_dup
48 50
      transitions.each do |old_status_id, transitions_by_new_status|
49 51
        transitions_by_new_status.each do |new_status_id, transition_by_rule|
50 52
          transition_by_rule.reject! {|rule, transition| transition == 'no_change'}
51 53
        end
52 54
      end
53
      WorkflowTransition.replace_transitions(@trackers, @roles, transitions)
55
      WorkflowTransition.replace_transitions(@trackers, @roles, transitions, @workspaces)
54 56
      flash[:notice] = l(:notice_successful_update)
55 57
    end
56 58
    redirect_to_referer_or edit_workflows_path
57 59
  end
58 60

  
59 61
  def permissions
60
    if @roles && @trackers
62
    if @roles && @trackers && @workspaces
61 63
      @fields = (Tracker::CORE_FIELDS_ALL - @trackers.map(&:disabled_core_fields).reduce(:&)).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
62 64
      @custom_fields = @trackers.map(&:custom_fields).flatten.uniq.sort
63
      @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles)
65
      @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles, @workspaces)
64 66
      @statuses.each {|status| @permissions[status.id] ||= {}}
65 67
    end
66 68
  end
67 69

  
68 70
  def update_permissions
69
    if @roles && @trackers && params[:permissions]
71
    if @roles && @trackers && @workspaces && params[:permissions]
70 72
      permissions = params[:permissions].deep_dup
71 73
      permissions.each do |field, rule_by_status_id|
72 74
        rule_by_status_id.reject! {|status_id, rule| rule == 'no_change'}
73 75
      end
74
      WorkflowPermission.replace_permissions(@trackers, @roles, permissions)
76
      WorkflowPermission.replace_permissions(@trackers, @roles, permissions, @workspaces)
75 77
      flash[:notice] = l(:notice_successful_update)
76 78
    end
77 79
    redirect_to_referer_or permissions_workflows_path
......
83 85

  
84 86
  def duplicate
85 87
    find_sources_and_targets
86
    if params[:source_tracker_id].blank? || params[:source_role_id].blank? ||
87
      (@source_tracker.nil? && @source_role.nil?)
88
    if params[:source_tracker_id].blank? || params[:source_role_id].blank? || params[:source_workspace_id].blank? ||
89
      (@source_tracker.nil? && @source_role.nil? && @source_workspace.nil?)
88 90
      flash.now[:error] = l(:error_workflow_copy_source)
89 91
      render :copy
90
    elsif @target_trackers.blank? || @target_roles.blank?
92
    elsif @target_trackers.blank? || @target_roles.blank? || @target_workspaces.blank?
91 93
      flash.now[:error] = l(:error_workflow_copy_target)
92 94
      render :copy
93 95
    else
94
      WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
96
      WorkflowRule.copy(@source_tracker, @source_role, @source_workspace, @target_trackers, @target_roles, @target_workspaces)
95 97
      flash[:notice] = l(:notice_successful_update)
96 98
      redirect_to copy_workflows_path(
97 99
        :source_tracker_id => @source_tracker,
98
        :source_role_id => @source_role
100
        :source_role_id => @source_role,
101
        :source_workspace_id => @source_workspace
99 102
      )
100 103
    end
101 104
  end
......
105 108
  def find_sources_and_targets
106 109
    @roles = Role.sorted.select(&:consider_workflow?)
107 110
    @trackers = Tracker.sorted
111
    @workspaces = Workspace.sorted
108 112
    if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
109 113
      @source_tracker = nil
110 114
    else
......
115 119
    else
116 120
      @source_role = Role.find_by_id(params[:source_role_id].to_i)
117 121
    end
122
    if params[:source_workspace_id].blank? || params[:source_workspace_id] == 'any'
123
      @source_workspace = nil
124
    else
125
      @source_workspace = Workspace.find_by_id(params[:source_workspace_id].to_i)
126
    end
118 127
    @target_trackers =
119 128
      if params[:target_tracker_ids].blank?
120 129
        nil
......
127 136
      else
128 137
        Role.where(:id => params[:target_role_ids]).to_a
129 138
      end
139
    @target_workspaces =
140
      if params[:target_workspace_ids].blank?
141
        nil
142
      else
143
        Workspace.where(:id => params[:target_workspace_ids]).to_a
144
      end
130 145
  end
131 146

  
132 147
  def find_trackers_roles_and_statuses_for_edit
133 148
    find_roles
134 149
    find_trackers
150
    find_workspaces
135 151
    find_statuses
136 152
  end
137 153

  
......
155 171
    @trackers = nil if @trackers.blank?
156 172
  end
157 173

  
174
  def find_workspaces
175
    ids = Array.wrap(params[:workspace_id])
176
    if ids == ['all']
177
      @workspaces = Workspace.sorted.to_a
178
    elsif ids.present?
179
      @workspaces = Workspace.where(:id => ids).to_a
180
    end
181
    @workspaces = nil if @workspaces.blank?
182
  end
183

  
158 184
  def find_statuses
159 185
    @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
160
    if @trackers && @used_statuses_only
186
    @used_workspaces_only = (params[:used_workspaces_only] == '0' ? false : true)
187
    if @used_statuses_only || @used_workspaces_only
161 188
      role_ids = Role.all.select(&:consider_workflow?).map(&:id)
162
      status_ids = WorkflowTransition.where(
163
        :tracker_id => @trackers.map(&:id), :role_id => role_ids
164
      ).where(
165
        'old_status_id <> new_status_id'
166
      ).distinct.pluck(:old_status_id, :new_status_id).flatten.uniq
167
      @statuses = IssueStatus.where(:id => status_ids).sorted.to_a.presence
189
      status_ids = WorkflowRule.where(:role_id => role_ids).where('old_status_id <> new_status_id')
190
      status_ids = status_ids.where(:tracker_id => @trackers.map(&:id)) if @trackers && @used_statuses_only
191
      status_ids = status_ids.where(:workspace_id => @workspaces.map(&:id)) if @workspaces && @used_workspaces_only
192
      status_ids = status_ids.distinct.pluck(:old_status_id, :new_status_id).flatten.uniq
193
      @statuses = IssueStatus.where(:id => status_ids).sorted.to_a
194
    else
195
      @statuses = IssueStatus.sorted.to_a
168 196
    end
169
    @statuses ||= IssueStatus.sorted.to_a
170 197
  end
171 198
end
app/controllers/workspaces_controller.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2017  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
class WorkspacesController < ApplicationController
19
  layout 'admin'
20
  self.main_menu = false
21

  
22
  before_action :require_admin, :except => :index
23
  before_action :require_admin_or_api_request, :only => :index
24
  accept_api_auth :index
25

  
26
  def index
27
    @workspaces = Workspace.sorted.to_a
28
    respond_to do |format|
29
      format.html { render :layout => false if request.xhr? }
30
      format.api
31
    end
32
  end
33

  
34
  def new
35
    @workspace = Workspace.new
36
  end
37

  
38
  def create
39
    @workspace = Workspace.new
40
    @workspace.safe_attributes = params[:workspace]
41
    if @workspace.save
42
      flash[:notice] = l(:notice_successful_create)
43
      redirect_to workspaces_path
44
    else
45
      render :action => 'new'
46
    end
47
  end
48

  
49
  def edit
50
    @workspace = Workspace.find(params[:id])
51
  end
52

  
53
  def update
54
    @workspace = Workspace.find(params[:id])
55
    @workspace.safe_attributes = params[:workspace]
56
    if @workspace.save
57
      respond_to do |format|
58
        format.html {
59
          flash[:notice] = l(:notice_successful_update)
60
          redirect_to workspaces_path(:page => params[:page])
61
        }
62
        format.js { head 200 }
63
      end
64
    else
65
      respond_to do |format|
66
        format.html { render :action => 'edit' }
67
        format.js { head 422 }
68
      end
69
    end
70
  end
71

  
72
  def destroy
73
    unless Project.where(:workspace_id => params[:id]).any? || params[:id] == "1"
74
      Workspace.find(params[:id]).destroy
75
    else
76
      flash[:error] = l(:error_unable_delete_workspace)
77
    end
78
    redirect_to workspaces_path
79
  end
80
end
app/helpers/members_helper.rb
48 48
    s + content_tag('span', links, :class => 'pagination')
49 49
  end
50 50

  
51
  def role_color_and_hide(role, project = nil)
52
    color_and_hide = ""
53
    if ! role.assignable
54
      color_and_hide += " style=color:darkblue"
55
    elsif ! project.nil? && ! role.workflow_rules.where(:workspace_id => project.workspace_id).any?
56
      color_and_hide += " class=unused style=color:darkred"
57
    end
58
    color_and_hide
59
  end
60

  
51 61
  # Returns inheritance information for an inherited member role
52 62
  def render_role_inheritance(member, role)
53 63
    content = member.role_inheritance(role).filter_map do |h|
app/helpers/projects_helper.rb
114 114
    principals_options_for_select(assignable_users, project.default_assigned_to)
115 115
  end
116 116

  
117
  def project_workspace_options(project)
118
    grouped = Hash.new {|h,k| h[k] = []}
119
    Workspace.all.sorted.each do |workspace|
120
      grouped[workspace.name] = workspace.id
121
    end
122
    options_for_select(grouped, project.workspace_id)
123
  end
124

  
125
  def used_workspaces_by_tracker
126
    WorkflowRule.pluck(:tracker_id, :workspace_id).uniq.group_by(&:first).map{|k,a| [k,a.map{|x| "ws-" + x.last.to_s}.join(" ")]}.to_h
127
  end
128

  
117 129
  def project_default_issue_query_options(project)
118 130
    public_queries = IssueQuery.only_public
119 131
    grouped = {
app/helpers/workflows_helper.rb
53 53
    html_options = {}
54 54

  
55 55
    if perm = permissions[status.id][name]
56
      if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size
56
      if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size * @workspaces.size
57 57
        options << [l(:label_no_change_option), "no_change"]
58 58
        selected = 'no_change'
59 59
      else
......
80 80
    if old_status == new_status
81 81
      check_box_tag(tag_name, "1", true,
82 82
                    {:disabled => true, :class => "old-status-#{old_status.try(:id) || 0} new-status-#{new_status.id}"})
83
    elsif w == 0 || w == @roles.size * @trackers.size
83
    elsif w == 0 || w == @roles.size * @trackers.size * @workspaces.size
84 84
      hidden_field_tag(tag_name, "0", :id => nil) +
85 85
      check_box_tag(tag_name, "1", w != 0,
86 86
                    :class => "old-status-#{old_status.try(:id) || 0} new-status-#{new_status.id}")
app/models/issue.rb
693 693
    workflow_permissions =
694 694
      WorkflowPermission.where(
695 695
        :tracker_id => tracker_id, :old_status_id => status_id,
696
        :role_id => roles.map(&:id)
696
        :role_id => roles.map(&:id),
697
        :workspace_id => project&.workspace_id
697 698
      ).to_a
698 699
    if workflow_permissions.any?
699 700
      workflow_rules = workflow_permissions.inject({}) do |h, wp|
......
1075 1076
      initial_status,
1076 1077
      roles_for_workflow(user),
1077 1078
      tracker,
1079
      project.workspace_id,
1078 1080
      author == user,
1079 1081
      assignee_transitions_allowed
1080
    )
1082
    ) if project.present?
1081 1083
    statuses << initial_status unless statuses.empty?
1082 1084
    statuses << default_status if include_default || (new_record? && statuses.empty?)
1083 1085

  
app/models/issue_status.rb
56 56
  end
57 57

  
58 58
  # Returns an array of all statuses the given role can switch to
59
  def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
60
    self.class.new_statuses_allowed(self, roles, tracker, author, assignee)
59
  def new_statuses_allowed_to(roles, tracker, workspace_id, author=false, assignee=false)
60
    self.class.new_statuses_allowed(self, roles, tracker, workspace_id, author, assignee)
61 61
  end
62 62
  alias :find_new_statuses_allowed_to :new_statuses_allowed_to
63 63

  
64
  def self.new_statuses_allowed(status, roles, tracker, author=false, assignee=false)
65
    if roles.present? && tracker
64
  def self.new_statuses_allowed(status, roles, tracker, workspace_id, author=false, assignee=false)
65
    if roles.present? && tracker && workspace_id
66 66
      status_id = status.try(:id) || 0
67 67

  
68 68
      scope = IssueStatus.
69 69
        joins(:workflow_transitions_as_new_status).
70
        where(:workflows => {:old_status_id => status_id, :role_id => roles.map(&:id), :tracker_id => tracker.id})
70
        where(:workflows => {:old_status_id => status_id, :role_id => roles.map(&:id), :tracker_id => tracker.id, :workspace_id => workspace_id})
71 71

  
72 72
      unless author && assignee
73 73
        if author || assignee
app/models/project.rb
41 41
  has_many :versions, :dependent => :destroy
42 42
  belongs_to :default_version, :class_name => 'Version'
43 43
  belongs_to :default_assigned_to, :class_name => 'Principal'
44
  belongs_to :workspace
44 45
  has_many :time_entries, :dependent => :destroy
45 46
  # Specific overridden Activities
46 47
  has_many :time_entry_activities, :dependent => :destroy
......
367 368
    @rolled_up_versions = nil
368 369
    @rolled_up_trackers = nil
369 370
    @rolled_up_statuses = nil
371
    @rolled_up_workspaces = nil
370 372
    @rolled_up_custom_fields = nil
371 373
    @all_issue_custom_fields = nil
372 374
    @all_time_entry_custom_fields = nil
......
489 491
      sorted
490 492
  end
491 493

  
494
  def rolled_up_workspaces(include_subprojects=true)
495
    if include_subprojects
496
      @rolled_up_workspaces ||= rolled_up_workspaces_base_scope.
497
      where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
498
    else
499
      rolled_up_workspaces_base_scope.
500
      where(:projects => {:id => id})
501
    end
502
  end
503

  
504
  def rolled_up_workspaces_base_scope
505
    Workspace.
506
    joins(projects: :enabled_modules).
507
    where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
508
    where(:enabled_modules => {:name => 'issue_tracking'}).
509
    distinct.
510
    sorted
511
  end
512

  
492 513
  def rolled_up_statuses
493 514
    issue_status_ids = WorkflowTransition.
494 515
      where(:tracker_id => rolled_up_trackers.map(&:id)).
516
      where(:workspace_id => rolled_up_workspaces.map(&:id)).
495 517
      distinct.
496 518
      pluck(:old_status_id, :new_status_id).
497 519
      flatten.
......
835 857
    'parent_id',
836 858
    'default_version_id',
837 859
    'default_issue_query_id',
860
    'workspace_id',
838 861
    'default_assigned_to_id')
839 862

  
840 863
  safe_attributes(
app/models/project_query.rb
36 36
    QueryColumn.new(:identifier, :sortable => "#{Project.table_name}.identifier"),
37 37
    QueryColumn.new(:parent_id, :sortable => "#{Project.table_name}.lft ASC", :default_order => 'desc', :caption => :field_parent),
38 38
    QueryColumn.new(:is_public, :sortable => "#{Project.table_name}.is_public", :groupable => true),
39
    QueryColumn.new(:created_on, :sortable => "#{Project.table_name}.created_on", :default_order => 'desc')
39
    QueryColumn.new(:created_on, :sortable => "#{Project.table_name}.created_on", :default_order => 'desc'),
40
    QueryColumn.new(:workspace, :sortable => "#{Project.table_name}.workspace")
40 41
  ]
41 42

  
42 43
  def self.default(project: nil, user: User.current)
......
77 78
      :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
78 79
    )
79 80
    add_available_filter "created_on", :type => :date_past
81
    add_available_filter "workspace_id",
82
      :type => :list,
83
      :values => Workspace.where.not(id: 1).order(:name).map {|w| [w.name, w.id.to_s]}.
84
        unshift([Workspace.find(1).name, Workspace.find(1).id.to_s])
80 85
    add_custom_fields_filters(project_custom_fields)
81 86
  end
82 87

  
app/models/role.rb
276 276
  end
277 277

  
278 278
  def copy_workflow_rules(source_role)
279
    WorkflowRule.copy(nil, source_role, nil, self)
279
    WorkflowRule.copy(nil, source_role, nil, nil, self, nil)
280 280
  end
281 281

  
282 282
  # Find all the roles that can be given to a project member
app/models/tracker.rb
143 143
  end
144 144

  
145 145
  def copy_workflow_rules(source_tracker)
146
    WorkflowRule.copy(source_tracker, nil, self, nil)
146
    WorkflowRule.copy(source_tracker, nil, nil, self, nil, nil)
147 147
  end
148 148

  
149 149
  # Returns the fields that are disabled for all the given trackers
app/models/workflow_permission.rb
22 22
  validates_presence_of :old_status
23 23
  validate :validate_field_name
24 24

  
25
  # Returns the workflow permissions for the given trackers and roles
25
  # Returns the workflow permissions for the given trackers, roles and workspaces
26 26
  # grouped by status_id
27 27
  #
28 28
  # Example:
29
  #   WorkflowPermission.rules_by_status_id trackers, roles
29
  #   WorkflowPermission.rules_by_status_id trackers, roles, workspaces
30 30
  #   # => {1 => {'start_date' => 'required', 'due_date' => 'readonly'}}
31
  def self.rules_by_status_id(trackers, roles)
32
    WorkflowPermission.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id)).inject({}) do |h, w|
31
  def self.rules_by_status_id(trackers, roles, workspaces)
32
    WorkflowPermission.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :workspace_id => workspaces.map(&:id)).inject({}) do |h, w|
33 33
      h[w.old_status_id] ||= {}
34 34
      h[w.old_status_id][w.field_name] ||= []
35 35
      h[w.old_status_id][w.field_name] << w.rule
......
37 37
    end
38 38
  end
39 39

  
40
  # Replaces the workflow permissions for the given trackers and roles
40
  # Replaces the workflow permissions for the given trackers, roles and workspaces
41 41
  #
42 42
  # Example:
43
  #   WorkflowPermission.replace_permissions trackers, roles, {'1' => {'start_date' => 'required', 'due_date' => 'readonly'}}
44
  def self.replace_permissions(trackers, roles, permissions)
43
  #   WorkflowPermission.replace_permissions trackers, roles, {'1' => {'start_date' => 'required', 'due_date' => 'readonly'}}, workspaces
44
  def self.replace_permissions(trackers, roles, permissions, workspaces)
45 45
    trackers = Array.wrap trackers
46 46
    roles = Array.wrap roles
47
    workspaces = Array.wrap workspaces
47 48

  
48 49
    transaction do
49 50
      permissions.each do |status_id, rule_by_field|
50 51
        rule_by_field.each do |field, rule|
51
          where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :old_status_id => status_id, :field_name => field).destroy_all
52
          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
52 53
          if rule.present?
53 54
            trackers.each do |tracker|
54 55
              roles.each do |role|
55
                WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule)
56
                workspaces.each do |workspace|
57
                  WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule, :workspace_id => workspace.id)
58
                end
56 59
              end
57 60
            end
58 61
          end
app/models/workflow_rule.rb
24 24
  belongs_to :tracker
25 25
  belongs_to :old_status, :class_name => 'IssueStatus'
26 26
  belongs_to :new_status, :class_name => 'IssueStatus'
27
  belongs_to :workspace
27 28

  
28
  validates_presence_of :role, :tracker
29
  validates_presence_of :role, :tracker, :workspace
29 30

  
30 31
  # Copies workflows from source to targets
31
  def self.copy(source_tracker, source_role, target_trackers, target_roles)
32
    unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role)
32
  def self.copy(source_tracker, source_role, source_workspace, target_trackers, target_roles, target_workspaces)
33
    unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role) || source_workspace.is_a?(Workspace)
33 34
      raise ArgumentError.new(
34
        "source_tracker or source_role must be specified, given: " \
35
          "#{source_tracker.class.name} and #{source_role.class.name}"
35
        "source_tracker, source_role or source_workspace must be specified, given: " \
36
          "#{source_tracker.class.name}, #{source_role.class.name} and #{source_workspace.class.name}"
36 37
      )
37 38
    end
38 39

  
39 40
    target_trackers = [target_trackers].flatten.compact
40 41
    target_roles = [target_roles].flatten.compact
42
    target_workspaces = [target_workspaces].flatten.compact
41 43

  
42 44
    target_trackers = Tracker.sorted.to_a if target_trackers.empty?
43 45
    target_roles = Role.all.select(&:consider_workflow?) if target_roles.empty?
46
    target_workspaces = Workspace.sorted.to_a if target_workspaces.empty?
44 47

  
45 48
    target_trackers.each do |target_tracker|
46 49
      target_roles.each do |target_role|
47
        copy_one(source_tracker || target_tracker,
48
                 source_role || target_role,
49
                 target_tracker,
50
                 target_role)
50
        target_workspaces.each do |target_workspace|
51
          copy_one(source_tracker || target_tracker,
52
                     source_role || target_role,
53
                     source_workspace || target_workspace,
54
                     target_tracker,
55
                     target_role,
56
                     target_workspace)
57
        end
51 58
      end
52 59
    end
53 60
  end
54 61

  
55 62
  # Copies a single set of workflows from source to target
56
  def self.copy_one(source_tracker, source_role, target_tracker, target_role)
63
  def self.copy_one(source_tracker, source_role, source_workspace, target_tracker, target_role, target_workspace)
57 64
    unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? &&
58 65
      source_role.is_a?(Role) && !source_role.new_record? &&
66
      source_workspace.is_a?(Workspace) && !source_workspace.new_record? &&
59 67
      target_tracker.is_a?(Tracker) && !target_tracker.new_record? &&
60
      target_role.is_a?(Role) && !target_role.new_record?
68
      target_role.is_a?(Role) && !target_role.new_record? &&
69
      target_workspace.is_a?(Workspace) && !target_workspace.new_record?
61 70

  
62 71
      raise ArgumentError.new("arguments can not be nil or unsaved objects")
63 72
    end
64 73

  
65
    if source_tracker == target_tracker && source_role == target_role
74
    if source_tracker == target_tracker && source_role == target_role && source_workspace == target_workspace
66 75
      false
67 76
    else
68 77
      transaction do
69
        where(:tracker_id => target_tracker.id, :role_id => target_role.id).delete_all
78
        where(:tracker_id => target_tracker.id, :role_id => target_role.id, :workspace_id => target_workspace.id).delete_all
70 79
        connection.insert(
71 80
          "INSERT INTO #{WorkflowRule.table_name}" \
72 81
            " (tracker_id, role_id, old_status_id, new_status_id," \
73
             " author, assignee, field_name, #{connection.quote_column_name 'rule'}, type)" \
82
             " author, assignee, field_name, #{connection.quote_column_name 'rule'}, type, workspace_id)" \
74 83
            " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id," \
75
                    " author, assignee, field_name, #{connection.quote_column_name 'rule'}, type" \
84
                    " author, assignee, field_name, #{connection.quote_column_name 'rule'}, type, #{target_workspace.id}" \
76 85
              " FROM #{WorkflowRule.table_name}" \
77
              " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}"
86
              " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id} AND workspace_id = #{source_workspace.id}"
78 87
        )
79 88
      end
80 89
      true
app/models/workflow_transition.rb
20 20
class WorkflowTransition < WorkflowRule
21 21
  validates_presence_of :new_status
22 22

  
23
  def self.replace_transitions(trackers, roles, transitions)
23
  def self.replace_transitions(trackers, roles, transitions, workspaces)
24 24
    trackers = Array.wrap trackers
25 25
    roles = Array.wrap roles
26
    workspaces = Array.wrap workspaces
26 27

  
27 28
    transaction do
28
      records = WorkflowTransition.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id)).to_a
29
      records = WorkflowTransition.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :workspace_id => workspaces.map(&:id)).to_a
29 30

  
30 31
      transitions.each do |old_status_id, transitions_by_new_status|
31 32
        transitions_by_new_status.each do |new_status_id, transition_by_rule|
32 33
          transition_by_rule.each do |rule, transition|
33 34
            trackers.each do |tracker|
34 35
              roles.each do |role|
35
                w = records.select do |r|
36
                  r.old_status_id == old_status_id.to_i &&
37
                  r.new_status_id == new_status_id.to_i &&
38
                  r.tracker_id == tracker.id &&
39
                  r.role_id == role.id &&
40
                  !r.destroyed?
41
                end
42
                if rule == 'always'
43
                  w = w.select {|r| !r.author && !r.assignee}
44
                else
45
                  w = w.select {|r| r.author || r.assignee}
46
                end
47
                if w.size > 1
48
                  w[1..-1].each(&:destroy)
49
                end
50
                w = w.first
51

  
52
                if ["1", true].include?(transition)
53
                  unless w
54
                    w = WorkflowTransition.
55
                          new(
56
                            :old_status_id => old_status_id,
57
                            :new_status_id => new_status_id,
58
                            :tracker_id => tracker.id,
59
                            :role_id => role.id
60
                          )
61
                    records << w
36
                workspaces.each do |workspace|
37
                  w = records.select do |r|
38
                    r.old_status_id == old_status_id.to_i &&
39
                    r.new_status_id == new_status_id.to_i &&
40
                    r.tracker_id == tracker.id &&
41
                    r.role_id == role.id &&
42
                    r.workspace_id == workspace.id &&
43
                    !r.destroyed?
62 44
                  end
63
                  w.author = true if rule == "author"
64
                  w.assignee = true if rule == "assignee"
65
                  w.save if w.changed?
66
                elsif w
67 45
                  if rule == 'always'
68
                    w.destroy
69
                  elsif rule == 'author'
70
                    if w.assignee
71
                      w.author = false
72
                      w.save if w.changed?
73
                    else
74
                      w.destroy
46
                    w = w.select {|r| !r.author && !r.assignee}
47
                  else
48
                    w = w.select {|r| r.author || r.assignee}
49
                  end
50
                  if w.size > 1
51
                    w[1..-1].each(&:destroy)
52
                  end
53
                  w = w.first
54

  
55
                  if ["1", true].include?(transition)
56
                    unless w
57
                      w = WorkflowTransition.
58
                            new(
59
                              :old_status_id => old_status_id,
60
                              :new_status_id => new_status_id,
61
                              :tracker_id => tracker.id,
62
                              :role_id => role.id,
63
                              :workspace_id => workspace.id
64
                            )
65
                      records << w
75 66
                    end
76
                  elsif rule == 'assignee'
77
                    if w.author
78
                      w.assignee = false
79
                      w.save if w.changed?
80
                    else
67
                    w.author = true if rule == "author"
68
                    w.assignee = true if rule == "assignee"
69
                    w.save if w.changed?
70
                  elsif w
71
                    if rule == 'always'
81 72
                      w.destroy
73
                    elsif rule == 'author'
74
                      if w.assignee
75
                        w.author = false
76
                        w.save if w.changed?
77
                      else
78
                        w.destroy
79
                      end
80
                    elsif rule == 'assignee'
81
                      if w.author
82
                        w.assignee = false
83
                        w.save if w.changed?
84
                      else
85
                        w.destroy
86
                      end
82 87
                    end
83 88
                  end
84 89
                end
app/models/workspace.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2017  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
class Workspace < ActiveRecord::Base
19
  include Redmine::SafeAttributes
20

  
21
  before_destroy :check_integrity
22
  has_many :projects
23
  has_many :workflow_rules, :dependent => :delete_all
24
  acts_as_positioned
25

  
26
  validates_presence_of :name
27
  validates_uniqueness_of :name
28
  validates_length_of :name, :maximum => 30
29

  
30
  scope :sorted, lambda { order(:position) }
31

  
32
  safe_attributes 'name',
33
    'description',
34
    'position'
35

  
36
  # Returns an array of IssueStatus that are used in the tracker's workflows
37
  def issue_statuses
38
    if @issue_statuses
39
      return @issue_statuses
40
    elsif new_record?
41
      return []
42
    end
43

  
44
    ids = WorkflowTransition.where(workspace_id: id).map{|w| [w.old_status_id, w.new_status_id]}.flatten.uniq
45
    @issue_statuses = IssueStatus.where(:id => ids).all.sort
46
  end
47

  
48
  def <=>(workspace)
49
    position <=> workspace.position
50
  end
51

  
52
  def to_s; name end
53

  
54
private
55
  def check_integrity
56
    raise Exception.new("Cannot delete workspace") if Project.where(:workspace_id => self.id).any?
57
  end
58
end
app/views/members/_edit.html.erb
3 3
                      :remote => request.xhr?,
4 4
                      :method => :put) do |f| %>
5 5
  <p>
6
    <% @roles.each do |role| %>
7
    <label class="block">
6
    <strong>
7
      <%= l(:label_role_plural) %>
8
      (<a onclick="$(this).closest('form').children('div.unused').toggle();"><%= l(:label_all) %></a>):
9
    </strong>
10
  </p>
11
  <% @roles.each do |role| %>
12
    <div<%= role_color_and_hide(role, @project) %>>
8 13
      <%= check_box_tag('membership[role_ids][]',
9 14
                        role.id, @member.roles.to_a.include?(role),
10 15
                        :id => nil,
11 16
                        :disabled => !@member.role_editable?(role)) %> <%= role %>
12 17
      <%= render_role_inheritance(@member, role) %>
13
    </label>
14
    <% end %>
15
  </p>
18
    </div>
19
  <% end %>
16 20
  <%= hidden_field_tag 'membership[role_ids][]', '', :id => nil %>
17 21
  <p>
18 22
    <%= submit_tag l(:button_save) %>
app/views/members/_new_form.html.erb
7 7
  </div>
8 8
</fieldset>
9 9
<fieldset class="box">
10
  <legend><%= toggle_checkboxes_link('.roles-selection input') %><%= l(:label_role_plural) %></legend>
10
  <legend><%= toggle_checkboxes_link('.roles-selection input:visible') %><%= l(:label_role_plural) %> (<a onclick='$(".unused").toggleClass("show");'><%= l(:label_all) %></a>)</legend>
11 11
  <div class="roles-selection">
12 12
    <% User.current.managed_roles(@project).each do |role| %>
13
      <label><%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %> <%= role %></label>
13
      <label<%= role_color_and_hide(role, @project) %>><%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %><%= role %></label>
14 14
    <% end %>
15 15
  </div>
16 16
</fieldset>
app/views/principal_memberships/_edit.html.erb
3 3
                          :remote => request.xhr?,
4 4
                          :method => :put) do %>
5 5
  <p>
6
    <strong>
7
      <%= l(:label_role_plural) %>
8
      (<a onclick="$(this).closest('form').children('div.unused').toggle();"><%= l(:label_all) %></a>):
9
    </strong>
10
  </p>
6 11
  <% @roles.each do |role| %>
7
    <label class="block">
12
    <div<%= role_color_and_hide(role, @membership.project) %>>
8 13
      <%= check_box_tag 'membership[role_ids][]',
9 14
            role.id, @membership.roles.to_a.include?(role),
10 15
            :id => nil,
11 16
            :disabled => !@membership.role_editable?(role) %> <%= role.name %>
12 17
      <%= render_role_inheritance(@membership, role) %>
13
    </label>
18
    </div>
14 19
  <% end %>
15
  </p>
16 20
  <%= hidden_field_tag 'membership[role_ids][]', '', :id => nil %>
17 21
  <p>
18 22
    <%= submit_tag l(:button_save) %>
app/views/principal_memberships/_new_form.html.erb
15 15
  <legend><%= toggle_checkboxes_link('.roles-selection input') %><%= l(:label_role_plural) %></legend>
16 16
  <div class="roles-selection">
17 17
  <% @roles.each do |role| %>
18
    <label>
18
    <label<%= role_color_and_hide(role) %>>
19 19
      <%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %>
20 20
      <%= role %>
21 21
    </label>
app/views/projects/_form.html.erb
19 19
    <p><%= label(:project, :parent_id, l(:field_parent)) %><%= parent_project_select_tag(@project) %></p>
20 20
<% end %>
21 21

  
22
<% if @project.safe_attribute? 'workspace_id' %>
23
<p><%= f.select :workspace_id, project_workspace_options(@project), {}, :onchange => "$('#project_trackers > label').hide(); $('#project_trackers > label.ws-' + this.value).show();" %></p>
24
<% end %>
25

  
22 26
<% if @project.safe_attribute? 'inherit_members' %>
23 27
<p><%= f.check_box :inherit_members %></p>
24 28
<% end %>
app/views/projects/index.api.rsb
8 8
      api.homepage    project.homepage
9 9
      api.parent(:id => project.parent.id, :name => project.parent.name) if project.parent && project.parent.visible?
10 10
      api.status      project.status
11
      api.workspace   project.workspace.id
11 12
      api.is_public   project.is_public?
12 13
      api.inherit_members project.inherit_members?
13 14

  
app/views/projects/settings/_issues.html.erb
2 2
  <%= hidden_field_tag 'tab', 'issues' %>
3 3

  
4 4
  <% unless @trackers.empty? %>
5
  <fieldset class="box tabular" id="project_trackers"><legend><%= toggle_checkboxes_link('#project_trackers input[type=checkbox]') %><%= l(:label_tracker_plural)%></legend>
5
  <fieldset class="box tabular" id="project_trackers"><legend><%= toggle_checkboxes_link('#project_trackers input[type=checkbox]:enabled:visible') %><%= l(:label_tracker_plural)%> (<a onclick="ws='.ws-' + $('#project_workspace_id').val(); $('#project_trackers > label' + ws).show(); $('#project_trackers > label:not(' + ws + ')').toggle();"><%= l(:label_all) %></a>)</legend>
6 6
  <% if User.current.admin? %>
7 7
    <div class="contextual"><%= link_to l(:label_administration), trackers_path, :class => "icon icon-settings" %></div>
8 8
  <% end %>
9
  <% uwbt=used_workspaces_by_tracker %>
9 10
  <% @trackers.each do |tracker| %>
10
    <label class="floating">
11
    <label class="floating <%= uwbt[tracker.id].to_s %>"<% unless uwbt[tracker.id].to_s.match(/\b#{@project.workspace_id}\b/) %>style="display: none;"<% end %>>
11 12
      <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.to_a.include?(tracker), :id => nil %>
12 13
      <%= tracker_name_tag tracker %>
13 14
    </label>
app/views/projects/show.api.rsb
5 5
  api.description @project.description
6 6
  api.homepage    @project.homepage
7 7
  api.parent(:id => @project.parent.id, :name => @project.parent.name) if @project.parent && @project.parent.visible?
8
  api.workspace(:id => @project.workspace.id, :name => @project.workspace.name) if User.current.admin?
8 9
  api.status      @project.status
9 10
  api.is_public   @project.is_public?
10 11
  api.inherit_members @project.inherit_members?
app/views/projects/show.html.erb
42 42
  <% render_custom_field_values(@project) do |custom_field, formatted| %>
43 43
    <li class="<%= custom_field.css_classes %>"><span class="label"><%= custom_field.name %>:</span> <%= formatted %></li>
44 44
  <% end %>
45
  <% if User.current.admin? %>
46
    <li><span class="label"><%=l(:field_workspace)%>:</span> <%= @project.workspace.name %></li>
47
  <% end %>
45 48
  </ul>
46 49
  <% end %>
47 50

  
app/views/workflows/_index_table.html.erb
1
<div class="autoscroll">
2
  <table class="list">
3
    <thead>
4
      <tr>
5
        <th>
6
        </th>
7
        <% @roles.each do |role| %>
8
          <% next unless @workflow_counts.select{|i,v| i[1]==role.id && i[2]==@workspace.id}.present? %>
9
          <th>
10
            <%= content_tag(role.builtin? ? 'em' : 'span', role.name) %>
11
          </th>
12
        <% end %>
13
      </tr>
14
    </thead>
15
    <tbody>
16
      <% @trackers.each do |tracker| -%>
17
      <% next unless @workflow_counts.select{|i,v| i[0]==tracker.id && i[2]==@workspace.id}.present? %>
18
        <tr>
19
          <td class="name"><%= tracker.name %></td>
20
          <% @roles.each do |role| -%>
21
          <% next unless @workflow_counts.select{|i,v| i[1]==role.id && i[2]==@workspace.id}.present? %>
22
            <td>
23
              <% count = @workflow_counts[[tracker.id, role.id, @workspace.id]] || 0 %>
24
              <%= link_to((count > 0 ? count : content_tag(:span, nil, :class => 'icon-only icon-not-ok')),
25
                {:action => 'edit', :role_id => role, :tracker_id => tracker, :workspace_id => @workspace},
26
                :title => l(:button_edit)) %>
27
            </td>
28
          <% end -%>
29
        </tr>
30
      <% end -%>
31
    </tbody>
32
  </table>
33
</div>
app/views/workflows/copy.html.erb
3 3
<%= form_tag duplicate_workflows_path, method: :post, id: 'workflow_copy_form' do %>
4 4
<fieldset class="tabular box">
5 5
<legend><%= l(:label_copy_source) %></legend>
6
<p>
7
  <label><%= l(:label_role) %></label>
8
  <%= select_tag('source_role_id',
9
                  content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '') +
10
                  content_tag('option', "--- #{ l(:label_copy_same_as_target) } ---", :value => 'any') +
11
                  options_from_collection_for_select(@roles, 'id', 'name', @source_role && @source_role.id)) %>
12
</p>
6 13
<p>
7 14
  <label><%= l(:label_tracker) %></label>
8 15
  <%= select_tag('source_tracker_id',
......
11 18
                  options_from_collection_for_select(@trackers, 'id', 'name', @source_tracker && @source_tracker.id)) %>
12 19
</p>
13 20
<p>
14
  <label><%= l(:label_role) %></label>
15
  <%= select_tag('source_role_id',
21
  <label><%= l(:label_workspace) %></label>
22
  <%= select_tag('source_workspace_id',
16 23
                  content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '') +
17 24
                  content_tag('option', "--- #{ l(:label_copy_same_as_target) } ---", :value => 'any') +
18
                  options_from_collection_for_select(@roles, 'id', 'name', @source_role && @source_role.id)) %>
25
                  options_from_collection_for_select(@workspaces, 'id', 'name', @source_workspace && @source_workspace.id)) %>
19 26
</p>
20 27
</fieldset>
21 28

  
22 29
<fieldset class="tabular box">
23 30
<legend><%= l(:label_copy_target) %></legend>
31
<p>
32
  <label><%= l(:label_role) %></label>
33
  <%= select_tag 'target_role_ids',
34
                  content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '', :disabled => true) +
35
                  options_from_collection_for_select(@roles, 'id', 'name', @target_roles && @target_roles.map(&:id)), :multiple => true %>
36
</p>
24 37
<p>
25 38
  <label><%= l(:label_tracker) %></label>
26 39
  <%= select_tag 'target_tracker_ids',
......
28 41
                  options_from_collection_for_select(@trackers, 'id', 'name', @target_trackers && @target_trackers.map(&:id)), :multiple => true %>
29 42
</p>
30 43
<p>
31
  <label><%= l(:label_role) %></label>
32
  <%= select_tag 'target_role_ids',
44
  <label><%= l(:label_workspace) %></label>
45
  <%= select_tag 'target_workspace_ids',
33 46
                  content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '', :disabled => true) +
34
                  options_from_collection_for_select(@roles, 'id', 'name', @target_roles && @target_roles.map(&:id)), :multiple => true %>
47
                  options_from_collection_for_select(@workspaces, 'id', 'name', @target_workspaces && @target_workspaces.map(&:id)), :multiple => true %>
35 48
</p>
36 49
</fieldset>
37 50
<%= submit_tag l(:button_copy) %>
app/views/workflows/edit.html.erb
4 4

  
5 5
<div class="tabs">
6 6
  <ul>
7
    <li><%= link_to l(:label_status_transitions), edit_workflows_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
8
    <li><%= link_to l(:label_fields_permissions), permissions_workflows_path(:role_id => @roles, :tracker_id => @trackers) %></li>
7
    <li><%= link_to l(:label_status_transitions), edit_workflows_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces), :class => 'selected' %></li>
8
    <li><%= link_to l(:label_fields_permissions), permissions_workflows_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces) %></li>
9 9
  </ul>
10 10
</div>
11 11

  
......
23 23
  <span class="toggle-multiselect icon-only"></span>
24 24
  </label>
25 25

  
26
  <label><%=l(:label_workspace)%>:
27
  <%= options_for_workflow_select 'workspace_id[]', Workspace.sorted, @workspaces, :id => 'workspace_id', :class => 'expandable' %>
28
  <span class="toggle-multiselect icon-only"></span>
29
  </label>
30

  
26 31
  <%= submit_tag l(:button_edit), :name => nil %>
27 32

  
28 33
  <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %>
29 34
  <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
30 35

  
36
  <%= hidden_field_tag 'used_workspaces_only', '0', :id => nil %>
37
  <label><%= check_box_tag 'used_workspaces_only', '1', @used_workspaces_only %> <%= l(:label_display_used_workspaces_only) %></label>
31 38
</p>
32 39
<% end %>
33 40

  
34
<% if @trackers && @roles && @statuses.any? %>
41
<% if @trackers && @roles && @workspaces && @statuses.any? %>
35 42
  <%= form_tag workflows_path, method: :patch, id: 'workflow_form' do %>
36 43
    <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %>
37 44
    <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %>
45
    <%= @workspaces.map {|workspace| hidden_field_tag 'workspace_id[]', workspace.id, :id => nil}.join.html_safe %>
38 46
    <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
47
    <%= hidden_field_tag 'used_workspaces_only', params[:used_workspaces_only], :id => nil %>
39 48
    <div class="autoscroll">
40 49
      <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
41 50

  
......
57 66
    </div>
58 67
    <%= submit_tag l(:button_save) %>
59 68
  <% end %>
60
<% end %>
69
<% end %>
app/views/workflows/index.html.erb
1 1
<%= title [l(:label_workflow), edit_workflows_path], l(:field_summary) %>
2 2

  
3 3
<% if @roles.empty? || @trackers.empty? %>
4
<p class="nodata"><%= l(:label_no_data) %></p>
4
  <p class="nodata"><%= l(:label_no_data) %></p>
5 5
<% else %>
6
<div class="autoscroll">
7
<table class="list">
8
<thead>
9
    <tr>
10
    <th></th>
11
    <% @roles.each do |role| %>
12
    <th>
13
        <%= content_tag(role.builtin? ? 'em' : 'span', role.name) %>
14
    </th>
15
    <% end %>
16
    </tr>
17
</thead>
18
<tbody>
19
<% @trackers.each do |tracker| -%>
20
<tr>
21
  <td class="name"><%= tracker.name %></td>
22
  <% @roles.each do |role| -%>
23
  <% count = @workflow_counts[[tracker.id, role.id]] || 0 %>
24
    <td>
25
      <%= link_to((count > 0 ? count : content_tag(:span, nil, :class => 'icon-only icon-not-ok')),
26
                  {:action => 'edit', :role_id => role, :tracker_id => tracker},
27
                  :title => l(:button_edit)) %>
28
    </td>
29
  <% end -%>
30
</tr>
31
<% end -%>
32
</tbody>
33
</table>
34
</div>
6
  <p><label><%=l(:label_workspace)%>:
7
    <select id="workspace_id" class="expandable" onchange="location.href=location.href.split('?')[0] + '?workspace_id=' + this.options[this.selectedIndex].value;">
8
      <%= options_for_select(@workspaces.pluck(:name, :id), @workspace.id) %>
9
    </select>
10
  </label></p>
11
  <div class="autoscroll ws">
12
    <%= render :partial => 'index_table' %>
13
  </div>
35 14
<% end %>
app/views/workflows/permissions.html.erb
4 4

  
5 5
<div class="tabs">
6 6
  <ul>
7
    <li><%= link_to l(:label_status_transitions), edit_workflows_path(:role_id => @roles, :tracker_id => @trackers) %></li>
8
    <li><%= link_to l(:label_fields_permissions), permissions_workflows_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
7
    <li><%= link_to l(:label_status_transitions), edit_workflows_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces) %></li>
8
    <li><%= link_to l(:label_fields_permissions), permissions_workflows_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces), :class => 'selected' %></li>
9 9
  </ul>
10 10
</div>
11 11

  
......
22 22
  <%= options_for_workflow_select 'tracker_id[]', Tracker.sorted, @trackers, :id => 'tracker_id', :class => 'expandable' %>
23 23
  <span class="toggle-multiselect icon-only"></span>
24 24
  </label>
25

  
26
  <label><%=l(:label_workspace)%>:
27
  <%= options_for_workflow_select 'workspace_id[]', Workspace.sorted, @workspaces, :id => 'workspace_id', :class => 'expandable' %>
28
  <span class="toggle-multiselect icon-only"></span>
29
  </label>
25 30
  <%= submit_tag l(:button_edit), :name => nil %>
26 31

  
27 32
  <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %>
28 33
  <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
34

  
35
  <%= hidden_field_tag 'used_workspaces_only', '0', :id => nil %>
36
  <label><%= check_box_tag 'used_workspaces_only', '1', @used_workspaces_only %> <%= l(:label_display_used_workspaces_only) %></label>
29 37
</p>
30 38
<% end %>
31 39

  
32
<% if @trackers && @roles && @statuses.any? %>
40
<% if @trackers && @roles && @workspaces && @statuses.any? %>
33 41
  <%= form_tag update_permissions_workflows_path, method: :patch, id: 'workflow_form' do %>
34 42
    <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %>
35 43
    <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %>
44
    <%= @workspaces.map {|workspace| hidden_field_tag 'workspace_id[]', workspace.id, :id => nil}.join.html_safe %>
36 45
    <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
46
    <%= hidden_field_tag 'used_workspaces_only', params[:used_workspaces_only], :id => nil %>
37 47
    <div class="autoscroll">
38 48
    <table class="list workflows fields_permissions">
39 49
    <thead>
app/views/workspaces/_form.html.erb
1
<%= error_messages_for 'workspace' %>
2

  
3
<div class="box tabular">
4
<p><%= f.text_field :name, :required => true %></p>
5
<p><%= f.text_field :description %></p>
6

  
7
<%= call_hook(:view_workspaces_form, :workspace => @workspace) %>
8
</div>
app/views/workspaces/edit.html.erb
1
<%= title [l(:label_workspace_plural), workspaces_path], @workspace.name %>
2

  
3
<%= labelled_form_for @workspace do |f| %>
4
  <%= render :partial => 'form', :locals => {:f => f} %>
5
  <%= submit_tag l(:button_save) %>
6
<% end %>
app/views/workspaces/index.api.rsb
1
api.array :workspaces do
2
  @workspaces.each do |workspace|
3
    api.workspace do
4
      api.id          workspace.id
5
      api.name        workspace.name
6
      api.description workspace.description
7
    end
8
  end
9
end
app/views/workspaces/index.html.erb
1
<div class="contextual">
2
<%= link_to l(:label_workspace_new), new_workspace_path, :class => 'icon icon-add' %>
3
</div>
4

  
5
<h2><%=l(:label_workspace_plural)%></h2>
6

  
7
<table class="list workspaces">
8
  <thead><tr>
9
  <th><%=l(:field_name)%></th>
10
  <th><%=l(:field_description)%></th>
11
  <th></th>
12
  </tr></thead>
13
  <tbody>
14
<% for workspace in @workspaces %>
15
  <tr class="<%= workspace.id == 1 ? "builtin" : "givable" %>">
16
  <td class="name"><%= link_to workspace.name, edit_workspace_path(workspace) %></td>
17
  <td class="description"><%= link_to workspace.description, edit_workspace_path(workspace) %></td>
18
  <td class="buttons">
19
    <%= reorder_handle(workspace) unless workspace.id == 1 %>
20
    <%= delete_link workspace_path(workspace) unless workspace.id == 1 %>
21
  </td>
22
  </tr>
23
<% end %>
24
  </tbody>
25
</table>
26

  
27
<% html_title(l(:label_workspace_plural)) -%>
28

  
29
<%= javascript_tag do %>
30
  $(function() { $("table.workspaces tbody").positionedItems({items: ".givable"}); });
31
<% end %>
app/views/workspaces/new.html.erb
1
<%= title [l(:label_workspace_plural), workspaces_path], l(:label_workspace_new) %>
2

  
3
<%= labelled_form_for @workspace do |f| %>
4
  <%= render :partial => 'form', :locals => {:f => f} %>
5
  <%= submit_tag l(:button_create) %>
6
<% end %>
config/locales/en.yml
646 646
  label_tracker_plural: Trackers
647 647
  label_tracker_all: All trackers
648 648
  label_tracker_new: New tracker
649
  label_workspace: Workspace
650
  label_workspace_plural: Workspaces
651
  label_workspace_new: New workspace
652
  error_unable_delete_workspace: Unable to delete workspace
653
  field_workspace: Workspace
649 654
  label_workflow: Workflow
650 655
  label_issue_status: Issue status
651 656
  label_issue_status_plural: Issue statuses
......
1001 1006
  label_copy_target: Target
1002 1007
  label_copy_same_as_target: Same as target
1003 1008
  label_display_used_statuses_only: Only display statuses that are used by this tracker
1009
  label_display_used_workspaces_only: Only display statuses that are used by this workspace
1004 1010
  label_api_access_key: API access key
1005 1011
  label_missing_api_access_key: Missing an API access key
1006 1012
  label_api_access_key_created_on: "API access key created %{value} ago"
config/locales/pt-BR.yml
378 378
  label_tracker: Tipo de tarefa
379 379
  label_tracker_plural: Tipos de tarefas
380 380
  label_tracker_new: Novo tipo
381
  label_workspace: Espaço de trabalho
382
  label_workspace_plural: Espaços de trabalho
383
  label_workspace_new: Novo espaço de trabalho
384
  error_unable_delete_workspace: Não foi possível excluir espaço de trabalho
385
  field_workspace: Espaço de trabalho
381 386
  label_workflow: Fluxo de trabalho
382 387
  label_issue_status: Situação da tarefa
383 388
  label_issue_status_plural: Situação das tarefas
......
871 876
  field_watcher: Observador
872 877
  permission_view_issues: Ver tarefas
873 878
  label_display_used_statuses_only: Somente exibir situações que são usadas por este tipo de tarefa
879
  label_display_used_workspaces_only: Somente situações deste espaço de trabalho
874 880
  label_revision_id: Revisão %{value}
875 881
  label_api_access_key: Chave de acesso à API
876 882
  button_show: Exibir
config/routes.rb
383 383
    end
384 384
  end
385 385

  
386
  resources :workspaces, :except => :show
387

  
386 388
  resources :workflows, only: [:index] do
387 389
    collection do
388 390
      get 'edit'
config/settings.yml
249 249
    - name
250 250
    - identifier
251 251
    - short_description
252
    - workspace
252 253
default_project_query:
253 254
  default: ''
254 255
issue_done_ratio:
db/migrate/20160314174310_add_workspace_to_projects.rb
1
class AddWorkspaceToProjects < ActiveRecord::Migration[4.2]
2
  def self.up
3
    add_column :projects, :workspace_id, :integer, :default => 1
4
  end
5

  
6
  def self.down
7
    remove_column :projects, :workspace_id
8
  end
9
end
db/migrate/20160314174311_add_workspace_to_workflows.rb
1
class AddWorkspaceToWorkflows < ActiveRecord::Migration[4.2]
2
  def self.up
3
    add_column :workflows, :workspace_id, :integer, :default => 1
4
  end
5

  
6
  def self.down
7
    remove_column :workflows, :workspace_id
8
  end
9
end
db/migrate/20160314174312_create_workspaces.rb
1
class CreateWorkspaces < ActiveRecord::Migration[4.2]
2
  def self.up
3
    create_table :workspaces do |t|
4
      t.string :name
5
      t.string :description
6
      t.integer :position, :default => nil, :null => true
7
    end
8

  
9
    # create default workspace
10
    unless Workspace.exists?(1)
11
      Workspace.create(:name => "Default", :description => "Default workspace", :position => 1)
... This diff was truncated because it exceeds the maximum size that can be displayed.
(5-5/5)