From: Jean-Philippe Lang Date: Sun, 15 Jul 2012 14:12:17 +0000 (+0000) Subject: Workflow enhancement: editable and required fields configurable by role, tracker... X-Git-Tag: 2.1.0~339 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=d7b669e50b1c863b748231dc8fb66a692a33cdd1;p=redmine.git Workflow enhancement: editable and required fields configurable by role, tracker and status (#703, #3521). git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@9977 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index a6365531b..246140f45 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -129,11 +129,7 @@ class IssuesController < ApplicationController format.html { render :action => 'new', :layout => !request.xhr? } format.js { render(:update) { |page| - if params[:project_change] - page.replace_html 'all_attributes', :partial => 'form' - else - page.replace_html 'attributes', :partial => 'attributes' - end + page.replace_html 'all_attributes', :partial => 'form' m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide' page << "if ($('log_time')) {Element.#{m}('log_time');}" } diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index b947cf946..790eb28d4 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -46,7 +46,7 @@ class RolesController < ApplicationController if request.post? && @role.save # workflow copy if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from])) - @role.workflows.copy(copy_from) + @role.workflow_rules.copy(copy_from) end flash[:notice] = l(:notice_successful_create) redirect_to :action => 'index' diff --git a/app/controllers/trackers_controller.rb b/app/controllers/trackers_controller.rb index a67583c16..5d4bfcaf1 100644 --- a/app/controllers/trackers_controller.rb +++ b/app/controllers/trackers_controller.rb @@ -45,7 +45,7 @@ class TrackersController < ApplicationController if request.post? and @tracker.save # workflow copy if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from])) - @tracker.workflows.copy(copy_from) + @tracker.workflow_rules.copy(copy_from) end flash[:notice] = l(:notice_successful_create) redirect_to :action => 'index' diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index c381666a3..3c49de8d5 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -23,7 +23,7 @@ class WorkflowsController < ApplicationController before_filter :find_trackers def index - @workflow_counts = Workflow.count_by_tracker_and_role + @workflow_counts = WorkflowTransition.count_by_tracker_and_role end def edit @@ -31,16 +31,15 @@ class WorkflowsController < ApplicationController @tracker = Tracker.find_by_id(params[:tracker_id]) if request.post? - Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) + WorkflowTransition.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) (params[:issue_status] || []).each { |status_id, transitions| transitions.each { |new_status_id, options| author = options.is_a?(Array) && options.include?('author') && !options.include?('always') assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always') - @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee) + WorkflowTransition.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee) } } if @role.save - flash[:notice] = l(:notice_successful_update) redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker return end @@ -53,7 +52,7 @@ class WorkflowsController < ApplicationController @statuses ||= IssueStatus.find(:all, :order => 'position') if @tracker && @role && @statuses.any? - workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id}) + workflows = WorkflowTransition.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id}) @workflows = {} @workflows['always'] = workflows.select {|w| !w.author && !w.assignee} @workflows['author'] = workflows.select {|w| w.author} @@ -61,6 +60,37 @@ class WorkflowsController < ApplicationController end end + def permissions + @role = Role.find_by_id(params[:role_id]) + @tracker = Tracker.find_by_id(params[:tracker_id]) + + if @role && @tracker + if request.post? + WorkflowPermission.destroy_all({:role_id => @role.id, :tracker_id => @tracker.id}) + (params[:permissions] || {}).each { |field, rule_by_status_id| + rule_by_status_id.each { |status_id, rule| + if rule.present? + WorkflowPermission.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule) + end + } + } + redirect_to :action => 'permissions', :role_id => @role, :tracker_id => @tracker + return + end + + @statuses = @tracker.issue_statuses + @fields = (Tracker::CORE_FIELDS_ALL - @tracker.disabled_core_fields).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]} + @custom_fields = @tracker.custom_fields + + @permissions = WorkflowPermission.where(:tracker_id => @tracker.id, :role_id => @role.id).all.inject({}) do |h, w| + h[w.old_status_id] ||= {} + h[w.old_status_id][w.field_name] = w.rule + h + end + @statuses.each {|status| @permissions[status.id] ||= {}} + end + end + def copy if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any' @@ -83,7 +113,7 @@ class WorkflowsController < ApplicationController elsif @target_trackers.nil? || @target_roles.nil? flash.now[:error] = l(:error_workflow_copy_target) else - Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles) + WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles) flash[:notice] = l(:notice_successful_update) redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role end diff --git a/app/helpers/custom_fields_helper.rb b/app/helpers/custom_fields_helper.rb index ff953d857..c55943b32 100644 --- a/app/helpers/custom_fields_helper.rb +++ b/app/helpers/custom_fields_helper.rb @@ -73,15 +73,17 @@ module CustomFieldsHelper end # Return custom field label tag - def custom_field_label_tag(name, custom_value) + def custom_field_label_tag(name, custom_value, options={}) + required = options[:required] || custom_value.custom_field.is_required? + content_tag "label", h(custom_value.custom_field.name) + - (custom_value.custom_field.is_required? ? " *".html_safe : ""), - :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}" + (required ? " *".html_safe : ""), + :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}" end # Return custom field tag with its label tag - def custom_field_tag_with_label(name, custom_value) - custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value) + def custom_field_tag_with_label(name, custom_value, options={}) + custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value) end def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil) diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb index 59b31d2ea..3dd514042 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -18,4 +18,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module WorkflowsHelper + def field_permission_tag(permissions, status, field) + name = field.is_a?(CustomField) ? field.id.to_s : field + select_tag("permissions[#{name}][#{status.id}]", + options_for_select([["", ""], ["Read-only", "readonly"], ["Required", "required"]], permissions[status.id][name]) + ) + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 5fc0f1d61..152953708 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -58,7 +58,7 @@ class Issue < ActiveRecord::Base validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 validates_numericality_of :estimated_hours, :allow_nil => true - validate :validate_issue + validate :validate_issue, :validate_required_fields scope :visible, lambda {|*args| { :include => :project, @@ -146,6 +146,11 @@ class Issue < ActiveRecord::Base super end + def reload(*args) + @workflow_rule_by_attribute = nil + super + end + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields def available_custom_fields (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : [] @@ -208,7 +213,9 @@ class Issue < ActiveRecord::Base def status_id=(sid) self.status = nil - write_attribute(:status_id, sid) + result = write_attribute(:status_id, sid) + @workflow_rule_by_attribute = nil + result end def priority_id=(pid) @@ -230,6 +237,7 @@ class Issue < ActiveRecord::Base self.tracker = nil result = write_attribute(:tracker_id, tid) @custom_field_values = nil + @workflow_rule_by_attribute = nil result end @@ -336,9 +344,10 @@ class Issue < ActiveRecord::Base :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) && user.allowed_to?(:manage_subtasks, issue.project)} - def safe_attribute_names(*args) - names = super(*args) + def safe_attribute_names(user=nil) + names = super names -= disabled_core_fields + names -= read_only_attribute_names(user) names end @@ -362,15 +371,15 @@ class Issue < ActiveRecord::Base self.tracker_id = t end - attrs = delete_unsafe_attributes(attrs, user) - return if attrs.empty? - - if attrs['status_id'] - unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i) - attrs.delete('status_id') + if (s = attrs.delete('status_id')) && safe_attribute?('status_id') + if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i) + self.status_id = s end end + attrs = delete_unsafe_attributes(attrs, user) + return if attrs.empty? + unless leaf? attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} end @@ -379,6 +388,14 @@ class Issue < ActiveRecord::Base attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i) end + if attrs['custom_field_values'].present? + attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s} + end + + if attrs['custom_fields'].present? + attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s} + end + # mass-assignment security bypass assign_attributes attrs, :without_protection => true end @@ -387,6 +404,76 @@ class Issue < ActiveRecord::Base tracker ? tracker.disabled_core_fields : [] end + # Returns the custom_field_values that can be edited by the given user + def editable_custom_field_values(user=nil) + custom_field_values.reject do |value| + read_only_attribute_names(user).include?(value.custom_field_id.to_s) + end + end + + # Returns the names of attributes that are read-only for user or the current user + # For users with multiple roles, the read-only fields are the intersection of + # read-only fields of each role + # The result is an array of strings where sustom fields are represented with their ids + # + # Examples: + # issue.read_only_attribute_names # => ['due_date', '2'] + # issue.read_only_attribute_names(user) # => [] + def read_only_attribute_names(user=nil) + workflow_rule_by_attribute(user).select {|attr, rule| rule == 'readonly'}.keys + end + + # Returns the names of required attributes for user or the current user + # For users with multiple roles, the required fields are the intersection of + # required fields of each role + # The result is an array of strings where sustom fields are represented with their ids + # + # Examples: + # issue.required_attribute_names # => ['due_date', '2'] + # issue.required_attribute_names(user) # => [] + def required_attribute_names(user=nil) + workflow_rule_by_attribute(user).select {|attr, rule| rule == 'required'}.keys + end + + # Returns true if the attribute is required for user + def required_attribute?(name, user=nil) + required_attribute_names(user).include?(name.to_s) + end + + # Returns a hash of the workflow rule by attribute for the given user + # + # Examples: + # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'} + def workflow_rule_by_attribute(user=nil) + return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil? + + user_real = user || User.current + roles = user_real.admin ? Role.all : user_real.roles_for_project(project) + return {} if roles.empty? + + result = {} + workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all + if workflow_permissions.any? + workflow_rules = workflow_permissions.inject({}) do |h, wp| + h[wp.field_name] ||= [] + h[wp.field_name] << wp.rule + h + end + workflow_rules.each do |attr, rules| + next if rules.size < roles.size + uniq_rules = rules.uniq + if uniq_rules.size == 1 + result[attr] = uniq_rules.first + else + result[attr] = 'required' + end + end + end + @workflow_rule_by_attribute = result if user.nil? + result + end + private :workflow_rule_by_attribute + def done_ratio if Issue.use_status_for_done_ratio? && status && status.default_done_ratio status.default_done_ratio @@ -448,6 +535,25 @@ class Issue < ActiveRecord::Base end end + # Validates the issue against additional workflow requirements + def validate_required_fields + user = new_record? ? author : current_journal.try(:user) + + required_attribute_names(user).each do |attribute| + if attribute =~ /^\d+$/ + attribute = attribute.to_i + v = custom_field_values.detect {|v| v.custom_field_id == attribute } + if v && v.value.blank? + errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank') + end + else + if respond_to?(attribute) && send(attribute).blank? + errors.add attribute, :blank + end + end + end + end + # Set the done_ratio using the status if that setting is set. This will keep the done_ratios # even if the user turns off the setting later def update_done_ratio_from_issue_status diff --git a/app/models/issue_status.rb b/app/models/issue_status.rb index 6973ca656..2d09d5e88 100644 --- a/app/models/issue_status.rb +++ b/app/models/issue_status.rb @@ -17,10 +17,10 @@ class IssueStatus < ActiveRecord::Base before_destroy :check_integrity - has_many :workflows, :foreign_key => "old_status_id" + has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id" acts_as_list - before_destroy :delete_workflows + before_destroy :delete_workflow_rules after_save :update_default validates_presence_of :name @@ -98,7 +98,7 @@ private end # Deletes associated workflows - def delete_workflows - Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}]) + def delete_workflow_rules + WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}]) end end diff --git a/app/models/role.rb b/app/models/role.rb index 8e8737afb..412e5a63c 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -47,9 +47,9 @@ class Role < ActiveRecord::Base } before_destroy :check_deletable - has_many :workflows, :dependent => :delete_all do + has_many :workflow_rules, :dependent => :delete_all do def copy(source_role) - Workflow.copy(nil, source_role, nil, proxy_association.owner) + WorkflowRule.copy(nil, source_role, nil, proxy_association.owner) end end diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 109e0f423..472b94b31 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -17,14 +17,17 @@ class Tracker < ActiveRecord::Base - # Other fields should be appended, not inserted! - CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio) + CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze + # Fields that can be disabled + # Other (future) fields should be appended, not inserted! + CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze + CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze before_destroy :check_integrity has_many :issues - has_many :workflows, :dependent => :delete_all do + has_many :workflow_rules, :dependent => :delete_all do def copy(source_tracker) - Workflow.copy(source_tracker, nil, proxy_association.owner, nil) + WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil) end end @@ -56,8 +59,8 @@ class Tracker < ActiveRecord::Base return [] end - ids = Workflow. - connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}"). + ids = WorkflowTransition. + connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'"). flatten. uniq diff --git a/app/models/workflow.rb b/app/models/workflow.rb deleted file mode 100644 index 36b4c7df8..000000000 --- a/app/models/workflow.rb +++ /dev/null @@ -1,89 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Workflow < ActiveRecord::Base - belongs_to :role - belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id' - belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id' - - validates_presence_of :role, :old_status, :new_status - - # Returns workflow transitions count by tracker and role - def self.count_by_tracker_and_role - counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id") - roles = Role.sorted.all - trackers = Tracker.sorted.all - - result = [] - trackers.each do |tracker| - t = [] - roles.each do |role| - row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s} - t << [role, (row.nil? ? 0 : row['c'].to_i)] - end - result << [tracker, t] - end - - result - end - - # 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") - end - - target_trackers = [target_trackers].flatten.compact - target_roles = [target_roles].flatten.compact - - target_trackers = Tracker.sorted.all if target_trackers.empty? - target_roles = Role.all if target_roles.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) - 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) - unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? && - source_role.is_a?(Role) && !source_role.new_record? && - target_tracker.is_a?(Tracker) && !target_tracker.new_record? && - target_role.is_a?(Role) && !target_role.new_record? - - raise ArgumentError.new("arguments can not be nil or unsaved objects") - end - - if source_tracker == target_tracker && source_role == target_role - false - else - transaction do - delete_all :tracker_id => target_tracker.id, :role_id => target_role.id - connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee)" + - " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee" + - " FROM #{Workflow.table_name}" + - " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}" - end - true - end - end -end diff --git a/app/models/workflow_permission.rb b/app/models/workflow_permission.rb new file mode 100644 index 000000000..72e95543f --- /dev/null +++ b/app/models/workflow_permission.rb @@ -0,0 +1,29 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 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 WorkflowPermission < WorkflowRule + validates_inclusion_of :rule, :in => %w(readonly required) + validate :validate_field_name + + protected + + def validate_field_name + unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/) + errors.add :field_name, :invalid + end + end +end diff --git a/app/models/workflow_rule.rb b/app/models/workflow_rule.rb new file mode 100644 index 000000000..2fc020ba1 --- /dev/null +++ b/app/models/workflow_rule.rb @@ -0,0 +1,73 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 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 WorkflowRule < ActiveRecord::Base + self.table_name = "#{table_name_prefix}workflows#{table_name_suffix}" + + belongs_to :role + belongs_to :tracker + belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id' + belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id' + + validates_presence_of :role, :tracker, :old_status + + # 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") + end + + target_trackers = [target_trackers].flatten.compact + target_roles = [target_roles].flatten.compact + + target_trackers = Tracker.sorted.all if target_trackers.empty? + target_roles = Role.all if target_roles.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) + 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) + unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? && + source_role.is_a?(Role) && !source_role.new_record? && + target_tracker.is_a?(Tracker) && !target_tracker.new_record? && + target_role.is_a?(Role) && !target_role.new_record? + + raise ArgumentError.new("arguments can not be nil or unsaved objects") + end + + if source_tracker == target_tracker && source_role == target_role + 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, rule, type)" + + " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, rule, type" + + " FROM #{WorkflowRule.table_name}" + + " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}" + end + true + end + end +end diff --git a/app/models/workflow_transition.rb b/app/models/workflow_transition.rb new file mode 100644 index 000000000..0c01edd04 --- /dev/null +++ b/app/models/workflow_transition.rb @@ -0,0 +1,39 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 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 WorkflowTransition < WorkflowRule + validates_presence_of :new_status + + # Returns workflow transitions count by tracker and role + def self.count_by_tracker_and_role + counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{table_name} WHERE type = 'WorkflowTransition' GROUP BY role_id, tracker_id") + roles = Role.sorted.all + trackers = Tracker.sorted.all + + result = [] + trackers.each do |tracker| + t = [] + roles.each do |role| + row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s} + t << [role, (row.nil? ? 0 : row['c'].to_i)] + end + result << [tracker, t] + end + + result + end +end diff --git a/app/views/issues/_attributes.html.erb b/app/views/issues/_attributes.html.erb index d2c13e82b..cc51cd435 100644 --- a/app/views/issues/_attributes.html.erb +++ b/app/views/issues/_attributes.html.erb @@ -4,6 +4,9 @@
<% if @issue.safe_attribute? 'status_id' %>

<%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %>

+<%= observe_field :issue_status_id, :url => project_issue_form_path(@project, :id => @issue), + :with => "Form.serialize('issue-form')" %> + <% else %>

<%= h(@issue.status.name) %>

<% end %> @@ -13,11 +16,11 @@ <% end %> <% if @issue.safe_attribute? 'assigned_to_id' %> -

<%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true %>

+

<%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true, :required => @issue.required_attribute?('assigned_to_id') %>

<% end %> <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %> -

<%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %> +

<%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true, :required => @issue.required_attribute?('category_id') %> <%= link_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'), {:url => new_project_issue_category_path(@issue.project), :method => 'get'}, :title => l(:label_issue_category_new), @@ -25,7 +28,7 @@ <% end %> <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %> -

<%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %> +

<%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true, :required => @issue.required_attribute?('fixed_version_id') %> <%= link_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'), {:url => new_project_version_path(@issue.project), :method => 'get'}, :title => l(:label_version_new), @@ -36,25 +39,25 @@

<% if @issue.safe_attribute? 'parent_issue_id' %> -

<%= f.text_field :parent_issue_id, :size => 10 %>

+

<%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %>

<%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @issue.project) }')" %> <% end %> <% if @issue.safe_attribute? 'start_date' %> -

<%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_start_date') if @issue.leaf? %>

+

<%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('start_date') %><%= calendar_for('issue_start_date') if @issue.leaf? %>

<% end %> <% if @issue.safe_attribute? 'due_date' %> -

<%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %>

+

<%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('due_date') %><%= calendar_for('issue_due_date') if @issue.leaf? %>

<% end %> <% if @issue.safe_attribute? 'estimated_hours' %> -

<%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %>

+

<%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %>

<% end %> <% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %> -

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>

+

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %>

<% end %>
diff --git a/app/views/issues/_form.html.erb b/app/views/issues/_form.html.erb index 9bc362377..4a13fa07a 100644 --- a/app/views/issues/_form.html.erb +++ b/app/views/issues/_form.html.erb @@ -9,7 +9,7 @@ <% if @issue.safe_attribute? 'project_id' %>

<%= f.select :project_id, project_tree_options_for_select(@issue.allowed_target_projects, :selected => @issue.project), :required => true %>

-<%= observe_field :issue_project_id, :url => project_issue_form_path(@project, :id => @issue, :project_change => '1'), +<%= observe_field :issue_project_id, :url => project_issue_form_path(@project, :id => @issue), :with => "Form.serialize('issue-form')" %> <% end %> @@ -25,7 +25,7 @@ <% if @issue.safe_attribute? 'description' %>

- + <%= f.label_for_field :description, :required => @issue.required_attribute?('description') %> <%= link_to_function image_tag('edit.png'), 'Element.hide(this); Effect.toggle("issue_description_and_toolbar", "appear", {duration:0.3})' unless @issue.new_record? %> <%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %> diff --git a/app/views/issues/_form_custom_fields.html.erb b/app/views/issues/_form_custom_fields.html.erb index 33b0b848b..4da98eb13 100644 --- a/app/views/issues/_form_custom_fields.html.erb +++ b/app/views/issues/_form_custom_fields.html.erb @@ -2,8 +2,8 @@

<% i = 0 %> <% split_on = (@issue.custom_field_values.size / 2.0).ceil - 1 %> -<% @issue.custom_field_values.each do |value| %> -

<%= custom_field_tag_with_label :issue, value %>

+<% @issue.editable_custom_field_values.each do |value| %> +

<%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>

<% if i == split_on -%>
<% end -%> diff --git a/app/views/trackers/index.html.erb b/app/views/trackers/index.html.erb index 0515ae335..4d10d857e 100644 --- a/app/views/trackers/index.html.erb +++ b/app/views/trackers/index.html.erb @@ -15,7 +15,7 @@ <% for tracker in @trackers %> "> <%= link_to h(tracker.name), edit_tracker_path(tracker) %> - <% unless tracker.workflows.count > 0 %><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)<% end %> + <% unless tracker.workflow_rules.count > 0 %><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)<% end %> <%= reorder_links('tracker', {:action => 'update', :id => tracker}, :put) %> <%= delete_link tracker_path(tracker) %> diff --git a/app/views/workflows/edit.html.erb b/app/views/workflows/edit.html.erb index 20eaff992..a634ee310 100644 --- a/app/views/workflows/edit.html.erb +++ b/app/views/workflows/edit.html.erb @@ -2,6 +2,13 @@

<%=l(:label_workflow)%>

+
+
    +
  • <%= link_to 'Status transitions', {:action => 'edit', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %>
  • +
  • <%= link_to 'Fields permissions', {:action => 'permissions', :role_id => @role, :tracker_id => @tracker} %>
  • +
+
+

<%=l(:text_workflow_edit)%>:

<%= form_tag({}, :method => 'get') do %> @@ -12,11 +19,11 @@ + <%= submit_tag l(:button_edit), :name => nil %> + <%= hidden_field_tag 'used_statuses_only', '0' %> -

-

-<%= submit_tag l(:button_edit), :name => nil %> +

<% end %> diff --git a/app/views/workflows/permissions.html.erb b/app/views/workflows/permissions.html.erb new file mode 100644 index 000000000..73cf91656 --- /dev/null +++ b/app/views/workflows/permissions.html.erb @@ -0,0 +1,91 @@ +<%= render :partial => 'action_menu' %> + +

<%=l(:label_workflow)%>

+ +
+
    +
  • <%= link_to 'Status transitions', {:action => 'edit', :role_id => @role, :tracker_id => @tracker} %>
  • +
  • <%= link_to 'Fields permissions', {:action => 'permissions', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %>
  • +
+
+ +

<%=l(:text_workflow_edit)%>:

+ +<%= form_tag({}, :method => 'get') do %> +

+ + + + + <%= submit_tag l(:button_edit), :name => nil %> +

+<% end %> + +<% if @tracker && @role && @statuses.any? %> + <%= form_tag({}, :id => 'workflow_form' ) do %> + <%= hidden_field_tag 'tracker_id', @tracker.id %> + <%= hidden_field_tag 'role_id', @role.id %> +
+ + + + + + + + + <% for status in @statuses %> + + <% end %> + + + + + + + <% @fields.each do |field, name| %> + "> + + <% for status in @statuses -%> + + <% end -%> + + <% end %> + <% if @custom_fields.any? %> + + + + <% @custom_fields.each do |field| %> + "> + + <% for status in @statuses -%> + + <% end -%> + + <% end %> + <% end %> + +
+ <%=l(:label_issue_status)%>
+ <%=h status.name %> +
+   + <%= l(:field_core_fields) %> +
+ <%=h name %> + + <%= field_permission_tag(@permissions, status, field) %> +
+   + <%= l(:label_custom_field_plural) %> +
+ <%=h field.name %> + + <%= field_permission_tag(@permissions, status, field) %> +
+
+ <%= submit_tag l(:button_save) %> + <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index e3ed8859a..0ee3908b1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -312,6 +312,7 @@ RedmineApp::Application.routes.draw do match 'workflows', :controller => 'workflows', :action => 'index', :via => :get match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post] + match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post] match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post] match 'settings', :controller => 'settings', :action => 'index', :via => :get match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post] diff --git a/db/migrate/20120714122000_add_workflows_type.rb b/db/migrate/20120714122000_add_workflows_type.rb new file mode 100644 index 000000000..f263f25df --- /dev/null +++ b/db/migrate/20120714122000_add_workflows_type.rb @@ -0,0 +1,9 @@ +class AddWorkflowsType < ActiveRecord::Migration + def up + add_column :workflows, :type, :string, :limit => 30 + end + + def down + remove_column :workflows, :type + end +end diff --git a/db/migrate/20120714122100_update_workflows_to_sti.rb b/db/migrate/20120714122100_update_workflows_to_sti.rb new file mode 100644 index 000000000..8ee5c6dc3 --- /dev/null +++ b/db/migrate/20120714122100_update_workflows_to_sti.rb @@ -0,0 +1,9 @@ +class UpdateWorkflowsToSti < ActiveRecord::Migration + def up + WorkflowRule.update_all "type = 'WorkflowTransition'" + end + + def down + WorkflowRule.update_all "type = NULL" + end +end diff --git a/db/migrate/20120714122200_add_workflows_rule_fields.rb b/db/migrate/20120714122200_add_workflows_rule_fields.rb new file mode 100644 index 000000000..6e9c7df70 --- /dev/null +++ b/db/migrate/20120714122200_add_workflows_rule_fields.rb @@ -0,0 +1,11 @@ +class AddWorkflowsRuleFields < ActiveRecord::Migration + def up + add_column :workflows, :field_name, :string, :limit => 30 + add_column :workflows, :rule, :string, :limit => 30 + end + + def down + remove_column :workflows, :field_name + remove_column :workflows, :rule + end +end diff --git a/lib/redmine/default_data/loader.rb b/lib/redmine/default_data/loader.rb index 2133ee668..910a6a319 100644 --- a/lib/redmine/default_data/loader.rb +++ b/lib/redmine/default_data/loader.rb @@ -140,7 +140,7 @@ module Redmine Tracker.find(:all).each { |t| IssueStatus.find(:all).each { |os| IssueStatus.find(:all).each { |ns| - Workflow.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns + WorkflowTransition.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns } } } @@ -148,7 +148,7 @@ module Redmine Tracker.find(:all).each { |t| [new, in_progress, resolved, feedback].each { |os| [in_progress, resolved, feedback, closed].each { |ns| - Workflow.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns + WorkflowTransition.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns } } } @@ -156,10 +156,10 @@ module Redmine Tracker.find(:all).each { |t| [new, in_progress, resolved, feedback].each { |os| [closed].each { |ns| - Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns + WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns } } - Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id) + WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id) } # Enumerations diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 57f2f24b6..013c1b726 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -433,6 +433,9 @@ ul.properties li span {font-style:italic;} #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; } #workflow_copy_form select { width: 200px; } +table.fields_permissions select {font-size:90%} +table.fields_permissions td.readonly {background:#ddd;} +table.fields_permissions td.required {background:#d88;} textarea#custom_field_possible_values {width: 99%} input#content_comments {width: 99%} @@ -504,7 +507,7 @@ input#time_entry_comments { width: 90%;} fieldset.settings label { display: block; } fieldset#notified_events .parent { padding-left: 20px; } -.required {color: #bb0000;} +span.required {color: #bb0000;} .summary {font-style: italic;} #attachments_fields input.description {margin-left: 8px; width:340px;} diff --git a/test/fixtures/workflows.yml b/test/fixtures/workflows.yml index 6126055ca..d544545b3 100644 --- a/test/fixtures/workflows.yml +++ b/test/fixtures/workflows.yml @@ -1,1615 +1,1884 @@ --- -workflows_189: +WorkflowTransitions_189: new_status_id: 5 role_id: 1 old_status_id: 2 id: 189 tracker_id: 3 -workflows_001: + type: WorkflowTransition +WorkflowTransitions_001: new_status_id: 2 role_id: 1 old_status_id: 1 id: 1 tracker_id: 1 -workflows_002: + type: WorkflowTransition +WorkflowTransitions_002: new_status_id: 3 role_id: 1 old_status_id: 1 id: 2 tracker_id: 1 -workflows_003: + type: WorkflowTransition +WorkflowTransitions_003: new_status_id: 4 role_id: 1 old_status_id: 1 id: 3 tracker_id: 1 -workflows_110: + type: WorkflowTransition +WorkflowTransitions_110: new_status_id: 6 role_id: 1 old_status_id: 4 id: 110 tracker_id: 2 -workflows_004: + type: WorkflowTransition +WorkflowTransitions_004: new_status_id: 5 role_id: 1 old_status_id: 1 id: 4 tracker_id: 1 -workflows_030: + type: WorkflowTransition +WorkflowTransitions_030: new_status_id: 5 role_id: 1 old_status_id: 6 id: 30 tracker_id: 1 -workflows_111: + type: WorkflowTransition +WorkflowTransitions_111: new_status_id: 1 role_id: 1 old_status_id: 5 id: 111 tracker_id: 2 -workflows_005: + type: WorkflowTransition +WorkflowTransitions_005: new_status_id: 6 role_id: 1 old_status_id: 1 id: 5 tracker_id: 1 -workflows_031: + type: WorkflowTransition +WorkflowTransitions_031: new_status_id: 2 role_id: 2 old_status_id: 1 id: 31 tracker_id: 1 -workflows_112: + type: WorkflowTransition +WorkflowTransitions_112: new_status_id: 2 role_id: 1 old_status_id: 5 id: 112 tracker_id: 2 -workflows_006: + type: WorkflowTransition +WorkflowTransitions_006: new_status_id: 1 role_id: 1 old_status_id: 2 id: 6 tracker_id: 1 -workflows_032: + type: WorkflowTransition +WorkflowTransitions_032: new_status_id: 3 role_id: 2 old_status_id: 1 id: 32 tracker_id: 1 -workflows_113: + type: WorkflowTransition +WorkflowTransitions_113: new_status_id: 3 role_id: 1 old_status_id: 5 id: 113 tracker_id: 2 -workflows_220: + type: WorkflowTransition +WorkflowTransitions_220: new_status_id: 6 role_id: 2 old_status_id: 2 id: 220 tracker_id: 3 -workflows_007: + type: WorkflowTransition +WorkflowTransitions_007: new_status_id: 3 role_id: 1 old_status_id: 2 id: 7 tracker_id: 1 -workflows_033: + type: WorkflowTransition +WorkflowTransitions_033: new_status_id: 4 role_id: 2 old_status_id: 1 id: 33 tracker_id: 1 -workflows_060: + type: WorkflowTransition +WorkflowTransitions_060: new_status_id: 5 role_id: 2 old_status_id: 6 id: 60 tracker_id: 1 -workflows_114: + type: WorkflowTransition +WorkflowTransitions_114: new_status_id: 4 role_id: 1 old_status_id: 5 id: 114 tracker_id: 2 -workflows_140: + type: WorkflowTransition +WorkflowTransitions_140: new_status_id: 6 role_id: 2 old_status_id: 4 id: 140 tracker_id: 2 -workflows_221: + type: WorkflowTransition +WorkflowTransitions_221: new_status_id: 1 role_id: 2 old_status_id: 3 id: 221 tracker_id: 3 -workflows_008: + type: WorkflowTransition +WorkflowTransitions_008: new_status_id: 4 role_id: 1 old_status_id: 2 id: 8 tracker_id: 1 -workflows_034: + type: WorkflowTransition +WorkflowTransitions_034: new_status_id: 5 role_id: 2 old_status_id: 1 id: 34 tracker_id: 1 -workflows_115: + type: WorkflowTransition +WorkflowTransitions_115: new_status_id: 6 role_id: 1 old_status_id: 5 id: 115 tracker_id: 2 -workflows_141: + type: WorkflowTransition +WorkflowTransitions_141: new_status_id: 1 role_id: 2 old_status_id: 5 id: 141 tracker_id: 2 -workflows_222: + type: WorkflowTransition +WorkflowTransitions_222: new_status_id: 2 role_id: 2 old_status_id: 3 id: 222 tracker_id: 3 -workflows_223: + type: WorkflowTransition +WorkflowTransitions_223: new_status_id: 4 role_id: 2 old_status_id: 3 id: 223 tracker_id: 3 -workflows_009: + type: WorkflowTransition +WorkflowTransitions_009: new_status_id: 5 role_id: 1 old_status_id: 2 id: 9 tracker_id: 1 -workflows_035: + type: WorkflowTransition +WorkflowTransitions_035: new_status_id: 6 role_id: 2 old_status_id: 1 id: 35 tracker_id: 1 -workflows_061: + type: WorkflowTransition +WorkflowTransitions_061: new_status_id: 2 role_id: 3 old_status_id: 1 id: 61 tracker_id: 1 -workflows_116: + type: WorkflowTransition +WorkflowTransitions_116: new_status_id: 1 role_id: 1 old_status_id: 6 id: 116 tracker_id: 2 -workflows_142: + type: WorkflowTransition +WorkflowTransitions_142: new_status_id: 2 role_id: 2 old_status_id: 5 id: 142 tracker_id: 2 -workflows_250: + type: WorkflowTransition +WorkflowTransitions_250: new_status_id: 6 role_id: 3 old_status_id: 2 id: 250 tracker_id: 3 -workflows_224: + type: WorkflowTransition +WorkflowTransitions_224: new_status_id: 5 role_id: 2 old_status_id: 3 id: 224 tracker_id: 3 -workflows_036: + type: WorkflowTransition +WorkflowTransitions_036: new_status_id: 1 role_id: 2 old_status_id: 2 id: 36 tracker_id: 1 -workflows_062: + type: WorkflowTransition +WorkflowTransitions_062: new_status_id: 3 role_id: 3 old_status_id: 1 id: 62 tracker_id: 1 -workflows_117: + type: WorkflowTransition +WorkflowTransitions_117: new_status_id: 2 role_id: 1 old_status_id: 6 id: 117 tracker_id: 2 -workflows_143: + type: WorkflowTransition +WorkflowTransitions_143: new_status_id: 3 role_id: 2 old_status_id: 5 id: 143 tracker_id: 2 -workflows_170: + type: WorkflowTransition +WorkflowTransitions_170: new_status_id: 6 role_id: 3 old_status_id: 4 id: 170 tracker_id: 2 -workflows_251: + type: WorkflowTransition +WorkflowTransitions_251: new_status_id: 1 role_id: 3 old_status_id: 3 id: 251 tracker_id: 3 -workflows_225: + type: WorkflowTransition +WorkflowTransitions_225: new_status_id: 6 role_id: 2 old_status_id: 3 id: 225 tracker_id: 3 -workflows_063: + type: WorkflowTransition +WorkflowTransitions_063: new_status_id: 4 role_id: 3 old_status_id: 1 id: 63 tracker_id: 1 -workflows_090: + type: WorkflowTransition +WorkflowTransitions_090: new_status_id: 5 role_id: 3 old_status_id: 6 id: 90 tracker_id: 1 -workflows_118: + type: WorkflowTransition +WorkflowTransitions_118: new_status_id: 3 role_id: 1 old_status_id: 6 id: 118 tracker_id: 2 -workflows_144: + type: WorkflowTransition +WorkflowTransitions_144: new_status_id: 4 role_id: 2 old_status_id: 5 id: 144 tracker_id: 2 -workflows_252: + type: WorkflowTransition +WorkflowTransitions_252: new_status_id: 2 role_id: 3 old_status_id: 3 id: 252 tracker_id: 3 -workflows_226: + type: WorkflowTransition +WorkflowTransitions_226: new_status_id: 1 role_id: 2 old_status_id: 4 id: 226 tracker_id: 3 -workflows_038: + type: WorkflowTransition +WorkflowTransitions_038: new_status_id: 4 role_id: 2 old_status_id: 2 id: 38 tracker_id: 1 -workflows_064: + type: WorkflowTransition +WorkflowTransitions_064: new_status_id: 5 role_id: 3 old_status_id: 1 id: 64 tracker_id: 1 -workflows_091: + type: WorkflowTransition +WorkflowTransitions_091: new_status_id: 2 role_id: 1 old_status_id: 1 id: 91 tracker_id: 2 -workflows_119: + type: WorkflowTransition +WorkflowTransitions_119: new_status_id: 4 role_id: 1 old_status_id: 6 id: 119 tracker_id: 2 -workflows_145: + type: WorkflowTransition +WorkflowTransitions_145: new_status_id: 6 role_id: 2 old_status_id: 5 id: 145 tracker_id: 2 -workflows_171: + type: WorkflowTransition +WorkflowTransitions_171: new_status_id: 1 role_id: 3 old_status_id: 5 id: 171 tracker_id: 2 -workflows_253: + type: WorkflowTransition +WorkflowTransitions_253: new_status_id: 4 role_id: 3 old_status_id: 3 id: 253 tracker_id: 3 -workflows_227: + type: WorkflowTransition +WorkflowTransitions_227: new_status_id: 2 role_id: 2 old_status_id: 4 id: 227 tracker_id: 3 -workflows_039: + type: WorkflowTransition +WorkflowTransitions_039: new_status_id: 5 role_id: 2 old_status_id: 2 id: 39 tracker_id: 1 -workflows_065: + type: WorkflowTransition +WorkflowTransitions_065: new_status_id: 6 role_id: 3 old_status_id: 1 id: 65 tracker_id: 1 -workflows_092: + type: WorkflowTransition +WorkflowTransitions_092: new_status_id: 3 role_id: 1 old_status_id: 1 id: 92 tracker_id: 2 -workflows_146: + type: WorkflowTransition +WorkflowTransitions_146: new_status_id: 1 role_id: 2 old_status_id: 6 id: 146 tracker_id: 2 -workflows_172: + type: WorkflowTransition +WorkflowTransitions_172: new_status_id: 2 role_id: 3 old_status_id: 5 id: 172 tracker_id: 2 -workflows_254: + type: WorkflowTransition +WorkflowTransitions_254: new_status_id: 5 role_id: 3 old_status_id: 3 id: 254 tracker_id: 3 -workflows_228: + type: WorkflowTransition +WorkflowTransitions_228: new_status_id: 3 role_id: 2 old_status_id: 4 id: 228 tracker_id: 3 -workflows_066: + type: WorkflowTransition +WorkflowTransitions_066: new_status_id: 1 role_id: 3 old_status_id: 2 id: 66 tracker_id: 1 -workflows_093: + type: WorkflowTransition +WorkflowTransitions_093: new_status_id: 4 role_id: 1 old_status_id: 1 id: 93 tracker_id: 2 -workflows_147: + type: WorkflowTransition +WorkflowTransitions_147: new_status_id: 2 role_id: 2 old_status_id: 6 id: 147 tracker_id: 2 -workflows_173: + type: WorkflowTransition +WorkflowTransitions_173: new_status_id: 3 role_id: 3 old_status_id: 5 id: 173 tracker_id: 2 -workflows_255: + type: WorkflowTransition +WorkflowTransitions_255: new_status_id: 6 role_id: 3 old_status_id: 3 id: 255 tracker_id: 3 -workflows_229: + type: WorkflowTransition +WorkflowTransitions_229: new_status_id: 5 role_id: 2 old_status_id: 4 id: 229 tracker_id: 3 -workflows_067: + type: WorkflowTransition +WorkflowTransitions_067: new_status_id: 3 role_id: 3 old_status_id: 2 id: 67 tracker_id: 1 -workflows_148: + type: WorkflowTransition +WorkflowTransitions_148: new_status_id: 3 role_id: 2 old_status_id: 6 id: 148 tracker_id: 2 -workflows_174: + type: WorkflowTransition +WorkflowTransitions_174: new_status_id: 4 role_id: 3 old_status_id: 5 id: 174 tracker_id: 2 -workflows_256: + type: WorkflowTransition +WorkflowTransitions_256: new_status_id: 1 role_id: 3 old_status_id: 4 id: 256 tracker_id: 3 -workflows_068: + type: WorkflowTransition +WorkflowTransitions_068: new_status_id: 4 role_id: 3 old_status_id: 2 id: 68 tracker_id: 1 -workflows_094: + type: WorkflowTransition +WorkflowTransitions_094: new_status_id: 5 role_id: 1 old_status_id: 1 id: 94 tracker_id: 2 -workflows_149: + type: WorkflowTransition +WorkflowTransitions_149: new_status_id: 4 role_id: 2 old_status_id: 6 id: 149 tracker_id: 2 -workflows_175: + type: WorkflowTransition +WorkflowTransitions_175: new_status_id: 6 role_id: 3 old_status_id: 5 id: 175 tracker_id: 2 -workflows_257: + type: WorkflowTransition +WorkflowTransitions_257: new_status_id: 2 role_id: 3 old_status_id: 4 id: 257 tracker_id: 3 -workflows_069: + type: WorkflowTransition +WorkflowTransitions_069: new_status_id: 5 role_id: 3 old_status_id: 2 id: 69 tracker_id: 1 -workflows_095: + type: WorkflowTransition +WorkflowTransitions_095: new_status_id: 6 role_id: 1 old_status_id: 1 id: 95 tracker_id: 2 -workflows_176: + type: WorkflowTransition +WorkflowTransitions_176: new_status_id: 1 role_id: 3 old_status_id: 6 id: 176 tracker_id: 2 -workflows_258: + type: WorkflowTransition +WorkflowTransitions_258: new_status_id: 3 role_id: 3 old_status_id: 4 id: 258 tracker_id: 3 -workflows_096: + type: WorkflowTransition +WorkflowTransitions_096: new_status_id: 1 role_id: 1 old_status_id: 2 id: 96 tracker_id: 2 -workflows_177: + type: WorkflowTransition +WorkflowTransitions_177: new_status_id: 2 role_id: 3 old_status_id: 6 id: 177 tracker_id: 2 -workflows_259: + type: WorkflowTransition +WorkflowTransitions_259: new_status_id: 5 role_id: 3 old_status_id: 4 id: 259 tracker_id: 3 -workflows_097: + type: WorkflowTransition +WorkflowTransitions_097: new_status_id: 3 role_id: 1 old_status_id: 2 id: 97 tracker_id: 2 -workflows_178: + type: WorkflowTransition +WorkflowTransitions_178: new_status_id: 3 role_id: 3 old_status_id: 6 id: 178 tracker_id: 2 -workflows_098: + type: WorkflowTransition +WorkflowTransitions_098: new_status_id: 4 role_id: 1 old_status_id: 2 id: 98 tracker_id: 2 -workflows_179: + type: WorkflowTransition +WorkflowTransitions_179: new_status_id: 4 role_id: 3 old_status_id: 6 id: 179 tracker_id: 2 -workflows_099: + type: WorkflowTransition +WorkflowTransitions_099: new_status_id: 5 role_id: 1 old_status_id: 2 id: 99 tracker_id: 2 -workflows_100: + type: WorkflowTransition +WorkflowTransitions_100: new_status_id: 6 role_id: 1 old_status_id: 2 id: 100 tracker_id: 2 -workflows_020: + type: WorkflowTransition +WorkflowTransitions_020: new_status_id: 6 role_id: 1 old_status_id: 4 id: 20 tracker_id: 1 -workflows_101: + type: WorkflowTransition +WorkflowTransitions_101: new_status_id: 1 role_id: 1 old_status_id: 3 id: 101 tracker_id: 2 -workflows_021: + type: WorkflowTransition +WorkflowTransitions_021: new_status_id: 1 role_id: 1 old_status_id: 5 id: 21 tracker_id: 1 -workflows_102: + type: WorkflowTransition +WorkflowTransitions_102: new_status_id: 2 role_id: 1 old_status_id: 3 id: 102 tracker_id: 2 -workflows_210: + type: WorkflowTransition +WorkflowTransitions_210: new_status_id: 5 role_id: 1 old_status_id: 6 id: 210 tracker_id: 3 -workflows_022: + type: WorkflowTransition +WorkflowTransitions_022: new_status_id: 2 role_id: 1 old_status_id: 5 id: 22 tracker_id: 1 -workflows_103: + type: WorkflowTransition +WorkflowTransitions_103: new_status_id: 4 role_id: 1 old_status_id: 3 id: 103 tracker_id: 2 -workflows_023: + type: WorkflowTransition +WorkflowTransitions_023: new_status_id: 3 role_id: 1 old_status_id: 5 id: 23 tracker_id: 1 -workflows_104: + type: WorkflowTransition +WorkflowTransitions_104: new_status_id: 5 role_id: 1 old_status_id: 3 id: 104 tracker_id: 2 -workflows_130: + type: WorkflowTransition +WorkflowTransitions_130: new_status_id: 6 role_id: 2 old_status_id: 2 id: 130 tracker_id: 2 -workflows_211: + type: WorkflowTransition +WorkflowTransitions_211: new_status_id: 2 role_id: 2 old_status_id: 1 id: 211 tracker_id: 3 -workflows_024: + type: WorkflowTransition +WorkflowTransitions_024: new_status_id: 4 role_id: 1 old_status_id: 5 id: 24 tracker_id: 1 -workflows_050: + type: WorkflowTransition +WorkflowTransitions_050: new_status_id: 6 role_id: 2 old_status_id: 4 id: 50 tracker_id: 1 -workflows_105: + type: WorkflowTransition +WorkflowTransitions_105: new_status_id: 6 role_id: 1 old_status_id: 3 id: 105 tracker_id: 2 -workflows_131: + type: WorkflowTransition +WorkflowTransitions_131: new_status_id: 1 role_id: 2 old_status_id: 3 id: 131 tracker_id: 2 -workflows_212: + type: WorkflowTransition +WorkflowTransitions_212: new_status_id: 3 role_id: 2 old_status_id: 1 id: 212 tracker_id: 3 -workflows_025: + type: WorkflowTransition +WorkflowTransitions_025: new_status_id: 6 role_id: 1 old_status_id: 5 id: 25 tracker_id: 1 -workflows_051: + type: WorkflowTransition +WorkflowTransitions_051: new_status_id: 1 role_id: 2 old_status_id: 5 id: 51 tracker_id: 1 -workflows_106: + type: WorkflowTransition +WorkflowTransitions_106: new_status_id: 1 role_id: 1 old_status_id: 4 id: 106 tracker_id: 2 -workflows_132: + type: WorkflowTransition +WorkflowTransitions_132: new_status_id: 2 role_id: 2 old_status_id: 3 id: 132 tracker_id: 2 -workflows_213: + type: WorkflowTransition +WorkflowTransitions_213: new_status_id: 4 role_id: 2 old_status_id: 1 id: 213 tracker_id: 3 -workflows_240: + type: WorkflowTransition +WorkflowTransitions_240: new_status_id: 5 role_id: 2 old_status_id: 6 id: 240 tracker_id: 3 -workflows_026: + type: WorkflowTransition +WorkflowTransitions_026: new_status_id: 1 role_id: 1 old_status_id: 6 id: 26 tracker_id: 1 -workflows_052: + type: WorkflowTransition +WorkflowTransitions_052: new_status_id: 2 role_id: 2 old_status_id: 5 id: 52 tracker_id: 1 -workflows_107: + type: WorkflowTransition +WorkflowTransitions_107: new_status_id: 2 role_id: 1 old_status_id: 4 id: 107 tracker_id: 2 -workflows_133: + type: WorkflowTransition +WorkflowTransitions_133: new_status_id: 4 role_id: 2 old_status_id: 3 id: 133 tracker_id: 2 -workflows_214: + type: WorkflowTransition +WorkflowTransitions_214: new_status_id: 5 role_id: 2 old_status_id: 1 id: 214 tracker_id: 3 -workflows_241: + type: WorkflowTransition +WorkflowTransitions_241: new_status_id: 2 role_id: 3 old_status_id: 1 id: 241 tracker_id: 3 -workflows_027: + type: WorkflowTransition +WorkflowTransitions_027: new_status_id: 2 role_id: 1 old_status_id: 6 id: 27 tracker_id: 1 -workflows_053: + type: WorkflowTransition +WorkflowTransitions_053: new_status_id: 3 role_id: 2 old_status_id: 5 id: 53 tracker_id: 1 -workflows_080: + type: WorkflowTransition +WorkflowTransitions_080: new_status_id: 6 role_id: 3 old_status_id: 4 id: 80 tracker_id: 1 -workflows_108: + type: WorkflowTransition +WorkflowTransitions_108: new_status_id: 3 role_id: 1 old_status_id: 4 id: 108 tracker_id: 2 -workflows_134: + type: WorkflowTransition +WorkflowTransitions_134: new_status_id: 5 role_id: 2 old_status_id: 3 id: 134 tracker_id: 2 -workflows_160: + type: WorkflowTransition +WorkflowTransitions_160: new_status_id: 6 role_id: 3 old_status_id: 2 id: 160 tracker_id: 2 -workflows_215: + type: WorkflowTransition +WorkflowTransitions_215: new_status_id: 6 role_id: 2 old_status_id: 1 id: 215 tracker_id: 3 -workflows_242: + type: WorkflowTransition +WorkflowTransitions_242: new_status_id: 3 role_id: 3 old_status_id: 1 id: 242 tracker_id: 3 -workflows_028: + type: WorkflowTransition +WorkflowTransitions_028: new_status_id: 3 role_id: 1 old_status_id: 6 id: 28 tracker_id: 1 -workflows_054: + type: WorkflowTransition +WorkflowTransitions_054: new_status_id: 4 role_id: 2 old_status_id: 5 id: 54 tracker_id: 1 -workflows_081: + type: WorkflowTransition +WorkflowTransitions_081: new_status_id: 1 role_id: 3 old_status_id: 5 id: 81 tracker_id: 1 -workflows_109: + type: WorkflowTransition +WorkflowTransitions_109: new_status_id: 5 role_id: 1 old_status_id: 4 id: 109 tracker_id: 2 -workflows_135: + type: WorkflowTransition +WorkflowTransitions_135: new_status_id: 6 role_id: 2 old_status_id: 3 id: 135 tracker_id: 2 -workflows_161: + type: WorkflowTransition +WorkflowTransitions_161: new_status_id: 1 role_id: 3 old_status_id: 3 id: 161 tracker_id: 2 -workflows_216: + type: WorkflowTransition +WorkflowTransitions_216: new_status_id: 1 role_id: 2 old_status_id: 2 id: 216 tracker_id: 3 -workflows_243: + type: WorkflowTransition +WorkflowTransitions_243: new_status_id: 4 role_id: 3 old_status_id: 1 id: 243 tracker_id: 3 -workflows_029: + type: WorkflowTransition +WorkflowTransitions_029: new_status_id: 4 role_id: 1 old_status_id: 6 id: 29 tracker_id: 1 -workflows_055: + type: WorkflowTransition +WorkflowTransitions_055: new_status_id: 6 role_id: 2 old_status_id: 5 id: 55 tracker_id: 1 -workflows_082: + type: WorkflowTransition +WorkflowTransitions_082: new_status_id: 2 role_id: 3 old_status_id: 5 id: 82 tracker_id: 1 -workflows_136: + type: WorkflowTransition +WorkflowTransitions_136: new_status_id: 1 role_id: 2 old_status_id: 4 id: 136 tracker_id: 2 -workflows_162: + type: WorkflowTransition +WorkflowTransitions_162: new_status_id: 2 role_id: 3 old_status_id: 3 id: 162 tracker_id: 2 -workflows_217: + type: WorkflowTransition +WorkflowTransitions_217: new_status_id: 3 role_id: 2 old_status_id: 2 id: 217 tracker_id: 3 -workflows_270: + type: WorkflowTransition +WorkflowTransitions_270: new_status_id: 5 role_id: 3 old_status_id: 6 id: 270 tracker_id: 3 -workflows_244: + type: WorkflowTransition +WorkflowTransitions_244: new_status_id: 5 role_id: 3 old_status_id: 1 id: 244 tracker_id: 3 -workflows_056: + type: WorkflowTransition +WorkflowTransitions_056: new_status_id: 1 role_id: 2 old_status_id: 6 id: 56 tracker_id: 1 -workflows_137: + type: WorkflowTransition +WorkflowTransitions_137: new_status_id: 2 role_id: 2 old_status_id: 4 id: 137 tracker_id: 2 -workflows_163: + type: WorkflowTransition +WorkflowTransitions_163: new_status_id: 4 role_id: 3 old_status_id: 3 id: 163 tracker_id: 2 -workflows_190: + type: WorkflowTransition +WorkflowTransitions_190: new_status_id: 6 role_id: 1 old_status_id: 2 id: 190 tracker_id: 3 -workflows_218: + type: WorkflowTransition +WorkflowTransitions_218: new_status_id: 4 role_id: 2 old_status_id: 2 id: 218 tracker_id: 3 -workflows_245: + type: WorkflowTransition +WorkflowTransitions_245: new_status_id: 6 role_id: 3 old_status_id: 1 id: 245 tracker_id: 3 -workflows_057: + type: WorkflowTransition +WorkflowTransitions_057: new_status_id: 2 role_id: 2 old_status_id: 6 id: 57 tracker_id: 1 -workflows_083: + type: WorkflowTransition +WorkflowTransitions_083: new_status_id: 3 role_id: 3 old_status_id: 5 id: 83 tracker_id: 1 -workflows_138: + type: WorkflowTransition +WorkflowTransitions_138: new_status_id: 3 role_id: 2 old_status_id: 4 id: 138 tracker_id: 2 -workflows_164: + type: WorkflowTransition +WorkflowTransitions_164: new_status_id: 5 role_id: 3 old_status_id: 3 id: 164 tracker_id: 2 -workflows_191: + type: WorkflowTransition +WorkflowTransitions_191: new_status_id: 1 role_id: 1 old_status_id: 3 id: 191 tracker_id: 3 -workflows_219: + type: WorkflowTransition +WorkflowTransitions_219: new_status_id: 5 role_id: 2 old_status_id: 2 id: 219 tracker_id: 3 -workflows_246: + type: WorkflowTransition +WorkflowTransitions_246: new_status_id: 1 role_id: 3 old_status_id: 2 id: 246 tracker_id: 3 -workflows_058: + type: WorkflowTransition +WorkflowTransitions_058: new_status_id: 3 role_id: 2 old_status_id: 6 id: 58 tracker_id: 1 -workflows_084: + type: WorkflowTransition +WorkflowTransitions_084: new_status_id: 4 role_id: 3 old_status_id: 5 id: 84 tracker_id: 1 -workflows_139: + type: WorkflowTransition +WorkflowTransitions_139: new_status_id: 5 role_id: 2 old_status_id: 4 id: 139 tracker_id: 2 -workflows_165: + type: WorkflowTransition +WorkflowTransitions_165: new_status_id: 6 role_id: 3 old_status_id: 3 id: 165 tracker_id: 2 -workflows_192: + type: WorkflowTransition +WorkflowTransitions_192: new_status_id: 2 role_id: 1 old_status_id: 3 id: 192 tracker_id: 3 -workflows_247: + type: WorkflowTransition +WorkflowTransitions_247: new_status_id: 3 role_id: 3 old_status_id: 2 id: 247 tracker_id: 3 -workflows_059: + type: WorkflowTransition +WorkflowTransitions_059: new_status_id: 4 role_id: 2 old_status_id: 6 id: 59 tracker_id: 1 -workflows_085: + type: WorkflowTransition +WorkflowTransitions_085: new_status_id: 6 role_id: 3 old_status_id: 5 id: 85 tracker_id: 1 -workflows_166: + type: WorkflowTransition +WorkflowTransitions_166: new_status_id: 1 role_id: 3 old_status_id: 4 id: 166 tracker_id: 2 -workflows_248: + type: WorkflowTransition +WorkflowTransitions_248: new_status_id: 4 role_id: 3 old_status_id: 2 id: 248 tracker_id: 3 -workflows_086: + type: WorkflowTransition +WorkflowTransitions_086: new_status_id: 1 role_id: 3 old_status_id: 6 id: 86 tracker_id: 1 -workflows_167: + type: WorkflowTransition +WorkflowTransitions_167: new_status_id: 2 role_id: 3 old_status_id: 4 id: 167 tracker_id: 2 -workflows_193: + type: WorkflowTransition +WorkflowTransitions_193: new_status_id: 4 role_id: 1 old_status_id: 3 id: 193 tracker_id: 3 -workflows_249: + type: WorkflowTransition +WorkflowTransitions_249: new_status_id: 5 role_id: 3 old_status_id: 2 id: 249 tracker_id: 3 -workflows_087: + type: WorkflowTransition +WorkflowTransitions_087: new_status_id: 2 role_id: 3 old_status_id: 6 id: 87 tracker_id: 1 -workflows_168: + type: WorkflowTransition +WorkflowTransitions_168: new_status_id: 3 role_id: 3 old_status_id: 4 id: 168 tracker_id: 2 -workflows_194: + type: WorkflowTransition +WorkflowTransitions_194: new_status_id: 5 role_id: 1 old_status_id: 3 id: 194 tracker_id: 3 -workflows_088: + type: WorkflowTransition +WorkflowTransitions_088: new_status_id: 3 role_id: 3 old_status_id: 6 id: 88 tracker_id: 1 -workflows_169: + type: WorkflowTransition +WorkflowTransitions_169: new_status_id: 5 role_id: 3 old_status_id: 4 id: 169 tracker_id: 2 -workflows_195: + type: WorkflowTransition +WorkflowTransitions_195: new_status_id: 6 role_id: 1 old_status_id: 3 id: 195 tracker_id: 3 -workflows_089: + type: WorkflowTransition +WorkflowTransitions_089: new_status_id: 4 role_id: 3 old_status_id: 6 id: 89 tracker_id: 1 -workflows_196: + type: WorkflowTransition +WorkflowTransitions_196: new_status_id: 1 role_id: 1 old_status_id: 4 id: 196 tracker_id: 3 -workflows_197: + type: WorkflowTransition +WorkflowTransitions_197: new_status_id: 2 role_id: 1 old_status_id: 4 id: 197 tracker_id: 3 -workflows_198: + type: WorkflowTransition +WorkflowTransitions_198: new_status_id: 3 role_id: 1 old_status_id: 4 id: 198 tracker_id: 3 -workflows_199: + type: WorkflowTransition +WorkflowTransitions_199: new_status_id: 5 role_id: 1 old_status_id: 4 id: 199 tracker_id: 3 -workflows_010: + type: WorkflowTransition +WorkflowTransitions_010: new_status_id: 6 role_id: 1 old_status_id: 2 id: 10 tracker_id: 1 -workflows_011: + type: WorkflowTransition +WorkflowTransitions_011: new_status_id: 1 role_id: 1 old_status_id: 3 id: 11 tracker_id: 1 -workflows_012: + type: WorkflowTransition +WorkflowTransitions_012: new_status_id: 2 role_id: 1 old_status_id: 3 id: 12 tracker_id: 1 -workflows_200: + type: WorkflowTransition +WorkflowTransitions_200: new_status_id: 6 role_id: 1 old_status_id: 4 id: 200 tracker_id: 3 -workflows_013: + type: WorkflowTransition +WorkflowTransitions_013: new_status_id: 4 role_id: 1 old_status_id: 3 id: 13 tracker_id: 1 -workflows_120: + type: WorkflowTransition +WorkflowTransitions_120: new_status_id: 5 role_id: 1 old_status_id: 6 id: 120 tracker_id: 2 -workflows_201: + type: WorkflowTransition +WorkflowTransitions_201: new_status_id: 1 role_id: 1 old_status_id: 5 id: 201 tracker_id: 3 -workflows_040: + type: WorkflowTransition +WorkflowTransitions_040: new_status_id: 6 role_id: 2 old_status_id: 2 id: 40 tracker_id: 1 -workflows_121: + type: WorkflowTransition +WorkflowTransitions_121: new_status_id: 2 role_id: 2 old_status_id: 1 id: 121 tracker_id: 2 -workflows_202: + type: WorkflowTransition +WorkflowTransitions_202: new_status_id: 2 role_id: 1 old_status_id: 5 id: 202 tracker_id: 3 -workflows_014: + type: WorkflowTransition +WorkflowTransitions_014: new_status_id: 5 role_id: 1 old_status_id: 3 id: 14 tracker_id: 1 -workflows_041: + type: WorkflowTransition +WorkflowTransitions_041: new_status_id: 1 role_id: 2 old_status_id: 3 id: 41 tracker_id: 1 -workflows_122: + type: WorkflowTransition +WorkflowTransitions_122: new_status_id: 3 role_id: 2 old_status_id: 1 id: 122 tracker_id: 2 -workflows_203: + type: WorkflowTransition +WorkflowTransitions_203: new_status_id: 3 role_id: 1 old_status_id: 5 id: 203 tracker_id: 3 -workflows_015: + type: WorkflowTransition +WorkflowTransitions_015: new_status_id: 6 role_id: 1 old_status_id: 3 id: 15 tracker_id: 1 -workflows_230: + type: WorkflowTransition +WorkflowTransitions_230: new_status_id: 6 role_id: 2 old_status_id: 4 id: 230 tracker_id: 3 -workflows_123: + type: WorkflowTransition +WorkflowTransitions_123: new_status_id: 4 role_id: 2 old_status_id: 1 id: 123 tracker_id: 2 -workflows_204: + type: WorkflowTransition +WorkflowTransitions_204: new_status_id: 4 role_id: 1 old_status_id: 5 id: 204 tracker_id: 3 -workflows_016: + type: WorkflowTransition +WorkflowTransitions_016: new_status_id: 1 role_id: 1 old_status_id: 4 id: 16 tracker_id: 1 -workflows_042: + type: WorkflowTransition +WorkflowTransitions_042: new_status_id: 2 role_id: 2 old_status_id: 3 id: 42 tracker_id: 1 -workflows_231: + type: WorkflowTransition +WorkflowTransitions_231: new_status_id: 1 role_id: 2 old_status_id: 5 id: 231 tracker_id: 3 -workflows_070: + type: WorkflowTransition +WorkflowTransitions_070: new_status_id: 6 role_id: 3 old_status_id: 2 id: 70 tracker_id: 1 -workflows_124: + type: WorkflowTransition +WorkflowTransitions_124: new_status_id: 5 role_id: 2 old_status_id: 1 id: 124 tracker_id: 2 -workflows_150: + type: WorkflowTransition +WorkflowTransitions_150: new_status_id: 5 role_id: 2 old_status_id: 6 id: 150 tracker_id: 2 -workflows_205: + type: WorkflowTransition +WorkflowTransitions_205: new_status_id: 6 role_id: 1 old_status_id: 5 id: 205 tracker_id: 3 -workflows_017: + type: WorkflowTransition +WorkflowTransitions_017: new_status_id: 2 role_id: 1 old_status_id: 4 id: 17 tracker_id: 1 -workflows_043: + type: WorkflowTransition +WorkflowTransitions_043: new_status_id: 4 role_id: 2 old_status_id: 3 id: 43 tracker_id: 1 -workflows_232: + type: WorkflowTransition +WorkflowTransitions_232: new_status_id: 2 role_id: 2 old_status_id: 5 id: 232 tracker_id: 3 -workflows_125: + type: WorkflowTransition +WorkflowTransitions_125: new_status_id: 6 role_id: 2 old_status_id: 1 id: 125 tracker_id: 2 -workflows_151: + type: WorkflowTransition +WorkflowTransitions_151: new_status_id: 2 role_id: 3 old_status_id: 1 id: 151 tracker_id: 2 -workflows_206: + type: WorkflowTransition +WorkflowTransitions_206: new_status_id: 1 role_id: 1 old_status_id: 6 id: 206 tracker_id: 3 -workflows_018: + type: WorkflowTransition +WorkflowTransitions_018: new_status_id: 3 role_id: 1 old_status_id: 4 id: 18 tracker_id: 1 -workflows_044: + type: WorkflowTransition +WorkflowTransitions_044: new_status_id: 5 role_id: 2 old_status_id: 3 id: 44 tracker_id: 1 -workflows_071: + type: WorkflowTransition +WorkflowTransitions_071: new_status_id: 1 role_id: 3 old_status_id: 3 id: 71 tracker_id: 1 -workflows_233: + type: WorkflowTransition +WorkflowTransitions_233: new_status_id: 3 role_id: 2 old_status_id: 5 id: 233 tracker_id: 3 -workflows_126: + type: WorkflowTransition +WorkflowTransitions_126: new_status_id: 1 role_id: 2 old_status_id: 2 id: 126 tracker_id: 2 -workflows_152: + type: WorkflowTransition +WorkflowTransitions_152: new_status_id: 3 role_id: 3 old_status_id: 1 id: 152 tracker_id: 2 -workflows_207: + type: WorkflowTransition +WorkflowTransitions_207: new_status_id: 2 role_id: 1 old_status_id: 6 id: 207 tracker_id: 3 -workflows_019: + type: WorkflowTransition +WorkflowTransitions_019: new_status_id: 5 role_id: 1 old_status_id: 4 id: 19 tracker_id: 1 -workflows_045: + type: WorkflowTransition +WorkflowTransitions_045: new_status_id: 6 role_id: 2 old_status_id: 3 id: 45 tracker_id: 1 -workflows_260: + type: WorkflowTransition +WorkflowTransitions_260: new_status_id: 6 role_id: 3 old_status_id: 4 id: 260 tracker_id: 3 -workflows_234: + type: WorkflowTransition +WorkflowTransitions_234: new_status_id: 4 role_id: 2 old_status_id: 5 id: 234 tracker_id: 3 -workflows_127: + type: WorkflowTransition +WorkflowTransitions_127: new_status_id: 3 role_id: 2 old_status_id: 2 id: 127 tracker_id: 2 -workflows_153: + type: WorkflowTransition +WorkflowTransitions_153: new_status_id: 4 role_id: 3 old_status_id: 1 id: 153 tracker_id: 2 -workflows_180: + type: WorkflowTransition +WorkflowTransitions_180: new_status_id: 5 role_id: 3 old_status_id: 6 id: 180 tracker_id: 2 -workflows_208: + type: WorkflowTransition +WorkflowTransitions_208: new_status_id: 3 role_id: 1 old_status_id: 6 id: 208 tracker_id: 3 -workflows_046: + type: WorkflowTransition +WorkflowTransitions_046: new_status_id: 1 role_id: 2 old_status_id: 4 id: 46 tracker_id: 1 -workflows_072: + type: WorkflowTransition +WorkflowTransitions_072: new_status_id: 2 role_id: 3 old_status_id: 3 id: 72 tracker_id: 1 -workflows_261: + type: WorkflowTransition +WorkflowTransitions_261: new_status_id: 1 role_id: 3 old_status_id: 5 id: 261 tracker_id: 3 -workflows_235: + type: WorkflowTransition +WorkflowTransitions_235: new_status_id: 6 role_id: 2 old_status_id: 5 id: 235 tracker_id: 3 -workflows_154: + type: WorkflowTransition +WorkflowTransitions_154: new_status_id: 5 role_id: 3 old_status_id: 1 id: 154 tracker_id: 2 -workflows_181: + type: WorkflowTransition +WorkflowTransitions_181: new_status_id: 2 role_id: 1 old_status_id: 1 id: 181 tracker_id: 3 -workflows_209: + type: WorkflowTransition +WorkflowTransitions_209: new_status_id: 4 role_id: 1 old_status_id: 6 id: 209 tracker_id: 3 -workflows_047: + type: WorkflowTransition +WorkflowTransitions_047: new_status_id: 2 role_id: 2 old_status_id: 4 id: 47 tracker_id: 1 -workflows_073: + type: WorkflowTransition +WorkflowTransitions_073: new_status_id: 4 role_id: 3 old_status_id: 3 id: 73 tracker_id: 1 -workflows_128: + type: WorkflowTransition +WorkflowTransitions_128: new_status_id: 4 role_id: 2 old_status_id: 2 id: 128 tracker_id: 2 -workflows_262: + type: WorkflowTransition +WorkflowTransitions_262: new_status_id: 2 role_id: 3 old_status_id: 5 id: 262 tracker_id: 3 -workflows_236: + type: WorkflowTransition +WorkflowTransitions_236: new_status_id: 1 role_id: 2 old_status_id: 6 id: 236 tracker_id: 3 -workflows_155: + type: WorkflowTransition +WorkflowTransitions_155: new_status_id: 6 role_id: 3 old_status_id: 1 id: 155 tracker_id: 2 -workflows_048: + type: WorkflowTransition +WorkflowTransitions_048: new_status_id: 3 role_id: 2 old_status_id: 4 id: 48 tracker_id: 1 -workflows_074: + type: WorkflowTransition +WorkflowTransitions_074: new_status_id: 5 role_id: 3 old_status_id: 3 id: 74 tracker_id: 1 -workflows_129: + type: WorkflowTransition +WorkflowTransitions_129: new_status_id: 5 role_id: 2 old_status_id: 2 id: 129 tracker_id: 2 -workflows_263: + type: WorkflowTransition +WorkflowTransitions_263: new_status_id: 3 role_id: 3 old_status_id: 5 id: 263 tracker_id: 3 -workflows_237: + type: WorkflowTransition +WorkflowTransitions_237: new_status_id: 2 role_id: 2 old_status_id: 6 id: 237 tracker_id: 3 -workflows_182: + type: WorkflowTransition +WorkflowTransitions_182: new_status_id: 3 role_id: 1 old_status_id: 1 id: 182 tracker_id: 3 -workflows_049: + type: WorkflowTransition +WorkflowTransitions_049: new_status_id: 5 role_id: 2 old_status_id: 4 id: 49 tracker_id: 1 -workflows_075: + type: WorkflowTransition +WorkflowTransitions_075: new_status_id: 6 role_id: 3 old_status_id: 3 id: 75 tracker_id: 1 -workflows_156: + type: WorkflowTransition +WorkflowTransitions_156: new_status_id: 1 role_id: 3 old_status_id: 2 id: 156 tracker_id: 2 -workflows_264: + type: WorkflowTransition +WorkflowTransitions_264: new_status_id: 4 role_id: 3 old_status_id: 5 id: 264 tracker_id: 3 -workflows_238: + type: WorkflowTransition +WorkflowTransitions_238: new_status_id: 3 role_id: 2 old_status_id: 6 id: 238 tracker_id: 3 -workflows_183: + type: WorkflowTransition +WorkflowTransitions_183: new_status_id: 4 role_id: 1 old_status_id: 1 id: 183 tracker_id: 3 -workflows_076: + type: WorkflowTransition +WorkflowTransitions_076: new_status_id: 1 role_id: 3 old_status_id: 4 id: 76 tracker_id: 1 -workflows_157: + type: WorkflowTransition +WorkflowTransitions_157: new_status_id: 3 role_id: 3 old_status_id: 2 id: 157 tracker_id: 2 -workflows_265: + type: WorkflowTransition +WorkflowTransitions_265: new_status_id: 6 role_id: 3 old_status_id: 5 id: 265 tracker_id: 3 -workflows_239: + type: WorkflowTransition +WorkflowTransitions_239: new_status_id: 4 role_id: 2 old_status_id: 6 id: 239 tracker_id: 3 -workflows_077: + type: WorkflowTransition +WorkflowTransitions_077: new_status_id: 2 role_id: 3 old_status_id: 4 id: 77 tracker_id: 1 -workflows_158: + type: WorkflowTransition +WorkflowTransitions_158: new_status_id: 4 role_id: 3 old_status_id: 2 id: 158 tracker_id: 2 -workflows_184: + type: WorkflowTransition +WorkflowTransitions_184: new_status_id: 5 role_id: 1 old_status_id: 1 id: 184 tracker_id: 3 -workflows_266: + type: WorkflowTransition +WorkflowTransitions_266: new_status_id: 1 role_id: 3 old_status_id: 6 id: 266 tracker_id: 3 -workflows_078: + type: WorkflowTransition +WorkflowTransitions_078: new_status_id: 3 role_id: 3 old_status_id: 4 id: 78 tracker_id: 1 -workflows_159: + type: WorkflowTransition +WorkflowTransitions_159: new_status_id: 5 role_id: 3 old_status_id: 2 id: 159 tracker_id: 2 -workflows_185: + type: WorkflowTransition +WorkflowTransitions_185: new_status_id: 6 role_id: 1 old_status_id: 1 id: 185 tracker_id: 3 -workflows_267: + type: WorkflowTransition +WorkflowTransitions_267: new_status_id: 2 role_id: 3 old_status_id: 6 id: 267 tracker_id: 3 -workflows_079: + type: WorkflowTransition +WorkflowTransitions_079: new_status_id: 5 role_id: 3 old_status_id: 4 id: 79 tracker_id: 1 -workflows_186: + type: WorkflowTransition +WorkflowTransitions_186: new_status_id: 1 role_id: 1 old_status_id: 2 id: 186 tracker_id: 3 -workflows_268: + type: WorkflowTransition +WorkflowTransitions_268: new_status_id: 3 role_id: 3 old_status_id: 6 id: 268 tracker_id: 3 -workflows_187: + type: WorkflowTransition +WorkflowTransitions_187: new_status_id: 3 role_id: 1 old_status_id: 2 id: 187 tracker_id: 3 -workflows_269: + type: WorkflowTransition +WorkflowTransitions_269: new_status_id: 4 role_id: 3 old_status_id: 6 id: 269 tracker_id: 3 -workflows_188: + type: WorkflowTransition +WorkflowTransitions_188: new_status_id: 4 role_id: 1 old_status_id: 2 id: 188 tracker_id: 3 + type: WorkflowTransition diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index bb80604c6..26b9dcb0b 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -823,7 +823,7 @@ class IssuesControllerTest < ActionController::TestCase def test_show_should_display_update_form_with_minimal_permissions Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes] - Workflow.delete_all :role_id => 1 + WorkflowTransition.delete_all :role_id => 1 @request.session[:user_id] = 2 get :show, :id => 1 @@ -1300,7 +1300,7 @@ class IssuesControllerTest < ActionController::TestCase def test_get_new_with_minimal_permissions Role.find(1).update_attribute :permissions, [:add_issues] - Workflow.delete_all :role_id => 1 + WorkflowTransition.delete_all :role_id => 1 @request.session[:user_id] = 2 get :new, :project_id => 1, :tracker_id => 1 @@ -1426,6 +1426,50 @@ class IssuesControllerTest < ActionController::TestCase :attributes => {:name => 'issue[custom_field_values][2]', :value => 'Custom field value'} end + def test_get_new_should_mark_required_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required') + @request.session[:user_id] = 2 + + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'label[for=issue_start_date]' do + assert_select 'span[class=required]', 0 + end + assert_select 'label[for=issue_due_date]' do + assert_select 'span[class=required]' + end + assert_select 'label[for=?]', "issue_custom_field_values_#{cf1.id}" do + assert_select 'span[class=required]', 0 + end + assert_select 'label[for=?]', "issue_custom_field_values_#{cf2.id}" do + assert_select 'span[class=required]' + end + end + + def test_get_new_should_not_display_readonly_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly') + @request.session[:user_id] = 2 + + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?]', 'issue[due_date]', 0 + assert_select 'input[name=?]', "issue[custom_field_values][#{cf1.id}]" + assert_select 'input[name=?]', "issue[custom_field_values][#{cf2.id}]", 0 + end + def test_get_new_without_tracker_id @request.session[:user_id] = 2 get :new, :project_id => 1 @@ -1463,7 +1507,7 @@ class IssuesControllerTest < ActionController::TestCase :description => 'This is the description', :priority_id => 5} assert_response :success - assert_template 'attributes' + assert_template 'form' issue = assigns(:issue) assert_kind_of Issue, issue @@ -1474,10 +1518,10 @@ class IssuesControllerTest < ActionController::TestCase def test_update_new_form_should_propose_transitions_based_on_initial_status @request.session[:user_id] = 2 - Workflow.delete_all - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4) + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4) xhr :post, :new, :project_id => 1, :issue => {:tracker_id => 1, @@ -1678,6 +1722,58 @@ class IssuesControllerTest < ActionController::TestCase assert_error_tag :content => /Database can't be blank/ end + def test_create_should_validate_required_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required') + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + post :create, :project_id => 1, :issue => { + :tracker_id => 2, + :status_id => 1, + :subject => 'Test', + :start_date => '', + :due_date => '', + :custom_field_values => {cf1.id.to_s => '', cf2.id.to_s => ''} + } + assert_response :success + assert_template 'new' + end + + assert_error_tag :content => /Due date can't be blank/i + assert_error_tag :content => /Bar can't be blank/i + end + + def test_create_should_ignore_readonly_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly') + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, :issue => { + :tracker_id => 2, + :status_id => 1, + :subject => 'Test', + :start_date => '2012-07-14', + :due_date => '2012-07-16', + :custom_field_values => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'} + } + assert_response 302 + end + + issue = Issue.first(:order => 'id DESC') + assert_equal Date.parse('2012-07-14'), issue.start_date + assert_nil issue.due_date + assert_equal 'value1', issue.custom_field_value(cf1) + assert_nil issue.custom_field_value(cf2) + end + def test_post_create_with_watchers @request.session[:user_id] = 2 ActionMailer::Base.deliveries.clear @@ -1917,7 +2013,7 @@ class IssuesControllerTest < ActionController::TestCase context "without workflow privilege" do setup do - Workflow.delete_all(["role_id = ?", Role.anonymous.id]) + WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id]) Role.anonymous.add_permission! :add_issues, :add_issue_notes end @@ -1976,9 +2072,9 @@ class IssuesControllerTest < ActionController::TestCase context "with workflow privilege" do setup do - Workflow.delete_all(["role_id = ?", Role.anonymous.id]) - Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3) - Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4) + WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id]) + WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3) + WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4) Role.anonymous.add_permission! :add_issues, :add_issue_notes end @@ -2286,7 +2382,7 @@ class IssuesControllerTest < ActionController::TestCase :description => 'This is the description', :priority_id => 5} assert_response :success - assert_template 'attributes' + assert_template 'form' issue = assigns(:issue) assert_kind_of Issue, issue @@ -2298,10 +2394,10 @@ class IssuesControllerTest < ActionController::TestCase def test_update_edit_form_should_propose_transitions_based_on_initial_status @request.session[:user_id] = 2 - Workflow.delete_all - Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1) - Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5) - Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 5, :new_status_id => 4) + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 5, :new_status_id => 4) xhr :put, :new, :project_id => 1, :id => 2, @@ -2317,7 +2413,6 @@ class IssuesControllerTest < ActionController::TestCase @request.session[:user_id] = 2 xhr :put, :new, :project_id => 1, :id => 1, - :project_change => '1', :issue => {:project_id => 2, :tracker_id => 2, :subject => 'This is the test_new issue', @@ -2845,13 +2940,13 @@ class IssuesControllerTest < ActionController::TestCase end def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues - Workflow.delete_all - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 1) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4) - Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1) - Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3) - Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5) + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 1) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5) @request.session[:user_id] = 2 get :bulk_edit, :ids => [1, 2] diff --git a/test/functional/roles_controller_test.rb b/test/functional/roles_controller_test.rb index 03eaf507d..327fad512 100644 --- a/test/functional/roles_controller_test.rb +++ b/test/functional/roles_controller_test.rb @@ -77,7 +77,7 @@ class RolesControllerTest < ActionController::TestCase assert_redirected_to '/roles' role = Role.find_by_name('RoleWithWorkflowCopy') assert_not_nil role - assert_equal Role.find(1).workflows.size, role.workflows.size + assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size end def test_edit diff --git a/test/functional/trackers_controller_test.rb b/test/functional/trackers_controller_test.rb index 7393088ae..4c4dd0d3a 100644 --- a/test/functional/trackers_controller_test.rb +++ b/test/functional/trackers_controller_test.rb @@ -66,7 +66,7 @@ class TrackersControllerTest < ActionController::TestCase assert_equal [1], tracker.project_ids.sort assert_equal Tracker::CORE_FIELDS, tracker.core_fields assert_equal [1, 6], tracker.custom_field_ids.sort - assert_equal 0, tracker.workflows.count + assert_equal 0, tracker.workflow_rules.count end def create_with_disabled_core_fields @@ -86,7 +86,7 @@ class TrackersControllerTest < ActionController::TestCase assert_redirected_to :action => 'index' tracker = Tracker.find_by_name('New tracker') assert_equal 0, tracker.projects.count - assert_equal Tracker.find(1).workflows.count, tracker.workflows.count + assert_equal Tracker.find(1).workflow_rules.count, tracker.workflow_rules.count end def test_create_with_failure diff --git a/test/functional/workflows_controller_test.rb b/test/functional/workflows_controller_test.rb index 5c37afc59..84250d346 100644 --- a/test/functional/workflows_controller_test.rb +++ b/test/functional/workflows_controller_test.rb @@ -37,7 +37,7 @@ class WorkflowsControllerTest < ActionController::TestCase assert_response :success assert_template 'index' - count = Workflow.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2') + count = WorkflowTransition.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2') assert_tag :tag => 'a', :content => count.to_s, :attributes => { :href => '/workflows/edit?role_id=1&tracker_id=2' } end @@ -51,9 +51,9 @@ class WorkflowsControllerTest < ActionController::TestCase end def test_get_edit_with_role_and_tracker - Workflow.delete_all - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3) - Workflow.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5) + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5) get :edit, :role_id => 2, :tracker_id => 1 assert_response :success @@ -79,7 +79,7 @@ class WorkflowsControllerTest < ActionController::TestCase end def test_get_edit_with_role_and_tracker_and_all_statuses - Workflow.delete_all + WorkflowTransition.delete_all get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0' assert_response :success @@ -102,9 +102,9 @@ class WorkflowsControllerTest < ActionController::TestCase } assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1' - assert_equal 3, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) - assert_not_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2}) - assert_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4}) + assert_equal 3, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) + assert_not_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2}) + assert_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4}) end def test_post_edit_with_additional_transitions @@ -115,27 +115,117 @@ class WorkflowsControllerTest < ActionController::TestCase } assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1' - assert_equal 4, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) + assert_equal 4, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) - w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5}) + w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5}) assert ! w.author assert ! w.assignee - w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1}) + w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1}) assert w.author assert ! w.assignee - w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2}) + w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2}) assert ! w.author assert w.assignee - w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4}) + w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4}) assert w.author assert w.assignee end def test_clear_workflow - assert Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0 + assert WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0 post :edit, :role_id => 2, :tracker_id => 1 - assert_equal 0, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) + assert_equal 0, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) + end + + def test_get_permissions + get :permissions + + assert_response :success + assert_template 'permissions' + assert_not_nil assigns(:roles) + assert_not_nil assigns(:trackers) + end + + def test_get_permissions_with_role_and_tracker + WorkflowPermission.delete_all + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required') + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly') + + get :permissions, :role_id => 1, :tracker_id => 2 + assert_response :success + assert_template 'permissions' + + assert_select 'input[name=role_id][value=1]' + assert_select 'input[name=tracker_id][value=2]' + + # Required field + assert_select 'select[name=?]', 'permissions[assigned_to_id][2]' do + assert_select 'option[value=]' + assert_select 'option[value=][selected=selected]', 0 + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=readonly][selected=selected]', 0 + assert_select 'option[value=required]', :text => 'Required' + assert_select 'option[value=required][selected=selected]' + end + + # Read-only field + assert_select 'select[name=?]', 'permissions[fixed_version_id][3]' do + assert_select 'option[value=]' + assert_select 'option[value=][selected=selected]', 0 + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=readonly][selected=selected]' + assert_select 'option[value=required]', :text => 'Required' + assert_select 'option[value=required][selected=selected]', 0 + end + + # Other field + assert_select 'select[name=?]', 'permissions[due_date][3]' do + assert_select 'option[value=]' + assert_select 'option[value=][selected=selected]', 0 + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=readonly][selected=selected]', 0 + assert_select 'option[value=required]', :text => 'Required' + assert_select 'option[value=required][selected=selected]', 0 + end + end + + def test_post_permissions + WorkflowPermission.delete_all + + post :permissions, :role_id => 1, :tracker_id => 2, :permissions => { + 'assigned_to_id' => {'1' => '', '2' => 'readonly', '3' => ''}, + 'fixed_version_id' => {'1' => 'required', '2' => 'readonly', '3' => ''}, + 'due_date' => {'1' => '', '2' => '', '3' => ''}, + } + assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2' + + workflows = WorkflowPermission.all + assert_equal 3, workflows.size + workflows.each do |workflow| + assert_equal 1, workflow.role_id + assert_equal 2, workflow.tracker_id + end + assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'} + assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'} + assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'} + end + + def test_post_permissions_should_clear_permissions + WorkflowPermission.delete_all + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required') + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') + wf1 = WorkflowPermission.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') + wf2 = WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly') + + post :permissions, :role_id => 1, :tracker_id => 2 + assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2' + + workflows = WorkflowPermission.all + assert_equal 2, workflows.size + assert wf1.reload + assert wf2.reload end def test_get_copy @@ -192,7 +282,7 @@ class WorkflowsControllerTest < ActionController::TestCase # Returns an array of status transitions that can be compared def status_transitions(conditions) - Workflow.find(:all, :conditions => conditions, + WorkflowTransition.find(:all, :conditions => conditions, :order => 'tracker_id, role_id, old_status_id, new_status_id').collect {|w| [w.old_status, w.new_status_id]} end end diff --git a/test/unit/issue_status_test.rb b/test/unit/issue_status_test.rb index 6fce33675..f12fbdd3d 100644 --- a/test/unit/issue_status_test.rb +++ b/test/unit/issue_status_test.rb @@ -36,8 +36,8 @@ class IssueStatusTest < ActiveSupport::TestCase assert_difference 'IssueStatus.count', -1 do assert status.destroy end - assert_nil Workflow.first(:conditions => {:old_status_id => status.id}) - assert_nil Workflow.first(:conditions => {:new_status_id => status.id}) + assert_nil WorkflowTransition.first(:conditions => {:old_status_id => status.id}) + assert_nil WorkflowTransition.first(:conditions => {:new_status_id => status.id}) end def test_destroy_status_in_use @@ -70,12 +70,12 @@ class IssueStatusTest < ActiveSupport::TestCase end def test_new_statuses_allowed_to - Workflow.delete_all + WorkflowTransition.delete_all - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true) status = IssueStatus.find(1) role = Role.find(1) tracker = Tracker.find(1) diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index fe8bdf2ac..a89b2b315 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -31,6 +31,10 @@ class IssueTest < ActiveSupport::TestCase include Redmine::I18n + def teardown + User.current = nil + end + def test_create issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, @@ -362,12 +366,12 @@ class IssueTest < ActiveSupport::TestCase end def test_new_statuses_allowed_to - Workflow.delete_all + WorkflowTransition.delete_all - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true) - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true) status = IssueStatus.find(1) role = Role.find(1) tracker = Tracker.find(1) @@ -390,7 +394,7 @@ class IssueTest < ActiveSupport::TestCase admin = User.find(1) issue = Issue.find(1) assert !admin.member_of?(issue.project) - expected_statuses = [issue.status] + Workflow.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort + expected_statuses = [issue.status] + WorkflowTransition.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort assert_equal expected_statuses, issue.new_statuses_allowed_to(admin) end @@ -403,7 +407,7 @@ class IssueTest < ActiveSupport::TestCase assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id) end - def test_safe_attributes_should_not_include_disabled_field + def test_safe_attributes_names_should_not_include_disabled_field tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id)) issue = Issue.new(:tracker => tracker) @@ -435,7 +439,7 @@ class IssueTest < ActiveSupport::TestCase assert_equal Date.parse('2012-07-14'), issue.due_date end - def test_safe_attributes_should_accept_target_tracker_fields + def test_safe_attributes_should_accept_target_tracker_enabled_fields source = Tracker.find(1) source.core_fields = [] source.save! @@ -449,6 +453,165 @@ class IssueTest < ActiveSupport::TestCase assert_equal Date.parse('2012-07-14'), issue.due_date end + def test_safe_attributes_should_not_include_readonly_fields + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1) + assert_equal %w(due_date), issue.read_only_attribute_names(user) + assert_not_include 'due_date', issue.safe_attribute_names(user) + + issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user + assert_equal Date.parse('2012-07-14'), issue.start_date + assert_nil issue.due_date + end + + def test_safe_attributes_should_not_include_readonly_custom_fields + cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1]) + cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1]) + + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1) + assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user) + assert_not_include cf2.id.to_s, issue.safe_attribute_names(user) + + issue.send :safe_attributes=, {'custom_field_values' => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'}}, user + assert_equal 'value1', issue.custom_field_value(cf1) + assert_nil issue.custom_field_value(cf2) + + issue.send :safe_attributes=, {'custom_fields' => [{'id' => cf1.id.to_s, 'value' => 'valuea'}, {'id' => cf2.id.to_s, 'value' => 'valueb'}]}, user + assert_equal 'valuea', issue.custom_field_value(cf1) + assert_nil issue.custom_field_value(cf2) + end + + def test_editable_custom_field_values_should_return_non_readonly_custom_values + cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1) + values = issue.editable_custom_field_values(user) + assert values.detect {|value| value.custom_field == cf1} + assert_nil values.detect {|value| value.custom_field == cf2} + + issue.tracker_id = 2 + values = issue.editable_custom_field_values(user) + assert values.detect {|value| value.custom_field == cf1} + assert values.detect {|value| value.custom_field == cf2} + end + + def test_safe_attributes_should_accept_target_tracker_writable_fields + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'start_date', :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + issue.send :safe_attributes=, {'start_date' => '2012-07-12', 'due_date' => '2012-07-14'}, user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_nil issue.due_date + + issue.send :safe_attributes=, {'start_date' => '2012-07-15', 'due_date' => '2012-07-16', 'tracker_id' => 2}, user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_equal Date.parse('2012-07-16'), issue.due_date + end + + def test_safe_attributes_should_accept_target_status_writable_fields + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + issue.send :safe_attributes=, {'start_date' => '2012-07-12', 'due_date' => '2012-07-14'}, user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_nil issue.due_date + + issue.send :safe_attributes=, {'start_date' => '2012-07-15', 'due_date' => '2012-07-16', 'status_id' => 2}, user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_equal Date.parse('2012-07-16'), issue.due_date + end + + def test_required_attributes_should_be_validated + cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'category_id', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf.id.to_s, :rule => 'required') + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'start_date', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf.id.to_s, :rule => 'required') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Required fields', :author => user) + assert_equal [cf.id.to_s, "category_id", "due_date"], issue.required_attribute_names(user).sort + assert !issue.save, "Issue was saved" + assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"], issue.errors.full_messages.sort + + issue.tracker_id = 2 + assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort + assert !issue.save, "Issue was saved" + assert_equal ["Foo can't be blank", "Start date can't be blank"], issue.errors.full_messages.sort + + issue.start_date = Date.today + issue.custom_field_values = {cf.id.to_s => 'bar'} + assert issue.save + end + + def test_required_attribute_names_for_multiple_roles_should_intersect_rules + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'required') + user = User.find(2) + member = Member.find(1) + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort + + member.role_ids = [1, 2] + member.save! + assert_equal [], issue.required_attribute_names(user.reload) + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'required') + assert_equal %w(due_date), issue.required_attribute_names(user) + + member.role_ids = [1, 2, 3] + member.save! + assert_equal [], issue.required_attribute_names(user.reload) + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'readonly') + # required + readonly => required + assert_equal %w(due_date), issue.required_attribute_names(user) + end + + def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'readonly') + user = User.find(2) + member = Member.find(1) + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort + + member.role_ids = [1, 2] + member.save! + assert_equal [], issue.read_only_attribute_names(user.reload) + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'readonly') + assert_equal %w(due_date), issue.read_only_attribute_names(user) + end + def test_copy issue = Issue.new.copy_from(1) assert issue.copy? diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb index 5a1c96471..e61280fc9 100644 --- a/test/unit/role_test.rb +++ b/test/unit/role_test.rb @@ -35,13 +35,13 @@ class RoleTest < ActiveSupport::TestCase def test_copy_workflows source = Role.find(1) - assert_equal 90, source.workflows.size + assert_equal 90, source.workflow_rules.size target = Role.new(:name => 'Target') assert target.save - target.workflows.copy(source) + target.workflow_rules.copy(source) target.reload - assert_equal 90, target.workflows.size + assert_equal 90, target.workflow_rules.size end def test_permissions_should_be_unserialized_with_its_coder diff --git a/test/unit/tracker_test.rb b/test/unit/tracker_test.rb index 91b259827..e1b1663e8 100644 --- a/test/unit/tracker_test.rb +++ b/test/unit/tracker_test.rb @@ -30,20 +30,20 @@ class TrackerTest < ActiveSupport::TestCase def test_copy_workflows source = Tracker.find(1) - assert_equal 89, source.workflows.size + assert_equal 89, source.workflow_rules.size target = Tracker.new(:name => 'Target') assert target.save - target.workflows.copy(source) + target.workflow_rules.copy(source) target.reload - assert_equal 89, target.workflows.size + assert_equal 89, target.workflow_rules.size end def test_issue_statuses tracker = Tracker.find(1) - Workflow.delete_all - Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3) - Workflow.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5) + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5) assert_kind_of Array, tracker.issue_statuses assert_kind_of IssueStatus, tracker.issue_statuses.first @@ -51,7 +51,7 @@ class TrackerTest < ActiveSupport::TestCase end def test_issue_statuses_empty - Workflow.delete_all("tracker_id = 1") + WorkflowTransition.delete_all("tracker_id = 1") assert_equal [], Tracker.find(1).issue_statuses end diff --git a/test/unit/workflow_test.rb b/test/unit/workflow_test.rb index 7adf2c85f..0ea39410d 100644 --- a/test/unit/workflow_test.rb +++ b/test/unit/workflow_test.rb @@ -21,17 +21,45 @@ class WorkflowTest < ActiveSupport::TestCase fixtures :roles, :trackers, :issue_statuses def test_copy - Workflow.delete_all - Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 2) - Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 3, :assignee => true) - Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 4, :author => true) + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 2) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 3, :assignee => true) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 4, :author => true) - assert_difference 'Workflow.count', 3 do - Workflow.copy(Tracker.find(2), Role.find(1), Tracker.find(3), Role.find(2)) + assert_difference 'WorkflowTransition.count', 3 do + WorkflowTransition.copy(Tracker.find(2), Role.find(1), Tracker.find(3), Role.find(2)) end - assert Workflow.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false}) - assert Workflow.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 3, :author => false, :assignee => true}) - assert Workflow.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 4, :author => true, :assignee => false}) + assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false}) + assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 3, :author => false, :assignee => true}) + assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 4, :author => true, :assignee => false}) + end + + def test_workflow_permission_should_validate_rule + wp = WorkflowPermission.new(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :field_name => 'due_date') + assert !wp.save + + wp.rule = 'foo' + assert !wp.save + + wp.rule = 'required' + assert wp.save + + wp.rule = 'readonly' + assert wp.save + end + + def test_workflow_permission_should_validate_field_name + wp = WorkflowPermission.new(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :rule => 'required') + assert !wp.save + + wp.field_name = 'foo' + assert !wp.save + + wp.field_name = 'due_date' + assert wp.save + + wp.field_name = '1' + assert wp.save end end