Feature #4015 ยป draft-4015.patch
| 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 |