Index: app/controllers/issues_controller.rb
===================================================================
--- app/controllers/issues_controller.rb	(revision 3739)
+++ app/controllers/issues_controller.rb	(working copy)
@@ -119,6 +119,7 @@
     @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
     @priorities = IssuePriority.all
     @time_entry = TimeEntry.new
+    @custom_values = @issue.custom_field_values
     respond_to do |format|
       format.html { render :template => 'issues/show.rhtml' }
       format.xml  { render :layout => false }
@@ -428,6 +429,7 @@
     @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
     @time_entry = TimeEntry.new
     
+    @custom_values = @issue.custom_field_values
     @notes = params[:notes]
     @issue.init_journal(User.current, @notes)
     # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
Index: app/helpers/custom_fields_helper.rb
===================================================================
--- app/helpers/custom_fields_helper.rb	(revision 3739)
+++ app/helpers/custom_fields_helper.rb	(working copy)
@@ -49,7 +49,12 @@
       blank_option = custom_field.is_required? ?
                        (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)
+      multi_image = custom_field.allow_multi ?
+                    link_to_function(image_tag('bullet_toggle_plus.png'), "toggle_multi_custom('#{custom_field.id}');", :style => "vertical-align: bottom;") :
+                    ''
+      multiple = custom_field.allow_multi && custom_value.value.is_a?(Array) && custom_value.value.length > 1
+      select_name = custom_field.allow_multi ? "#{name}[custom_multi_values][#{custom_field.id}][]" : field_name
+      select_tag(select_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id, :multiple => multiple) + multi_image
     else
       text_field_tag(field_name, custom_value.value, :id => field_id)
     end
@@ -92,7 +97,7 @@
   # Return a string used to display a custom value
   def show_value(custom_value)
     return "" unless custom_value
-    format_value(custom_value.value, custom_value.custom_field.field_format)
+    format_value((custom_value.value.is_a?(Array) ? custom_value.value.join("\n") : custom_value.value), custom_value.custom_field.field_format)
   end
   
   # Return a string used to display a custom value
Index: app/models/custom_field.rb
===================================================================
--- app/models/custom_field.rb	(revision 3739)
+++ app/models/custom_field.rb	(working copy)
@@ -34,6 +34,8 @@
   def before_validation
     # make sure these fields are not searchable
     self.searchable = false if %w(int float date bool).include?(field_format)
+    # make sure only list field_format have allow_multi option
+    self.allow_multi = false unless field_format == 'list'
     true
   end
   
Index: app/models/custom_value.rb
===================================================================
--- app/models/custom_value.rb	(revision 3739)
+++ app/models/custom_value.rb	(working copy)
@@ -65,3 +65,48 @@
     end
   end
 end
+
+class CustomValuesCollection < Array
+  attr_accessor :custom_field
+  
+  def initialize(custom_field, custom_values=[])
+    @custom_field = custom_field if custom_field.is_a?(CustomField)    
+    custom_values.map{ |x| self << x }
+    self
+  end
+
+  def value
+    self.uniq.map(&:value).delete_if {|x| x.blank?}
+  end
+
+  def value=(new_value)
+    self.delete_if{ |x| true }
+    new_value.map{ |x| self << x }
+  end
+  
+  def save
+    self.compact.each(&:save)
+  end
+
+  def valid?
+    self.inject(true){ |bool,v| bool && v.valid? }
+  end
+
+  def validate
+    self.uniq.map(&:validate)
+  end
+
+  def custom_field_id
+    @custom_field.id
+  end
+
+  def method_missing(symbol, *args)
+    if @custom_field.respond_to?(symbol)
+      @custom_field.send(symbol, *args)
+    elsif self.first && self.first.respond_to?(symbol)
+      self.first.send(symbol, *args)
+    else
+      super
+    end
+  end
+end
Index: app/models/issue.rb
===================================================================
--- app/models/issue.rb	(revision 3739)
+++ app/models/issue.rb	(working copy)
@@ -211,6 +211,7 @@
     due_date
     done_ratio
     estimated_hours
+    custom_multi_values
     custom_field_values
     lock_version
   ) unless const_defined?(:SAFE_ATTRIBUTES)
@@ -318,7 +319,7 @@
     @issue_before_change = self.clone
     @issue_before_change.status = self.status
     @custom_values_before_change = {}
-    self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
+    custom_field_values.each {|c| @custom_values_before_change.store c.custom_field.id, c.value }
     # Make sure updated_on is updated when adding a note.
     updated_on_will_change!
     @current_journal
@@ -781,7 +782,7 @@
                                                       :value => send(c)) unless send(c)==@issue_before_change.send(c)
       }
       # custom fields changes
-      custom_values.each {|c|
+      custom_field_values.each {|c|
         next if (@custom_values_before_change[c.custom_field_id]==c.value ||
                   (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
         @current_journal.details << JournalDetail.new(:property => 'cf', 
Index: app/models/journal_detail.rb
===================================================================
--- app/models/journal_detail.rb	(revision 3739)
+++ app/models/journal_detail.rb	(working copy)
@@ -19,6 +19,8 @@
   belongs_to :journal
   
   def before_save
+    self.value = value.join(", ") if value && value.is_a?(Array)
+    self.old_value = old_value.join(", ") if old_value && old_value.is_a?(Array)
     self.value = value[0..254] if value && value.is_a?(String)
     self.old_value = old_value[0..254] if old_value && old_value.is_a?(String)
   end
Index: app/views/custom_fields/_form.rhtml
===================================================================
--- app/views/custom_fields/_form.rhtml	(revision 3739)
+++ app/views/custom_fields/_form.rhtml	(working copy)
@@ -5,6 +5,7 @@
 function toggle_custom_field_format() {
   format = $("custom_field_field_format");
   p_length = $("custom_field_min_length");
+  p_multi = $("custom_field_allow_multi");
   p_regexp = $("custom_field_regexp");
   p_values = $("custom_field_possible_values");
   p_searchable = $("custom_field_searchable");
@@ -19,6 +20,7 @@
       Element.hide(p_regexp.parentNode);
       if (p_searchable) Element.show(p_searchable.parentNode);
       Element.show(p_values);
+      Element.show(p_multi.parentNode);
       break;
     case "bool":
       p_default.setAttribute('type','checkbox');
@@ -26,12 +28,14 @@
       Element.hide(p_regexp.parentNode);
       if (p_searchable) Element.hide(p_searchable.parentNode);
       Element.hide(p_values);
+      Element.hide(p_multi.parentNode);
       break;
     case "date":
       Element.hide(p_length.parentNode);
       Element.hide(p_regexp.parentNode);
       if (p_searchable) Element.hide(p_searchable.parentNode);
       Element.hide(p_values);
+      Element.hide(p_multi.parentNode);
       break;
     case "float":
     case "int":
@@ -39,12 +43,14 @@
       Element.show(p_regexp.parentNode);
       if (p_searchable) Element.hide(p_searchable.parentNode);
       Element.hide(p_values);
+      Element.hide(p_multi.parentNode);
       break;
     default:
       Element.show(p_length.parentNode);
       Element.show(p_regexp.parentNode);
       if (p_searchable) Element.show(p_searchable.parentNode);
       Element.hide(p_values);
+      Element.hide(p_multi.parentNode);
       break;
   }
 }
@@ -83,6 +89,7 @@
     <p><%= f.check_box :is_for_all %></p>
     <p><%= f.check_box :is_filter %></p>
     <p><%= f.check_box :searchable %></p>
+    <p><%= f.check_box :allow_multi %></p>
     
 <% when "UserCustomField" %>
     <p><%= f.check_box :is_required %></p>
Index: app/views/issues/_form_custom_fields.rhtml
===================================================================
--- app/views/issues/_form_custom_fields.rhtml	(revision 3739)
+++ app/views/issues/_form_custom_fields.rhtml	(working copy)
@@ -1,3 +1,15 @@
+<script type="text/javascript">
+//<![CDATA[
+function toggle_multi_custom(field) {
+  select = $('issue_custom_field_values_' + field);
+    if (select.multiple == true) {
+      select.multiple = false;
+    } else {
+      select.multiple = true;
+  }
+}
+//]]>
+</script>
 <div class="splitcontentleft">
 <% i = 0 %>
 <% split_on = (@issue.custom_field_values.size / 2.0).ceil - 1 %>
Index: config/locales/en.yml
===================================================================
--- config/locales/en.yml	(revision 3739)
+++ config/locales/en.yml	(working copy)
@@ -272,6 +272,7 @@
   field_column_names: Columns
   field_time_zone: Time zone
   field_searchable: Searchable
+  field_allow_multi: Allow multiple choices
   field_default_value: Default value
   field_comments_sorting: Display comments
   field_parent_title: Parent page
Index: config/locales/fr.yml
===================================================================
--- config/locales/fr.yml	(revision 3739)
+++ config/locales/fr.yml	(working copy)
@@ -291,6 +291,7 @@
   field_column_names: Colonnes
   field_time_zone: Fuseau horaire
   field_searchable: Utilisé pour les recherches
+  field_allow_multi: Permettre les choix multiples
   field_default_value: Valeur par défaut
   field_comments_sorting: Afficher les commentaires
   field_parent_title: Page parent
Index: config/locales/ja.yml
===================================================================
--- config/locales/ja.yml	(revision 3739)
+++ config/locales/ja.yml	(working copy)
@@ -303,6 +303,7 @@
   field_column_names: 項目
   field_time_zone: タイムゾーン
   field_searchable: 検索条件に設定可能とする
+  field_allow_multi: 複数選択可能
   field_default_value: デフォルト値
   field_comments_sorting: コメントを表示
   field_parent_title: 親ページ
Index: config/locales/zh-TW.yml
===================================================================
--- config/locales/zh-TW.yml	(revision 3739)
+++ config/locales/zh-TW.yml	(working copy)
@@ -363,6 +363,7 @@
   field_column_names: 欄位
   field_time_zone: 時區
   field_searchable: 可用做搜尋條件
+  field_allow_multi: 允許多重選擇
   field_default_value: 預設值
   field_comments_sorting: 註解排序
   field_parent_title: 父頁面
Index: config/locales/zh.yml
===================================================================
--- config/locales/zh.yml	(revision 3739)
+++ config/locales/zh.yml	(working copy)
@@ -291,6 +291,7 @@
   field_column_names: 列
   field_time_zone: 时区
   field_searchable: 可用作搜索条件
+  field_allow_multi: 允许多选
   field_default_value: 默认值
   field_comments_sorting: 显示注释
   field_parent_title: 上级页面
Index: db/migrate/20100512172200_add_custom_fields_multi.rb
===================================================================
--- db/migrate/20100512172200_add_custom_fields_multi.rb	(revision 0)
+++ db/migrate/20100512172200_add_custom_fields_multi.rb	(revision 0)
@@ -0,0 +1,9 @@
+class AddCustomFieldsMulti < ActiveRecord::Migration
+  def self.up
+    add_column :custom_fields, :allow_multi, :boolean, :default => false
+  end
+
+  def self.down
+    remove_column :custom_fields, :allow_multi
+  end
+end
Index: test/unit/custom_value_test.rb
===================================================================
--- test/unit/custom_value_test.rb	(revision 3739)
+++ test/unit/custom_value_test.rb	(working copy)
@@ -78,6 +78,29 @@
     assert v.valid?
   end
 
+  def test_multi_list_field_validation
+    f = CustomField.new(:field_format => 'list', :allow_multi => true, :possible_values => ['value1', 'value2'])
+    v = CustomValuesCollection.new(:custom_field => f, :value => [])
+    assert v.valid?
+    v << CustomValue.new(:custom_field => f, :value => 'value1')
+    assert v.valid?
+    v << CustomValue.new(:custom_field => f, :value => 'value2')
+    assert v.valid?
+    v << CustomValue.new(:custom_field => f, :value => 'abc')
+    assert !v.valid?
+  end
+
+  def test_multi_list_field_value
+    f = CustomField.new(:field_format => 'list', :allow_multi => true, :possible_values => ['value1', 'value2'])
+    v = CustomValuesCollection.new(:custom_field => f, :value => [])
+    v << CustomValue.new(:custom_field => f, :value => 'value1')
+    c = CustomValue.new(:custom_field => f, :value => 'value2')
+    v << c
+    assert_equal ['value1', 'value2'], v.value
+    v << c
+    assert ['value1', 'value2'], v.value
+  end
+
   def test_int_field_validation
     f = CustomField.new(:field_format => 'int')
     v = CustomValue.new(:custom_field => f, :value => '')
Index: vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb
===================================================================
--- vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb	(revision 3739)
+++ vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb	(working copy)
@@ -54,12 +54,63 @@
           @custom_field_values_changed = true
           values = values.stringify_keys
           custom_field_values.each do |custom_value|
-            custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s)
+            if custom_value.is_a? CustomValuesCollection
+              if custom_value.empty?
+                values.each do |key, value|
+                  if (custom_value.custom_field_id == key.to_i && value.is_a?(Array))
+                    if value.empty?
+                      custom_value << custom_values.build(:custom_field => custom_value.custom_field, :value => nil)
+                    else
+                      value.each do |v|
+                        custom_value << custom_values.build(:custom_field => custom_value.custom_field, :value => v)
+                      end
+                    end
+                  end
+                end
+              end
+              CustomValue # otherwise Rails doesn't know the CustomValuesCollection class
+              custom_value = CustomValuesCollection.new custom_value.custom_field, custom_value
+              #end
+            else
+              custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s)
+            end
           end if values.is_a?(Hash)
         end
         
         def custom_field_values
-          @custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil) }
+          @custom_field_values ||= available_custom_fields.collect do |x|
+            if x.allow_multi
+              CustomValue # otherwise Rails doesn't know the CustomValuesCollection class
+              CustomValuesCollection.new x, custom_values.select{ |v| v.custom_field == x }
+            else
+              custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil)
+            end
+          end
+        end
+        
+        def custom_multi_values=(values)
+          values.each do |key, value|
+            value.delete_if {|v| v.to_s == ""} if value.length > 1
+          end
+          
+          @old_custom_values ||= custom_values.select{ |x| x.custom_field.allow_multi }
+          @custom_field_values_changed = true
+          values = values.stringify_keys
+          values.each do |key, value|
+            custom_value = custom_field_values.detect{ |c| c.custom_field.id == key.to_i && c.allow_multi }
+            value.each do |v|
+              old = @old_custom_values.detect{ |u| u.custom_field == custom_value.custom_field && u.value == v }
+              if old.blank?
+                custom_value << custom_values.build(:custom_field => custom_value.custom_field, :value => v)
+              else
+                custom_value << old unless custom_value.include?(old)
+                @old_custom_values.delete old
+              end
+            end if values.is_a?(Hash) && custom_value != nil && values.has_key?(custom_value.custom_field.id.to_s)
+          end
+          #delete old normal values
+          @custom_field_values.each { |c| c.delete_if{ |x| @old_custom_values.include?(x) } if c.is_a?(CustomValuesCollection) }
+          @custom_values.delete_if { |c| @old_custom_values.include?(c) }
         end
         
         def custom_field_values_changed?
@@ -73,6 +124,7 @@
         
         def save_custom_field_values
           custom_field_values.each(&:save)
+          @old_custom_values.each(&:destroy) unless @old_custom_values.blank?
           @custom_field_values_changed = false
           @custom_field_values = nil
         end
