Project

General

Profile

Feature #3848 » 0001-Allow-the-current-user-to-log-time-for-other-users.patch

Marius BĂLTEANU, 2018-06-21 23:16

View differences:

app/controllers/timelog_controller.rb
26 26
  before_action :find_optional_issue, :only => [:new, :create]
27 27
  before_action :find_optional_project, :only => [:index, :report]
28 28

  
29
  before_action :authorize_logging_time_for_other_users, :only => [:create, :update]
30

  
29 31
  accept_rss_auth :index
30 32
  accept_api_auth :index, :show, :create, :update, :destroy
31 33

  
......
90 92
  end
91 93

  
92 94
  def new
93
    @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
95
    @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :author => User.current, :spent_on => User.current.today)
94 96
    @time_entry.safe_attributes = params[:time_entry]
95 97
  end
96 98

  
97 99
  def create
98
    @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
100
    @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :author => User.current, :user => User.current, :spent_on => User.current.today)
99 101
    @time_entry.safe_attributes = params[:time_entry]
100 102
    if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
101 103
      render_403
......
144 146

  
145 147
  def update
146 148
    @time_entry.safe_attributes = params[:time_entry]
147

  
148 149
    call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
149 150

  
150 151
    if @time_entry.save
......
243 244
    end
244 245
  end
245 246

  
247
  def authorize_logging_time_for_other_users
248
    if !User.current.allowed_to?(:log_time_for_other_users, @project) && params['time_entry'].present? && params['time_entry']['user_id'].present? && params['time_entry']['user_id'].to_i != User.current.id
249
      render_error :message => l(:error_not_allowed_to_log_time_for_other_users), :status => 403
250
      return false
251
    end
252
  end
253

  
246 254
  def find_time_entries
247 255
    @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).
248 256
      preload(:project => :time_entry_activities).
app/helpers/timelog_helper.rb
42 42
    collection
43 43
  end
44 44

  
45
  def user_collection_for_select_options(time_entry)
46
    collection = time_entry.assignable_users
47
    principals_options_for_select(collection, time_entry.user_id)
48
  end
49

  
45 50
  def select_hours(data, criteria, value)
46 51
    if value.to_s.empty?
47 52
      data.select {|row| row[criteria].blank? }
app/models/time_entry.rb
22 22
  belongs_to :project
23 23
  belongs_to :issue
24 24
  belongs_to :user
25
  belongs_to :author, :class_name => 'User'
25 26
  belongs_to :activity, :class_name => 'TimeEntryActivity'
26 27

  
27 28
  acts_as_customizable
......
39 40
                            :author_key => :user_id,
40 41
                            :scope => joins(:project).preload(:project)
41 42

  
42
  validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
43
  validates_presence_of :author_id, :user_id, :activity_id, :project_id, :hours, :spent_on
43 44
  validates_presence_of :issue_id, :if => lambda { Setting.timelog_required_fields.include?('issue_id') }
44 45
  validates_presence_of :comments, :if => lambda { Setting.timelog_required_fields.include?('comments') }
45 46
  validates_numericality_of :hours, :allow_nil => true, :message => :invalid
46 47
  validates_length_of :comments, :maximum => 1024, :allow_nil => true
47 48
  validates :spent_on, :date => true
48 49
  before_validation :set_project_if_nil
50
  before_validation :set_author_if_nil
49 51
  validate :validate_time_entry
50 52

  
51 53
  scope :visible, lambda {|*args|
......
60 62
    where("#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}")
61 63
  }
62 64

  
63
  safe_attributes 'hours', 'comments', 'project_id', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields'
65
  safe_attributes 'user_id', 'hours', 'comments', 'project_id', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields'
64 66

  
65 67
  # Returns a SQL conditions string used to find all time entries visible by the specified user
66 68
  def self.visible_condition(user, options={})
......
119 121
    self.project = issue.project if issue && project.nil?
120 122
  end
121 123

  
124
  def set_author_if_nil
125
    self.author = User.current if author.nil?
126
  end
127

  
122 128
  def validate_time_entry
123 129
    if hours
124 130
      errors.add :hours, :invalid if hours < 0
......
134 140
      end
135 141
    end
136 142
    errors.add :project_id, :invalid if project.nil?
143
    errors.add :user_id, :invalid if (user_id != author_id && !self.assignable_users.map(&:id).include?(user_id))
137 144
    errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project) || @invalid_issue_id
138 145
    errors.add :activity_id, :inclusion if activity_id_changed? && project && !project.activities.include?(activity)
139 146
  end
......
177 184
    editable_custom_field_values(user).map(&:custom_field).uniq
178 185
  end
179 186

  
187
  def assignable_users
188
    users = []
189
    if project
190
      users = project.members.active.preload(:user)
191
      users = users.map(&:user).select{ |u| u.allowed_to?(:log_time, project) }
192
    end
193
    users << User.current if User.current.logged? && !users.include?(User.current)
194
    users
195
  end
196

  
180 197
  private
181 198

  
182 199
  # Returns the hours that were logged in other time entries for the same user and the same day
app/models/time_entry_query.rb
25 25
    QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
26 26
    QueryColumn.new(:created_on, :sortable => "#{TimeEntry.table_name}.created_on", :default_order => 'desc'),
27 27
    QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => :label_week),
28
    QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement}),
28 29
    QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
29 30
    QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
30 31
    QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
......
75 76
      :type => :list_optional, :values => lambda { author_values }
76 77
    )
77 78

  
79
    add_available_filter("author_id",
80
      :type => :list_optional, :values => lambda { author_values }
81
    )
82

  
78 83
    activities = (project ? project.activities : TimeEntryActivity.shared)
79 84
    add_available_filter("activity_id",
80 85
      :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
app/views/timelog/_form.html.erb
16 16
      <%=  link_to_issue(@time_entry.issue) if @time_entry.issue.try(:visible?) %>
17 17
    </span>
18 18
  </p>
19
  <% if User.current.allowed_to?(:log_time_for_other_users, @project) %>
20
    <p><%= f.select :user_id, user_collection_for_select_options(@time_entry), :required => true %></p>
21
  <% end %>
19 22
  <p><%= f.date_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
20 23
  <p><%= f.hours_field :hours, :size => 6, :required => true %></p>
21 24
  <p><%= f.text_field :comments, :size => 100, :maxlength => 1024, :required => Setting.timelog_required_fields.include?('comments') %></p>
config/locales/en.yml
222 222
  warning_fields_cleared_on_bulk_edit: "Changes will result in the automatic deletion of values from one or more fields on the selected objects"
223 223
  error_exceeds_maximum_hours_per_day: "Cannot log more than %{max_hours} hours on the same day (%{logged_hours} hours have already been logged)"
224 224
  error_can_not_delete_auth_source: "This authentication mode is in use and cannot be deleted."
225
  error_not_allowed_to_log_time_for_other_users: "Your role is not allowed to log time for other users"
225 226

  
226 227
  mail_subject_lost_password: "Your %{value} password"
227 228
  mail_body_lost_password: 'To change your password, click on the following link:'
......
534 535
  permission_manage_subtasks: Manage subtasks
535 536
  permission_manage_related_issues: Manage related issues
536 537
  permission_import_issues: Import issues
538
  permission_log_foreign_time: Log spent time for other users
537 539

  
538 540
  project_module_issue_tracking: Issue tracking
539 541
  project_module_time_tracking: Time tracking
db/migrate/20180501132547_add_author_id_to_time_entries.rb
1
class AddAuthorIdToTimeEntries < ActiveRecord::Migration[5.1]
2
  def up
3
    add_column :time_entries, :author_id, :integer, :default => nil, :after => :project_id
4
    # Copy existing user_id to author_id
5
    TimeEntry.update_all('author_id = user_id')
6
  end
7

  
8
  def down
9
    remove_column :time_entries, :author_id
10
  end
11
end
lib/redmine.rb
126 126
    map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
127 127
    map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
128 128
    map.permission :manage_project_activities, {:projects => :settings, :project_enumerations => [:update, :destroy]}, :require => :member
129
    map.permission :log_time_for_other_users, :require => :member
129 130
  end
130 131

  
131 132
  map.project_module :news do |map|
test/fixtures/time_entries.yml
1
--- 
2
time_entries_001: 
1
---
2
time_entries_001:
3 3
  created_on: 2007-03-23 12:54:18 +01:00
4 4
  tweek: 12
5 5
  tmonth: 3
......
12 12
  id: 1
13 13
  hours: 4.25
14 14
  user_id: 2
15
  author_id: 2
15 16
  tyear: 2007
16
time_entries_002: 
17
time_entries_002:
17 18
  created_on: 2007-03-23 14:11:04 +01:00
18 19
  tweek: 11
19 20
  tmonth: 3
......
26 27
  id: 2
27 28
  hours: 150.0
28 29
  user_id: 1
30
  author_id: 1
29 31
  tyear: 2007
30
time_entries_003: 
32
time_entries_003:
31 33
  created_on: 2007-04-21 12:20:48 +02:00
32 34
  tweek: 16
33 35
  tmonth: 4
......
40 42
  id: 3
41 43
  hours: 1.0
42 44
  user_id: 1
45
  author_id: 1
43 46
  tyear: 2007
44
time_entries_004: 
47
time_entries_004:
45 48
  created_on: 2007-04-22 12:20:48 +02:00
46 49
  tweek: 16
47 50
  tmonth: 4
......
50 53
  updated_on: 2007-04-22 12:20:48 +02:00
51 54
  activity_id: 10
52 55
  spent_on: 2007-04-22
53
  issue_id: 
56
  issue_id:
54 57
  id: 4
55 58
  hours: 7.65
56 59
  user_id: 1
60
  author_id: 1
57 61
  tyear: 2007
58
time_entries_005: 
62
time_entries_005:
59 63
  created_on: 2011-03-22 12:20:48 +02:00
60 64
  tweek: 12
61 65
  tmonth: 3
......
64 68
  updated_on: 2011-03-22 12:20:48 +02:00
65 69
  activity_id: 10
66 70
  spent_on: 2011-03-22
67
  issue_id: 
71
  issue_id:
68 72
  id: 5
69 73
  hours: 7.65
70 74
  user_id: 1
75
  author_id: 1
71 76
  tyear: 2011
72
  
77

  
test/functional/project_enumerations_controller_test.rb
143 143
        :enumerations => {
144 144
          "9"=> {
145 145
            "parent_id"=>"9", "custom_field_values"=> {
146
            "7" => "1"}, "active"=>"0"} # Design, De-activate      
147
            
146
            "7" => "1"}, "active"=>"0"} # Design, De-activate
147

  
148 148
          }
149 149
      }
150 150
    assert_response :redirect
......
163 163
    # TODO: Need to cause an exception on create but these tests
164 164
    # aren't setup for mocking.  Just create a record now so the
165 165
    # second one is a dupicate
166
    user = User.find(1)
166 167
    parent = TimeEntryActivity.find(9)
167 168
    TimeEntryActivity.create!({:name => parent.name, :project_id => 1,
168 169
                               :position => parent.position, :active => true, :parent_id => 9})
169
    TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1),
170
    TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => user, :author => user,
170 171
                       :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
171 172
    assert_equal 3, TimeEntry.where(:activity_id => 9, :project_id => 1).count
172 173
    assert_equal 1, TimeEntry.where(:activity_id => 10, :project_id => 1).count
test/functional/timelog_controller_test.rb
102 102
    assert_select 'option', :text => 'Inactive Activity', :count => 0
103 103
  end
104 104

  
105
  def test_new_should_show_user_select_if_user_has_permission
106
    Role.find_by_name('Manager').add_permission! :log_time_for_other_users
107
    @request.session[:user_id] = 2
108

  
109
    get :new, :params => {:project_id => 1}
110
    assert_response :success
111
    assert_select 'select[name=?]', 'time_entry[user_id]' do
112
      assert_select 'option', 3
113
      assert_select 'option[value=?]', '2', 2
114
      assert_select 'option[value=?]', '3', 1
115
      # locked members should not be available
116
      assert_select 'option[value=?]', '4', 0
117
    end
118
  end
119

  
120
  def test_new_user_select_should_include_current_user_if_is_logged
121
    @request.session[:user_id] = 1
122

  
123
    get :new, :params => {:project_id => 1}
124
    assert_response :success
125
    assert_select 'select[name=?]', 'time_entry[user_id]' do
126
      assert_select 'option[value=?]', '1', :text => '<< me >>'
127
      assert_select 'option[value=?]', '1', :text => 'Redmine Admin'
128
    end
129
  end
130

  
131
  def test_new_should_not_show_user_select_if_user_does_not_have_permission
132
    @request.session[:user_id] = 2
133

  
134
    get :new, :params => {:project_id => 1}
135
    assert_response :success
136
    assert_select 'select[name=?]', 'time_entry[user_id]', 0
137
  end
138

  
105 139
  def test_post_new_as_js_should_update_activity_options
106 140
    @request.session[:user_id] = 3
107 141
    post :new, :params => {:time_entry => {:project_id => 1}, :format => 'js'}
......
268 302
    assert !response.body.include?('issue_that_is_not_visible')
269 303
  end
270 304

  
305
  def test_create_for_other_user
306
    Role.find_by_name('Manager').add_permission! :log_time_for_other_users
307
    @request.session[:user_id] = 2
308

  
309
    post :create, :params => {
310
      :project_id => 1,
311
      :time_entry => {:comments => 'Some work on TimelogControllerTest',
312
        # Not the default activity
313
        :activity_id => '11',
314
        :spent_on => '2008-03-14',
315
        :issue_id => '1',
316
        :hours => '7.3',
317
        :user_id => '3'
318
      }
319
    }
320

  
321
    assert_redirected_to '/projects/ecookbook/time_entries'
322

  
323
    t = TimeEntry.last
324
    assert_equal 3, t.user_id
325
    assert_equal 2, t.author_id
326
  end
327

  
328
  def test_create_for_other_user_should_deny_for_user_without_permission
329
    Role.find_by_name('Manager').remove_permission! :log_time_for_other_users
330
    @request.session[:user_id] = 2
331

  
332
    post :create, :params => {
333
      :project_id => 1,
334
      :time_entry => {:comments => 'Some work on TimelogControllerTest',
335
        # Not the default activity
336
        :activity_id => '11',
337
        :spent_on => '2008-03-14',
338
        :issue_id => '1',
339
        :hours => '7.3',
340
        :user_id => '3'
341
      }
342
    }
343

  
344
    assert_response 403
345
    assert_select 'p[id=?]', 'errorExplanation', :text => 'Your role is not allowed to log time for other users'
346
  end
347

  
271 348
  def test_create_and_continue_at_project_level
272 349
    @request.session[:user_id] = 2
273 350
    assert_difference 'TimeEntry.count' do
......
533 610
    assert_select_error /Issue is invalid/
534 611
  end
535 612

  
613
  def test_update_should_deny_changing_user_for_user_without_permission
614
    Role.find_by_name('Manager').remove_permission! :log_time_for_other_users
615
    @request.session[:user_id] = 2
616

  
617
    put :update, :params => {
618
      :id => 3,
619
      :time_entry => {
620
        :user_id => '3'
621
      }
622
    }
623

  
624
    assert_response 403
625
    assert_select 'p[id=?]', 'errorExplanation', :text => 'Your role is not allowed to log time for other users'
626
  end
627

  
536 628
  def test_get_bulk_edit
537 629
    @request.session[:user_id] = 2
538 630

  
......
899 991
  end
900 992

  
901 993
  def test_index_should_sort_by_spent_on_and_created_on
902
    t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
903
    t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
904
    t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
994
    t1 = TimeEntry.create!(:author => User.find(1), :user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
995
    t2 = TimeEntry.create!(:author => User.find(1), :user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
996
    t3 = TimeEntry.create!(:author => User.find(1), :user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
905 997

  
906 998
    get :index, :params => {
907 999
      :project_id => 1,
......
1046 1138
    assert_select 'td.issue-category', :text => 'Printing'
1047 1139
  end
1048 1140

  
1141
  def test_index_with_author_filter
1142
    get :index, :params => {
1143
      :project_id => 'ecookbook',
1144
      :f => ['author_id'],
1145
      :op => {'author_id' => '='},
1146
      :v => {'author_id' => ['2']}
1147
    }
1148
    assert_response :success
1149
    assert_equal ['1'], css_select('input[name="ids[]"]').map {|e| e.attr('value')}
1150
  end
1151

  
1152
  def test_index_with_author_column
1153
    get :index, :params => {
1154
      :project_id => 'ecookbook',
1155
      :c => %w(project spent_on issue comments hours author)
1156
    }
1157

  
1158
    assert_response :success
1159
    assert_select 'td.author', :text => 'Redmine Admin'
1160
  end
1161

  
1049 1162
  def test_index_with_issue_category_sort
1050 1163
    issue = Issue.find(3)
1051 1164
    issue.category_id = 2
test/object_helpers.rb
142 142
  def TimeEntry.generate(attributes={})
143 143
    entry = TimeEntry.new(attributes)
144 144
    entry.user ||= User.find(2)
145
    entry.author ||= entry.user
145 146
    entry.issue ||= Issue.find(1) unless entry.project
146 147
    entry.project ||= entry.issue.project
147 148
    entry.activity ||= TimeEntryActivity.first
test/unit/lib/redmine/export/pdf/issues_pdf_test.rb
27 27
    query = IssueQuery.new(:project => Project.find(1), :name => '_')
28 28
    query.column_names = [:subject, :spent_hours]
29 29
    issue = Issue.find(2)
30
    TimeEntry.create(:spent_on => Date.today, :hours => 4.3432, :user => User.find(1),
30
    user = User.find(1)
31
    time_entry = TimeEntry.create!(:spent_on => Date.today, :hours => 4.3432, :user => user, :author => user,
31 32
                     :project_id => 1, :issue => issue, :activity => TimeEntryActivity.first)
33

  
32 34
    results = fetch_row_values(issue, query, 0)
33 35
    assert_equal ["2", "Add ingredients categories", "4.34"], results
34 36
  end
test/unit/time_entry_test.rb
168 168
                          :issue    => issue,
169 169
                          :project  => project,
170 170
                          :user     => anon,
171
                          :author     => anon,
171 172
                          :activity => activity)
172 173
    assert_equal 1, te.errors.count
173 174
  end
......
206 207
  def test_create_with_required_issue_id_and_comment_should_be_validated
207 208
    set_language_if_valid 'en'
208 209
    with_settings :timelog_required_fields => ['issue_id' , 'comments'] do
209
      entry = TimeEntry.new(:project => Project.find(1), :spent_on => Date.today, :user => User.find(1), :activity => TimeEntryActivity.first, :hours => 1)
210
      entry = TimeEntry.new(:project => Project.find(1), :spent_on => Date.today, :author => User.find(1), :user => User.find(1), :activity => TimeEntryActivity.first, :hours => 1)
210 211

  
211 212
      assert !entry.save
212 213
      assert_equal ["Comment cannot be blank", "Issue cannot be blank"], entry.errors.full_messages.sort
213 214
    end
214 215
  end
216

  
217
  def test_create_should_validate_user_id
218
    entry = TimeEntry.new(:spent_on => '2010-01-01',
219
                          :hours    => 10,
220
                          :project_id => 1,
221
                          :user_id    => 4)
222

  
223
    assert !entry.save
224
    assert_equal ["User is invalid"], entry.errors.full_messages.sort
225
  end
226

  
227
  def test_assignable_users_should_include_active_project_members_with_log_time_permission
228
    Role.find(2).remove_permission! :log_time
229
    time_entry = TimeEntry.find(1)
230

  
231
    assert_equal [2], time_entry.assignable_users.map(&:id)
232
  end
215 233
end
(20-20/21)