Commit d44dabc6 authored by jplang's avatar jplang

Adds Enumeration custom field format (#21060).

Similar to List format but stores possible values as records.

git-svn-id: https://svn.redmine.org/redmine/trunk@14745 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent 1f158b49
# 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
......@@ -18,6 +18,10 @@
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}\''
......
# 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
$('#content').html('<%= escape_javascript(render(:template => 'custom_field_enumerations/index')) %>');
$('#custom_field_enumeration_name').focus();
<%= 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 %>
<%= 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 %>
<% 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>
......@@ -970,6 +970,7 @@ en:
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
......
......@@ -988,6 +988,7 @@ fr:
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
......
......@@ -311,7 +311,10 @@ Rails.application.routes.draw do
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]
......
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
......@@ -539,7 +539,7 @@ module Redmine
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
......@@ -636,7 +636,6 @@ module Redmine
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
......@@ -674,6 +673,32 @@ module Redmine
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'
......
......@@ -77,8 +77,8 @@ pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
#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;}
......@@ -483,6 +483,7 @@ select.expandable {vertical-align:top;}
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%}
......
# 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
......@@ -27,4 +27,11 @@ class RoutingCustomFieldsTest < Redmine::RoutingTest
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
# 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment