Project

General

Profile

Patch #44169 » 0001-wip.patch

Marius BĂLTEANU, 2026-06-14 00:39

View differences:

app/controllers/context_menus/base_controller.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
module ContextMenus
21
  class BaseController < ApplicationController
22
    layout false
23
    helper :context_menus
24
    helper_method :url_for
25

  
26
    def url_for(options = nil)
27
      if options.is_a?(Hash) && options[:controller].present?
28
        controller_name = options[:controller].to_s
29
        unless controller_name.start_with?('/')
30
          options = options.dup
31
          options[:controller] = "/#{controller_name}"
32
        end
33
      end
34
      super
35
    end
36

  
37
    private
38

  
39
    def render_context_menu(template_name)
40
      @back = back_url
41
      render :template => "context_menus/#{template_name}"
42
    end
43
  end
44
end
app/controllers/context_menus/projects_controller.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
module ContextMenus
21
  class ProjectsController < BaseController
22
    before_action :require_admin
23
    before_action :find_projects
24

  
25
    def index
26
      render_context_menu 'projects'
27
    end
28

  
29
    private
30

  
31
    def find_projects
32
      @projects = Project.where(id: params[:ids]).to_a
33
      if @projects.empty?
34
        render_404
35
        return
36
      end
37

  
38
      if @projects.size == 1
39
        @project = @projects.first
40
      end
41
    end
42
  end
43
end
app/controllers/context_menus/time_entries_controller.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
module ContextMenus
21
  class TimeEntriesController < BaseController
22
    before_action :find_time_entries
23

  
24
    def index
25
      @activities = @projects.map(&:activities).reduce(:&)
26

  
27
      edit_allowed = @time_entries.all? {|t| t.editable_by?(User.current)}
28
      @can = {:edit => edit_allowed, :delete => edit_allowed}
29
      @back = back_url
30

  
31
      @options_by_custom_field = {}
32
      if @can[:edit]
33
        custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?).select {|field| field.format.bulk_edit_supported}
34
        custom_fields.each do |field|
35
          values = field.possible_values_options(@projects)
36
          if values.present?
37
            @options_by_custom_field[field] = values
38
          end
39
        end
40
      end
41

  
42
      render_context_menu 'time_entries'
43
    end
44

  
45
    private
46

  
47
    def find_time_entries
48
      @time_entries = TimeEntry.where(:id => params[:ids]).
49
        preload(:project => :time_entry_activities).
50
        preload(:user).to_a
51

  
52
      if @time_entries.blank? || !@time_entries.all?(&:visible?)
53
        render_404;
54
        return
55
      end
56

  
57
      if @time_entries.size == 1
58
        @time_entry = @time_entries.first
59
      end
60

  
61
      @projects = @time_entries.filter_map(&:project).uniq
62
      @project = @projects.first if @projects.size == 1
63
    end
64
  end
65
end
app/controllers/context_menus_controller.rb
83 83
    render :layout => false
84 84
  end
85 85

  
86
  def time_entries
87
    @time_entries = TimeEntry.where(:id => params[:ids]).
88
      preload(:project => :time_entry_activities).
89
      preload(:user).to_a
90

  
91
    if @time_entries.blank? || !@time_entries.all?(&:visible?)
92
      render_404;
93
      return
94
    end
95

  
96
    if @time_entries.size == 1
97
      @time_entry = @time_entries.first
98
    end
99

  
100
    @projects = @time_entries.filter_map(&:project).uniq
101
    @project = @projects.first if @projects.size == 1
102
    @activities = @projects.map(&:activities).reduce(:&)
103

  
104
    edit_allowed = @time_entries.all? {|t| t.editable_by?(User.current)}
105
    @can = {:edit => edit_allowed, :delete => edit_allowed}
106
    @back = back_url
107

  
108
    @options_by_custom_field = {}
109
    if @can[:edit]
110
      custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?).select {|field| field.format.bulk_edit_supported}
111
      custom_fields.each do |field|
112
        values = field.possible_values_options(@projects)
113
        if values.present?
114
          @options_by_custom_field[field] = values
115
        end
116
      end
117
    end
118

  
119
    render :layout => false
120
  end
121

  
122
  def projects
123
    @projects = Project.where(id: params[:ids]).to_a
124
    if @projects.empty?
125
      render_404
126
      return
127
    end
128

  
129
    if @projects.size == 1
130
      @project = @projects.first
131
    end
132
    render layout: false
133
  end
134

  
135 86
  def users
136 87
    @users = User.where(id: params[:ids]).to_a
137 88

  
config/routes.rb
258 258
    post 'add_attachment', :on => :member
259 259
  end
260 260

  
261
  match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
261
  match '/time_entries/context_menu', :to => 'context_menus/time_entries#index', :as => :time_entries_context_menu, :via => [:get, :post]
262 262

  
263 263
  resources :time_entries, :controller => 'timelog', :except => :destroy do
264 264
    member do
......
384 384
  post 'admin/test_email', :to => 'admin#test_email', :as => 'test_email'
385 385
  post 'admin/default_configuration', :to => 'admin#default_configuration'
386 386

  
387
  match '/admin/projects_context_menu', :to => 'context_menus#projects', :as => 'projects_context_menu', :via => [:get, :post]
387
  match '/admin/projects_context_menu', :to => 'context_menus/projects#index', :as => 'projects_context_menu', :via => [:get, :post]
388 388

  
389 389
  resources :auth_sources do
390 390
    member do
test/functional/context_menus/projects_controller.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../../test_helper'
21

  
22
module ContextMenus
23
  class ProjectsControllerTest < Redmine::ControllerTest
24
    def test_index_admin_user
25
      @request.session[:user_id] = 1
26

  
27
      get(
28
        :index,
29
        :params => {
30
          :ids => [1, 2]
31
        }
32
      )
33

  
34
      assert_response :success
35
    end
36

  
37
    def test_index_not_admin_user
38
      @request.session[:user_id] = 2
39

  
40
      get(
41
        :index,
42
        :params => {
43
          :ids => [1, 2]
44
        }
45
      )
46

  
47
      assert_response :forbidden
48
    end
49
  end
50
end
test/functional/context_menus/time_entries_controller.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../../test_helper'
21

  
22
module ContextMenus
23
  class TimeEntriesControllerTest < Redmine::ControllerTest
24
    def test_context_menu_for_one_time_entry
25
      @request.session[:user_id] = 2
26
      get(
27
        :index,
28
        :params => {
29
          :ids => [1]
30
        }
31
      )
32
      assert_response :success
33

  
34
      assert_select 'a:not(.disabled)', :text => 'Edit'
35
    end
36

  
37
    def test_time_entries_context_menu
38
      @request.session[:user_id] = 2
39
      get(
40
        :index,
41
        :params => {
42
          :ids => [1, 2]
43
        }
44
      )
45
      assert_response :success
46

  
47
      assert_select 'a:not(.disabled)', :text => 'Bulk edit'
48
    end
49

  
50
    def test_time_entries_context_menu_should_include_custom_fields
51
      field = TimeEntryCustomField.generate!(:name => "Field", :field_format => "list", :possible_values => ["foo", "bar"])
52

  
53
      @request.session[:user_id] = 2
54
      get(
55
        :index,
56
        :params => {
57
          :ids => [1, 2]
58
        }
59
      )
60
      assert_response :success
61

  
62
      assert_select "li.cf_#{field.id}" do
63
        assert_select 'a[href="#"]', :text => "Field"
64
        assert_select 'ul' do
65
          assert_select 'a', 3
66
          assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&ids%5B%5D=2&time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=foo", :text => 'foo'
67
          assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&ids%5B%5D=2&time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=bar", :text => 'bar'
68
          assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&ids%5B%5D=2&time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
69
        end
70
      end
71
    end
72

  
73
    def test_time_entries_context_menu_with_time_entry_that_is_not_visible_should_fail
74
      project = Project.find(2)
75
      project.enable_module!(:time_tracking)
76
      time_entry = TimeEntry.generate!(project: project)
77

  
78
      @request.session[:user_id] = 2
79

  
80
      get(
81
        :index,
82
        :params => {
83
          :ids => [1, 5, time_entry.id]
84
        }
85
      )
86

  
87
      assert_response :not_found
88
    end
89

  
90
    def test_time_entries_context_menu_with_edit_own_time_entries_permission
91
      @request.session[:user_id] = 2
92
      Role.find_by_name('Manager').remove_permission! :edit_time_entries
93
      Role.find_by_name('Manager').add_permission! :edit_own_time_entries
94
      ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
95
      get(
96
        :index,
97
        :params => {
98
          :ids => ids
99
        }
100
      )
101
      assert_response :success
102

  
103
      assert_select 'a:not(.disabled)', :text => 'Bulk edit'
104
    end
105

  
106
    def test_time_entries_context_menu_without_edit_permission
107
      @request.session[:user_id] = 2
108
      Role.find_by_name('Manager').remove_permission! :edit_time_entries
109
      get(
110
        :index,
111
        :params => {
112
          :ids => [1, 2]
113
        }
114
      )
115
      assert_response :success
116

  
117
      assert_select 'a.disabled', :text => 'Bulk edit'
118
    end
119
  end
120
end
test/functional/context_menus_controller_test.rb
400 400
    assert_response :not_found
401 401
  end
402 402

  
403
  def test_time_entries_context_menu
404
    @request.session[:user_id] = 2
405
    get(
406
      :time_entries,
407
      :params => {
408
        :ids => [1, 2]
409
      }
410
    )
411
    assert_response :success
412

  
413
    assert_select 'a:not(.disabled)', :text => 'Bulk edit'
414
  end
415

  
416
  def test_context_menu_for_one_time_entry
417
    @request.session[:user_id] = 2
418
    get(
419
      :time_entries,
420
      :params => {
421
        :ids => [1]
422
      }
423
    )
424
    assert_response :success
425

  
426
    assert_select 'a:not(.disabled)', :text => 'Edit'
427
  end
428

  
429
  def test_time_entries_context_menu_should_include_custom_fields
430
    field = TimeEntryCustomField.generate!(:name => "Field", :field_format => "list", :possible_values => ["foo", "bar"])
431

  
432
    @request.session[:user_id] = 2
433
    get(
434
      :time_entries,
435
      :params => {
436
        :ids => [1, 2]
437
      }
438
    )
439
    assert_response :success
440

  
441
    assert_select "li.cf_#{field.id}" do
442
      assert_select 'a[href="#"]', :text => "Field"
443
      assert_select 'ul' do
444
        assert_select 'a', 3
445
        assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&ids%5B%5D=2&time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=foo", :text => 'foo'
446
        assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&ids%5B%5D=2&time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=bar", :text => 'bar'
447
        assert_select 'a[href=?]', "/time_entries/bulk_update?ids%5B%5D=1&ids%5B%5D=2&time_entry%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
448
      end
449
    end
450
  end
451

  
452
  def test_projects_context_menu_admin_user
453
    @request.session[:user_id] = 1
454

  
455
    get(
456
      :projects,
457
      :params => {
458
        :ids => [1, 2]
459
      }
460
    )
461

  
462
    assert_response :success
463
  end
464

  
465
  def test_projects_context_menu_not_admin_user
466
    @request.session[:user_id] = 2
467

  
468
    get(
469
      :projects,
470
      :params => {
471
        :ids => [1, 2]
472
      }
473
    )
474

  
475
    assert_response :forbidden
476
  end
477

  
478
  def test_time_entries_context_menu_with_time_entry_that_is_not_visible_should_fail
479
    project = Project.find(2)
480
    project.enable_module!(:time_tracking)
481
    time_entry = TimeEntry.generate!(project: project)
482

  
483
    @request.session[:user_id] = 2
484

  
485
    get(
486
      :time_entries,
487
      :params => {
488
        :ids => [1, 5, time_entry.id]
489
      }
490
    )
491

  
492
    assert_response :not_found
493
  end
494

  
495
  def test_time_entries_context_menu_with_edit_own_time_entries_permission
496
    @request.session[:user_id] = 2
497
    Role.find_by_name('Manager').remove_permission! :edit_time_entries
498
    Role.find_by_name('Manager').add_permission! :edit_own_time_entries
499
    ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
500
    get(
501
      :time_entries,
502
      :params => {
503
        :ids => ids
504
      }
505
    )
506
    assert_response :success
507

  
508
    assert_select 'a:not(.disabled)', :text => 'Bulk edit'
509
  end
510

  
511
  def test_time_entries_context_menu_without_edit_permission
512
    @request.session[:user_id] = 2
513
    Role.find_by_name('Manager').remove_permission! :edit_time_entries
514
    get(
515
      :time_entries,
516
      :params => {
517
        :ids => [1, 2]
518
      }
519
    )
520
    assert_response :success
521

  
522
    assert_select 'a.disabled', :text => 'Bulk edit'
523
  end
524

  
525 403
  def test_context_menu_should_include_delete_for_allowed_back_urls
526 404
    @request.session[:user_id] = 2
527 405
    %w[
    (1-1/1)