diff options
author | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2012-07-15 14:12:17 +0000 |
---|---|---|
committer | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2012-07-15 14:12:17 +0000 |
commit | d7b669e50b1c863b748231dc8fb66a692a33cdd1 (patch) | |
tree | 59001bb1ae7cb03a9c8ce92e8ffb9b874c56f4af /app | |
parent | 54d55a360a21569b4a76070b52177e778d5521c7 (diff) | |
download | redmine-d7b669e50b1c863b748231dc8fb66a692a33cdd1.tar.gz redmine-d7b669e50b1c863b748231dc8fb66a692a33cdd1.zip |
Workflow enhancement: editable and required fields configurable by role, tracker and status (#703, #3521).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@9977 e93f8b46-1217-0410-a6f0-8f06a7374b81
Diffstat (limited to 'app')
-rw-r--r-- | app/controllers/issues_controller.rb | 6 | ||||
-rw-r--r-- | app/controllers/roles_controller.rb | 2 | ||||
-rw-r--r-- | app/controllers/trackers_controller.rb | 2 | ||||
-rw-r--r-- | app/controllers/workflows_controller.rb | 42 | ||||
-rw-r--r-- | app/helpers/custom_fields_helper.rb | 12 | ||||
-rw-r--r-- | app/helpers/workflows_helper.rb | 6 | ||||
-rw-r--r-- | app/models/issue.rb | 126 | ||||
-rw-r--r-- | app/models/issue_status.rb | 8 | ||||
-rw-r--r-- | app/models/role.rb | 4 | ||||
-rw-r--r-- | app/models/tracker.rb | 15 | ||||
-rw-r--r-- | app/models/workflow_permission.rb | 29 | ||||
-rw-r--r-- | app/models/workflow_rule.rb (renamed from app/models/workflow.rb) | 32 | ||||
-rw-r--r-- | app/models/workflow_transition.rb | 39 | ||||
-rw-r--r-- | app/views/issues/_attributes.html.erb | 19 | ||||
-rw-r--r-- | app/views/issues/_form.html.erb | 4 | ||||
-rw-r--r-- | app/views/issues/_form_custom_fields.html.erb | 4 | ||||
-rw-r--r-- | app/views/trackers/index.html.erb | 2 | ||||
-rw-r--r-- | app/views/workflows/edit.html.erb | 13 | ||||
-rw-r--r-- | app/views/workflows/permissions.html.erb | 91 |
19 files changed, 376 insertions, 80 deletions
diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index a6365531b..246140f45 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -129,11 +129,7 @@ class IssuesController < ApplicationController format.html { render :action => 'new', :layout => !request.xhr? } format.js { render(:update) { |page| - if params[:project_change] - page.replace_html 'all_attributes', :partial => 'form' - else - page.replace_html 'attributes', :partial => 'attributes' - end + page.replace_html 'all_attributes', :partial => 'form' m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide' page << "if ($('log_time')) {Element.#{m}('log_time');}" } diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index b947cf946..790eb28d4 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -46,7 +46,7 @@ class RolesController < ApplicationController if request.post? && @role.save # workflow copy if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from])) - @role.workflows.copy(copy_from) + @role.workflow_rules.copy(copy_from) end flash[:notice] = l(:notice_successful_create) redirect_to :action => 'index' diff --git a/app/controllers/trackers_controller.rb b/app/controllers/trackers_controller.rb index a67583c16..5d4bfcaf1 100644 --- a/app/controllers/trackers_controller.rb +++ b/app/controllers/trackers_controller.rb @@ -45,7 +45,7 @@ class TrackersController < ApplicationController if request.post? and @tracker.save # workflow copy if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from])) - @tracker.workflows.copy(copy_from) + @tracker.workflow_rules.copy(copy_from) end flash[:notice] = l(:notice_successful_create) redirect_to :action => 'index' diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index c381666a3..3c49de8d5 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -23,7 +23,7 @@ class WorkflowsController < ApplicationController before_filter :find_trackers def index - @workflow_counts = Workflow.count_by_tracker_and_role + @workflow_counts = WorkflowTransition.count_by_tracker_and_role end def edit @@ -31,16 +31,15 @@ class WorkflowsController < ApplicationController @tracker = Tracker.find_by_id(params[:tracker_id]) if request.post? - Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) + WorkflowTransition.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) (params[:issue_status] || []).each { |status_id, transitions| transitions.each { |new_status_id, options| author = options.is_a?(Array) && options.include?('author') && !options.include?('always') assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always') - @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee) + WorkflowTransition.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee) } } if @role.save - flash[:notice] = l(:notice_successful_update) redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker return end @@ -53,7 +52,7 @@ class WorkflowsController < ApplicationController @statuses ||= IssueStatus.find(:all, :order => 'position') if @tracker && @role && @statuses.any? - workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id}) + workflows = WorkflowTransition.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id}) @workflows = {} @workflows['always'] = workflows.select {|w| !w.author && !w.assignee} @workflows['author'] = workflows.select {|w| w.author} @@ -61,6 +60,37 @@ class WorkflowsController < ApplicationController end end + def permissions + @role = Role.find_by_id(params[:role_id]) + @tracker = Tracker.find_by_id(params[:tracker_id]) + + if @role && @tracker + if request.post? + WorkflowPermission.destroy_all({:role_id => @role.id, :tracker_id => @tracker.id}) + (params[:permissions] || {}).each { |field, rule_by_status_id| + rule_by_status_id.each { |status_id, rule| + if rule.present? + WorkflowPermission.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule) + end + } + } + redirect_to :action => 'permissions', :role_id => @role, :tracker_id => @tracker + return + end + + @statuses = @tracker.issue_statuses + @fields = (Tracker::CORE_FIELDS_ALL - @tracker.disabled_core_fields).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]} + @custom_fields = @tracker.custom_fields + + @permissions = WorkflowPermission.where(:tracker_id => @tracker.id, :role_id => @role.id).all.inject({}) do |h, w| + h[w.old_status_id] ||= {} + h[w.old_status_id][w.field_name] = w.rule + h + end + @statuses.each {|status| @permissions[status.id] ||= {}} + end + end + def copy if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any' @@ -83,7 +113,7 @@ class WorkflowsController < ApplicationController elsif @target_trackers.nil? || @target_roles.nil? flash.now[:error] = l(:error_workflow_copy_target) else - Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles) + WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles) flash[:notice] = l(:notice_successful_update) redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role end diff --git a/app/helpers/custom_fields_helper.rb b/app/helpers/custom_fields_helper.rb index ff953d857..c55943b32 100644 --- a/app/helpers/custom_fields_helper.rb +++ b/app/helpers/custom_fields_helper.rb @@ -73,15 +73,17 @@ module CustomFieldsHelper end # Return custom field label tag - def custom_field_label_tag(name, custom_value) + def custom_field_label_tag(name, custom_value, options={}) + required = options[:required] || custom_value.custom_field.is_required? + content_tag "label", h(custom_value.custom_field.name) + - (custom_value.custom_field.is_required? ? " <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) diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb index 59b31d2ea..3dd514042 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -18,4 +18,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module WorkflowsHelper + def field_permission_tag(permissions, status, field) + name = field.is_a?(CustomField) ? field.id.to_s : field + select_tag("permissions[#{name}][#{status.id}]", + options_for_select([["", ""], ["Read-only", "readonly"], ["Required", "required"]], permissions[status.id][name]) + ) + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 5fc0f1d61..152953708 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -58,7 +58,7 @@ class Issue < ActiveRecord::Base validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 validates_numericality_of :estimated_hours, :allow_nil => true - validate :validate_issue + validate :validate_issue, :validate_required_fields scope :visible, lambda {|*args| { :include => :project, @@ -146,6 +146,11 @@ class Issue < ActiveRecord::Base super end + def reload(*args) + @workflow_rule_by_attribute = nil + super + end + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields def available_custom_fields (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : [] @@ -208,7 +213,9 @@ class Issue < ActiveRecord::Base def status_id=(sid) self.status = nil - write_attribute(:status_id, sid) + result = write_attribute(:status_id, sid) + @workflow_rule_by_attribute = nil + result end def priority_id=(pid) @@ -230,6 +237,7 @@ class Issue < ActiveRecord::Base self.tracker = nil result = write_attribute(:tracker_id, tid) @custom_field_values = nil + @workflow_rule_by_attribute = nil result end @@ -336,9 +344,10 @@ class Issue < ActiveRecord::Base :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) && user.allowed_to?(:manage_subtasks, issue.project)} - def safe_attribute_names(*args) - names = super(*args) + def safe_attribute_names(user=nil) + names = super names -= disabled_core_fields + names -= read_only_attribute_names(user) names end @@ -362,15 +371,15 @@ class Issue < ActiveRecord::Base self.tracker_id = t end - attrs = delete_unsafe_attributes(attrs, user) - return if attrs.empty? - - if attrs['status_id'] - unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i) - attrs.delete('status_id') + if (s = attrs.delete('status_id')) && safe_attribute?('status_id') + if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i) + self.status_id = s end end + attrs = delete_unsafe_attributes(attrs, user) + return if attrs.empty? + unless leaf? attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} end @@ -379,6 +388,14 @@ class Issue < ActiveRecord::Base attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i) end + if attrs['custom_field_values'].present? + attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s} + end + + if attrs['custom_fields'].present? + attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s} + end + # mass-assignment security bypass assign_attributes attrs, :without_protection => true end @@ -387,6 +404,76 @@ class Issue < ActiveRecord::Base tracker ? tracker.disabled_core_fields : [] end + # Returns the custom_field_values that can be edited by the given user + def editable_custom_field_values(user=nil) + custom_field_values.reject do |value| + read_only_attribute_names(user).include?(value.custom_field_id.to_s) + end + end + + # Returns the names of attributes that are read-only for user or the current user + # For users with multiple roles, the read-only fields are the intersection of + # read-only fields of each role + # The result is an array of strings where sustom fields are represented with their ids + # + # Examples: + # issue.read_only_attribute_names # => ['due_date', '2'] + # issue.read_only_attribute_names(user) # => [] + def read_only_attribute_names(user=nil) + workflow_rule_by_attribute(user).select {|attr, rule| rule == 'readonly'}.keys + end + + # Returns the names of required attributes for user or the current user + # For users with multiple roles, the required fields are the intersection of + # required fields of each role + # The result is an array of strings where sustom fields are represented with their ids + # + # Examples: + # issue.required_attribute_names # => ['due_date', '2'] + # issue.required_attribute_names(user) # => [] + def required_attribute_names(user=nil) + workflow_rule_by_attribute(user).select {|attr, rule| rule == 'required'}.keys + end + + # Returns true if the attribute is required for user + def required_attribute?(name, user=nil) + required_attribute_names(user).include?(name.to_s) + end + + # Returns a hash of the workflow rule by attribute for the given user + # + # Examples: + # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'} + def workflow_rule_by_attribute(user=nil) + return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil? + + user_real = user || User.current + roles = user_real.admin ? Role.all : user_real.roles_for_project(project) + return {} if roles.empty? + + result = {} + workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all + if workflow_permissions.any? + workflow_rules = workflow_permissions.inject({}) do |h, wp| + h[wp.field_name] ||= [] + h[wp.field_name] << wp.rule + h + end + workflow_rules.each do |attr, rules| + next if rules.size < roles.size + uniq_rules = rules.uniq + if uniq_rules.size == 1 + result[attr] = uniq_rules.first + else + result[attr] = 'required' + end + end + end + @workflow_rule_by_attribute = result if user.nil? + result + end + private :workflow_rule_by_attribute + def done_ratio if Issue.use_status_for_done_ratio? && status && status.default_done_ratio status.default_done_ratio @@ -448,6 +535,25 @@ class Issue < ActiveRecord::Base end end + # Validates the issue against additional workflow requirements + def validate_required_fields + user = new_record? ? author : current_journal.try(:user) + + required_attribute_names(user).each do |attribute| + if attribute =~ /^\d+$/ + attribute = attribute.to_i + v = custom_field_values.detect {|v| v.custom_field_id == attribute } + if v && v.value.blank? + errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank') + end + else + if respond_to?(attribute) && send(attribute).blank? + errors.add attribute, :blank + end + end + end + end + # Set the done_ratio using the status if that setting is set. This will keep the done_ratios # even if the user turns off the setting later def update_done_ratio_from_issue_status diff --git a/app/models/issue_status.rb b/app/models/issue_status.rb index 6973ca656..2d09d5e88 100644 --- a/app/models/issue_status.rb +++ b/app/models/issue_status.rb @@ -17,10 +17,10 @@ class IssueStatus < ActiveRecord::Base before_destroy :check_integrity - has_many :workflows, :foreign_key => "old_status_id" + has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id" acts_as_list - before_destroy :delete_workflows + before_destroy :delete_workflow_rules after_save :update_default validates_presence_of :name @@ -98,7 +98,7 @@ private end # Deletes associated workflows - def delete_workflows - Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}]) + def delete_workflow_rules + WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}]) end end diff --git a/app/models/role.rb b/app/models/role.rb index 8e8737afb..412e5a63c 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -47,9 +47,9 @@ class Role < ActiveRecord::Base } before_destroy :check_deletable - has_many :workflows, :dependent => :delete_all do + has_many :workflow_rules, :dependent => :delete_all do def copy(source_role) - Workflow.copy(nil, source_role, nil, proxy_association.owner) + WorkflowRule.copy(nil, source_role, nil, proxy_association.owner) end end diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 109e0f423..472b94b31 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -17,14 +17,17 @@ class Tracker < ActiveRecord::Base - # Other fields should be appended, not inserted! - CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio) + CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze + # Fields that can be disabled + # Other (future) fields should be appended, not inserted! + CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze + CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze before_destroy :check_integrity has_many :issues - has_many :workflows, :dependent => :delete_all do + has_many :workflow_rules, :dependent => :delete_all do def copy(source_tracker) - Workflow.copy(source_tracker, nil, proxy_association.owner, nil) + WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil) end end @@ -56,8 +59,8 @@ class Tracker < ActiveRecord::Base return [] end - ids = Workflow. - connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}"). + ids = WorkflowTransition. + connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'"). flatten. uniq diff --git a/app/models/workflow_permission.rb b/app/models/workflow_permission.rb new file mode 100644 index 000000000..72e95543f --- /dev/null +++ b/app/models/workflow_permission.rb @@ -0,0 +1,29 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WorkflowPermission < WorkflowRule + validates_inclusion_of :rule, :in => %w(readonly required) + validate :validate_field_name + + protected + + def validate_field_name + unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/) + errors.add :field_name, :invalid + end + end +end diff --git a/app/models/workflow.rb b/app/models/workflow_rule.rb index 36b4c7df8..2fc020ba1 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow_rule.rb @@ -15,31 +15,15 @@ # 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 +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, :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 + validates_presence_of :role, :tracker, :old_status # Copies workflows from source to targets def self.copy(source_tracker, source_role, target_trackers, target_roles) @@ -78,9 +62,9 @@ class Workflow < ActiveRecord::Base 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}" + + 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 diff --git a/app/models/workflow_transition.rb b/app/models/workflow_transition.rb new file mode 100644 index 000000000..0c01edd04 --- /dev/null +++ b/app/models/workflow_transition.rb @@ -0,0 +1,39 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WorkflowTransition < WorkflowRule + validates_presence_of :new_status + + # Returns workflow transitions count by tracker and role + def self.count_by_tracker_and_role + counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{table_name} WHERE type = 'WorkflowTransition' GROUP BY role_id, tracker_id") + roles = Role.sorted.all + trackers = Tracker.sorted.all + + result = [] + trackers.each do |tracker| + t = [] + roles.each do |role| + row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s} + t << [role, (row.nil? ? 0 : row['c'].to_i)] + end + result << [tracker, t] + end + + result + end +end diff --git a/app/views/issues/_attributes.html.erb b/app/views/issues/_attributes.html.erb index d2c13e82b..cc51cd435 100644 --- a/app/views/issues/_attributes.html.erb +++ b/app/views/issues/_attributes.html.erb @@ -4,6 +4,9 @@ <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 %> @@ -13,11 +16,11 @@ <% 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), @@ -36,25 +39,25 @@ <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> diff --git a/app/views/issues/_form.html.erb b/app/views/issues/_form.html.erb index 9bc362377..4a13fa07a 100644 --- a/app/views/issues/_form.html.erb +++ b/app/views/issues/_form.html.erb @@ -9,7 +9,7 @@ <% if @issue.safe_attribute? 'project_id' %> <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 %> diff --git a/app/views/issues/_form_custom_fields.html.erb b/app/views/issues/_form_custom_fields.html.erb index 33b0b848b..4da98eb13 100644 --- a/app/views/issues/_form_custom_fields.html.erb +++ b/app/views/issues/_form_custom_fields.html.erb @@ -2,8 +2,8 @@ <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 -%> diff --git a/app/views/trackers/index.html.erb b/app/views/trackers/index.html.erb index 0515ae335..4d10d857e 100644 --- a/app/views/trackers/index.html.erb +++ b/app/views/trackers/index.html.erb @@ -15,7 +15,7 @@ <% for tracker in @trackers %> <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) %> diff --git a/app/views/workflows/edit.html.erb b/app/views/workflows/edit.html.erb index 20eaff992..a634ee310 100644 --- a/app/views/workflows/edit.html.erb +++ b/app/views/workflows/edit.html.erb @@ -2,6 +2,13 @@ <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 %> @@ -12,11 +19,11 @@ <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 index 000000000..73cf91656 --- /dev/null +++ b/app/views/workflows/permissions.html.erb @@ -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);"> </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);"> </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 %> |