]> source.dussan.org Git - redmine.git/commitdiff
Merged 15430, 15464 to 15469, 15475, 15476 (#285, #7839).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Mon, 6 Jun 2016 09:41:50 +0000 (09:41 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Mon, 6 Jun 2016 09:41:50 +0000 (09:41 +0000)
git-svn-id: http://svn.redmine.org/redmine/branches/3.3-stable@15478 e93f8b46-1217-0410-a6f0-8f06a7374b81

23 files changed:
app/controllers/context_menus_controller.rb
app/controllers/issues_controller.rb
app/helpers/issues_helper.rb
app/models/issue.rb
app/models/issue_import.rb
app/models/mail_handler.rb
app/models/role.rb
app/views/issues/_action_menu.html.erb
app/views/issues/_edit.html.erb
app/views/issues/_form.html.erb
app/views/issues/_history.html.erb
app/views/issues/index.html.erb
app/views/issues/show.html.erb
app/views/roles/_form.html.erb
config/locales/en.yml
config/locales/fr.yml
db/migrate/20160529063352_add_roles_settings.rb [new file with mode: 0644]
lib/redmine.rb
public/javascripts/application.js
public/stylesheets/application.css
test/functional/issues_controller_test.rb
test/functional/roles_controller_test.rb
test/unit/issue_test.rb

index 1e5652d09a770e8319244b2dd1a510ee63a0e3e2..66ec35085179b67b1e6987420ce192ba401bf251 100644 (file)
@@ -29,11 +29,11 @@ class ContextMenusController < ApplicationController
 
     @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
 
-    @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
+    @can = {:edit => @issues.all?(&:attributes_editable?),
             :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
             :copy => User.current.allowed_to?(:copy_issues, @projects) && Issue.allowed_target_projects.any?,
             :add_watchers => User.current.allowed_to?(:add_issue_watchers, @projects),
-            :delete => User.current.allowed_to?(:delete_issues, @projects)
+            :delete => @issues.all?(&:deletable?)
             }
     if @project
       if @issue
@@ -41,12 +41,11 @@ class ContextMenusController < ApplicationController
       else
         @assignables = @project.assignable_users
       end
-      @trackers = @project.trackers
     else
       #when multiple projects, we only keep the intersection of each set
       @assignables = @projects.map(&:assignable_users).reduce(:&)
-      @trackers = @projects.map(&:trackers).reduce(:&)
     end
+    @trackers = @projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
     @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
 
     @priorities = IssuePriority.active.reverse
index cf402de226824182a0d7bb47baba7998b6e83eb2..67956667a07d59b9f533c352eb51e5c8913a0422 100644 (file)
@@ -211,6 +211,10 @@ class IssuesController < ApplicationController
       unless User.current.allowed_to?(:copy_issues, @projects)
         raise ::Unauthorized
       end
+    else
+      unless @issues.all?(&:attributes_editable?)
+        raise ::Unauthorized
+      end
     end
 
     @allowed_projects = Issue.allowed_target_projects
@@ -230,7 +234,7 @@ class IssuesController < ApplicationController
     end
     @custom_fields = @issues.map{|i|i.editable_custom_fields}.reduce(:&)
     @assignables = target_projects.map(&:assignable_users).reduce(:&)
-    @trackers = target_projects.map(&:trackers).reduce(:&)
+    @trackers = target_projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
     @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
     @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
     if @copy
@@ -263,6 +267,10 @@ class IssuesController < ApplicationController
       unless User.current.allowed_to?(:add_issues, target_projects)
         raise ::Unauthorized
       end
+    else
+      unless @issues.all?(&:attributes_editable?)
+        raise ::Unauthorized
+      end
     end
 
     unsaved_issues = []
@@ -316,6 +324,7 @@ class IssuesController < ApplicationController
   end
 
   def destroy
+    raise Unauthorized unless @issues.all?(&:deletable?)
     @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
     if @hours > 0
       case params[:todo]
@@ -465,9 +474,15 @@ class IssuesController < ApplicationController
     @issue.safe_attributes = attrs
 
     if @issue.project
-      @issue.tracker ||= @issue.project.trackers.first
+      @issue.tracker ||= @issue.allowed_target_trackers.first
       if @issue.tracker.nil?
-        render_error l(:error_no_tracker_in_project)
+        if @issue.project.trackers.any?
+          # None of the project trackers is allowed to the user
+          render_error :message => l(:error_no_tracker_allowed_for_new_issue_in_project), :status => 403
+        else
+          # Project has no trackers
+          render_error l(:error_no_tracker_in_project)
+        end
         return false
       end
       if @issue.status.nil?
index b505765d8e1cce363f215e1905a96d92cad02f15..ce6d9f8f22f230ea8e35aad9ff5bbf91013e716a 100644 (file)
@@ -168,6 +168,16 @@ module IssuesHelper
     link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
   end
 
+  def trackers_options_for_select(issue)
+    trackers = issue.allowed_target_trackers
+    if issue.new_record? && issue.parent_issue_id.present?
+      trackers = trackers.reject do |tracker|
+        issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
+      end
+    end
+    trackers.collect {|t| [t.name, t.id]}
+  end
+
   class IssueFieldsRows
     include ActionView::Helpers::TagHelper
 
index 0ee8ff3d31c4bd48a51c1603efb50c4d4a0c4420..e0b99e4e6bb0fff5e0f9163d90f4abb0a3f2d921 100644 (file)
@@ -120,10 +120,10 @@ class Issue < ActiveRecord::Base
   # Returns a SQL conditions string used to find all issues visible by the specified user
   def self.visible_condition(user, options={})
     Project.allowed_to_condition(user, :view_issues, options) do |role, user|
-      if user.id && user.logged?
+      sql = if user.id && user.logged?
         case role.issues_visibility
         when 'all'
-          nil
+          '1=1'
         when 'default'
           user_ids = [user.id] + user.groups.map(&:id).compact
           "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
@@ -136,13 +136,22 @@ class Issue < ActiveRecord::Base
       else
         "(#{table_name}.is_private = #{connection.quoted_false})"
       end
+      unless role.permissions_all_trackers?(:view_issues)
+        tracker_ids = role.permissions_tracker_ids(:view_issues)
+        if tracker_ids.any?
+          sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
+        else
+          sql = '1=0'
+        end
+      end
+      sql
     end
   end
 
   # Returns true if usr or current user is allowed to view the issue
   def visible?(usr=nil)
     (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
-      if user.logged?
+      visible = if user.logged?
         case role.issues_visibility
         when 'all'
           true
@@ -156,17 +165,36 @@ class Issue < ActiveRecord::Base
       else
         !self.is_private?
       end
+      unless role.permissions_all_trackers?(:view_issues)
+        visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
+      end
+      visible
     end
   end
 
-  # Returns true if user or current user is allowed to edit or add a note to the issue
+  # Returns true if user or current user is allowed to edit or add notes to the issue
   def editable?(user=User.current)
-    attributes_editable?(user) || user.allowed_to?(:add_issue_notes, project)
+    attributes_editable?(user) || notes_addable?(user)
   end
 
   # Returns true if user or current user is allowed to edit the issue
   def attributes_editable?(user=User.current)
-    user.allowed_to?(:edit_issues, project)
+    user_tracker_permission?(user, :edit_issues)
+  end
+
+  # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
+  def attachments_editable?(user=User.current)
+    attributes_editable?(user)
+  end
+
+  # Returns true if user or current user is allowed to add notes to the issue
+  def notes_addable?(user=User.current)
+    user_tracker_permission?(user, :add_issue_notes)
+  end
+
+  # Returns true if user or current user is allowed to delete the issue
+  def deletable?(user=User.current)
+    user_tracker_permission?(user, :delete_issues)
   end
 
   def initialize(attributes=nil, *args)
@@ -416,10 +444,10 @@ class Issue < ActiveRecord::Base
     'custom_fields',
     'lock_version',
     'notes',
-    :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
+    :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
 
   safe_attributes 'notes',
-    :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
+    :if => lambda {|issue, user| issue.notes_addable?(user)}
 
   safe_attributes 'private_notes',
     :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
@@ -434,7 +462,7 @@ class Issue < ActiveRecord::Base
     }
 
   safe_attributes 'parent_issue_id',
-    :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
+    :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
       user.allowed_to?(:manage_subtasks, issue.project)}
 
   def safe_attribute_names(user=nil)
@@ -479,12 +507,14 @@ class Issue < ActiveRecord::Base
     end
 
     if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
-      self.tracker_id = t
+      if allowed_target_trackers(user).where(:id => t.to_i).exists?
+        self.tracker_id = t
+      end
     end
     if project
-      # Set the default tracker to accept custom field values
+      # Set a default tracker to accept custom field values
       # even if tracker is not specified
-      self.tracker ||= project.trackers.first
+      self.tracker ||= allowed_target_trackers(user).first
     end
 
     statuses_allowed = new_statuses_allowed_to(user)
@@ -822,16 +852,6 @@ class Issue < ActiveRecord::Base
     !leaf?
   end
 
-  def assignable_trackers
-    trackers = project.trackers
-    if new_record? && parent_issue_id.present?
-      trackers = trackers.reject do |tracker|
-        tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
-      end
-    end
-    trackers
-  end
-
   # Users the issue can be assigned to
   def assignable_users
     users = project.assignable_users.to_a
@@ -1373,9 +1393,43 @@ class Issue < ActiveRecord::Base
     end
     Project.where(condition).having_trackers
   end
+  # Returns a scope of trackers that user can assign the issue to
+  def allowed_target_trackers(user=User.current)
+    self.class.allowed_target_trackers(project, user, tracker_id_was)
+  end
+
+  # Returns a scope of trackers that user can assign project issues to
+  def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
+    if project
+      scope = project.trackers.sorted
+      unless user.admin?
+        roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
+        unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
+          tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
+          if current_tracker
+            tracker_ids << current_tracker
+          end
+          scope = scope.where(:id => tracker_ids)
+        end
+      end
+      scope
+    else
+      Tracker.none
+    end
+  end
 
   private
 
+  def user_tracker_permission?(user, permission)
+    if user.admin?
+      true
+    else
+      roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
+      roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
+    end
+  end
+
   def after_project_change
     # Update project_id on related time entries
     TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
index b6b20a1b19c8696c1cedc9f0da195f5936e9f5bc..5b19ac966e163a8ff388cb9780455225334d1085 100644 (file)
@@ -37,7 +37,7 @@ class IssueImport < Import
   # Returns a scope of trackers that user is allowed to
   # import issue to
   def allowed_target_trackers
-    project.trackers
+    Issue.allowed_target_trackers(project, user)
   end
 
   def tracker
index 40bd02a0789bc26d18a87a850d6c8cd0f066fca6..a619f115dd2d5211f380ad95dad7dde7abe8b7c9 100644 (file)
@@ -199,7 +199,14 @@ class MailHandler < ActionMailer::Base
     end
 
     issue = Issue.new(:author => user, :project => project)
-    issue.safe_attributes = issue_attributes_from_keywords(issue)
+    attributes = issue_attributes_from_keywords(issue)
+    if handler_options[:no_permission_check]
+      issue.tracker_id = attributes['tracker_id']
+      if project
+        issue.tracker_id ||= project.trackers.first.try(:id)
+      end
+    end
+    issue.safe_attributes = attributes
     issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
     issue.subject = cleaned_up_subject
     if issue.subject.blank?
@@ -420,10 +427,6 @@ class MailHandler < ActionMailer::Base
       'done_ratio' => get_keyword(:done_ratio, :format => '(\d|10)?0')
     }.delete_if {|k, v| v.blank? }
 
-    if issue.new_record? && attrs['tracker_id'].nil?
-      attrs['tracker_id'] = issue.project.trackers.first.try(:id)
-    end
-
     attrs
   end
 
index defbc311dcca0f0aa3051c40777121417e81c9fd..89538aa4d8f79690d13e2f6debdea7d387ae2829 100644 (file)
@@ -73,6 +73,7 @@ class Role < ActiveRecord::Base
   acts_as_positioned :scope => :builtin
 
   serialize :permissions, ::Role::PermissionsAttributeCoder
+  store :settings, :accessors => [:permissions_all_trackers, :permissions_tracker_ids]
   attr_protected :builtin
 
   validates_presence_of :name
@@ -188,6 +189,56 @@ class Role < ActiveRecord::Base
     setable_permissions
   end
 
+  def permissions_tracker_ids(*args)
+    if args.any?
+      Array(permissions_tracker_ids[args.first.to_s]).map(&:to_i)
+    else
+      super || {}
+    end
+  end
+
+  def permissions_tracker_ids=(arg)
+    h = arg.to_hash
+    h.values.each {|v| v.reject!(&:blank?)}
+    super(h)
+  end
+
+  # Returns true if tracker_id belongs to the list of
+  # trackers for which permission is given 
+  def permissions_tracker_ids?(permission, tracker_id)
+    permissions_tracker_ids(permission).include?(tracker_id)
+  end
+
+  def permissions_all_trackers
+    super || {}
+  end
+
+  def permissions_all_trackers=(arg)
+    super(arg.to_hash)
+  end
+
+  # Returns true if permission is given for all trackers
+  def permissions_all_trackers?(permission)
+    permissions_all_trackers[permission.to_s].to_s != '0'
+  end
+
+  # Sets the trackers that are allowed for a permission.
+  # tracker_ids can be an array of tracker ids or :all for
+  # no restrictions.
+  #
+  # Examples:
+  #   role.set_permission_trackers :add_issues, [1, 3]
+  #   role.set_permission_trackers :add_issues, :all
+  def set_permission_trackers(permission, tracker_ids)
+    h = {permission.to_s => (tracker_ids == :all ? '1' : '0')}
+    self.permissions_all_trackers = permissions_all_trackers.merge(h)
+
+    h = {permission.to_s => (tracker_ids == :all ? [] : tracker_ids)}
+    self.permissions_tracker_ids = permissions_tracker_ids.merge(h)
+
+    self
+  end
+
   # Find all the roles that can be given to a project member
   def self.find_all_givable
     Role.givable.to_a
index c3304626d198cfca70df3a24e8afb9ca78c07046..b535faec9070e0c1eb4647ecf96abe26b3dfe7f0 100644 (file)
@@ -3,5 +3,5 @@
 <%= link_to l(:button_log_time), new_issue_time_entry_path(@issue), :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project) %>
 <%= watcher_link(@issue, User.current) %>
 <%= link_to l(:button_copy), project_copy_issue_path(@project, @issue), :class => 'icon icon-copy' if User.current.allowed_to?(:copy_issues, @project) && Issue.allowed_target_projects.any? %>
-<%= link_to l(:button_delete), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_issues, @project) %>
+<%= link_to l(:button_delete), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, :method => :delete, :class => 'icon icon-del' if @issue.deletable? %>
 </div>
index 24a01cad064ce13beecd478f351f4aabe1864110..67e33246dc0c3acfcb11b222b482ab5e14a10dad 100644 (file)
         <% end %>
     </fieldset>
     <% end %>
-
-    <fieldset><legend><%= l(:field_notes) %></legend>
-    <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %>
-    <%= wikitoolbar_for 'issue_notes' %>
-
-    <% if @issue.safe_attribute? 'private_notes' %>
-    <%= f.check_box :private_notes, :no_label => true %> <label for="issue_private_notes"><%= l(:field_private_notes) %></label>
+    <% if @issue.notes_addable? %>
+      <fieldset><legend><%= l(:field_notes) %></legend>
+      <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %>
+      <%= wikitoolbar_for 'issue_notes' %>
+  
+      <% if @issue.safe_attribute? 'private_notes' %>
+      <%= f.check_box :private_notes, :no_label => true %> <label for="issue_private_notes"><%= l(:field_private_notes) %></label>
+      <% end %>
+  
+      <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
+      </fieldset>
+  
+      <fieldset><legend><%= l(:label_attachment_plural) %></legend>
+      <p><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
+      </fieldset>
     <% end %>
-
-    <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
-    </fieldset>
-
-    <fieldset><legend><%= l(:label_attachment_plural) %></legend>
-    <p><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
-    </fieldset>
     </div>
 
     <%= f.hidden_field :lock_version %>
index 788a2f38e15d3d3534787be99d0da454a2059999..1e82b6f8432ac55f40281ca32fdb7c884a172b5f 100644 (file)
@@ -14,7 +14,7 @@
 <% end %>
 
 <% if @issue.safe_attribute? 'tracker_id' %>
-<p><%= f.select :tracker_id, @issue.assignable_trackers.collect {|t| [t.name, t.id]}, {:required => true},
+<p><%= f.select :tracker_id, trackers_options_for_select(@issue), {:required => true},
                 :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)" %></p>
 <% end %>
 
index 993dfee3c3c9a64b07d7bcb3a00fd19d0d1b92b0..307ac909bc7a43b18254beeac479aa5a345dbb15 100644 (file)
@@ -1,4 +1,4 @@
-<% reply_links = authorize_for('issues', 'edit') -%>
+<% reply_links = issue.notes_addable? -%>
 <% for journal in journals %>
   <div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
     <div id="note-<%= journal.indice %>">
index 7dcba2629b3affc843bc24d1c2abf5fc1e3f1bcf..fd762023fbf4362f31d110e60194a752f5bd9cf7 100644 (file)
@@ -1,5 +1,5 @@
 <div class="contextual">
-  <% if User.current.allowed_to?(:add_issues, @project, :global => true) && (@project.nil? || @project.trackers.any?) %>
+  <% if User.current.allowed_to?(:add_issues, @project, :global => true) && (@project.nil? || Issue.allowed_target_trackers(@project).any?) %>
     <%= link_to l(:label_issue_new), _new_project_issue_path(@project), :class => 'icon icon-add new-issue' %>
   <% end %>
 </div>
index 70a7fe165ee1f47672582fcfb7df8cf04e93125c..2cbff32e512ac19015ead0dd96052c40d0bfd64f 100644 (file)
@@ -77,7 +77,7 @@ end %>
 <% if @issue.description? %>
 <div class="description">
   <div class="contextual">
-  <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if authorize_for('issues', 'edit') %>
+  <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if @issue.notes_addable? %>
   </div>
 
   <p><strong><%=l(:field_description)%></strong></p>
index 84d5de1859792bd90194f29d33c0011110f939fc..39acc34a40b323faa10486bf1f7df10a4df6eb75 100644 (file)
@@ -7,17 +7,17 @@
   <% end %>
 
   <% unless @role.anonymous? %>
-    <p><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
+    <p class="view_issues_shown"><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
   <% end %>
 
   <% unless @role.anonymous? %>
-    <p><%= f.select :time_entries_visibility, Role::TIME_ENTRIES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
+    <p class="view_time_entries_shown"><%= f.select :time_entries_visibility, Role::TIME_ENTRIES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
   <% end %>
 
   <p><%= f.select :users_visibility, Role::USERS_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
 
   <% unless @role.builtin? %>
-    <p id="manage_members_options">
+    <p class="manage_members_shown">
       <label><%= l(:label_member_management) %></label>
       <label class="block">
         <%= radio_button_tag 'role[all_roles_managed]', 1, @role.all_roles_managed?, :id => 'role_all_roles_managed_on',
@@ -52,7 +52,8 @@
     <% perms_by_module[mod].each do |permission| %>
         <label class="floating">
         <%= check_box_tag 'role[permissions][]', permission.name, (@role.permissions.include? permission.name),
-              :id => "role_permissions_#{permission.name}" %>
+              :id => "role_permissions_#{permission.name}",
+              :data => {:shows => ".#{permission.name}_shown"} %>
         <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
         </label>
     <% end %>
 <%= hidden_field_tag 'role[permissions][]', '' %>
 </div>
 
-<%= javascript_tag do %>
-$(document).ready(function(){
-  $("#role_permissions_manage_members").change(function(){
-    $("#manage_members_options").toggle($(this).is(":checked"));
-  }).change();
-});
+<div id="role-permissions-trackers" class="view_issues_shown">
+<h3><%= l(:label_issue_tracking) %></h3>
+<% permissions = %w(view_issues add_issues edit_issues add_issue_notes delete_issues) %>
+
+<div class="autoscroll">
+<table class="list">
+  <thead>
+    <tr>
+      <th><%= l(:label_tracker) %></th>
+      <% permissions.each do |permission| %>
+      <th class="<%= "#{permission}_shown" %>"><%= l("permission_#{permission}") %></th>
+      <% end %>
+  </thead>
+  <tbody>
+    <tr>
+      <td class="name"><b><%= l(:label_tracker_all) %></b></td>
+      <% permissions.each do |permission| %>
+      <td class="<%= "#{permission}_shown" %>">
+        <%= hidden_field_tag "role[permissions_all_trackers][#{permission}]", '0', :id => nil %>
+        <%= check_box_tag "role[permissions_all_trackers][#{permission}]",
+              '1',
+              @role.permissions_all_trackers?(permission),
+              :class => "#{permission}_shown",
+              :data => {:disables => ".#{permission}_tracker"} %>
+      </td>
+      <% end %>
+    </tr>
+    <% Tracker.sorted.all.each do |tracker| %>
+    <tr class="<%= cycle("odd", "even") %>">
+      <td class="name"><%= tracker.name %></td>
+      <% permissions.each do |permission| %>
+      <td class="<%= "#{permission}_shown" %>"><%= check_box_tag "role[permissions_tracker_ids][#{permission}][]",
+                tracker.id,
+                @role.permissions_tracker_ids?(permission, tracker.id),
+                :class => "#{permission}_tracker",
+                :id => "role_permissions_tracker_ids_add_issues_#{tracker.id}" %></td>
+      <% end %>
+    </tr>
+    <% end %>
+  </tbody>
+</table>
+</div>
+
+<% permissions.each do |permission| %>
+  <%= hidden_field_tag "role[permissions_tracker_ids][#{permission}][]", '' %>
 <% end %>
+</div>
index 9dd03560c41056f940f6973aa699efa0f3a0a57f..522cb5227bfd19e80065bea0e2037b281720106c 100644 (file)
@@ -213,6 +213,7 @@ en:
   error_can_not_read_import_file: "An error occurred while reading the file to import"
   error_attachment_extension_not_allowed: "Attachment extension %{extension} is not allowed"
   error_ldap_bind_credentials: "Invalid LDAP Account/Password"
+  error_no_tracker_allowed_for_new_issue_in_project: "The project doesn't have any trackers for which you can create an issue"
 
   mail_subject_lost_password: "Your %{value} password"
   mail_body_lost_password: 'To change your password, click on the following link:'
@@ -561,6 +562,7 @@ en:
   label_member_plural: Members
   label_tracker: Tracker
   label_tracker_plural: Trackers
+  label_tracker_all: All trackers
   label_tracker_new: New tracker
   label_workflow: Workflow
   label_issue_status: Issue status
index 6a72024f6b33ae386fa9b7966b629c2e5c80d7e6..760c84c95741f930a3439dcb25ed142a02d1f31a 100644 (file)
@@ -233,6 +233,7 @@ fr:
   error_can_not_read_import_file: "Une erreur est survenue lors de la lecture du fichier à importer"
   error_attachment_extension_not_allowed: "L'extension %{extension} n'est pas autorisée"
   error_ldap_bind_credentials: "Identifiant ou mot de passe LDAP incorrect"
+  error_no_tracker_allowed_for_new_issue_in_project: "Le projet ne dispose d'aucun tracker sur lequel vous pouvez créer une demande"
 
   mail_subject_lost_password: "Votre mot de passe %{value}"
   mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
@@ -573,6 +574,7 @@ fr:
   label_member_plural: Membres
   label_tracker: Tracker
   label_tracker_plural: Trackers
+  label_tracker_all: Tous les trackers
   label_tracker_new: Nouveau tracker
   label_workflow: Workflow
   label_issue_status: Statut de demandes
diff --git a/db/migrate/20160529063352_add_roles_settings.rb b/db/migrate/20160529063352_add_roles_settings.rb
new file mode 100644 (file)
index 0000000..921187e
--- /dev/null
@@ -0,0 +1,5 @@
+class AddRolesSettings < ActiveRecord::Migration
+  def change
+    add_column :roles, :settings, :text
+  end
+end
\ No newline at end of file
index 1d37a523b5196ff6a5d80ed9261500d489440cb9..6dfc880ff20f561243de7937effe4eaee6f4180f 100644 (file)
@@ -88,8 +88,6 @@ Redmine::AccessControl.map do |map|
   map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
 
   map.project_module :issue_tracking do |map|
-    # Issue categories
-    map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
     # Issues
     map.permission :view_issues, {:issues => [:index, :show],
                                   :auto_complete => [:issues],
@@ -120,62 +118,64 @@ Redmine::AccessControl.map do |map|
     map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]}
     map.permission :delete_issue_watchers, {:watchers => :destroy}
     map.permission :import_issues, {:imports => [:new, :create, :settings, :mapping, :run, :show]}
+    # Issue categories
+    map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
   end
 
   map.project_module :time_tracking do |map|
-    map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
     map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
+    map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
     map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
     map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
     map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
   end
 
   map.project_module :news do |map|
-    map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member
     map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true
+    map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member
     map.permission :comment_news, {:comments => :create}
   end
 
   map.project_module :documents do |map|
+    map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
     map.permission :add_documents, {:documents => [:new, :create, :add_attachment], :attachments => :upload}, :require => :loggedin
     map.permission :edit_documents, {:documents => [:edit, :update, :add_attachment], :attachments => :upload}, :require => :loggedin
     map.permission :delete_documents, {:documents => [:destroy]}, :require => :loggedin
-    map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
   end
 
   map.project_module :files do |map|
-    map.permission :manage_files, {:files => [:new, :create], :attachments => :upload}, :require => :loggedin
     map.permission :view_files, {:files => :index, :versions => :download}, :read => true
+    map.permission :manage_files, {:files => [:new, :create], :attachments => :upload}, :require => :loggedin
   end
 
   map.project_module :wiki do |map|
-    map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
-    map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
-    map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member
     map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true
-    map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
     map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true
+    map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
     map.permission :edit_wiki_pages, :wiki => [:new, :edit, :update, :preview, :add_attachment], :attachments => :upload
+    map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
+    map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member
     map.permission :delete_wiki_pages_attachments, {}
     map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
+    map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
   end
 
   map.project_module :repository do |map|
-    map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
-    map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
     map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true
+    map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
     map.permission :commit_access, {}
     map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
+    map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
   end
 
   map.project_module :boards do |map|
-    map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
     map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true
     map.permission :add_messages, {:messages => [:new, :reply, :quote], :attachments => :upload}
     map.permission :edit_messages, {:messages => :edit, :attachments => :upload}, :require => :member
     map.permission :edit_own_messages, {:messages => :edit, :attachments => :upload}, :require => :loggedin
     map.permission :delete_messages, {:messages => :destroy}, :require => :member
     map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
+    map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
   end
 
   map.project_module :calendar do |map|
@@ -233,7 +233,7 @@ Redmine::MenuManager.map :project_menu do |menu|
   menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
   menu.push :new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, :param => :project_id, :caption => :label_issue_new,
               :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) },
-              :if => Proc.new { |p| Setting.new_project_issue_tab_enabled? && p.trackers.any? },
+              :if => Proc.new { |p| Setting.new_project_issue_tab_enabled? && Issue.allowed_target_trackers(p).any? },
               :permission => :add_issues
   menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
   menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
index 743b14ff66609339a1eb9e60a2c5507d3777de75..347611bb28c70cc795f9fe56ee2ca1cdf1ca8529 100644 (file)
@@ -719,9 +719,10 @@ function toggleDisabledOnChange() {
   var checked = $(this).is(':checked');
   $($(this).data('disables')).attr('disabled', checked);
   $($(this).data('enables')).attr('disabled', !checked);
+  $($(this).data('shows')).toggle(checked);
 }
 function toggleDisabledInit() {
-  $('input[data-disables], input[data-enables]').each(toggleDisabledOnChange);
+  $('input[data-disables], input[data-enables], input[data-shows]').each(toggleDisabledOnChange);
 }
 
 (function ( $ ) {
@@ -751,7 +752,7 @@ function toggleDisabledInit() {
 }( jQuery ));
 
 $(document).ready(function(){
-  $('#content').on('change', 'input[data-disables], input[data-enables]', toggleDisabledOnChange);
+  $('#content').on('change', 'input[data-disables], input[data-enables], input[data-shows]', toggleDisabledOnChange);
   toggleDisabledInit();
 });
 
index 83060eaa26c6303847df2b1dc0688d3483f4e993..c276ebd279b752cff262b4db4e0347d8fbaca48c 100644 (file)
@@ -152,6 +152,7 @@ table.list td.buttons img, div.buttons img {vertical-align:middle;}
 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
 table.list table.progress td {padding-right:0px;}
 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
+#role-permissions-trackers table.list th {white-space:normal;}
 
 .table-list-cell {display: table-cell; vertical-align: top; padding:2px; }
 
index 32f9d8f11c60e9048ccb370702b197b6a6a1d4db..dc50d13310dcc475a3cacfcbcf663df4d6182423 100644 (file)
@@ -1864,6 +1864,31 @@ class IssuesControllerTest < ActionController::TestCase
     end
   end
 
+  def test_new_should_propose_allowed_trackers
+    role = Role.find(1)
+    role.set_permission_trackers 'add_issues', [1, 3]
+    role.save!
+    @request.session[:user_id] = 2
+
+    get :new, :project_id => 1
+    assert_response :success
+    assert_select 'select[name=?]', 'issue[tracker_id]' do
+      assert_select 'option', 2
+      assert_select 'option[value="1"]'
+      assert_select 'option[value="3"]'
+    end
+  end
+
+  def test_new_without_allowed_trackers_should_respond_with_403
+    role = Role.find(1)
+    role.set_permission_trackers 'add_issues', []
+    role.save!
+    @request.session[:user_id] = 2
+
+    get :new, :project_id => 1
+    assert_response 403
+  end
+
   def test_new_should_preselect_default_version
     version = Version.generate!(:project_id => 1)
     Project.find(1).update_attribute :default_version_id, version.id
@@ -2432,6 +2457,23 @@ class IssuesControllerTest < ActionController::TestCase
     assert_nil issue.custom_field_value(cf2)
   end
 
+  def test_create_should_ignore_unallowed_trackers
+    role = Role.find(1)
+    role.set_permission_trackers :add_issues, [3]
+    role.save!
+    @request.session[:user_id] = 2
+
+    issue = new_record(Issue) do
+      post :create, :project_id => 1, :issue => {
+        :tracker_id => 1,
+        :status_id => 1,
+        :subject => 'Test'
+      }
+      assert_response 302
+    end
+    assert_equal 3, issue.tracker_id
+  end
+
   def test_post_create_with_watchers
     @request.session[:user_id] = 2
     ActionMailer::Base.deliveries.clear
@@ -3830,6 +3872,30 @@ class IssuesControllerTest < ActionController::TestCase
     assert_redirected_to '/issues/11?issue_count=3&issue_position=2&next_issue_id=12&prev_issue_id=8'
   end
 
+  def test_update_with_permission_on_tracker_should_be_allowed
+    role = Role.find(1)
+    role.set_permission_trackers :edit_issues, [1]
+    role.save!
+    issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Original subject')
+
+    @request.session[:user_id] = 2
+    put :update, :id => issue.id, :issue => {:subject => 'Changed subject'}
+    assert_response 302
+    assert_equal 'Changed subject', issue.reload.subject
+  end
+
+  def test_update_without_permission_on_tracker_should_be_denied
+    role = Role.find(1)
+    role.set_permission_trackers :edit_issues, [1]
+    role.save!
+    issue = Issue.generate!(:project_id => 1, :tracker_id => 2, :subject => 'Original subject')
+
+    @request.session[:user_id] = 2
+    put :update, :id => issue.id, :issue => {:subject => 'Changed subject'}
+    assert_response 302
+    assert_equal 'Original subject', issue.reload.subject
+  end
+
   def test_get_bulk_edit
     @request.session[:user_id] = 2
     get :bulk_edit, :ids => [1, 3]
@@ -4660,6 +4726,32 @@ class IssuesControllerTest < ActionController::TestCase
     assert_response 404
   end
 
+  def test_destroy_with_permission_on_tracker_should_be_allowed
+    role = Role.find(1)
+    role.set_permission_trackers :delete_issues, [1]
+    role.save!
+    issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
+
+    @request.session[:user_id] = 2
+    assert_difference 'Issue.count', -1 do
+      delete :destroy, :id => issue.id
+    end
+    assert_response 302
+  end
+
+  def test_destroy_without_permission_on_tracker_should_be_denied
+    role = Role.find(1)
+    role.set_permission_trackers :delete_issues, [2]
+    role.save!
+    issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
+
+    @request.session[:user_id] = 2
+    assert_no_difference 'Issue.count' do
+      delete :destroy, :id => issue.id
+    end
+    assert_response 403
+  end
+
   def test_default_search_scope
     get :index
 
index 8ce4693955a2dbe9d421830e2b3abc730dae9c62..915b658c9d721df74cc5d895d0104a5b87f33a01 100644 (file)
@@ -132,6 +132,22 @@ class RolesControllerTest < ActionController::TestCase
     assert_equal [:edit_project], role.permissions
   end
 
+  def test_update_trackers_permissions
+    put :update, :id => 1, :role => {
+      :permissions_all_trackers => {'add_issues' => '0'},
+      :permissions_tracker_ids => {'add_issues' => ['1', '3', '']}
+    }
+
+    assert_redirected_to '/roles'
+    role = Role.find(1)
+
+    assert_equal({'add_issues' => '0'}, role.permissions_all_trackers)
+    assert_equal({'add_issues' => ['1', '3']}, role.permissions_tracker_ids)
+
+    assert_equal false, role.permissions_all_trackers?(:add_issues)
+    assert_equal [1, 3], role.permissions_tracker_ids(:add_issues).sort
+  end
+
   def test_update_with_failure
     put :update, :id => 1, :role => {:name => ''}
     assert_response :success
index d7c7d231bfe524224e046a5ea7f4db760161170e..3b7391a3c5a31e8cab5974dadc7853219a2d32cf 100644 (file)
@@ -342,6 +342,69 @@ class IssueTest < ActiveSupport::TestCase
     assert_include issue, issues
   end
 
+  def test_visible_scope_for_member_with_limited_tracker_ids
+    role = Role.find(1)
+    role.set_permission_trackers :view_issues, [2]
+    role.save!
+    user = User.find(2)
+
+    issues = Issue.where(:project_id => 1).visible(user).to_a
+    assert issues.any?
+    assert_equal [2], issues.map(&:tracker_id).uniq
+
+    assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
+  end
+
+  def test_visible_scope_should_consider_tracker_ids_on_each_project
+    user = User.generate!
+
+    project1 = Project.generate!
+    role1 = Role.generate!
+    role1.add_permission! :view_issues
+    role1.set_permission_trackers :view_issues, :all
+    role1.save!
+    User.add_to_project(user, project1, role1)
+
+    project2 = Project.generate!
+    role2 = Role.generate!
+    role2.add_permission! :view_issues
+    role2.set_permission_trackers :view_issues, [2]
+    role2.save!
+    User.add_to_project(user, project2, role2)
+
+    visible_issues = [
+      Issue.generate!(:project => project1, :tracker_id => 1),
+      Issue.generate!(:project => project1, :tracker_id => 2),
+      Issue.generate!(:project => project2, :tracker_id => 2)
+    ]
+    hidden_issue = Issue.generate!(:project => project2, :tracker_id => 1)
+
+    issues = Issue.where(:project_id => [project1.id, project2.id]).visible(user)
+    assert_equal visible_issues.map(&:id), issues.ids.sort
+
+    assert visible_issues.all? {|issue| issue.visible?(user)}
+    assert !hidden_issue.visible?(user)
+  end
+
+  def test_visible_scope_should_not_consider_roles_without_view_issues_permission
+    user = User.generate!
+    role1 = Role.generate!
+    role1.remove_permission! :view_issues
+    role1.set_permission_trackers :view_issues, :all
+    role1.save!
+    role2 = Role.generate!
+    role2.add_permission! :view_issues
+    role2.set_permission_trackers :view_issues, [2]
+    role2.save!
+    User.add_to_project(user, Project.find(1), [role1, role2])
+
+    issues = Issue.where(:project_id => 1).visible(user).to_a
+    assert issues.any?
+    assert_equal [2], issues.map(&:tracker_id).uniq
+
+    assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
+  end
+
   def test_visible_scope_for_admin
     user = User.find(1)
     user.members.each(&:destroy)
@@ -737,9 +800,10 @@ class IssueTest < ActiveSupport::TestCase
     target = Tracker.find(2)
     target.core_fields = %w(assigned_to_id due_date)
     target.save!
+    user = User.find(2)
 
-    issue = Issue.new(:tracker => source)
-    issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
+    issue = Issue.new(:project => Project.find(1), :tracker => source)
+    issue.send :safe_attributes=, {'tracker_id' => 2, 'due_date' => '2012-07-14'}, user
     assert_equal target, issue.tracker
     assert_equal Date.parse('2012-07-14'), issue.due_date
   end
@@ -1437,6 +1501,91 @@ class IssueTest < ActiveSupport::TestCase
     assert_not_include project, Issue.allowed_target_projects(User.find(1))
   end
 
+  def test_allowed_target_trackers_with_one_role_allowed_on_all_trackers
+    user = User.generate!
+    role = Role.generate!
+    role.add_permission! :add_issues
+    role.set_permission_trackers :add_issues, :all
+    role.save!
+    User.add_to_project(user, Project.find(1), role)
+
+    assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
+  end
+
+  def test_allowed_target_trackers_with_one_role_allowed_on_some_trackers
+    user = User.generate!
+    role = Role.generate!
+    role.add_permission! :add_issues
+    role.set_permission_trackers :add_issues, [1, 3]
+    role.save!
+    User.add_to_project(user, Project.find(1), role)
+
+    assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
+  end
+
+  def test_allowed_target_trackers_with_two_roles_allowed_on_some_trackers
+    user = User.generate!
+    role1 = Role.generate!
+    role1.add_permission! :add_issues
+    role1.set_permission_trackers :add_issues, [1]
+    role1.save!
+    role2 = Role.generate!
+    role2.add_permission! :add_issues
+    role2.set_permission_trackers :add_issues, [3]
+    role2.save!
+    User.add_to_project(user, Project.find(1), [role1, role2])
+
+    assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
+  end
+
+  def test_allowed_target_trackers_with_two_roles_allowed_on_all_trackers_and_some_trackers
+    user = User.generate!
+    role1 = Role.generate!
+    role1.add_permission! :add_issues
+    role1.set_permission_trackers :add_issues, :all
+    role1.save!
+    role2 = Role.generate!
+    role2.add_permission! :add_issues
+    role2.set_permission_trackers :add_issues, [1, 3]
+    role2.save!
+    User.add_to_project(user, Project.find(1), [role1, role2])
+
+    assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
+  end
+
+  def test_allowed_target_trackers_should_not_consider_roles_without_add_issues_permission
+    user = User.generate!
+    role1 = Role.generate!
+    role1.remove_permission! :add_issues
+    role1.set_permission_trackers :add_issues, :all
+    role1.save!
+    role2 = Role.generate!
+    role2.add_permission! :add_issues
+    role2.set_permission_trackers :add_issues, [1, 3]
+    role2.save!
+    User.add_to_project(user, Project.find(1), [role1, role2])
+
+    assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
+  end
+
+  def test_allowed_target_trackers_without_project_should_be_empty
+    issue = Issue.new
+    assert_nil issue.project
+    assert_equal [], issue.allowed_target_trackers(User.find(2)).ids
+  end
+
+  def test_allowed_target_trackers_should_include_current_tracker
+    user = User.generate!
+    role = Role.generate!
+    role.add_permission! :add_issues
+    role.set_permission_trackers :add_issues, [3]
+    role.save!
+    User.add_to_project(user, Project.find(1), role)
+
+    issue = Issue.generate!(:project => Project.find(1), :tracker => Tracker.find(1))
+    assert_equal [1, 3], issue.allowed_target_trackers(user).ids.sort
+  end
+
   def test_move_to_another_project_with_same_category
     issue = Issue.find(1)
     issue.project = Project.find(2)