Project

General

Profile

Feature #3088 » 3088-v4.patch

Yuichi HARADA, 2022-08-01 06:40

View differences:

app/controllers/projects_controller.rb
189 189

  
190 190
        if User.current.allowed_to_view_all_time_entries?(@project)
191 191
          @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
192
        end
193
        if User.current.allowed_to?(:view_estimated_hours, @project)
192 194
          @total_estimated_hours = Issue.visible.where(cond).sum(:estimated_hours).to_f
193 195
        end
194 196

  
app/models/issue.rb
490 490
    'start_date',
491 491
    'due_date',
492 492
    'done_ratio',
493
    'estimated_hours',
494 493
    'custom_field_values',
495 494
    'custom_fields',
496 495
    'lock_version',
......
520 519
    'deleted_attachment_ids',
521 520
    :if => lambda {|issue, user| issue.attachments_deletable?(user)})
522 521

  
522
  safe_attributes 'estimated_hours',
523
    :if => lambda {|issue, user| user.allowed_to?(:view_estimated_hours, issue.project)}
524

  
523 525
  def safe_attribute_names(user=nil)
524 526
    names = super
525 527
    names -= disabled_core_fields
app/models/issue_query.rb
47 47
                    :groupable => true),
48 48
    QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date", :groupable => true),
49 49
    QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date", :groupable => true),
50
    QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours",
51
                    :totalable => true),
52
    QueryColumn.new(
53
      :total_estimated_hours,
54
      :sortable =>
55
        lambda do
56
          "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" \
57
          " WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')}" \
58
          " AND subtasks.root_id = #{Issue.table_name}.root_id" \
59
          " AND subtasks.lft >= #{Issue.table_name}.lft" \
60
          " AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)"
61
        end,
62
      :default_order => 'desc'),
63 50
    QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
64 51
    TimestampQueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on",
65 52
                             :default_order => 'desc', :groupable => true),
......
205 192
    add_available_filter "closed_on", :type => :date_past
206 193
    add_available_filter "start_date", :type => :date
207 194
    add_available_filter "due_date", :type => :date
208
    add_available_filter "estimated_hours", :type => :float
209

  
210
    if User.current.allowed_to?(:view_time_entries, project, :global => true)
211
      add_available_filter "spent_time", :type => :float, :label => :label_spent_time
195
    if User.current.allowed_to?(:view_estimated_hours, nil, :global => true)
196
      add_available_filter "estimated_hours", :type => :float
212 197
    end
213

  
214 198
    add_available_filter "done_ratio", :type => :integer
215 199

  
216 200
    if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
......
283 267
    @available_columns = self.class.available_columns.dup
284 268
    @available_columns += issue_custom_fields.visible.collect {|cf| QueryCustomFieldColumn.new(cf)}
285 269

  
270
    if User.current.allowed_to?(:view_estimated_hours, project, :global => true)
271
      # insert the columns after due_date or at the end
272
      index = @available_columns.rindex {|column| column.name == :due_date}
273
      index = (index ? index + 1 : -1)
274

  
275
      @available_columns.insert index, QueryColumn.new(:estimated_hours,
276
        :sortable => "#{Issue.table_name}.estimated_hours",
277
        :totalable => true
278
      )
279
      index = (index.negative? ? index : index + 1)
280
      @available_columns.insert index, QueryColumn.new(:total_estimated_hours,
281
        :sortable => -> {
282
          "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" \
283
            " WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')} AND" \
284
            " subtasks.root_id = #{Issue.table_name}.root_id AND" \
285
            " subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)"
286
        },
287
        :default_order => 'desc'
288
      )
289
    end
290

  
286 291
    if User.current.allowed_to?(:view_time_entries, project, :global => true)
287
      # insert the columns after total_estimated_hours or at the end
288
      index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
292
      # insert the columns after total_estimated_hours or the columns after due_date or at the end
293
      index = @available_columns.rindex {|column| column.name == :total_estimated_hours || column.name == :due_date }
289 294
      index = (index ? index + 1 : -1)
290 295

  
291 296
      subselect = "SELECT SUM(hours) FROM #{TimeEntry.table_name}" +
......
307 312
        " WHERE (#{TimeEntry.visible_condition(User.current)})" +
308 313
        " AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt"
309 314

  
315
      index = (index.negative? ? index : index + 1)
310 316
      @available_columns.insert(
311
        index + 1,
317
        index,
312 318
        QueryColumn.new(:total_spent_hours,
313 319
                        :sortable => "COALESCE((#{subselect}), 0)",
314 320
                        :default_order => 'desc',
app/models/journal.rb
111 111
        detail.custom_field && detail.custom_field.visible_by?(project, user)
112 112
      elsif detail.property == 'relation'
113 113
        Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user)
114
      elsif detail.property == 'attr' && detail.prop_key == 'estimated_hours'
115
        user.allowed_to?(:view_estimated_hours, project)
114 116
      else
115 117
        true
116 118
      end
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
68 68
  unless @issue.disabled_core_fields.include?('done_ratio')
69 69
    rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :legend => "#{@issue.done_ratio}%"), :class => 'progress'
70 70
  end
71
  unless @issue.disabled_core_fields.include?('estimated_hours')
71
  if User.current.allowed_to?(:view_estimated_hours, @project) && !@issue.disabled_core_fields.include?('estimated_hours')
72 72
    rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
73 73
  end
74 74
  if User.current.allowed_to?(:view_time_entries, @project) && @issue.total_spent_hours > 0
app/views/projects/show.html.erb
97 97
  </div>
98 98
  <% end %>
99 99

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

  
......
137 140
	<ul class="subprojects">
138 141
	  <% @subprojects.each do |project| %>
139 142
	  <li><%= link_to(project.name, project_path(project), :class => project.css_classes).html_safe %></li>
140
	  <% end %> 
143
	  <% end %>
141 144
    </ul>
142 145
  </div>
143 146
  <% 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
548 548
  permission_delete_issue_watchers: Delete watchers
549 549
  permission_log_time: Log spent time
550 550
  permission_view_time_entries: View spent time
551
  permission_view_estimated_hours: View estimated time
551 552
  permission_edit_time_entries: Edit time logs
552 553
  permission_edit_own_time_entries: Edit own time logs
553 554
  permission_view_news: View news
db/migrate/20220729101502_add_view_estimated_hours_to_all_existing_roles.rb
1
class AddViewEstimatedHoursToAllExistingRoles < ActiveRecord::Migration[6.1]
2
  def up
3
    Role.all.each { |role| role.add_permission! :view_estimated_hours }
4
  end
5

  
6
  def down
7
    Role.all.each { |role| role.remove_permission! :view_estimated_hours }
8
  end
9
end
lib/redmine/default_data/loader.rb
71 71
                  :view_calendar,
72 72
                  :log_time,
73 73
                  :view_time_entries,
74
                  :view_estimated_hours,
74 75
                  :view_news,
75 76
                  :comment_news,
76 77
                  :view_documents,
......
102 103
                  :view_calendar,
103 104
                  :log_time,
104 105
                  :view_time_entries,
106
                  :view_estimated_hours,
105 107
                  :view_news,
106 108
                  :comment_news,
107 109
                  :view_documents,
......
122 124
                                                            :view_gantt,
123 125
                                                            :view_calendar,
124 126
                                                            :view_time_entries,
127
                                                            :view_estimated_hours,
125 128
                                                            :view_news,
126 129
                                                            :comment_news,
127 130
                                                            :view_documents,
......
137 140
                                                           :view_gantt,
138 141
                                                           :view_calendar,
139 142
                                                           :view_time_entries,
143
                                                           :view_estimated_hours,
140 144
                                                           :view_news,
141 145
                                                           :view_documents,
142 146
                                                           :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
lib/redmine/preparation.rb
84 84

  
85 85
        map.project_module :time_tracking do |map|
86 86
          map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
87
          map.permission :view_estimated_hours, {}, :read => true
87 88
          map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
88 89
          map.permission :edit_time_entries,
89 90
                         {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]},
test/fixtures/roles.yml
35 35
    - :view_calendar
36 36
    - :log_time
37 37
    - :view_time_entries
38
    - :view_estimated_hours
38 39
    - :edit_time_entries
39 40
    - :delete_time_entries
40 41
    - :import_time_entries
......
102 103
    - :view_calendar
103 104
    - :log_time
104 105
    - :view_time_entries
106
    - :view_estimated_hours
105 107
    - :edit_own_time_entries
106 108
    - :view_news
107 109
    - :manage_news
......
151 153
    - :view_calendar
152 154
    - :log_time
153 155
    - :view_time_entries
156
    - :view_estimated_hours
154 157
    - :view_news
155 158
    - :manage_news
156 159
    - :comment_news
......
191 194
    - :view_calendar
192 195
    - :log_time
193 196
    - :view_time_entries
197
    - :view_estimated_hours
194 198
    - :view_news
195 199
    - :comment_news
196 200
    - :view_documents
......
218 222
    - :view_gantt
219 223
    - :view_calendar
220 224
    - :view_time_entries
225
    - :view_estimated_hours
221 226
    - :view_news
222 227
    - :view_documents
223 228
    - :view_wiki_pages
test/functional/issues_controller_test.rb
1581 1581
    assert_select 'table.issues td.total_estimated_hours'
1582 1582
  end
1583 1583

  
1584
  def test_index_should_not_show_estimated_hours_column_without_permission
1585
    Role.anonymous.remove_permission! :view_estimated_hours
1586
    get(
1587
      :index,
1588
      :params => {
1589
        :set_filter => 1,
1590
        :c => %w(subject estimated_hours)
1591
      }
1592
    )
1593
    assert_select 'td.estimated_hours', 0
1594
  end
1595

  
1584 1596
  def test_index_should_not_show_spent_hours_column_without_permission
1585 1597
    Role.anonymous.remove_permission! :view_time_entries
1586 1598
    get(
test/functional/projects_controller_test.rb
837 837
    end
838 838
  end
839 839

  
840
  def test_show_by_non_admin_user_with_view_estimated_hours_permission_should_show_estimated_time
841
    @request.session[:user_id] = 2 # manager
842
    get(:show, :params => {:id => 'ecookbook'})
843

  
844
    assert_select 'div.spent_time.box>ul' do
845
      assert_select '>li', :text => 'Estimated time: 203:30 hours'
846
    end
847
  end
848

  
849
  def test_show_by_non_admin_user_without_view_estimated_hours_permission_should_not_show_estimated_time
850
    Role.find_by(name: 'Manager').remove_permission! :view_estimated_hours
851
    @request.session[:user_id] = 2 # manager
852
    get(:show, :params => {:id => 'ecookbook'})
853

  
854
    assert_select 'div.spent_time.box>ul' do
855
      assert_select '>li', :text => 'Estimated time: 203.50 hours', :count => 0
856
    end
857
  end
858

  
840 859
  def test_settings
841 860
    @request.session[:user_id] = 2 # manager
842 861
    get(:settings, :params => {:id => 1})
test/functional/versions_controller_test.rb
165 165
      assert_select 'a', :text => '1 open'
166 166
    end
167 167

  
168
    assert_select '.time-tracking td.total-hours a:first-child', :text => '2:00 hours'
168
    assert_select '.time-tracking tr:first-child' do
169
      assert_select 'th', :text => 'Estimated time'
170
      assert_select 'td.total-hours a', :text => '2:00 hours'
171
    end
172

  
173
    Role.non_member.remove_permission! :view_estimated_hours
174

  
175
    get :show, :params => {:id => 4}
176
    assert_response :success
177

  
178
    assert_select 'p.progress-info' do
179
      assert_select 'a', :text => '1 issue'
180
      assert_select 'a', :text => '1 open'
181
    end
182

  
183
    assert_select '.time-tracking th', :text => 'Estimated time', :count => 0
169 184
  end
170 185

  
171 186
  def test_show_should_link_to_spent_time_on_version
test/integration/api_test/issues_test.rb
489 489
    end
490 490
  end
491 491

  
492
  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
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.xml'
502

  
503
    assert_equal 'application/xml; charset=utf-8', response.content_type
504
    assert_select 'issue' do
505
      assert_select 'estimated_hours',       false
506
      assert_select 'total_estimated_hours', false
507
      assert_select 'spent_hours',           parent.spent_hours.to_s
508
      assert_select 'total_spent_hours',     (parent.spent_hours.to_f + 2.5).to_s
509
    end
510
  end
511

  
492 512
  test "GET /issues/:id.xml should contains visible spent_hours only" do
493 513
    user = User.find_by_login('jsmith')
494 514
    Role.find(1).update(:time_entries_visibility => 'own')
......
537 557
    assert_nil json['issue']['total_spent_hours']
538 558
  end
539 559

  
560
  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
561
    parent = Issue.find(3)
562
    parent.update_columns :estimated_hours => 2.0
563
    child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0)
564
    TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today,
565
                      :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id)
566
    # remove permission!
567
    Role.anonymous.remove_permission! :view_estimated_hours
568

  
569
    get '/issues/3.json'
570

  
571
    assert_equal 'application/json; charset=utf-8', response.content_type
572
    json = ActiveSupport::JSON.decode(response.body)
573
    assert_nil json['issue']['estimated_hours']
574
    assert_nil json['issue']['total_estimated_hours']
575
    assert_equal parent.spent_hours, json['issue']['spent_hours']
576
    assert_equal (parent.spent_hours.to_f + 2.5), json['issue']['total_spent_hours']
577
  end
578

  
540 579
  test "GET /issues/:id.json should contains visible spent_hours only" do
541 580
    user = User.find_by_login('jsmith')
542 581
    Role.find(1).update(:time_entries_visibility => 'own')
test/unit/journal_test.rb
245 245
    assert_equal 2, visible_details.size
246 246
  end
247 247

  
248
  def test_visible_details_should_have_privilege_to_view_estimated_hours
249
    journal = Journal.generate! do |j|
250
      j.details <<
251
        JournalDetail.new(:property => 'attr', :prop_key => 'estimated_hours', :old_value => '200.00', :value => '100.00')
252
    end
253
    project = journal.journalized.project
254

  
255
    user = User.find(2)
256
    assert user.allowed_to?(:view_estimated_hours, project)
257
    visible_details = journal.visible_details(user)
258
    assert_equal 1, visible_details.size
259
    detail = visible_details.first
260
    assert_equal 'attr', detail.property
261
    assert_equal 'estimated_hours', detail.prop_key
262
    assert_equal '200.00', detail.old_value
263
    assert_equal '100.00', detail.value
264

  
265
    Role.anonymous.remove_permission!(:view_estimated_hours)
266
    assert !User.anonymous.allowed_to?(:view_estimated_hours, project)
267
    visible_details = journal.visible_details(User.anonymous)
268
    assert_equal 0, visible_details.size
269
  end
270

  
248 271
  def test_attachments
249 272
    journal = Journal.new
250 273
    [0, 1].map{ |i| Attachment.generate!(:file => mock_file_with_options(:original_filename => "image#{i}.png")) }.each do |attachment|
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 =
47 51
      submit_email(
48 52
        'ticket_on_given_project.eml',
......
74 78
  end
75 79

  
76 80
  def test_add_issue_with_all_overrides
81
    project = Project.find_by_name('OnlineStore')
82
    project.enabled_module_names += [:time_tracking]
83
    project.save!
84

  
77 85
    issue = submit_email('ticket_on_given_project.eml', :allow_override => 'all')
78 86
    assert issue.is_a?(Issue)
79 87
    assert !issue.new_record?
(6-6/6)