--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2015 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class CustomFieldEnumerationsController < ApplicationController
+ layout 'admin'
+
+ before_filter :require_admin
+ before_filter :find_custom_field
+ before_filter :find_enumeration, :only => :destroy
+
+ def index
+ @values = @custom_field.enumerations.order(:position)
+ end
+
+ def create
+ @value = @custom_field.enumerations.build(params[:custom_field_enumeration])
+ @value.save
+ respond_to do |format|
+ format.html { redirect_to custom_field_enumerations_path(@custom_field) }
+ format.js
+ end
+ end
+
+ def update_each
+ if CustomFieldEnumeration.update_each(@custom_field, params[:custom_field_enumerations])
+ flash[:notice] = l(:notice_successful_update)
+ end
+ redirect_to :action => 'index'
+ end
+
+ def destroy
+ reassign_to = @custom_field.enumerations.find_by_id(params[:reassign_to_id])
+ if reassign_to.nil? && @value.in_use?
+ @enumerations = @custom_field.enumerations - [@value]
+ render :action => 'destroy'
+ return
+ end
+ @value.destroy(reassign_to)
+ redirect_to custom_field_enumerations_path(@custom_field)
+ end
+
+ private
+
+ def find_custom_field
+ @custom_field = CustomField.find(params[:custom_field_id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_enumeration
+ @value = @custom_field.enumerations.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
class CustomField < ActiveRecord::Base
include Redmine::SubclassFactory
+ has_many :enumerations,
+ lambda { order(:position) },
+ :class_name => 'CustomFieldEnumeration',
+ :dependent => :delete_all
has_many :custom_values, :dependent => :delete_all
has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
acts_as_list :scope => 'type = \'#{self.class}\''
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2015 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class CustomFieldEnumeration < ActiveRecord::Base
+ belongs_to :custom_field
+ attr_accessible :name, :active, :position
+
+ validates_presence_of :name, :position, :custom_field_id
+ validates_length_of :name, :maximum => 60
+ validates_numericality_of :position, :only_integer => true
+ before_create :set_position
+
+ scope :active, lambda { where(:active => true) }
+
+ def to_s
+ name.to_s
+ end
+
+ def objects_count
+ custom_values.count
+ end
+
+ def in_use?
+ objects_count > 0
+ end
+
+ alias :destroy_without_reassign :destroy
+ def destroy(reassign_to=nil)
+ if reassign_to
+ custom_values.update_all(:value => reassign_to.id.to_s)
+ end
+ destroy_without_reassign
+ end
+
+ def custom_values
+ custom_field.custom_values.where(:value => id.to_s)
+ end
+
+ def self.update_each(custom_field, attributes)
+ return unless attributes.is_a?(Hash)
+ transaction do
+ attributes.each do |enumeration_id, enumeration_attributes|
+ enumeration = custom_field.enumerations.find_by_id(enumeration_id)
+ if enumeration
+ enumeration.attributes = enumeration_attributes
+ unless enumeration.save
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+ end
+ end
+
+ private
+
+ def set_position
+ max = self.class.where(:custom_field_id => custom_field_id).maximum(:position) || 0
+ self.position = max + 1
+ end
+end
--- /dev/null
+$('#content').html('<%= escape_javascript(render(:template => 'custom_field_enumerations/index')) %>');
+$('#custom_field_enumeration_name').focus();
--- /dev/null
+<%= title [l(:label_custom_field_plural), custom_fields_path],
+ [l(@custom_field.type_name), custom_fields_path(:tab => @custom_field.class.name)],
+ @custom_field.name %>
+
+<%= form_tag(custom_field_enumeration_path(@custom_field, @value), :method => :delete) do %>
+<div class="box">
+<p><strong><%= l(:text_enumeration_destroy_question, :name => @value.name, :count => @value.objects_count) %></strong></p>
+<p><label for='reassign_to_id'><%= l(:text_enumeration_category_reassign_to) %></label>
+<%= select_tag('reassign_to_id', content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '') + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p>
+</div>
+
+<%= submit_tag l(:button_apply) %>
+<%= link_to l(:button_cancel), custom_field_enumerations_path(@custom_field) %>
+<% end %>
--- /dev/null
+<%= title [l(:label_custom_field_plural), custom_fields_path],
+ [l(@custom_field.type_name), custom_fields_path(:tab => @custom_field.class.name)],
+ @custom_field.name %>
+
+<% if @custom_field.enumerations.any? %>
+<%= form_tag custom_field_enumerations_path(@custom_field), :method => 'put' do %>
+<div class="box">
+ <ul id="custom_field_enumerations" class="flat">
+ <% @custom_field.enumerations.each_with_index do |value, position| %>
+ <li>
+ <span class="sort-handle ui-icon ui-icon-arrowthick-2-n-s"></span>
+ <%= hidden_field_tag "custom_field_enumerations[#{value.id}][position]", position, :class => 'position' %>
+ <%= text_field_tag "custom_field_enumerations[#{value.id}][name]", value.name, :size => 40 %>
+ <%= hidden_field_tag "custom_field_enumerations[#{value.id}][active]", 0 %>
+ <label>
+ <%= check_box_tag "custom_field_enumerations[#{value.id}][active]", 1, value.active? %>
+ <%= l(:field_active) %>
+ </label>
+ <%= delete_link custom_field_enumeration_path(@custom_field, value) %>
+ </li>
+ <% end %>
+ </ul>
+</div>
+<p>
+ <%= submit_tag(l(:button_save)) %> |
+ <%= link_to l(:button_back), edit_custom_field_path(@custom_field) %>
+</p>
+<% end %>
+<% end %>
+
+<p><%= l(:label_enumeration_new) %></p>
+
+<%= form_tag custom_field_enumerations_path(@custom_field), :method => 'post', :remote => true do %>
+ <p><%= text_field_tag 'custom_field_enumeration[name]', '', :size => 40 %>
+ <%= submit_tag(l(:button_add)) %></p>
+<% end %>
+
+<%= javascript_tag do %>
+$(function() {
+ $("#custom_field_enumerations").sortable({
+ handle: ".sort-handle",
+ update: function(event, ui) {
+ $("#custom_field_enumerations li").each(function(){
+ $(this).find("input.position").val($(this).index()+1);
+ });
+ }
+ });
+});
+<% end %>
--- /dev/null
+<% unless @custom_field.new_record? %>
+<p>
+ <label><%= l(:field_possible_values) %></label>
+ <%= link_to l(:button_edit), custom_field_enumerations_path(@custom_field), :class => 'icon icon-edit' %>
+</p>
+<% if @custom_field.enumerations.active.any? %>
+ <p><%= f.select :default_value, @custom_field.enumerations.active.map{|v| [v.name, v.id.to_s]}, :include_blank => true %></p>
+<% end %>
+<% end %>
+
+<p><%= f.text_field :url_pattern, :size => 50, :label => :label_link_values_to %></p>
+<p><%= edit_tag_style_tag f %></p>
label_file_content_preview: File content preview
label_create_missing_values: Create missing values
label_api: API
+ label_field_format_enumeration: Key/value list
button_login: Login
button_submit: Submit
label_file_content_preview: Aperçu du contenu du fichier
label_create_missing_values: Créer les valeurs manquantes
label_api: API
+ label_field_format_enumeration: Liste clé/valeur
button_login: Connexion
button_submit: Soumettre
post 'update_issue_done_ratio'
end
end
- resources :custom_fields, :except => :show
+ resources :custom_fields, :except => :show do
+ resources :enumerations, :controller => 'custom_field_enumerations', :except => [:show, :new, :edit]
+ put 'enumerations', :to => 'custom_field_enumerations#update_each'
+ end
resources :roles do
collection do
match 'permissions', :via => [:get, :post]
--- /dev/null
+class CreateCustomFieldEnumerations < ActiveRecord::Migration
+ def change
+ create_table :custom_field_enumerations do |t|
+ t.integer :custom_field_id, :null => false
+ t.string :name, :null => false
+ t.boolean :active, :default => true, :null => false
+ t.integer :position, :default => 1, :null => false
+ end
+ end
+end
add 'list'
self.searchable_supported = true
self.form_partial = 'custom_fields/formats/list'
-
+
def possible_custom_value_options(custom_value)
options = possible_values_options(custom_value.custom_field)
missing = [custom_value.value].flatten.reject(&:blank?) - options
missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
if missing.any?
options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
- options.sort_by!(&:first)
end
options
end
protected :value_join_alias
end
+ class EnumerationFormat < RecordList
+ add 'enumeration'
+ self.form_partial = 'custom_fields/formats/enumeration'
+
+ def label
+ "label_field_format_enumeration"
+ end
+
+ def target_class
+ @target_class ||= CustomFieldEnumeration
+ end
+
+ def possible_values_options(custom_field, object=nil)
+ possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
+ end
+
+ def possible_values_records(custom_field, object=nil)
+ custom_field.enumerations.active
+ end
+
+ def value_from_keyword(custom_field, keyword, object)
+ value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword)
+ value ? value.id : nil
+ end
+ end
+
class UserFormat < RecordList
add 'user'
self.form_partial = 'custom_fields/formats/user'
#sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
* html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
#sidebar .contextual { margin-right: 1em; }
-#sidebar ul {margin: 0; padding: 0;}
-#sidebar ul li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
+#sidebar ul, ul.flat {margin: 0; padding: 0;}
+#sidebar ul li, ul.flat li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
#content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
* html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
textarea#custom_field_possible_values {width: 95%; resize:vertical}
textarea#custom_field_default_value {width: 95%; resize:vertical}
+.sort-handle {display:inline-block; vertical-align:middle;}
input#content_comments {width: 99%}
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2015 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class CustomFieldEnumerationsControllerTest < ActionController::TestCase
+ fixtures :users, :email_addresses
+
+ def setup
+ @request.session[:user_id] = 1
+ @field = GroupCustomField.create!(:name => 'List', :field_format => 'enumeration', :is_required => false)
+ @foo = CustomFieldEnumeration.new(:name => 'Foo')
+ @bar = CustomFieldEnumeration.new(:name => 'Bar')
+ @field.enumerations << @foo
+ @field.enumerations << @bar
+ end
+
+ def test_index
+ get :index, :custom_field_id => @field.id
+ assert_response :success
+ assert_template 'index'
+ end
+
+ def test_create
+ assert_difference 'CustomFieldEnumeration.count' do
+ post :create, :custom_field_id => @field.id, :custom_field_enumeration => { :name => 'Baz' }
+ assert_redirected_to "/custom_fields/#{@field.id}/enumerations"
+ end
+
+ assert_equal 3, @field.reload.enumerations.count
+ enum = @field.enumerations.last
+ assert_equal 'Baz', enum.name
+ assert_equal true, enum.active
+ assert_equal 3, enum.position
+ end
+
+ def test_create_xhr
+ assert_difference 'CustomFieldEnumeration.count' do
+ xhr :post, :create, :custom_field_id => @field.id, :custom_field_enumeration => { :name => 'Baz' }
+ assert_response :success
+ end
+ end
+
+ def test_update_each
+ put :update_each, :custom_field_id => @field.id, :custom_field_enumerations => {
+ @bar.id => {:position => "1", :name => "Baz", :active => "1"},
+ @foo.id => {:position => "2", :name => "Foo", :active => "0"}
+ }
+ assert_response 302
+
+ @bar.reload
+ assert_equal "Baz", @bar.name
+ assert_equal true, @bar.active
+ assert_equal 1, @bar.position
+
+ @foo.reload
+ assert_equal "Foo", @foo.name
+ assert_equal false, @foo.active
+ assert_equal 2, @foo.position
+ end
+
+ def test_destroy
+ assert_difference 'CustomFieldEnumeration.count', -1 do
+ delete :destroy, :custom_field_id => @field.id, :id => @foo.id
+ assert_redirected_to "/custom_fields/#{@field.id}/enumerations"
+ end
+
+ assert_equal 1, @field.reload.enumerations.count
+ enum = @field.enumerations.last
+ assert_equal 'Bar', enum.name
+ end
+
+ def test_destroy_enumeration_in_use_should_display_destroy_form
+ group = Group.generate!
+ group.custom_field_values = {@field.id.to_s => @foo.id.to_s}
+ group.save!
+
+ assert_no_difference 'CustomFieldEnumeration.count' do
+ delete :destroy, :custom_field_id => @field.id, :id => @foo.id
+ assert_response 200
+ assert_template 'destroy'
+ end
+ end
+
+ def test_destroy_enumeration_in_use_should_destroy_and_reassign_values
+ group = Group.generate!
+ group.custom_field_values = {@field.id.to_s => @foo.id.to_s}
+ group.save!
+
+ assert_difference 'CustomFieldEnumeration.count', -1 do
+ delete :destroy, :custom_field_id => @field.id, :id => @foo.id, :reassign_to_id => @bar.id
+ assert_response 302
+ end
+
+ assert_equal @bar.id.to_s, group.reload.custom_field_value(@field)
+ end
+end
should_route 'PUT /custom_fields/2' => 'custom_fields#update', :id => '2'
should_route 'DELETE /custom_fields/2' => 'custom_fields#destroy', :id => '2'
end
+
+ def test_custom_field_enumerations
+ should_route 'GET /custom_fields/3/enumerations' => 'custom_field_enumerations#index', :custom_field_id => '3'
+ should_route 'POST /custom_fields/3/enumerations' => 'custom_field_enumerations#create', :custom_field_id => '3'
+ should_route 'PUT /custom_fields/3/enumerations' => 'custom_field_enumerations#update_each', :custom_field_id => '3'
+ should_route 'DELETE /custom_fields/3/enumerations/6' => 'custom_field_enumerations#destroy', :custom_field_id => '3', :id => '6'
+ end
end
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2015 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.expand_path('../../../../../test_helper', __FILE__)
+require 'redmine/field_format'
+
+class Redmine::EnumerationFieldFormatTest < ActionView::TestCase
+ include ApplicationHelper
+
+ def setup
+ @field = IssueCustomField.create!(:name => 'List', :field_format => 'enumeration', :is_required => false)
+ @foo = CustomFieldEnumeration.new(:name => 'Foo')
+ @bar = CustomFieldEnumeration.new(:name => 'Bar')
+ @field.enumerations << @foo
+ @field.enumerations << @bar
+ end
+
+ def test_edit_tag_should_contain_possible_values
+ value = CustomFieldValue.new(:custom_field => @field, :customized => Issue.new)
+
+ tag = @field.format.edit_tag(self, 'id', 'name', value)
+ assert_select_in tag, 'select' do
+ assert_select 'option', 3
+ assert_select 'option[value=""]'
+ assert_select 'option[value=?]', @foo.id.to_s, :text => 'Foo'
+ assert_select 'option[value=?]', @bar.id.to_s, :text => 'Bar'
+ end
+ end
+
+ def test_edit_tag_should_select_current_value
+ value = CustomFieldValue.new(:custom_field => @field, :customized => Issue.new, :value => @bar.id.to_s)
+
+ tag = @field.format.edit_tag(self, 'id', 'name', value)
+ assert_select_in tag, 'select' do
+ assert_select 'option[selected=selected]', 1
+ assert_select 'option[value=?][selected=selected]', @bar.id.to_s, :text => 'Bar'
+ end
+ end
+
+ def test_edit_tag_with_multiple_should_select_current_values
+ @field.multiple = true
+ @field.save!
+ value = CustomFieldValue.new(:custom_field => @field, :customized => Issue.new, :value => [@foo.id.to_s, @bar.id.to_s])
+
+ tag = @field.format.edit_tag(self, 'id', 'name', value)
+ assert_select_in tag, 'select[multiple=multiple]' do
+ assert_select 'option[selected=selected]', 2
+ assert_select 'option[value=?][selected=selected]', @foo.id.to_s, :text => 'Foo'
+ assert_select 'option[value=?][selected=selected]', @bar.id.to_s, :text => 'Bar'
+ end
+ end
+
+ def test_edit_tag_with_check_box_style_should_contain_possible_values
+ @field.edit_tag_style = 'check_box'
+ @field.save!
+ value = CustomFieldValue.new(:custom_field => @field, :customized => Issue.new)
+
+ tag = @field.format.edit_tag(self, 'id', 'name', value)
+ assert_select_in tag, 'span' do
+ assert_select 'input[type=radio]', 3
+ assert_select 'label', :text => '(none)' do
+ assert_select 'input[value=""]'
+ end
+ assert_select 'label', :text => 'Foo' do
+ assert_select 'input[value=?]', @foo.id.to_s
+ end
+ assert_select 'label', :text => 'Bar' do
+ assert_select 'input[value=?]', @bar.id.to_s
+ end
+ end
+ end
+end