Feature #29470 » 0001-Option-to-set-a-tracker-only-as-subtask.patch
| app/helpers/workflows_helper.rb | ||
|---|---|---|
| 42 | 42 |
select_tag name, option_tags, {:multiple => multiple}.merge(options)
|
| 43 | 43 |
end |
| 44 | 44 | |
| 45 |
def field_required?(field) |
|
| 46 |
field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field) |
|
| 45 |
def field_required?(field, trackers=[]) |
|
| 46 |
if field == 'parent_issue_id' |
|
| 47 |
subtasks = trackers.select {|t| t.subtask_only?}
|
|
| 48 |
subtasks.count == trackers.count |
|
| 49 |
else |
|
| 50 |
field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field) |
|
| 51 |
end |
|
| 47 | 52 |
end |
| 48 | 53 | |
| 49 |
def field_permission_tag(permissions, status, field, roles) |
|
| 54 |
def field_permission_tag(permissions, status, field, roles, trackers=[])
|
|
| 50 | 55 |
name = field.is_a?(CustomField) ? field.id.to_s : field |
| 51 | 56 |
options = [["", ""], [l(:label_readonly), "readonly"]] |
| 52 |
options << [l(:label_required), "required"] unless field_required?(field) |
|
| 57 |
options << [l(:label_required), "required"] unless field_required?(field, trackers)
|
|
| 53 | 58 |
html_options = {}
|
| 54 | 59 | |
| 55 | 60 |
if perm = permissions[status.id][name] |
| app/models/issue.rb | ||
|---|---|---|
| 64 | 64 |
validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
|
| 65 | 65 |
validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
|
| 66 | 66 |
validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
|
| 67 |
validates_presence_of :parent_issue_id, :if => Proc.new {|issue| issue.tracker && issue.tracker.subtask_only? && issue.tracker_id_changed?}
|
|
| 67 | 68 | |
| 68 | 69 |
validates_length_of :subject, :maximum => 255 |
| 69 | 70 |
validates_inclusion_of :done_ratio, :in => 0..100 |
| ... | ... | |
| 623 | 624 |
# Returns the names of attributes that are read-only for user or the current user |
| 624 | 625 |
# For users with multiple roles, the read-only fields are the intersection of |
| 625 | 626 |
# read-only fields of each role |
| 626 |
# The result is an array of strings where sustom fields are represented with their ids
|
|
| 627 |
# The result is an array of strings where custom fields are represented with their ids
|
|
| 627 | 628 |
# |
| 628 | 629 |
# Examples: |
| 629 | 630 |
# issue.read_only_attribute_names # => ['due_date', '2'] |
| ... | ... | |
| 635 | 636 |
# Returns the names of required attributes for user or the current user |
| 636 | 637 |
# For users with multiple roles, the required fields are the intersection of |
| 637 | 638 |
# required fields of each role |
| 638 |
# The result is an array of strings where sustom fields are represented with their ids
|
|
| 639 |
# The result is an array of strings where custom fields are represented with their ids
|
|
| 639 | 640 |
# |
| 640 | 641 |
# Examples: |
| 641 | 642 |
# issue.required_attribute_names # => ['due_date', '2'] |
| app/models/tracker.rb | ||
|---|---|---|
| 70 | 70 |
'name', |
| 71 | 71 |
'default_status_id', |
| 72 | 72 |
'is_in_roadmap', |
| 73 |
'subtask_only', |
|
| 73 | 74 |
'core_fields', |
| 74 | 75 |
'position', |
| 75 | 76 |
'custom_field_ids', |
| app/views/issues/_attributes.html.erb | ||
|---|---|---|
| 61 | 61 |
<div class="splitcontentright"> |
| 62 | 62 |
<% if @issue.safe_attribute? 'parent_issue_id' %> |
| 63 | 63 |
<p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, |
| 64 |
:required => @issue.required_attribute?('parent_issue_id') %></p>
|
|
| 65 |
<%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript(auto_complete_issues_path(:project_id => @issue.project, :scope => Setting.cross_project_subtasks, :status => @issue.closed? ? 'c' : 'o', :issue_id => @issue.id))}')" %>
|
|
| 64 |
:required => @issue.required_attribute?('parent_issue_id') || @issue.tracker.subtask_only? %></p>
|
|
| 65 |
<%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @issue.project, :scope => Setting.cross_project_subtasks, :status => @issue.closed? ? 'c' : 'o', :issue_id => @issue.id)}')" %>
|
|
| 66 | 66 |
<% end %> |
| 67 | 67 | |
| 68 | 68 |
<% if @issue.safe_attribute? 'start_date' %> |
| app/views/trackers/_form.html.erb | ||
|---|---|---|
| 10 | 10 |
:include_blank => @tracker.default_status.nil?, |
| 11 | 11 |
:required => true %> |
| 12 | 12 |
</p> |
| 13 |
<p><%= f.check_box :subtask_only %></p> |
|
| 13 | 14 |
<p><%= f.check_box :is_in_roadmap %></p> |
| 14 | 15 |
<p><%= f.text_area :description, :rows => 4 %></p> |
| 15 | 16 |
<p> |
| app/views/trackers/index.api.rsb | ||
|---|---|---|
| 4 | 4 |
api.id tracker.id |
| 5 | 5 |
api.name tracker.name |
| 6 | 6 |
api.default_status(:id => tracker.default_status.id, :name => tracker.default_status.name) unless tracker.default_status.nil? |
| 7 |
api.subtask_only tracker.subtask_only? |
|
| 7 | 8 |
api.description tracker.description |
| 8 | 9 |
end |
| 9 | 10 |
end |
| app/views/trackers/index.html.erb | ||
|---|---|---|
| 9 | 9 |
<thead><tr> |
| 10 | 10 |
<th><%=l(:label_tracker)%></th> |
| 11 | 11 |
<th><%=l(:field_default_status)%></th> |
| 12 |
<th><%=l(:field_subtask_only)%></th> |
|
| 12 | 13 |
<th><%=l(:field_description)%></th> |
| 13 | 14 |
<th></th> |
| 14 | 15 |
<th></th> |
| ... | ... | |
| 18 | 19 |
<tr> |
| 19 | 20 |
<td class="name"><%= link_to tracker.name, edit_tracker_path(tracker) %></td> |
| 20 | 21 |
<td><%= tracker.default_status.name %></td> |
| 22 |
<td class="subtask"><%= checked_image tracker.subtask_only? %></td> |
|
| 21 | 23 |
<td class="description"><%= tracker.description %></td> |
| 22 | 24 |
<td> |
| 23 | 25 |
<% unless tracker.workflow_rules.exists? %> |
| app/views/workflows/permissions.html.erb | ||
|---|---|---|
| 61 | 61 |
<% @fields.each do |field, name| %> |
| 62 | 62 |
<tr> |
| 63 | 63 |
<td class="name"> |
| 64 |
<%= name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %>
|
|
| 64 |
<%= name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field, @trackers) %>
|
|
| 65 | 65 |
</td> |
| 66 | 66 |
<% for status in @statuses -%> |
| 67 | 67 |
<td class="<%= @permissions[status.id][field].try(:join, ' ') %>" title="<%= name %> (<%= status.name %>)"> |
| 68 |
<%= field_permission_tag(@permissions, status, field, @roles) %> |
|
| 68 |
<%= field_permission_tag(@permissions, status, field, @roles, @trackers) %>
|
|
| 69 | 69 |
<% unless status == @statuses.last %><a href="#" class="repeat-value">»</a><% end %> |
| 70 | 70 |
</td> |
| 71 | 71 |
<% end -%> |
| config/locales/en.yml | ||
|---|---|---|
| 395 | 395 |
field_history_default_tab: Issue's history default tab |
| 396 | 396 |
field_unique_id: Unique ID |
| 397 | 397 |
field_toolbar_language_options: Code highlighting toolbar languages |
| 398 |
field_subtask_only: Subtask only |
|
| 398 | 399 | |
| 399 | 400 |
setting_app_title: Application title |
| 400 | 401 |
setting_welcome_text: Welcome text |
| db/migrate/20190625215748_add_subtask_only_to_trackers.rb | ||
|---|---|---|
| 1 |
class AddSubtaskOnlyToTrackers < ActiveRecord::Migration[5.2] |
|
| 2 |
def up |
|
| 3 |
add_column :trackers, :subtask_only, :boolean, :default => false |
|
| 4 |
end |
|
| 5 | ||
| 6 |
def down |
|
| 7 |
remove_column :trackers, :subtask_only |
|
| 8 |
end |
|
| 9 |
end |
|
| test/fixtures/projects_trackers.yml | ||
|---|---|---|
| 1 |
---
|
|
| 2 |
projects_trackers_001:
|
|
| 1 |
--- |
|
| 2 |
projects_trackers_001: |
|
| 3 | 3 |
project_id: 4 |
| 4 | 4 |
tracker_id: 3 |
| 5 |
projects_trackers_002:
|
|
| 5 |
projects_trackers_002: |
|
| 6 | 6 |
project_id: 1 |
| 7 | 7 |
tracker_id: 1 |
| 8 |
projects_trackers_003:
|
|
| 8 |
projects_trackers_003: |
|
| 9 | 9 |
project_id: 5 |
| 10 | 10 |
tracker_id: 1 |
| 11 |
projects_trackers_004:
|
|
| 11 |
projects_trackers_004: |
|
| 12 | 12 |
project_id: 1 |
| 13 | 13 |
tracker_id: 2 |
| 14 |
projects_trackers_005:
|
|
| 14 |
projects_trackers_005: |
|
| 15 | 15 |
project_id: 5 |
| 16 | 16 |
tracker_id: 2 |
| 17 |
projects_trackers_006:
|
|
| 17 |
projects_trackers_006: |
|
| 18 | 18 |
project_id: 5 |
| 19 | 19 |
tracker_id: 3 |
| 20 |
projects_trackers_007:
|
|
| 20 |
projects_trackers_007: |
|
| 21 | 21 |
project_id: 2 |
| 22 | 22 |
tracker_id: 1 |
| 23 |
projects_trackers_008:
|
|
| 23 |
projects_trackers_008: |
|
| 24 | 24 |
project_id: 2 |
| 25 | 25 |
tracker_id: 2 |
| 26 |
projects_trackers_009:
|
|
| 26 |
projects_trackers_009: |
|
| 27 | 27 |
project_id: 2 |
| 28 | 28 |
tracker_id: 3 |
| 29 |
projects_trackers_010:
|
|
| 29 |
projects_trackers_010: |
|
| 30 | 30 |
project_id: 3 |
| 31 | 31 |
tracker_id: 2 |
| 32 |
projects_trackers_011:
|
|
| 32 |
projects_trackers_011: |
|
| 33 | 33 |
project_id: 3 |
| 34 | 34 |
tracker_id: 3 |
| 35 |
projects_trackers_012:
|
|
| 35 |
projects_trackers_012: |
|
| 36 | 36 |
project_id: 4 |
| 37 | 37 |
tracker_id: 1 |
| 38 |
projects_trackers_013:
|
|
| 38 |
projects_trackers_013: |
|
| 39 | 39 |
project_id: 4 |
| 40 | 40 |
tracker_id: 2 |
| 41 |
projects_trackers_014:
|
|
| 41 |
projects_trackers_014: |
|
| 42 | 42 |
project_id: 1 |
| 43 | 43 |
tracker_id: 3 |
| 44 |
projects_trackers_015:
|
|
| 44 |
projects_trackers_015: |
|
| 45 | 45 |
project_id: 6 |
| 46 | 46 |
tracker_id: 1 |
| 47 | 47 |
projects_trackers_016: |
| 48 | 48 |
project_id: 3 |
| 49 | 49 |
tracker_id: 1 |
| 50 |
projects_trackers_017: |
|
| 51 |
project_id: 1 |
|
| 52 |
tracker_id: 4 |
|
| test/fixtures/trackers.yml | ||
|---|---|---|
| 1 |
---
|
|
| 2 |
trackers_001:
|
|
| 1 |
--- |
|
| 2 |
trackers_001: |
|
| 3 | 3 |
name: Bug |
| 4 | 4 |
id: 1 |
| 5 | 5 |
is_in_chlog: true |
| 6 | 6 |
default_status_id: 1 |
| 7 | 7 |
position: 1 |
| 8 | 8 |
description: Description for Bug tracker |
| 9 |
trackers_002: |
|
| 9 |
subtask_only: false |
|
| 10 |
trackers_002: |
|
| 10 | 11 |
name: Feature request |
| 11 | 12 |
id: 2 |
| 12 | 13 |
is_in_chlog: true |
| 13 | 14 |
default_status_id: 1 |
| 14 | 15 |
position: 2 |
| 15 | 16 |
description: Description for Feature request tracker |
| 16 |
trackers_003: |
|
| 17 |
subtask_only: false |
|
| 18 |
trackers_003: |
|
| 17 | 19 |
name: Support request |
| 18 | 20 |
id: 3 |
| 19 | 21 |
is_in_chlog: false |
| 20 | 22 |
default_status_id: 1 |
| 21 | 23 |
position: 3 |
| 24 |
subtask_only: false |
|
| 25 |
trackers_004: |
|
| 26 |
name: Subtask |
|
| 27 |
id: 4 |
|
| 28 |
is_in_chlog: false |
|
| 29 |
default_status_id: 1 |
|
| 30 |
position: 4 |
|
| 31 |
subtask_only: true |
|
| test/functional/issues_controller_test.rb | ||
|---|---|---|
| 3169 | 3169 |
get(:new, :params => {:project_id => 1})
|
| 3170 | 3170 |
assert_response :success |
| 3171 | 3171 |
assert_select 'select[name=?]', 'issue[tracker_id]' do |
| 3172 |
assert_select 'option', 3
|
|
| 3172 |
assert_select 'option', 4
|
|
| 3173 | 3173 |
assert_select 'option[value="1"][selected=selected]' |
| 3174 | 3174 |
end |
| 3175 | 3175 |
end |
| ... | ... | |
| 3190 | 3190 |
) |
| 3191 | 3191 |
assert_response :success |
| 3192 | 3192 |
assert_select 'select[name=?]', 'issue[tracker_id]' do |
| 3193 |
assert_select 'option', 2
|
|
| 3193 |
assert_select 'option', 3
|
|
| 3194 | 3194 |
assert_select 'option[value="2"][selected=selected]' |
| 3195 | 3195 |
assert_select 'option[value="1"]', 0 |
| 3196 | 3196 |
end |
| test/functional/trackers_controller_test.rb | ||
|---|---|---|
| 31 | 31 |
get :index |
| 32 | 32 |
assert_response :success |
| 33 | 33 |
assert_select 'table.trackers' |
| 34 | ||
| 35 |
# assert subtask column |
|
| 36 |
assert_select 'td.subtask .icon-checked', 1 |
|
| 34 | 37 |
end |
| 35 | 38 | |
| 36 | 39 |
def test_index_by_anonymous_should_redirect_to_login_form |
| ... | ... | |
| 49 | 52 |
get :new |
| 50 | 53 |
assert_response :success |
| 51 | 54 |
assert_select 'input[name=?]', 'tracker[name]' |
| 55 |
assert_select 'input[name=?]', 'tracker[subtask_only]' |
|
| 52 | 56 |
assert_select 'select[name=?]', 'tracker[default_status_id]' do |
| 53 | 57 |
assert_select 'option[value=?][selected=selected]', IssueStatus.sorted.first.id.to_s |
| 54 | 58 |
end |
| ... | ... | |
| 72 | 76 |
assert_equal Tracker::CORE_FIELDS, tracker.core_fields |
| 73 | 77 |
assert_equal [1, 6], tracker.custom_field_ids.sort |
| 74 | 78 |
assert_equal 0, tracker.workflow_rules.count |
| 79 |
assert !tracker.subtask_only |
|
| 75 | 80 |
end |
| 76 | 81 | |
| 77 | 82 |
def test_create_with_disabled_core_fields |
| test/functional/workflows_controller_test.rb | ||
|---|---|---|
| 246 | 246 |
end |
| 247 | 247 |
end |
| 248 | 248 | |
| 249 |
def test_get_permissions_with_role_and_subtask_tracker_should_mark_parent_task_field_required |
|
| 250 |
get :permissions, :params => {:role_id => 1, :tracker_id => 4}
|
|
| 251 |
assert_response :success |
|
| 252 | ||
| 253 |
assert_select 'td.name', :text => 'Parent task *' |
|
| 254 |
end |
|
| 255 | ||
| 256 |
def test_get_permissions_with_role_and_multiple_subtask_trackers_should_mark_parent_task_field_required |
|
| 257 |
subtask = Tracker.find(1) |
|
| 258 |
subtask.subtask_only = true |
|
| 259 |
subtask.save! |
|
| 260 | ||
| 261 |
get :permissions, :params => {:role_id => 1, :tracker_id => [1, 4]}
|
|
| 262 |
assert_response :success |
|
| 263 | ||
| 264 |
assert_select 'td.name', :text => 'Parent task *' |
|
| 265 |
end |
|
| 266 | ||
| 267 |
def test_get_permissions_with_role_and_mixed_trackers_should_not_mark_parent_task_field_required |
|
| 268 |
get :permissions, :params => {:role_id => 1, :tracker_id => [1, 4]}
|
|
| 269 |
assert_response :success |
|
| 270 | ||
| 271 |
assert_select 'td.name', :text => 'Parent task' |
|
| 272 |
end |
|
| 273 | ||
| 249 | 274 |
def test_get_permissions_with_required_custom_field_should_not_show_required_option |
| 250 | 275 |
cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true) |
| 251 | 276 | |
| test/integration/api_test/trackers_test.rb | ||
|---|---|---|
| 30 | 30 | |
| 31 | 31 |
assert_select 'trackers[type=array] tracker id', :text => '2' do |
| 32 | 32 |
assert_select '~ name', :text => 'Feature request' |
| 33 |
assert_select '~ subtask_only', :text => 'false' |
|
| 33 | 34 |
assert_select '~ description', :text => 'Description for Feature request tracker' |
| 34 | 35 |
end |
| 36 | ||
| 37 |
assert_select 'trackers[type=array] tracker id', :text => '4' do |
|
| 38 |
assert_select '~ subtask_only', :text => 'true' |
|
| 39 |
end |
|
| 35 | 40 |
end |
| 36 | 41 |
end |
| test/unit/issue_test.rb | ||
|---|---|---|
| 214 | 214 |
assert_include 'Parent task is invalid', issue.errors.full_messages |
| 215 | 215 |
end |
| 216 | 216 | |
| 217 |
def test_create_with_subtask_tracker |
|
| 218 |
issue = Issue.new(:project_id => 1, :tracker_id => 4, |
|
| 219 |
:author_id => 1, :subject => 'Group assignment', |
|
| 220 |
:parent_issue_id => 1) |
|
| 221 | ||
| 222 |
assert issue.save |
|
| 223 |
assert_equal 1, issue.parent_issue_id |
|
| 224 |
end |
|
| 225 | ||
| 226 |
def test_create_with_subtask_tracker_should_require_parent_task |
|
| 227 |
issue = Issue.new(:project_id => 1, :tracker_id => 4, |
|
| 228 |
:author_id => 1, :subject => 'Group assignment') |
|
| 229 | ||
| 230 |
assert !issue.save |
|
| 231 |
assert_include 'Parent task cannot be blank', issue.errors.full_messages |
|
| 232 |
end |
|
| 233 | ||
| 217 | 234 |
def assert_visibility_match(user, issues) |
| 218 | 235 |
assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
|
| 219 | 236 |
end |
| ... | ... | |
| 1689 | 1706 |
role.save! |
| 1690 | 1707 |
User.add_to_project(user, Project.find(1), role) |
| 1691 | 1708 | |
| 1692 |
assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort |
|
| 1709 |
assert_equal [1, 2, 3, 4], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
|
|
| 1693 | 1710 |
end |
| 1694 | 1711 | |
| 1695 | 1712 |
def test_allowed_target_trackers_with_one_role_allowed_on_some_trackers |
| ... | ... | |
| 1730 | 1747 |
role2.save! |
| 1731 | 1748 |
User.add_to_project(user, Project.find(1), [role1, role2]) |
| 1732 | 1749 | |
| 1733 |
assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort |
|
| 1750 |
assert_equal [1, 2, 3, 4], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
|
|
| 1734 | 1751 |
end |
| 1735 | 1752 | |
| 1736 | 1753 |
def test_allowed_target_trackers_should_not_consider_roles_without_add_issues_permission |
| test/unit/lib/redmine/acts/positioned_without_scope_test.rb | ||
|---|---|---|
| 26 | 26 |
t = Tracker.generate |
| 27 | 27 |
t.save! |
| 28 | 28 | |
| 29 |
assert_equal 4, t.reload.position
|
|
| 29 |
assert_equal 5, t.reload.position
|
|
| 30 | 30 |
end |
| 31 | 31 | |
| 32 | 32 |
def test_create_should_insert_at_given_position |
| ... | ... | |
| 35 | 35 |
t.save! |
| 36 | 36 | |
| 37 | 37 |
assert_equal 2, t.reload.position |
| 38 |
assert_equal [1, 3, 4, 2], Tracker.order(:id).pluck(:position) |
|
| 38 |
assert_equal [1, 3, 4, 5, 2], Tracker.order(:id).pluck(:position)
|
|
| 39 | 39 |
end |
| 40 | 40 | |
| 41 | 41 |
def test_destroy_should_remove_position |
| ... | ... | |
| 43 | 43 |
Tracker.generate! |
| 44 | 44 |
t.destroy |
| 45 | 45 | |
| 46 |
assert_equal [1, 2, 3, 4], Tracker.order(:id).pluck(:position) |
|
| 46 |
assert_equal [1, 2, 3, 4, 5], Tracker.order(:id).pluck(:position)
|
|
| 47 | 47 |
end |
| 48 | 48 | |
| 49 | 49 |
def test_update_should_update_positions |
| 50 | 50 |
t = Tracker.generate! |
| 51 |
assert_equal 4, t.position
|
|
| 51 |
assert_equal 5, t.position
|
|
| 52 | 52 | |
| 53 | 53 |
t.position = 2 |
| 54 | 54 |
t.save! |
| 55 |
assert_equal [1, 3, 4, 2], Tracker.order(:id).pluck(:position) |
|
| 55 |
assert_equal [1, 3, 4, 5, 2], Tracker.order(:id).pluck(:position)
|
|
| 56 | 56 |
end |
| 57 | 57 |
end |
| test/unit/tracker_test.rb | ||
|---|---|---|
| 140 | 140 |
assert tracker.respond_to?(:description) |
| 141 | 141 |
assert_equal tracker.description, "Description for Bug tracker" |
| 142 | 142 |
end |
| 143 | ||
| 144 |
def test_set_tracker_subtask_only |
|
| 145 |
tracker = Tracker.find(1) |
|
| 146 |
tracker.subtask_only = true |
|
| 147 | ||
| 148 |
assert tracker.save! |
|
| 149 |
assert tracker.subtask_only? |
|
| 150 |
end |
|
| 143 | 151 |
end |