Project

General

Profile

Feature #3088 » 3088.patch

Yuichi HARADA, 2019-04-26 08:10

View differences:

app/controllers/projects_controller.rb
161 161

  
162 162
    if User.current.allowed_to_view_all_time_entries?(@project)
163 163
      @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
164
    end
165
    if User.current.allowed_to?(:view_estimated_hours, @project)
164 166
      @total_estimated_hours = Issue.visible.where(cond).sum(:estimated_hours).to_f
165 167
    end
166 168

  
app/models/issue.rb
465 465
    'start_date',
466 466
    'due_date',
467 467
    'done_ratio',
468
    'estimated_hours',
469 468
    'custom_field_values',
470 469
    'custom_fields',
471 470
    'lock_version',
......
494 493
  safe_attributes 'deleted_attachment_ids',
495 494
    :if => lambda {|issue, user| issue.attachments_deletable?(user)}
496 495

  
496
  safe_attributes 'estimated_hours',
497
    :if => lambda {|issue, user| user.allowed_to?(:view_estimated_hours, issue.project)}
498

  
497 499
  def safe_attribute_names(user=nil)
498 500
    names = super
499 501
    names -= disabled_core_fields
app/models/issue_query.rb
38 38
    QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
39 39
    QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date", :groupable => true),
40 40
    QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date", :groupable => true),
41
    QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
42
    QueryColumn.new(:total_estimated_hours,
43
      :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
44
        " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
45
      :default_order => 'desc'),
46 41
    QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
47 42
    TimestampQueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc', :groupable => true),
48 43
    TimestampQueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc', :groupable => true),
......
136 131
    add_available_filter "closed_on", :type => :date_past
137 132
    add_available_filter "start_date", :type => :date
138 133
    add_available_filter "due_date", :type => :date
139
    add_available_filter "estimated_hours", :type => :float
134
    if User.current.allowed_to?(:view_estimated_hours, nil, :global => true)
135
      add_available_filter "estimated_hours", :type => :float
136
    end
140 137
    add_available_filter "done_ratio", :type => :integer
141 138

  
142 139
    if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
......
195 192
    @available_columns = self.class.available_columns.dup
196 193
    @available_columns += issue_custom_fields.visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
197 194

  
195
    if User.current.allowed_to?(:view_estimated_hours, project, :global => true)
196
      # insert the columns after due_date or at the end
197
      index = @available_columns.rindex {|column| column.name == :due_date}
198
      index = (index ? index + 1 : -1)
199

  
200
      @available_columns.insert index, QueryColumn.new(:estimated_hours,
201
        :sortable => "#{Issue.table_name}.estimated_hours",
202
        :totalable => true
203
      )
204
      index = (index.negative? ? index : index + 1)
205
      @available_columns.insert index, QueryColumn.new(:total_estimated_hours,
206
        :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
207
          " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
208
        :default_order => 'desc'
209
      )
210
    end
211

  
198 212
    if User.current.allowed_to?(:view_time_entries, project, :global => true)
199
      # insert the columns after total_estimated_hours or at the end
200
      index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
213
      # insert the columns after total_estimated_hours or the columns after due_date or at the end
214
      index = @available_columns.rindex {|column| column.name == :total_estimated_hours || column.name == :due_date }
201 215
      index = (index ? index + 1 : -1)
202 216

  
203 217
      subselect = "SELECT SUM(hours) FROM #{TimeEntry.table_name}" +
......
217 231
        " WHERE (#{TimeEntry.visible_condition(User.current)})" +
218 232
        " AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt"
219 233

  
220
      @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
234
      index = (index.negative? ? index : index + 1)
235
      @available_columns.insert index, QueryColumn.new(:total_spent_hours,
221 236
        :sortable => "COALESCE((#{subselect}), 0)",
222 237
        :default_order => 'desc',
223 238
        :caption => :label_total_spent_time
app/views/issues/show.api.rsb
16 16
  api.due_date @issue.due_date
17 17
  api.done_ratio @issue.done_ratio
18 18
  api.is_private @issue.is_private
19
  api.estimated_hours @issue.estimated_hours
20
  api.total_estimated_hours @issue.total_estimated_hours
19
  if User.current.allowed_to?(:view_estimated_hours, @project)
20
    api.estimated_hours @issue.estimated_hours
21
    api.total_estimated_hours @issue.total_estimated_hours
22
  end
21 23
  if User.current.allowed_to?(:view_time_entries, @project)
22 24
    api.spent_hours(@issue.spent_hours)
23 25
    api.total_spent_hours(@issue.total_spent_hours)
app/views/issues/show.html.erb
64 64
  unless @issue.disabled_core_fields.include?('done_ratio')
65 65
    rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :legend => "#{@issue.done_ratio}%"), :class => 'progress'
66 66
  end
67
  unless @issue.disabled_core_fields.include?('estimated_hours')
67
  if User.current.allowed_to?(:view_estimated_hours, @project) && !@issue.disabled_core_fields.include?('estimated_hours')
68 68
    rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
69 69
  end
70 70
  if User.current.allowed_to?(:view_time_entries, @project) && @issue.total_spent_hours > 0
app/views/projects/show.html.erb
81 81
  </div>
82 82
  <% end %>
83 83

  
84
  <% if User.current.allowed_to?(:view_time_entries, @project) %>
84
  <% allowed_to_view_time_entries, allowed_to_view_estimated_hours = User.current.allowed_to?(:view_time_entries, @project), User.current.allowed_to?(:view_estimated_hours, @project) %>
85
  <% if allowed_to_view_time_entries || allowed_to_view_estimated_hours %>
85 86
  <div class="spent_time box">
86 87
    <h3 class="icon icon-time"><%= l(:label_time_tracking) %></h3>
87 88
    <ul>
88
      <% if @total_estimated_hours.present? %>
89
        <li><%= l(:field_estimated_hours) %>: <%= l_hours(@total_estimated_hours) %>
90
      <% end %>
91
      <% if @total_hours.present? %>
92
          <li><%= l(:label_spent_time) %>: <%= l_hours(@total_hours) %>
93
      <% end %>
89
    <% if @total_estimated_hours.present? && allowed_to_view_estimated_hours %>
90
      <li><%= l(:field_estimated_hours) %>: <%= l_hours(@total_estimated_hours) %>
91
    <% end %>
92
    <% if @total_hours.present? && allowed_to_view_time_entries %>
93
      <li><%= l(:label_spent_time) %>: <%= l_hours(@total_hours) %>
94
    <% end %>
94 95
    </ul>
96
    <% if allowed_to_view_time_entries %>
95 97
    <p>
96
    <% if User.current.allowed_to?(:log_time, @project) %>
98
      <% if User.current.allowed_to?(:log_time, @project) %>
97 99
      <%= link_to l(:button_log_time), new_project_time_entry_path(@project) %> |
98
    <% end %>
100
      <% end %>
99 101
    <%= link_to(l(:label_details), project_time_entries_path(@project)) %> |
100 102
    <%= link_to(l(:label_report), report_project_time_entries_path(@project)) %>
101 103
    </p>
104
    <% end %>
102 105
  </div>
103
<% end %>
106
  <% end %>
104 107
  <%= call_hook(:view_projects_show_left, :project => @project) %>
105 108
</div>
106 109

  
......
121 124
	<ul class="subprojects">
122 125
	  <% @subprojects.each do |project| %>
123 126
	  <li><%= link_to(project.name, project_path(project), :class => project.css_classes).html_safe %></li>
124
	  <% end %> 
127
	  <% end %>
125 128
    </ul>
126 129
  </div>
127 130
  <% end %>
app/views/versions/show.html.erb
13 13
<%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
14 14

  
15 15
<div id="version-summary">
16
<% if @version.visible_fixed_issues.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
16
<% allowed_to_view_all_time_entries, allowed_to_view_estimated_hours = User.current.allowed_to_view_all_time_entries?(@project), User.current.allowed_to?(:view_estimated_hours, @project) %>
17
<% if (@version.visible_fixed_issues.estimated_hours > 0 && allowed_to_view_estimated_hours) || allowed_to_view_all_time_entries %>
17 18
<fieldset class="time-tracking"><legend><%= l(:label_time_tracking) %></legend>
18 19
<table>
20
<% if allowed_to_view_estimated_hours %>
19 21
<tr>
20 22
    <th><%= l(:field_estimated_hours) %></th>
21 23
    <td class="total-hours"><%= link_to html_hours(l_hours(@version.visible_fixed_issues.estimated_hours)),
22 24
                                        project_issues_path(@version.project, :set_filter => 1, :status_id => '*', :fixed_version_id => @version.id, :c => [:tracker, :status, :subject, :estimated_hours], :t => [:estimated_hours]) %></td>
23 25
</tr>
24
<% if User.current.allowed_to_view_all_time_entries?(@project) %>
26
<% end %>
27
<% if allowed_to_view_all_time_entries %>
25 28
<tr>
26 29
    <th><%= l(:label_spent_time) %></th>
27 30
    <td class="total-hours"><%= link_to html_hours(l_hours(@version.spent_hours)),
config/locales/en.yml
508 508
  permission_delete_issue_watchers: Delete watchers
509 509
  permission_log_time: Log spent time
510 510
  permission_view_time_entries: View spent time
511
  permission_view_estimated_hours: View estimated time
511 512
  permission_edit_time_entries: Edit time logs
512 513
  permission_edit_own_time_entries: Edit own time logs
513 514
  permission_view_news: View news
lib/redmine.rb
125 125

  
126 126
  map.project_module :time_tracking do |map|
127 127
    map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
128
    map.permission :view_estimated_hours, {}, :read => true
128 129
    map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
129 130
    map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
130 131
    map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
lib/redmine/default_data/loader.rb
67 67
                                                      :view_calendar,
68 68
                                                      :log_time,
69 69
                                                      :view_time_entries,
70
                                                      :view_estimated_hours,
70 71
                                                      :view_news,
71 72
                                                      :comment_news,
72 73
                                                      :view_documents,
......
94 95
                                                    :view_calendar,
95 96
                                                    :log_time,
96 97
                                                    :view_time_entries,
98
                                                    :view_estimated_hours,
97 99
                                                    :view_news,
98 100
                                                    :comment_news,
99 101
                                                    :view_documents,
......
113 115
                                                            :view_gantt,
114 116
                                                            :view_calendar,
115 117
                                                            :view_time_entries,
118
                                                            :view_estimated_hours,
116 119
                                                            :view_news,
117 120
                                                            :comment_news,
118 121
                                                            :view_documents,
......
128 131
                                                           :view_gantt,
129 132
                                                           :view_calendar,
130 133
                                                           :view_time_entries,
134
                                                           :view_estimated_hours,
131 135
                                                           :view_news,
132 136
                                                           :view_documents,
133 137
                                                           :view_wiki_pages,
lib/redmine/export/pdf/issues_pdf_helper.rb
57 57
          right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
58 58
          right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
59 59
          right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
60
          right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
60
          right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] if \
61
            User.current.allowed_to?(:view_estimated_hours, issue.project) && !issue.disabled_core_fields.include?('estimated_hours')
61 62
          right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
62 63

  
63 64
          rows = left.size > right.size ? left.size : right.size
test/fixtures/roles.yml
1
--- 
2
roles_001: 
1
---
2
roles_001:
3 3
  name: Manager
4 4
  id: 1
5 5
  builtin: 0
6 6
  issues_visibility: all
7 7
  users_visibility: all
8 8
  permissions: |
9
    --- 
9
    ---
10 10
    - :add_project
11 11
    - :edit_project
12 12
    - :close_project
......
34 34
    - :view_calendar
35 35
    - :log_time
36 36
    - :view_time_entries
37
    - :view_estimated_hours
37 38
    - :edit_time_entries
38 39
    - :delete_time_entries
39 40
    - :view_news
......
67 68
    - :import_issues
68 69

  
69 70
  position: 1
70
roles_002: 
71
roles_002:
71 72
  name: Developer
72 73
  id: 2
73 74
  builtin: 0
74 75
  issues_visibility: default
75 76
  users_visibility: all
76 77
  permissions: |
77
    --- 
78
    ---
78 79
    - :edit_project
79 80
    - :manage_members
80 81
    - :manage_versions
......
93 94
    - :view_calendar
94 95
    - :log_time
95 96
    - :view_time_entries
97
    - :view_estimated_hours
96 98
    - :edit_own_time_entries
97 99
    - :view_news
98 100
    - :manage_news
......
117 119
    - :view_changesets
118 120

  
119 121
  position: 2
120
roles_003: 
122
roles_003:
121 123
  name: Reporter
122 124
  id: 3
123 125
  builtin: 0
124 126
  issues_visibility: default
125 127
  users_visibility: all
126 128
  permissions: |
127
    --- 
129
    ---
128 130
    - :edit_project
129 131
    - :manage_members
130 132
    - :manage_versions
......
140 142
    - :view_calendar
141 143
    - :log_time
142 144
    - :view_time_entries
145
    - :view_estimated_hours
143 146
    - :view_news
144 147
    - :manage_news
145 148
    - :comment_news
......
160 163
    - :view_changesets
161 164

  
162 165
  position: 3
163
roles_004: 
166
roles_004:
164 167
  name: Non member
165 168
  id: 4
166 169
  builtin: 1
167 170
  issues_visibility: default
168 171
  users_visibility: all
169 172
  permissions: |
170
    --- 
173
    ---
171 174
    - :view_issues
172 175
    - :add_issues
173 176
    - :edit_issues
......
178 181
    - :view_calendar
179 182
    - :log_time
180 183
    - :view_time_entries
184
    - :view_estimated_hours
181 185
    - :view_news
182 186
    - :comment_news
183 187
    - :view_documents
......
192 196
    - :view_changesets
193 197

  
194 198
  position: 1
195
roles_005: 
199
roles_005:
196 200
  name: Anonymous
197 201
  id: 5
198 202
  builtin: 2
199 203
  issues_visibility: default
200 204
  users_visibility: all
201 205
  permissions: |
202
    --- 
206
    ---
203 207
    - :view_issues
204 208
    - :add_issue_notes
205 209
    - :view_gantt
206 210
    - :view_calendar
207 211
    - :view_time_entries
212
    - :view_estimated_hours
208 213
    - :view_news
209 214
    - :view_documents
210 215
    - :view_wiki_pages
......
215 220
    - :view_changesets
216 221

  
217 222
  position: 1
218

  
test/functional/issues_controller_test.rb
1262 1262
    assert_select 'table.issues td.total_estimated_hours'
1263 1263
  end
1264 1264

  
1265
  def test_index_should_not_show_estimated_hours_column_without_permission
1266
    Role.anonymous.remove_permission! :view_estimated_hours
1267
    get :index, :params => {
1268
        :set_filter => 1,
1269
        :c => %w(subject estimated_hours)
1270
      }
1271
    assert_select 'td.estimated_hours', 0
1272
  end
1273

  
1265 1274
  def test_index_should_not_show_spent_hours_column_without_permission
1266 1275
    Role.anonymous.remove_permission! :view_time_entries
1267 1276
    get :index, :params => {
test/functional/projects_controller_test.rb
589 589
    end
590 590
  end
591 591

  
592
  def test_show_by_non_admin_user_with_view_estimated_hours_permission_should_show_estimated_time
593
    @request.session[:user_id] = 2 # manager
594
    get :show, :params => {
595
        :id => 'ecookbook'
596
      }
597

  
598
    assert_select 'div.spent_time.box>ul' do
599
      assert_select '>li', :text => 'Estimated time: 203.50 hours'
600
    end
601
  end
602

  
603
  def test_show_by_non_admin_user_without_view_estimated_hours_permission_should_not_show_estimated_time
604
    Role.find_by_name('Manager').remove_permission! :view_estimated_hours
605
    @request.session[:user_id] = 2 # manager
606
    get :show, :params => {
607
        :id => 'ecookbook'
608
      }
609

  
610
    assert_select 'div.spent_time.box>ul' do
611
      assert_select '>li', :text => 'Estimated time: 203.50 hours', :count => 0
612
    end
613
  end
614

  
592 615
  def test_settings
593 616
    @request.session[:user_id] = 2 # manager
594 617
    get :settings, :params => {
test/functional/versions_controller_test.rb
127 127
      assert_select 'a', :text => '1 open'
128 128
    end
129 129

  
130
    assert_select '.time-tracking td.total-hours a:first-child', :text => '2.00 hours'
130
    assert_select '.time-tracking tr:first-child' do
131
      assert_select 'th', :text => 'Estimated time'
132
      assert_select 'td.total-hours a', :text => '2.00 hours'
133
    end
134

  
135
    Role.non_member.remove_permission! :view_estimated_hours
136

  
137
    get :show, :params => {:id => 4}
138
    assert_response :success
139

  
140
    assert_select 'p.progress-info' do
141
      assert_select 'a', :text => '1 issue'
142
      assert_select 'a', :text => '1 open'
143
    end
144

  
145
    assert_select '.time-tracking th', :text => 'Estimated time', :count => 0
131 146
  end
132 147

  
133 148
  def test_show_should_link_to_spent_time_on_version
test/integration/api_test/issues_test.rb
419 419
    end
420 420
  end
421 421

  
422
  test "GET /issues/:id.xml should contains total_spent_hours, and should not contains estimated_hours and total_estimated_hours when permission does not exists" do
423
    parent = Issue.find(3)
424
    parent.update_columns :estimated_hours => 2.0
425
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
426
    TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today,
427
                      :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id)
428
    # remove permission!
429
    Role.anonymous.remove_permission! :view_estimated_hours
430

  
431
    get '/issues/3.xml'
432

  
433
    assert_equal 'application/xml', response.content_type
434
    assert_select 'issue' do
435
      assert_select 'estimated_hours',       false
436
      assert_select 'total_estimated_hours', false
437
      assert_select 'spent_hours',           parent.spent_hours.to_s
438
      assert_select 'total_spent_hours',     (parent.spent_hours.to_f + 2.5).to_s
439
    end
440
  end
441

  
422 442
  test "GET /issues/:id.xml should contains visible spent_hours only" do
423 443
    user = User.find_by_login('jsmith')
424 444
    Role.find(1).update(:time_entries_visibility => 'own')
......
469 489
    assert_nil json['issue']['total_spent_hours']
470 490
  end
471 491

  
492
  test "GET /issues/:id.json should contains total_spent_hours, and should not contains estimated_hours and total_estimated_hours when permission does not exists" do
493
    parent = Issue.find(3)
494
    parent.update_columns :estimated_hours => 2.0
495
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
496
    TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today,
497
                      :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id)
498
    # remove permission!
499
    Role.anonymous.remove_permission! :view_estimated_hours
500

  
501
    get '/issues/3.json'
502

  
503
    assert_equal 'application/json', response.content_type
504
    json = ActiveSupport::JSON.decode(response.body)
505
    assert_nil json['issue']['estimated_hours']
506
    assert_nil json['issue']['total_estimated_hours']
507
    assert_equal parent.spent_hours, json['issue']['spent_hours']
508
    assert_equal (parent.spent_hours.to_f + 2.5), json['issue']['total_spent_hours']
509
  end
510

  
472 511
  test "GET /issues/:id.json should contains visible spent_hours only" do
473 512
    user = User.find_by_login('jsmith')
474 513
    Role.find(1).update(:time_entries_visibility => 'own')
test/unit/mail_handler_test.rb
42 42
  end
43 43

  
44 44
  def test_add_issue_with_specific_overrides
45
    project = Project.find_by_name('OnlineStore')
46
    project.enabled_module_names += [:time_tracking]
47
    project.save!
48

  
45 49
    issue = submit_email('ticket_on_given_project.eml',
46 50
      :allow_override => ['status', 'start_date', 'due_date', 'assigned_to',
47 51
                          'fixed_version', 'estimated_hours', 'done_ratio', 'parent_issue']
......
69 73
  end
70 74

  
71 75
  def test_add_issue_with_all_overrides
76
    project = Project.find_by_name('OnlineStore')
77
    project.enabled_module_names += [:time_tracking]
78
    project.save!
79

  
72 80
    issue = submit_email('ticket_on_given_project.eml', :allow_override => 'all')
73 81
    assert issue.is_a?(Issue)
74 82
    assert !issue.new_record?
(2-2/6)