Project

General

Profile

Feature #3088 » 3088-v2.patch

Yuichi HARADA, 2020-01-30 02:44

View differences:

app/controllers/projects_controller.rb
174 174

  
175 175
    if User.current.allowed_to_view_all_time_entries?(@project)
176 176
      @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
177
    end
178
    if User.current.allowed_to?(:view_estimated_hours, @project)
177 179
      @total_estimated_hours = Issue.visible.where(cond).sum(:estimated_hours).to_f
178 180
    end
179 181

  
app/models/issue.rb
467 467
    'start_date',
468 468
    'due_date',
469 469
    'done_ratio',
470
    'estimated_hours',
471 470
    'custom_field_values',
472 471
    'custom_fields',
473 472
    'lock_version',
......
498 497
    'deleted_attachment_ids',
499 498
    :if => lambda {|issue, user| issue.attachments_deletable?(user)})
500 499

  
500
  safe_attributes 'estimated_hours',
501
    :if => lambda {|issue, user| user.allowed_to?(:view_estimated_hours, issue.project)}
502

  
501 503
  def safe_attribute_names(user=nil)
502 504
    names = super
503 505
    names -= disabled_core_fields
app/models/issue_query.rb
37 37
    QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
38 38
    QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date", :groupable => true),
39 39
    QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date", :groupable => true),
40
    QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
41
    QueryColumn.new(
42
      :total_estimated_hours,
43
      :sortable => -> {
44
                     "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
45
        " WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')} AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)"
46
                   },
47
      :default_order => 'desc'),
48 40
    QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
49 41
    TimestampQueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc', :groupable => true),
50 42
    TimestampQueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc', :groupable => true),
......
155 147
    add_available_filter "closed_on", :type => :date_past
156 148
    add_available_filter "start_date", :type => :date
157 149
    add_available_filter "due_date", :type => :date
158
    add_available_filter "estimated_hours", :type => :float
159

  
160
    if User.current.allowed_to?(:view_time_entries, project, :global => true)
161
      add_available_filter "spent_time", :type => :float, :label => :label_spent_time
150
    if User.current.allowed_to?(:view_estimated_hours, nil, :global => true)
151
      add_available_filter "estimated_hours", :type => :float
162 152
    end
163

  
164 153
    add_available_filter "done_ratio", :type => :integer
165 154

  
166 155
    if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
......
225 214
    @available_columns = self.class.available_columns.dup
226 215
    @available_columns += issue_custom_fields.visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
227 216

  
217
    if User.current.allowed_to?(:view_estimated_hours, project, :global => true)
218
      # insert the columns after due_date or at the end
219
      index = @available_columns.rindex {|column| column.name == :due_date}
220
      index = (index ? index + 1 : -1)
221

  
222
      @available_columns.insert index, QueryColumn.new(:estimated_hours,
223
        :sortable => "#{Issue.table_name}.estimated_hours",
224
        :totalable => true
225
      )
226
      index = (index.negative? ? index : index + 1)
227
      @available_columns.insert index, QueryColumn.new(:total_estimated_hours,
228
        :sortable => -> {
229
                       "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
230
          " WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')} AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)"
231
                     },
232
        :default_order => 'desc'
233
      )
234
    end
235

  
228 236
    if User.current.allowed_to?(:view_time_entries, project, :global => true)
229
      # insert the columns after total_estimated_hours or at the end
230
      index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
237
      # insert the columns after total_estimated_hours or the columns after due_date or at the end
238
      index = @available_columns.rindex {|column| column.name == :total_estimated_hours || column.name == :due_date }
231 239
      index = (index ? index + 1 : -1)
232 240

  
233 241
      subselect = "SELECT SUM(hours) FROM #{TimeEntry.table_name}" +
......
249 257
        " WHERE (#{TimeEntry.visible_condition(User.current)})" +
250 258
        " AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt"
251 259

  
260
      index = (index.negative? ? index : index + 1)
252 261
      @available_columns.insert(
253
        index + 1,
262
        index,
254 263
        QueryColumn.new(:total_spent_hours,
255 264
                        :sortable => "COALESCE((#{subselect}), 0)",
256 265
                        :default_order => 'desc',
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
94 94
  </div>
95 95
  <% end %>
96 96

  
97
  <% if User.current.allowed_to?(:view_time_entries, @project) %>
97
  <% 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) %>
98
  <% if allowed_to_view_time_entries || allowed_to_view_estimated_hours %>
98 99
  <div class="spent_time box">
99 100
    <h3 class="icon icon-time"><%= l(:label_time_tracking) %></h3>
100 101
    <ul>
101
      <% if @total_estimated_hours.present? %>
102
        <li><%= l(:field_estimated_hours) %>: <%= l_hours(@total_estimated_hours) %>
103
      <% end %>
104
      <% if @total_hours.present? %>
105
          <li><%= l(:label_spent_time) %>: <%= l_hours(@total_hours) %>
106
      <% end %>
102
    <% if @total_estimated_hours.present? && allowed_to_view_estimated_hours %>
103
      <li><%= l(:field_estimated_hours) %>: <%= l_hours(@total_estimated_hours) %>
104
    <% end %>
105
    <% if @total_hours.present? && allowed_to_view_time_entries %>
106
      <li><%= l(:label_spent_time) %>: <%= l_hours(@total_hours) %>
107
    <% end %>
107 108
    </ul>
109
    <% if allowed_to_view_time_entries %>
108 110
    <p>
109
    <% if User.current.allowed_to?(:log_time, @project) %>
111
      <% if User.current.allowed_to?(:log_time, @project) %>
110 112
      <%= link_to l(:button_log_time), new_project_time_entry_path(@project) %> |
111
    <% end %>
113
      <% end %>
112 114
    <%= link_to(l(:label_details), project_time_entries_path(@project)) %> |
113 115
    <%= link_to(l(:label_report), report_project_time_entries_path(@project)) %>
114 116
    </p>
117
    <% end %>
115 118
  </div>
116
<% end %>
119
  <% end %>
117 120
  <%= call_hook(:view_projects_show_left, :project => @project) %>
118 121
</div>
119 122

  
......
134 137
	<ul class="subprojects">
135 138
	  <% @subprojects.each do |project| %>
136 139
	  <li><%= link_to(project.name, project_path(project), :class => project.css_classes).html_safe %></li>
137
	  <% end %> 
140
	  <% end %>
138 141
    </ul>
139 142
  </div>
140 143
  <% end %>
app/views/versions/show.html.erb
14 14
<%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
15 15

  
16 16
<div id="version-summary">
17
<% if @version.visible_fixed_issues.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
17
<% 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) %>
18
<% if (@version.visible_fixed_issues.estimated_hours > 0 && allowed_to_view_estimated_hours) || allowed_to_view_all_time_entries %>
18 19
<fieldset class="time-tracking"><legend><%= l(:label_time_tracking) %></legend>
19 20
<table>
21
<% if allowed_to_view_estimated_hours %>
20 22
<tr>
21 23
    <th><%= l(:field_estimated_hours) %></th>
22 24
    <td class="total-hours"><%= link_to html_hours(l_hours(@version.visible_fixed_issues.estimated_hours)),
23 25
                                        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>
24 26
</tr>
25
<% if User.current.allowed_to_view_all_time_entries?(@project) %>
27
<% end %>
28
<% if allowed_to_view_all_time_entries %>
26 29
<tr>
27 30
    <th><%= l(:label_spent_time) %></th>
28 31
    <td class="total-hours"><%= link_to html_hours(l_hours(@version.spent_hours)),
config/locales/en.yml
519 519
  permission_delete_issue_watchers: Delete watchers
520 520
  permission_log_time: Log spent time
521 521
  permission_view_time_entries: View spent time
522
  permission_view_estimated_hours: View estimated time
522 523
  permission_edit_time_entries: Edit time logs
523 524
  permission_edit_own_time_entries: Edit own time logs
524 525
  permission_view_news: View news
lib/redmine.rb
127 127

  
128 128
  map.project_module :time_tracking do |map|
129 129
    map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
130
    map.permission :view_estimated_hours, {}, :read => true
130 131
    map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
131 132
    map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
132 133
    map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
lib/redmine/default_data/loader.rb
68 68
                                                      :view_calendar,
69 69
                                                      :log_time,
70 70
                                                      :view_time_entries,
71
                                                      :view_estimated_hours,
71 72
                                                      :view_news,
72 73
                                                      :comment_news,
73 74
                                                      :view_documents,
......
95 96
                                                    :view_calendar,
96 97
                                                    :log_time,
97 98
                                                    :view_time_entries,
99
                                                    :view_estimated_hours,
98 100
                                                    :view_news,
99 101
                                                    :comment_news,
100 102
                                                    :view_documents,
......
114 116
                                                            :view_gantt,
115 117
                                                            :view_calendar,
116 118
                                                            :view_time_entries,
119
                                                            :view_estimated_hours,
117 120
                                                            :view_news,
118 121
                                                            :comment_news,
119 122
                                                            :view_documents,
......
129 132
                                                           :view_gantt,
130 133
                                                           :view_calendar,
131 134
                                                           :view_time_entries,
135
                                                           :view_estimated_hours,
132 136
                                                           :view_news,
133 137
                                                           :view_documents,
134 138
                                                           :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
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
    - :import_time_entries
......
94 95
    - :view_calendar
95 96
    - :log_time
96 97
    - :view_time_entries
98
    - :view_estimated_hours
97 99
    - :edit_own_time_entries
98 100
    - :view_news
99 101
    - :manage_news
......
141 143
    - :view_calendar
142 144
    - :log_time
143 145
    - :view_time_entries
146
    - :view_estimated_hours
144 147
    - :view_news
145 148
    - :manage_news
146 149
    - :comment_news
......
179 182
    - :view_calendar
180 183
    - :log_time
181 184
    - :view_time_entries
185
    - :view_estimated_hours
182 186
    - :view_news
183 187
    - :comment_news
184 188
    - :view_documents
......
206 210
    - :view_gantt
207 211
    - :view_calendar
208 212
    - :view_time_entries
213
    - :view_estimated_hours
209 214
    - :view_news
210 215
    - :view_documents
211 216
    - :view_wiki_pages
test/functional/issues_controller_test.rb
1519 1519
    assert_select 'table.issues td.total_estimated_hours'
1520 1520
  end
1521 1521

  
1522
  def test_index_should_not_show_estimated_hours_column_without_permission
1523
    Role.anonymous.remove_permission! :view_estimated_hours
1524
    get :index, :params => {
1525
        :set_filter => 1,
1526
        :c => %w(subject estimated_hours)
1527
      }
1528
    assert_select 'td.estimated_hours', 0
1529
  end
1530

  
1522 1531
  def test_index_should_not_show_spent_hours_column_without_permission
1523 1532
    Role.anonymous.remove_permission! :view_time_entries
1524 1533
    get(
test/functional/projects_controller_test.rb
759 759
    end
760 760
  end
761 761

  
762
  def test_show_by_non_admin_user_with_view_estimated_hours_permission_should_show_estimated_time
763
    @request.session[:user_id] = 2 # manager
764
    get :show, :params => {
765
        :id => 'ecookbook'
766
      }
767

  
768
    assert_select 'div.spent_time.box>ul' do
769
      assert_select '>li', :text => 'Estimated time: 203.50 hours'
770
    end
771
  end
772

  
773
  def test_show_by_non_admin_user_without_view_estimated_hours_permission_should_not_show_estimated_time
774
    Role.find_by_name('Manager').remove_permission! :view_estimated_hours
775
    @request.session[:user_id] = 2 # manager
776
    get :show, :params => {
777
        :id => 'ecookbook'
778
      }
779

  
780
    assert_select 'div.spent_time.box>ul' do
781
      assert_select '>li', :text => 'Estimated time: 203.50 hours', :count => 0
782
    end
783
  end
784

  
762 785
  def test_settings
763 786
    @request.session[:user_id] = 2 # manager
764 787
    get(:settings, :params => {:id => 1})
test/functional/versions_controller_test.rb
161 161
      assert_select 'a', :text => '1 open'
162 162
    end
163 163

  
164
    assert_select '.time-tracking td.total-hours a:first-child', :text => '2.00 hours'
164
    assert_select '.time-tracking tr:first-child' do
165
      assert_select 'th', :text => 'Estimated time'
166
      assert_select 'td.total-hours a', :text => '2.00 hours'
167
    end
168

  
169
    Role.non_member.remove_permission! :view_estimated_hours
170

  
171
    get :show, :params => {:id => 4}
172
    assert_response :success
173

  
174
    assert_select 'p.progress-info' do
175
      assert_select 'a', :text => '1 issue'
176
      assert_select 'a', :text => '1 open'
177
    end
178

  
179
    assert_select '.time-tracking th', :text => 'Estimated time', :count => 0
165 180
  end
166 181

  
167 182
  def test_show_should_link_to_spent_time_on_version
test/integration/api_test/issues_test.rb
427 427
    end
428 428
  end
429 429

  
430
  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
431
    parent = Issue.find(3)
432
    parent.update_columns :estimated_hours => 2.0
433
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
434
    TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today,
435
                      :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id)
436
    # remove permission!
437
    Role.anonymous.remove_permission! :view_estimated_hours
438

  
439
    get '/issues/3.xml'
440

  
441
    assert_equal 'application/xml', response.content_type
442
    assert_select 'issue' do
443
      assert_select 'estimated_hours',       false
444
      assert_select 'total_estimated_hours', false
445
      assert_select 'spent_hours',           parent.spent_hours.to_s
446
      assert_select 'total_spent_hours',     (parent.spent_hours.to_f + 2.5).to_s
447
    end
448
  end
449

  
430 450
  test "GET /issues/:id.xml should contains visible spent_hours only" do
431 451
    user = User.find_by_login('jsmith')
432 452
    Role.find(1).update(:time_entries_visibility => 'own')
......
475 495
    assert_nil json['issue']['total_spent_hours']
476 496
  end
477 497

  
498
  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
499
    parent = Issue.find(3)
500
    parent.update_columns :estimated_hours => 2.0
501
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
502
    TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today,
503
                      :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id)
504
    # remove permission!
505
    Role.anonymous.remove_permission! :view_estimated_hours
506

  
507
    get '/issues/3.json'
508

  
509
    assert_equal 'application/json', response.content_type
510
    json = ActiveSupport::JSON.decode(response.body)
511
    assert_nil json['issue']['estimated_hours']
512
    assert_nil json['issue']['total_estimated_hours']
513
    assert_equal parent.spent_hours, json['issue']['spent_hours']
514
    assert_equal (parent.spent_hours.to_f + 2.5), json['issue']['total_spent_hours']
515
  end
516

  
478 517
  test "GET /issues/:id.json should contains visible spent_hours only" do
479 518
    user = User.find_by_login('jsmith')
480 519
    Role.find(1).update(:time_entries_visibility => 'own')
test/unit/mail_handler_test.rb
43 43
  end
44 44

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

  
46 50
    issue = submit_email('ticket_on_given_project.eml',
47 51
                         :allow_override =>
48 52
                           ['status', 'start_date', 'due_date', 'assigned_to',
......
72 76
  end
73 77

  
74 78
  def test_add_issue_with_all_overrides
79
    project = Project.find_by_name('OnlineStore')
80
    project.enabled_module_names += [:time_tracking]
81
    project.save!
82

  
75 83
    issue = submit_email('ticket_on_given_project.eml', :allow_override => 'all')
76 84
    assert issue.is_a?(Issue)
77 85
    assert !issue.new_record?
(3-3/6)