Project

General

Profile

Feature #4015 ยป draft-4015.patch

Mizuki ISHIKAWA, 2020-06-11 04:55

View differences:

app/controllers/projects_controller.rb
39 39
  helper :repositories
40 40
  helper :members
41 41
  helper :trackers
42
  helper :settings
42 43

  
43 44
  # Lists visible projects
44 45
  def index
app/controllers/settings_controller.rb
36 36
  def edit
37 37
    @notifiables = Redmine::Notifiable.all
38 38
    if request.post?
39
      errors = Setting.set_all_from_params(params[:settings].to_unsafe_hash)
39
      if params[:project_id]
40
        find_project(params[:project_id])
41
        unless User.current.allowed_to?(:edit_project, @project)
42
          raise ::Unauthorized
43
        end
44
      end
45

  
46
      errors = Setting.set_all_from_params(params[:settings].to_unsafe_hash, @project)
40 47
      if errors.blank?
41 48
        flash[:notice] = l(:notice_successful_update)
42
        redirect_to settings_path(:tab => params[:tab])
49
        redirect_to @project ? settings_project_path(@project, :tab => params[:tab]) : settings_path(:tab => params[:tab])
43 50
        return
44 51
      else
45 52
        @setting_errors = errors
app/helpers/projects_helper.rb
39 39
        {:name => 'boards', :action => :manage_boards,
40 40
         :partial => 'projects/settings/boards', :label => :label_board_plural},
41 41
        {:name => 'activities', :action => :manage_project_activities,
42
         :partial => 'projects/settings/activities', :label => :label_time_tracking}
42
         :partial => 'projects/settings/activities', :label => :label_time_tracking},
43
        {:name => 'overwrite_global_settings', :action => :edit_project,
44
         :partial => 'projects/settings/overwrite_global_settings', :label => :label_overwrite_global_settings}
43 45
      ]
44 46
    tabs.
45 47
      select {|tab| User.current.allowed_to?(tab[:action], @project)}.
app/helpers/settings_helper.rb
43 43
    content_tag('div', content_tag('ul', s), :id => 'errorExplanation')
44 44
  end
45 45

  
46
  def setting_value(setting)
46
  def setting_value(setting, project)
47 47
    value = nil
48 48
    if params[:settings]
49 49
      value = params[:settings][setting]
50 50
    end
51
    value || Setting.send(setting)
51
    value || (project ? project.send("setting_#{setting}") : Setting.send(setting))
52 52
  end
53 53

  
54 54
  def setting_select(setting, choices, options={})
......
57 57
    end
58 58
    setting_label(setting, options).html_safe +
59 59
      select_tag("settings[#{setting}]",
60
                 options_for_select(choices, setting_value(setting).to_s),
60
                 options_for_select(choices, setting_value(setting, options[:project]).to_s),
61 61
                 options).html_safe
62 62
  end
63 63

  
64 64
  def setting_multiselect(setting, choices, options={})
65
    setting_values = setting_value(setting)
65
    setting_values = setting_value(setting, options[:project])
66 66
    setting_values = [] unless setting_values.is_a?(Array)
67 67

  
68 68
    content_tag("label", l(options[:label] || "setting_#{setting}")) +
......
84 84

  
85 85
  def setting_text_field(setting, options={})
86 86
    setting_label(setting, options).html_safe +
87
      text_field_tag("settings[#{setting}]", setting_value(setting), options).html_safe
87
      text_field_tag("settings[#{setting}]", setting_value(setting, options[:project]), options).html_safe
88 88
  end
89 89

  
90 90
  def setting_text_area(setting, options={})
91 91
    setting_label(setting, options).html_safe +
92
      text_area_tag("settings[#{setting}]", setting_value(setting), options).html_safe
92
      text_area_tag("settings[#{setting}]", setting_value(setting, options[:project]), options).html_safe
93 93
  end
94 94

  
95 95
  def setting_check_box(setting, options={})
96 96
    setting_label(setting, options).html_safe +
97 97
      hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe +
98
        check_box_tag("settings[#{setting}]", 1, setting_value(setting).to_s != '0', options).html_safe
98
        check_box_tag("settings[#{setting}]", 1, setting_value(setting, options[:project]).to_s != '0', options).html_safe
99 99
  end
100 100

  
101 101
  def setting_label(setting, options={})
......
109 109
  end
110 110

  
111 111
  # Renders a notification field for a Redmine::Notifiable option
112
  def notification_field(notifiable)
112
  def notification_field(notifiable, options={})
113 113
    tag_data = notifiable.parent.present? ?
114 114
      {:parent_notifiable => notifiable.parent} :
115 115
      {:disables => "input[data-parent-notifiable=#{notifiable.name}]"}
116 116
    tag = check_box_tag('settings[notified_events][]',
117 117
                        notifiable.name,
118
                        setting_value('notified_events').include?(notifiable.name),
118
                        setting_value('notified_events', options[:project]).include?(notifiable.name),
119 119
                        :id => nil,
120 120
                        :data => tag_data)
121 121
    text = l_or_humanize(notifiable.name, :prefix => 'label_')
122
    options = {}
123 122
    if notifiable.parent.present?
124 123
      options[:class] = "parent"
125 124
    end
app/models/issue_query.rb
274 274

  
275 275
  def default_columns_names
276 276
    @default_columns_names ||= begin
277
      default_columns = Setting.issue_list_default_columns.map(&:to_sym)
277
      default_columns = (project ? project.setting_issue_list_default_columns : Setting.issue_list_default_columns).map(&:to_sym)
278 278

  
279 279
      project.present? ? default_columns : [:project] | default_columns
280 280
    end
app/models/project.rb
58 58
                          :class_name => 'IssueCustomField',
59 59
                          :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
60 60
                          :association_foreign_key => 'custom_field_id'
61
  has_many :settings, dependent: :destroy
61 62

  
62 63
  acts_as_attachable :view_permission => :view_files,
63 64
                     :edit_permission => :manage_files,
......
910 911
    end
911 912
  end
912 913

  
914
  # Define getter and setter for each setting per project
915
  # Then setting values can be read using: project.setting_some_setting_name
916
  # or set using project.setting_some_setting_name = "some value"
917
  def self.define_setting(name)
918
    project_setting_src = <<~END_SRC
919
      def setting_#{name}
920
        Setting[:#{name}, self]
921
      end
922

  
923
      def setting_#{name}?
924
        Setting[:#{name}, self].to_i > 0
925
      end
926

  
927
      def setting_#{name}=(value)
928
        Setting[:#{name}, self] = value
929
      end
930
    END_SRC
931
    self.class_eval project_setting_src
932
  end
933

  
913 934
  private
914 935

  
915 936
  def update_inherited_members
app/models/setting.rb
18 18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 19

  
20 20
class Setting < ActiveRecord::Base
21
  belongs_to :project, optional: true
22

  
21 23
  PASSWORD_CHAR_CLASSES = {
22 24
    'uppercase'     => /[A-Z]/,
23 25
    'lowercase'     => /[a-z]/,
......
85 87
  cattr_accessor :available_settings
86 88
  self.available_settings ||= {}
87 89

  
88
  validates_uniqueness_of :name, :if => Proc.new {|setting| setting.new_record? || setting.name_changed?}
90
  validates_uniqueness_of :name, :scope => :project_id, :if => Proc.new {|setting| setting.new_record? || setting.name_changed?}
89 91
  validates_inclusion_of :name, :in => Proc.new {available_settings.keys}
90 92
  validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting|
91 93
    (s = available_settings[setting.name]) && s['format'] == 'int'
......
112 114
  end
113 115

  
114 116
  # Returns the value of the setting named name
115
  def self.[](name)
116
    @cached_settings[name] ||= find_or_default(name).value
117
  # Example:
118
  #   Setting[name]
119
  #   Setting[name, project]
120
  def self.[](name, project=nil)
121
    if project
122
      setting = find_or_default(name, project)
123
      setting = find_or_default(name) if setting.new_record?
124
      setting.value
125
    else
126
      @cached_settings[name] ||= find_or_default(name).value
127
    end
117 128
  end
118 129

  
119
  def self.[]=(name, v)
120
    setting = find_or_default(name)
130
  # Example:
131
  #   Setting[name, project]=v
132
  #   Setting[name]=v
133
  def self.[]=(name, project=nil, v)
134
    setting = find_or_default(name, project)
121 135
    setting.value = v || ''
122
    @cached_settings[name] = nil
136
    @cached_settings[name] = nil if project.nil?
123 137
    setting.save
124 138
    setting.value
125 139
  end
126 140

  
127 141
  # Updates multiple settings from params and sends a security notification if needed
128
  def self.set_all_from_params(settings)
142
  def self.set_all_from_params(settings, project=nil)
129 143
    return nil unless settings.is_a?(Hash)
130 144

  
131 145
    settings = settings.dup.symbolize_keys
......
138 152
      next unless available_settings[name.to_s]
139 153

  
140 154
      previous_value = Setting[name]
141
      set_from_params name, value
155
      set_from_params(name, value, project)
142 156
      if available_settings[name.to_s]['security_notifications'] && Setting[name] != previous_value
143 157
        changes << name
144 158
      end
......
187 201
  end
188 202

  
189 203
  # Sets a setting value from params
190
  def self.set_from_params(name, params)
204
  def self.set_from_params(name, params, project=nil)
191 205
    params = params.dup
192 206
    params.delete_if {|v| v.blank? } if params.is_a?(Array)
193 207
    params.symbolize_keys! if params.is_a?(Hash)
194 208

  
195 209
    m = "#{name}_from_params"
196 210
    if respond_to? m
197
      self[name.to_sym] = send m, params
211
      self[name.to_sym, project] = send m, params
198 212
    else
199
      self[name.to_sym] = params
213
      self[name.to_sym, project] = params
200 214
    end
201 215
  end
202 216

  
......
294 308
      end
295 309
    END_SRC
296 310
    class_eval src, __FILE__, __LINE__
311

  
312
    # Define getters and setters to handle project specific settings
313
    Project.define_setting(name)
297 314
  end
298 315

  
299 316
  def self.load_available_settings
......
333 350

  
334 351
  # Returns the Setting instance for the setting named name
335 352
  # (record found in database or new record with default value)
336
  def self.find_or_default(name)
353
  def self.find_or_default(name, project=nil)
337 354
    name = name.to_s
338 355
    raise "There's no setting named #{name}" unless available_settings.has_key?(name)
339 356

  
340
    setting = where(:name => name).order(:id => :desc).first
357
    if ActiveRecord::Base.connection.column_exists?(:settings, :project_id)
358
      setting = where(:name => name, :project => project).order(:id => :desc).first
359
    else
360
      setting = where(:name => name).order(:id => :desc).first
361
    end
341 362
    unless setting
342 363
      setting = new
343 364
      setting.name = name
365
      setting.project = project if ActiveRecord::Base.connection.column_exists?(:settings, :project_id)
344 366
      setting.value = available_settings[name]['default']
345 367
    end
346 368
    setting
app/views/projects/settings/_overwrite_global_settings.html.erb
1
<%= form_tag(settings_edit_path(project_id: @project.id)) do %>
2
  <%= hidden_field_tag 'tab', 'overwrite_global_settings' %>
3

  
4
  <fieldset class="box">
5
    <legend><%= l(:setting_issue_list_default_columns) %></legend>
6
    <%= render_query_columns_selection(
7
          IssueQuery.new(:column_names => @project.setting_issue_list_default_columns),
8
          :name => 'settings[issue_list_default_columns]') %>
9
  </fieldset>
10

  
11
  <p><%= submit_tag l(:button_save) %></p>
12
<% end %>
db/migrate/20200609043401_add_project_id_to_setting.rb
1
class AddProjectIdToSetting < ActiveRecord::Migration[5.2]
2
  def change
3
    add_reference :settings, :project, foreign_key: true
4
  end
5
end
test/functional/issues_controller_test.rb
1395 1395

  
1396 1396
  def test_index_with_default_columns_should_respect_default_columns_order
1397 1397
    columns = ['assigned_to', 'subject', 'status', 'tracker']
1398
    with_settings :issue_list_default_columns => columns do
1398
    with_settings :issue_list_default_columns => columns  do
1399 1399
      get(
1400 1400
        :index,
1401 1401
        :params => {
......
1407 1407
    end
1408 1408
  end
1409 1409

  
1410
  def test_index_with_default_columns_per_project_should_respect_default_columns_order_per_project
1411
    columns = ['assigned_to', 'subject', 'status', 'tracker']
1412
    with_settings(:issue_list_default_columns => columns) do
1413
      with_project_settings(Project.find(1), :issue_list_default_columns => columns) do
1414
        get(
1415
          :index,
1416
          :params => {
1417
            :project_id => 1,
1418
            :set_filter => 1
1419
          }
1420
        )
1421
        assert_equal ["#", "Assignee", "Subject", "Status", "Tracker"], columns_in_issues_list
1422
      end
1423
    end
1424
  end
1425

  
1410 1426
  def test_index_with_custom_field_column
1411 1427
    columns = %w(tracker subject cf_2)
1412 1428
    get(
test/test_helper.rb
103 103
    saved_settings.each {|k, v| Setting[k] = v} if saved_settings
104 104
  end
105 105

  
106
  def with_project_settings(project, options, &block)
107
    options.each {|k, v| Setting[k, project] = v}
108
    yield
109
  ensure
110
    options.each {|k, _v| Setting.find_by(project: project, name: k).destroy }
111
  end
112

  
106 113
  # Yields the block with user as the current user
107 114
  def with_current_user(user, &block)
108 115
    saved_user = User.current
    (1-1/1)