diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index cca3fe62..081d8f89 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -194,6 +194,7 @@ class IssuesController < ApplicationController :change_status => User.current.allowed_to?(:change_issue_status, @project), :add => User.current.allowed_to?(:add_issues, @project), :move => User.current.allowed_to?(:move_issues, @project), + :copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), :delete => User.current.allowed_to?(:delete_issues, @project)} render :layout => false end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 0f50cd78..37a78869 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -57,11 +57,13 @@ class ProjectsController < ApplicationController # Add a new project def add @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") + @trackers = Tracker.all @root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}") @project = Project.new(params[:project]) @project.enabled_module_names = Redmine::AccessControl.available_project_modules if request.get? @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) } + @project.trackers = Tracker.all else @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids] @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } @@ -80,7 +82,7 @@ class ProjectsController < ApplicationController @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} @subprojects = @project.active_children @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") - @trackers = Tracker.find(:all, :order => 'position') + @trackers = @project.trackers @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false]) @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id]) @total_hours = @project.time_entries.sum(:hours) @@ -92,6 +94,7 @@ class ProjectsController < ApplicationController @custom_fields = IssueCustomField.find(:all) @issue_category ||= IssueCategory.new @member ||= @project.members.new + @trackers = Tracker.all @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } @repository ||= @project.repository @wiki ||= @project.wiki @@ -207,7 +210,7 @@ class ProjectsController < ApplicationController @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue]) @issue.project = @project @issue.author = User.current - @issue.tracker ||= Tracker.find(params[:tracker_id]) + @issue.tracker ||= @project.trackers.find(params[:tracker_id]) default_status = IssueStatus.default unless default_status @@ -293,6 +296,7 @@ class ProjectsController < ApplicationController def move_issues @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids] redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues + @projects = [] # find projects to which the user is allowed to move the issue if User.current.admin? @@ -301,14 +305,14 @@ class ProjectsController < ApplicationController else User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)} end - # issue can be moved to any tracker - @trackers = Tracker.find(:all) - if request.post? && params[:new_project_id] && @projects.collect(&:id).include?(params[:new_project_id].to_i) && params[:new_tracker_id] - new_project = Project.find_by_id(params[:new_project_id]) - new_tracker = params[:new_tracker_id].blank? ? nil : Tracker.find_by_id(params[:new_tracker_id]) + @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] + @target_project ||= @project + @trackers = @target_project.trackers + if request.post? + new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) unsaved_issue_ids = [] @issues.each do |issue| - unsaved_issue_ids << issue.id unless issue.move_to(new_project, new_tracker) + unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) end if unsaved_issue_ids.empty? flash[:notice] = l(:notice_successful_update) unless @issues.empty? @@ -316,7 +320,9 @@ class ProjectsController < ApplicationController flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) end redirect_to :controller => 'issues', :action => 'index', :project_id => @project + return end + render :layout => false if request.xhr? end # Add a news to @project @@ -354,13 +360,13 @@ class ProjectsController < ApplicationController # Show changelog for @project def changelog - @trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position') + @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position') retrieve_selected_tracker_ids(@trackers) @versions = @project.versions.sort end def roadmap - @trackers = Tracker.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position') + @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true]) retrieve_selected_tracker_ids(@trackers) @versions = @project.versions.sort @versions = @versions.select {|v| !v.completed? } unless params[:completed] diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 6b95944a..e18e117a 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -25,7 +25,7 @@ class ReportsController < ApplicationController case params[:detail] when "tracker" @field = "tracker_id" - @rows = Tracker.find :all, :order => 'position' + @rows = @project.trackers @data = issues_by_tracker @report_title = l(:field_tracker) render :template => "reports/issue_report_details" @@ -60,7 +60,7 @@ class ReportsController < ApplicationController @report_title = l(:field_subproject) render :template => "reports/issue_report_details" else - @trackers = Tracker.find(:all, :order => 'position') + @trackers = @project.trackers @versions = @project.versions.sort @priorities = Enumeration::get_values('IPRI') @categories = @project.issue_categories diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5b78db71..4b1b1a95 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -190,7 +190,7 @@ module ProjectsHelper end if Object.const_defined?(:Magick) def new_issue_selector - trackers = Tracker.find(:all, :order => 'position') + trackers = @project.trackers # can't use form tag inside helper content_tag('form', select_tag('tracker_id', '' + options_from_collection_for_select(trackers, 'id', 'name'), :onchange => "if (this.value != '') {this.form.submit()}"), diff --git a/app/models/custom_value.rb b/app/models/custom_value.rb index afe4c1af..c3d6b7bb 100644 --- a/app/models/custom_value.rb +++ b/app/models/custom_value.rb @@ -31,9 +31,9 @@ protected when 'float' begin; !value.blank? && Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end when 'date' - errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.empty? + errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.blank? when 'list' - errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include? value or value.empty? + errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) or value.blank? end end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 60cca405..f7b01ea6 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -40,7 +40,7 @@ class Issue < ActiveRecord::Base acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} - validates_presence_of :subject, :description, :priority, :tracker, :author, :status + validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 validates_numericality_of :estimated_hours, :allow_nil => true @@ -106,6 +106,10 @@ class Issue < ActiveRecord::Base end end + def validate_on_create + errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker) + end + def before_create # default assignment based on category if assigned_to.nil? && category && category.assigned_to diff --git a/app/models/project.rb b/app/models/project.rb index afaa049c..be46d618 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -24,6 +24,7 @@ class Project < ActiveRecord::Base has_many :users, :through => :members has_many :custom_values, :dependent => :delete_all, :as => :customized has_many :enabled_modules, :dependent => :delete_all + has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] has_many :issue_changes, :through => :issues, :source => :journals has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 90ef3191..6de2a098 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -29,6 +29,10 @@ class Tracker < ActiveRecord::Base def to_s; name end + def self.all + find(:all, :order => 'position') + end + private def check_integrity raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id]) diff --git a/app/views/issues/_sidebar.rhtml b/app/views/issues/_sidebar.rhtml index 3b42ce46..6e61755e 100644 --- a/app/views/issues/_sidebar.rhtml +++ b/app/views/issues/_sidebar.rhtml @@ -1,4 +1,4 @@ -<% if authorize_for('projects', 'add_issue') %> +<% if authorize_for('projects', 'add_issue') && @project.trackers.any? %>

<%= l(:label_issue_new) %>

<%= l(:label_tracker) %>: <%= new_issue_selector %> <% end %> diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index e44911da..3af49fb0 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/issues/context_menu.rhtml @@ -32,7 +32,7 @@
  • <%= context_menu_link l(:button_copy), {:controller => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue}, - :class => 'icon-copy', :disabled => !@can[:add] %>
  • + :class => 'icon-copy', :disabled => !@can[:copy] %>
  • <%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon-move', :disabled => !@can[:move] %>
  • <%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml index aa30f1ea..885ccf4b 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -17,16 +17,32 @@ <% for @custom_value in @custom_values %>

    <%= custom_field_tag_with_label @custom_value %>

    <% end %> + + +<% unless @trackers.empty? %> +
    <%=l(:label_tracker_plural)%> +<% @trackers.each do |tracker| %> + +<% end %> +<%= hidden_field_tag 'project[tracker_ids][]', '' %> +
    +<% end %> <% unless @custom_fields.empty? %> -

    +

    <%=l(:label_custom_field_plural)%> <% for custom_field in @custom_fields %> + +<% end %> +
    <% end %> - + <% content_for :header_tags do %> <%= javascript_include_tag 'calendar/calendar' %> diff --git a/app/views/projects/add.rhtml b/app/views/projects/add.rhtml index 4818cae4..e3ee0b59 100644 --- a/app/views/projects/add.rhtml +++ b/app/views/projects/add.rhtml @@ -3,13 +3,13 @@ <% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %> <%= render :partial => 'form', :locals => { :f => f } %> -
    -

    +

    <%= l(:label_module_plural) %> <% Redmine::AccessControl.available_project_modules.each do |m| %> -<%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %> <%= m.to_s.humanize %> -<% end %>

    -
    - + +<% end %> + <%= submit_tag l(:button_save) %> <% end %> diff --git a/app/views/projects/move_issues.rhtml b/app/views/projects/move_issues.rhtml index b2965303..95eaf9de 100644 --- a/app/views/projects/move_issues.rhtml +++ b/app/views/projects/move_issues.rhtml @@ -1,7 +1,7 @@

    <%=l(:button_move)%>

    -<% form_tag({:action => 'move_issues', :id => @project}, :class => "tabular") do %> +<% form_tag({:action => 'move_issues', :id => @project}, :class => 'tabular', :id => 'move_form') do %>

    @@ -15,7 +15,12 @@

    -<%= select_tag "new_project_id", options_from_collection_for_select(@projects, "id", "name", @project.id) %>

    +<%= select_tag "new_project_id", + options_from_collection_for_select(@projects, 'id', 'name', @target_project.id), + :onchange => remote_function(:url => {:action => 'move_issues' , :id => @project}, + :method => :get, + :update => 'content', + :with => "Form.serialize('move_form')") %>

    <%= select_tag "new_tracker_id", "" + options_from_collection_for_select(@trackers, "id", "name") %>

    diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index 94df0bf3..458e7975 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -56,7 +56,7 @@
    <% content_for :sidebar do %> - <% if authorize_for('projects', 'add_issue') %> + <% if authorize_for('projects', 'add_issue') && @project.trackers.any? %>

    <%= l(:label_issue_new) %>

    <%= l(:label_tracker) %>: <%= new_issue_selector %> <% end %> diff --git a/db/migrate/081_create_projects_trackers.rb b/db/migrate/081_create_projects_trackers.rb new file mode 100644 index 00000000..70fea188 --- /dev/null +++ b/db/migrate/081_create_projects_trackers.rb @@ -0,0 +1,19 @@ +class CreateProjectsTrackers < ActiveRecord::Migration + def self.up + create_table :projects_trackers, :id => false do |t| + t.column :project_id, :integer, :default => 0, :null => false + t.column :tracker_id, :integer, :default => 0, :null => false + end + add_index :projects_trackers, :project_id, :name => :projects_trackers_project_id + + # Associates all trackers to all projects (as it was before) + tracker_ids = Tracker.find(:all).collect(&:id) + Project.find(:all).each do |project| + project.tracker_ids = tracker_ids + end + end + + def self.down + drop_table :projects_trackers + end +end diff --git a/test/fixtures/projects_trackers.yml b/test/fixtures/projects_trackers.yml new file mode 100644 index 00000000..cfca5b22 --- /dev/null +++ b/test/fixtures/projects_trackers.yml @@ -0,0 +1,46 @@ +--- +projects_trackers_012: + project_id: 4 + tracker_id: 3 +projects_trackers_001: + project_id: 1 + tracker_id: 1 +projects_trackers_013: + project_id: 5 + tracker_id: 1 +projects_trackers_002: + project_id: 1 + tracker_id: 2 +projects_trackers_014: + project_id: 5 + tracker_id: 2 +projects_trackers_003: + project_id: 1 + tracker_id: 3 +projects_trackers_015: + project_id: 5 + tracker_id: 3 +projects_trackers_004: + project_id: 2 + tracker_id: 1 +projects_trackers_005: + project_id: 2 + tracker_id: 2 +projects_trackers_006: + project_id: 2 + tracker_id: 3 +projects_trackers_007: + project_id: 3 + tracker_id: 1 +projects_trackers_008: + project_id: 3 + tracker_id: 2 +projects_trackers_009: + project_id: 3 + tracker_id: 3 +projects_trackers_010: + project_id: 4 + tracker_id: 1 +projects_trackers_011: + project_id: 4 + tracker_id: 2 diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index e6c06cf5..c41adaa6 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -22,7 +22,7 @@ require 'projects_controller' class ProjectsController; def rescue_action(e) raise e end; end class ProjectsControllerTest < Test::Unit::TestCase - fixtures :projects, :users, :roles, :members, :issues, :journals, :journal_details, :trackers, :issue_statuses, :enabled_modules, :enumerations + fixtures :projects, :users, :roles, :members, :issues, :journals, :journal_details, :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations def setup @controller = ProjectsController.new diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index 6ffbe0a8..da91dd02 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -18,7 +18,7 @@ require File.dirname(__FILE__) + '/../test_helper' class IssueTest < Test::Unit::TestCase - fixtures :projects, :users, :members, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries + fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries def test_category_based_assignment issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)