]> source.dussan.org Git - redmine.git/commitdiff
Workflow enhancement: editable and required fields configurable by role, tracker...
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 15 Jul 2012 14:12:17 +0000 (14:12 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 15 Jul 2012 14:12:17 +0000 (14:12 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@9977 e93f8b46-1217-0410-a6f0-8f06a7374b81

36 files changed:
app/controllers/issues_controller.rb
app/controllers/roles_controller.rb
app/controllers/trackers_controller.rb
app/controllers/workflows_controller.rb
app/helpers/custom_fields_helper.rb
app/helpers/workflows_helper.rb
app/models/issue.rb
app/models/issue_status.rb
app/models/role.rb
app/models/tracker.rb
app/models/workflow.rb [deleted file]
app/models/workflow_permission.rb [new file with mode: 0644]
app/models/workflow_rule.rb [new file with mode: 0644]
app/models/workflow_transition.rb [new file with mode: 0644]
app/views/issues/_attributes.html.erb
app/views/issues/_form.html.erb
app/views/issues/_form_custom_fields.html.erb
app/views/trackers/index.html.erb
app/views/workflows/edit.html.erb
app/views/workflows/permissions.html.erb [new file with mode: 0644]
config/routes.rb
db/migrate/20120714122000_add_workflows_type.rb [new file with mode: 0644]
db/migrate/20120714122100_update_workflows_to_sti.rb [new file with mode: 0644]
db/migrate/20120714122200_add_workflows_rule_fields.rb [new file with mode: 0644]
lib/redmine/default_data/loader.rb
public/stylesheets/application.css
test/fixtures/workflows.yml
test/functional/issues_controller_test.rb
test/functional/roles_controller_test.rb
test/functional/trackers_controller_test.rb
test/functional/workflows_controller_test.rb
test/unit/issue_status_test.rb
test/unit/issue_test.rb
test/unit/role_test.rb
test/unit/tracker_test.rb
test/unit/workflow_test.rb

index a6365531bc32dd453f0ac133510fb00f73281c7c..246140f45c75469bd34fb5271b9b9e7daac3afce 100644 (file)
@@ -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');}"
         }
index b947cf94681bd65fec1435c25dfa31208a0077df..790eb28d4ac6b7ba7687e415f414b87ba025e6bd 100644 (file)
@@ -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'
index a67583c16387d321a8b29239f7a1b63c26c63773..5d4bfcaf1bcd57112d4d08ed75bdb9b9a3377495 100644 (file)
@@ -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'
index c381666a32f3b67802548d976c8f1e023b296aa8..3c49de8d578ac4e534d51f0dc58b2452a7a796d9 100644 (file)
@@ -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
index ff953d857c0f684c1764789e38c8eb00197d9247..c55943b32b3d262f5f71b9e7df3f9316eab1d048 100644 (file)
@@ -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? ? " <span class=\"required\">*</span>".html_safe : ""),
-       :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
+      (required ? " <span class=\"required\">*</span>".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)
index 59b31d2eab853236ca382836894e4332540e26f2..3dd5140428199f836bee167804fffe973d5a1d64 100644 (file)
 # 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
index 5fc0f1d615f5b2bbaf5e5aa9eb812f3ed597e61e..152953708bfd39ab7ab11e8d55c6e263b16b089d 100644 (file)
@@ -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
index 6973ca656264c1012f8efa595cac9a6535227cf9..2d09d5e8850917b7d05e8063bd1f1a765c1c6efc 100644 (file)
 
 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
index 8e8737afb0f74744b35b96669fc2d3e37d5ef497..412e5a63cd759a8cf00d8589a37050ec29e80c7a 100644 (file)
@@ -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
 
index 109e0f42393f803016c11581be4f00f18867eaa3..472b94b31a1f23437376043423e7b7d70db39a5a 100644 (file)
 
 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 (file)
index 36b4c7d..0000000
+++ /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 (file)
index 0000000..72e9554
--- /dev/null
@@ -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 (file)
index 0000000..2fc020b
--- /dev/null
@@ -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 (file)
index 0000000..0c01edd
--- /dev/null
@@ -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
index d2c13e82be9575ed59a47de7b5815fcf160e7032..cc51cd43579c679bff3c1bcf1f3b543187fe9191 100644 (file)
@@ -4,6 +4,9 @@
 <div class="splitcontentleft">
 <% if @issue.safe_attribute? 'status_id'  %>
 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
+<%= observe_field :issue_status_id, :url => project_issue_form_path(@project, :id => @issue),
+                                     :with => "Form.serialize('issue-form')" %>
+
 <% else %>
 <p><label><%= l(:field_status) %></label> <%= h(@issue.status.name) %></p>
 <% end %>
 <% end %>
 
 <% if @issue.safe_attribute? 'assigned_to_id' %>
-<p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true %></p>
+<p><%= 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') %></p>
 <% end %>
 
 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
-<p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
+<p><%= 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? %>
-<p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %>
+<p><%= 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),
 
 <div class="splitcontentright">
 <% if @issue.safe_attribute? 'parent_issue_id' %>
-<p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10 %></p>
+<p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %></p>
 <div id="parent_issue_candidates" class="autocomplete"></div>
 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @issue.project) }')" %>
 <% end %>
 
 <% if @issue.safe_attribute? 'start_date' %>
-<p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
+<p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('start_date') %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
 <% end %>
 
 <% if @issue.safe_attribute? 'due_date' %>
-<p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
+<p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('due_date') %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
 <% end %>
 
 <% if @issue.safe_attribute? 'estimated_hours' %>
-<p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %></p>
+<p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
 <% end %>
 
 <% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %>
-<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
+<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
 <% end %>
 </div>
 </div>
index 9bc362377e251ad8d2e3c4f3829f7b86e839f286..4a13fa07a8f4180fdafe6bd0c4bf2c82c349097e 100644 (file)
@@ -9,7 +9,7 @@
 
 <% if @issue.safe_attribute? 'project_id' %>
 <p><%= f.select :project_id, project_tree_options_for_select(@issue.allowed_target_projects, :selected => @issue.project), :required => true %></p>
-<%= 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' %>
 <p>
-  <label><%= l(:field_description) %></label>
+  <%= 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 %>
index 33b0b848b07612fbda37498225eed89c55a9bff8..4da98eb133119f1123c25ddafaf11011abec2574 100644 (file)
@@ -2,8 +2,8 @@
 <div class="splitcontentleft">
 <% i = 0 %>
 <% split_on = (@issue.custom_field_values.size / 2.0).ceil - 1 %>
-<% @issue.custom_field_values.each do |value| %>
-  <p><%= custom_field_tag_with_label :issue, value %></p>
+<% @issue.editable_custom_field_values.each do |value| %>
+  <p><%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %></p>
 <% if i == split_on -%>
 </div><div class="splitcontentright">
 <% end -%>
index 0515ae3355e5fe5eb59347e3a0d9f1664a00cb19..4d10d857e71a6dd2d38f5c376f7964e7eacd6566 100644 (file)
@@ -15,7 +15,7 @@
 <% for tracker in @trackers %>
   <tr class="<%= cycle("odd", "even") %>">
   <td><%= link_to h(tracker.name), edit_tracker_path(tracker) %></td>
-  <td align="center"><% unless tracker.workflows.count > 0 %><span class="icon icon-warning"><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)</span><% end %></td>
+  <td align="center"><% unless tracker.workflow_rules.count > 0 %><span class="icon icon-warning"><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)</span><% end %></td>
   <td align="center" style="width:15%;"><%= reorder_links('tracker', {:action => 'update', :id => tracker}, :put) %></td>
   <td class="buttons">
     <%= delete_link tracker_path(tracker) %>
index 20eaff992d9006a36b22e9f14d1299f3e104c727..a634ee310aa19031d52cd87813a4ea142a80548c 100644 (file)
@@ -2,6 +2,13 @@
 
 <h2><%=l(:label_workflow)%></h2>
 
+<div class="tabs">
+  <ul>
+    <li><%= link_to 'Status transitions', {:action => 'edit', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %></li>
+    <li><%= link_to 'Fields permissions', {:action => 'permissions', :role_id => @role, :tracker_id => @tracker} %></li>
+  </ul>
+</div>
+
 <p><%=l(:text_workflow_edit)%>:</p>
 
 <%= form_tag({}, :method => 'get') do %>
   <label><%=l(:label_tracker)%>:
   <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %></label>
 
+  <%= submit_tag l(:button_edit), :name => nil %>
+
   <%= hidden_field_tag 'used_statuses_only', '0' %>
   <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
-</p>
-<p>
-<%= submit_tag l(:button_edit), :name => nil %>
+
 </p>
 <% end %>
 
diff --git a/app/views/workflows/permissions.html.erb b/app/views/workflows/permissions.html.erb
new file mode 100644 (file)
index 0000000..73cf916
--- /dev/null
@@ -0,0 +1,91 @@
+<%= render :partial => 'action_menu' %>
+
+<h2><%=l(:label_workflow)%></h2>
+
+<div class="tabs">
+  <ul>
+    <li><%= link_to 'Status transitions', {:action => 'edit', :role_id => @role, :tracker_id => @tracker} %></li>
+    <li><%= link_to 'Fields permissions', {:action => 'permissions', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %></li>
+  </ul>
+</div>
+
+<p><%=l(:text_workflow_edit)%>:</p>
+
+<%= form_tag({}, :method => 'get') do %>
+<p>
+  <label><%=l(:label_role)%>:
+  <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
+
+  <label><%=l(:label_tracker)%>:
+  <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %></label>
+
+  <%= submit_tag l(:button_edit), :name => nil %>
+</p>
+<% 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 %>
+    <div class="autoscroll">
+    <table class="list fields_permissions">
+    <thead>
+      <tr>
+        <th align="left">
+        </th>
+        <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_issue_status)%></th>
+      </tr>
+      <tr>
+        <td></td>
+        <% for status in @statuses %>
+        <td width="<%= 75 / @statuses.size %>%" align="center">
+          <%=h status.name %>
+        </td>
+        <% end %>
+      </tr>
+    </thead>
+    <tbody>
+      <tr class="group open">
+        <td colspan="<%= @statuses.size + 1 %>">
+          <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
+          <%= l(:field_core_fields) %>
+        </td>
+      </tr>
+      <% @fields.each do |field, name| %>
+      <tr class="<%= cycle("odd", "even") %>">
+        <td>
+          <%=h name %>
+        </td>
+        <% for status in @statuses -%>
+        <td align="center" class="<%= @permissions[status.id][field] %>">
+          <%= field_permission_tag(@permissions, status, field) %>
+        </td>
+        <% end -%>
+      </tr>
+      <% end %>
+      <% if @custom_fields.any? %>
+        <tr class="group open">
+          <td colspan="<%= @statuses.size + 1 %>">
+            <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
+            <%= l(:label_custom_field_plural) %>
+          </td>
+        </tr>
+        <% @custom_fields.each do |field| %>
+        <tr class="<%= cycle("odd", "even") %>">
+          <td>
+            <%=h field.name %>
+          </td>
+          <% for status in @statuses -%>
+          <td align="center" class="<%= @permissions[status.id][field.id.to_s] %>">
+            <%= field_permission_tag(@permissions, status, field) %>
+          </td>
+          <% end -%>
+        </tr>
+        <% end %>
+      <% end %>
+    </tbody>
+    </table>
+    </div>
+    <%= submit_tag l(:button_save) %>
+  <% end %>
+<% end %>
index e3ed8859aad146d206536ea72f87bb752d9f12b4..0ee3908b12332eb22ebccac26c439a4588daecf4 100644 (file)
@@ -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 (file)
index 0000000..f263f25
--- /dev/null
@@ -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 (file)
index 0000000..8ee5c6d
--- /dev/null
@@ -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 (file)
index 0000000..6e9c7df
--- /dev/null
@@ -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
index 2133ee66885bb98786ed77fe109c629e4620da61..910a6a3194a49ab1105c96b8e7bb9757daeb4690 100644 (file)
@@ -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
index 57f2f24b616ddcc4fabb1066c317aed283f11eae..013c1b7266fc30ee40073fc66a21fc3283ea9cb8 100644 (file)
@@ -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;}
index 6126055cacd0ab45541f98ae6cda6ac2ee9aacb7..d544545b33122642826db299e3d25074e68a272c 100644 (file)
 --- 
-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
index bb80604c6b2679e7a7d38f0c2fb99677af716cab..26b9dcb0b1d4b5eda747bf5e556013b0dcc35e50 100644 (file)
@@ -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]
 
index 03eaf507d6afc51f2dd7e35aa173685f95143c4b..327fad51211ecd3b3ddf7a88254984df4f681aef 100644 (file)
@@ -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
index 7393088aefb4f28536f93449dc68f2eb2936b4c3..4c4dd0d3a545e58cdc295413a91c660a672271e6 100644 (file)
@@ -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
index 5c37afc5995b4ab13c39c4322f796f2272282d4f..84250d346e0b8aeff652b4cf64b6ce2a2c48eea0 100644 (file)
@@ -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&amp;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
index 6fce33675648c3918f2cb92bc1dc9adb61d9c2a9..f12fbdd3dde8f812734847262eff39bdcf2006cd 100644 (file)
@@ -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)
index fe8bdf2acdb243f70a060499523cbcc156fd6936..a89b2b31594daa05796027405e88de524fdcbe89 100644 (file)
@@ -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?
index 5a1c96471d8b0058cb11771cb74e3deb0914354d..e61280fc90595006d189839da0ad1eaadcbda4c9 100644 (file)
@@ -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
index 91b259827890a3f5ccb31d35721e6b3e6eea4d1c..e1b1663e813f94cab241233d4cd98a5cf72093ef 100644 (file)
@@ -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
 
index 7adf2c85f94236cc5a263eed93154155ad8fd50c..0ea39410d61da1a5b78ec6c06bbcc346df7c036f 100644 (file)
@@ -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