Permalink
Browse files

Provides a MultiSelect capability for custom_fields

This commit provides various fixes to support 'multi-select' custom
fields.  These render as comma seperate single line lists when
issues are displayed and drop-down lists when editing the issue.

Filtering is also supported, allowing for filtering by:
'all' values have been selected
'no' values have been selected
1 or more specified values have been selected
1 or more specific values have not been selected (returns issues where none have been selected currently)

I hope this proves useful for others.
  • Loading branch information...
1 parent 159ed30 commit 90213d234e30d35b21de4977e47a957690450248 @ciaranj committed Nov 24, 2009
@@ -49,6 +49,8 @@ def custom_field_tag(name, custom_value)
(custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
'<option></option>'
select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
+ when "list_ms"
+ select_tag(field_name, options_for_select(custom_field.possible_values, custom_value.value.split(",")), :id => field_id, :multiple=> true, :size=>4)
else
text_field_tag(field_name, custom_value.value, :id => field_id)
end
@@ -81,10 +83,22 @@ def format_value(value, field_format)
begin; format_date(value.to_date); rescue; value end
when "bool"
l(value == "1" ? :general_text_Yes : :general_text_No)
+ when "list_ms"
+ value.gsub(",",", ") #try and encourage browser wrapping by throwing a space in there
else
value
end
end
+
+ # Return a number that represents the number of 'rows' that this control consumes
+ def get_display_size_value(custom_value)
+ return 1 unless custom_value
+
+ case custom_value.custom_field.field_format
+ when "list_ms" then 3 # Whilst the list is size 4, 3 seems to work more aesthetically :)
+ else 1
+ end
+ end
# Return an array of custom field formats which can be used in select_tag
def custom_field_formats_for_select
@@ -25,8 +25,9 @@ class CustomField < ActiveRecord::Base
"int" => { :name => :label_integer, :order => 3 },
"float" => { :name => :label_float, :order => 4 },
"list" => { :name => :label_list, :order => 5 },
- "date" => { :name => :label_date, :order => 6 },
- "bool" => { :name => :label_boolean, :order => 7 }
+ "list_ms" => { :name => :label_multi_select_list, :order => 6 },
+ "date" => { :name => :label_date, :order => 7 },
+ "bool" => { :name => :label_boolean, :order => 8 }
}.freeze
validates_presence_of :name, :field_format
@@ -47,7 +48,7 @@ def before_validation
end
def validate
- if self.field_format == "list"
+ if self.field_format == "list" || self.field_format == "list_ms"
errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
end
View
@@ -42,6 +42,13 @@ def to_s
value.to_s
end
+ def value=(val)
+ if custom_field.field_format == "list_ms" && val.kind_of?(Array)
+ val= val.join(",")
+ end
+ write_attribute(:value, val)
+ end
+
protected
def validate
if value.blank?
@@ -61,6 +68,9 @@ def validate
errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
when 'list'
errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value)
+ when 'list_ms'
+ # forget about space trimming for now...
+ errors.add(:value, :inclusion) unless (value.split(",") & custom_field.possible_values).size >0
end
end
end
View
@@ -401,53 +401,90 @@ def statement
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
+
+ list_ms_custom_field= nil;
+ if is_custom_filter && field =~ /^cf_(\d+)$/
+ cf= CustomField.find_by_id($1)
+ list_ms_custom_field= cf unless cf.nil? || cf.field_format != "list_ms"
+ end
sql = ''
- case operator
- when "="
- sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
- when "!"
- sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
- when "!*"
- sql = "#{db_table}.#{db_field} IS NULL"
- sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
- when "*"
- sql = "#{db_table}.#{db_field} IS NOT NULL"
- sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
- when ">="
- sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
- when "<="
- sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
- when "o"
- sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
- when "c"
- sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
- when ">t-"
- sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
- when "<t-"
- sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
- when "t-"
- sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
- when ">t+"
- sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
- when "<t+"
- sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
- when "t+"
- sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
- when "t"
- sql = date_range_clause(db_table, db_field, 0, 0)
- when "w"
- from = l(:general_first_day_of_week) == '7' ?
- # week starts on sunday
- ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
- # week starts on monday (Rails default)
- Time.now.at_beginning_of_week
- sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
- when "~"
- sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
- when "!~"
- sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
+ if list_ms_custom_field.nil?
+ case operator
+ when "="
+ sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
+ when "!"
+ sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
+ when "!*"
+ sql = "#{db_table}.#{db_field} IS NULL"
+ sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
+ when "*"
+ sql = "#{db_table}.#{db_field} IS NOT NULL"
+ sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
+ when ">="
+ sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
+ when "<="
+ sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
+ when "o"
+ sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
+ when "c"
+ sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
+ when ">t-"
+ sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
+ when "<t-"
+ sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
+ when "t-"
+ sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
+ when ">t+"
+ sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
+ when "<t+"
+ sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
+ when "t+"
+ sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
+ when "t"
+ sql = date_range_clause(db_table, db_field, 0, 0)
+ when "w"
+ from = l(:general_first_day_of_week) == '7' ?
+ # week starts on sunday
+ ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
+ # week starts on monday (Rails default)
+ Time.now.at_beginning_of_week
+ sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
+ when "~"
+ sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
+ when "!~"
+ sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
+ end
+ else
+ case operator
+ when "=" # is
+ if value.size == 1
+ sql = "#{db_table}.#{db_field} LIKE ('%#{connection.quote_string(value[0])}%')"
+ else
+ statements=[]
+ value.each do |val|
+ statements << "#{db_table}.#{db_field} LIKE ('%#{connection.quote_string(val)}%')"
+ end
+ sql << "(" << statements.join(" OR ") << ")"
+ end
+ when "!" # is not
+ if value.size == 1
+ sql = "#{db_table}.#{db_field} NOT LIKE ('%#{connection.quote_string(value[0])}%')"
+ else
+ value.each do |val|
+ statements << "#{db_table}.#{db_field} NOT LIKE ('%#{connection.quote_string(val)}%')"
+ end
+ sql << "(" << statements.join(" AND ") << ")"
+ end
+ when "!*" # none
+ sql = "#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} = ''"
+ when "*" # all
+ statements=[]
+ list_ms_custom_field.possible_values.each do |val|
+ statements << "#{db_table}.#{db_field} LIKE ('%#{connection.quote_string(val)}%')"
+ end
+ sql << "(" << statements.join(" AND ") << ")"
+ end
end
-
return sql
end
@@ -458,7 +495,7 @@ def add_custom_fields_filters(custom_fields)
case field.field_format
when "text"
options = { :type => :text, :order => 20 }
- when "list"
+ when "list","list_ms"
options = { :type => :list_optional, :values => field.possible_values, :order => 20}
when "date"
options = { :type => :date, :order => 20 }
@@ -20,6 +20,12 @@ function toggle_custom_field_format() {
if (p_searchable) Element.show(p_searchable.parentNode);
Element.show(p_values);
break;
+ case "list_ms":
+ Element.hide(p_length.parentNode);
+ Element.hide(p_regexp.parentNode);
+ if (p_searchable) Element.show(p_searchable.parentNode);
+ Element.show(p_values);
+ break;
case "bool":
p_default.setAttribute('type','checkbox');
Element.hide(p_length.parentNode);
@@ -1,12 +1,17 @@
<div class="splitcontentleft">
-<% i = 0 %>
-<% split_on = (@issue.custom_field_values.size / 2.0).ceil - 1 %>
-<% @issue.custom_field_values.each do |value| %>
+<% i = 0
+ not_split_yet= true
+ total_size= @issue.custom_field_values.inject(0) do |total_size, value|
+ total_size + get_display_size_value(value)
+ end
+ split_on = (total_size / 2.0).ceil - 1
+ @issue.custom_field_values.each do |value| %>
<p><%= custom_field_tag_with_label :issue, value %></p>
-<% if i == split_on -%>
+<% if i+ get_display_size_value(value) >= split_on && not_split_yet
+ not_split_yet= false-%>
</div><div class="splitcontentright">
<% end -%>
-<% i += 1 -%>
+<% i += get_display_size_value(value) -%>
<% end -%>
</div>
<div style="clear:both;"> </div>
View
@@ -454,6 +454,7 @@ en:
label_and_its_subprojects: "{{value}} and its subprojects"
label_min_max_length: Min - Max length
label_list: List
+ label_multi_select_list: List (Multi-Select)
label_date: Date
label_integer: Integer
label_float: Float

3 comments on commit 90213d2

Could you make this work for current available redmine 1.2?

Owner

ciaranj replied Sep 12, 2011

Probably, but I've not got the time or access to a 1.2v of redmine atm I'm afraid :(

That is sad. Would be a great feature. And it seems there is currently no progress on this feature.

Please sign in to comment.