Skip to content

Commit

Permalink
Provides a MultiSelect capability for custom_fields
Browse files Browse the repository at this point in the history
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
ciaranj committed Nov 24, 2009
1 parent 159ed30 commit 90213d2
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 54 deletions.
14 changes: 14 additions & 0 deletions app/helpers/custom_fields_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions app/models/custom_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions app/models/custom_value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
Expand Down
129 changes: 83 additions & 46 deletions app/models/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 }
Expand Down
6 changes: 6 additions & 0 deletions app/views/custom_fields/_form.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 10 additions & 5 deletions app/views/issues/_form_custom_fields.rhtml
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

3 comments on commit 90213d2

@sveneisenschmidt
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make this work for current available redmine 1.2?

@ciaranj
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@sveneisenschmidt
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Please sign in to comment.