Index: test/unit/custom_value_test.rb
===================================================================
--- test/unit/custom_value_test.rb	(Revision 7204)
+++ test/unit/custom_value_test.rb	(Arbeitskopie)
@@ -75,10 +75,33 @@
     v.value = 'abc'
     assert !v.valid?
     v.value = 'value2'
-    assert v.valid?
-  end
+     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
+   def test_int_field_validation
     f = CustomField.new(:field_format => 'int')
     v = CustomValue.new(:custom_field => f, :value => '')
     assert v.valid?
Index: app/helpers/custom_fields_helper.rb
===================================================================
--- app/helpers/custom_fields_helper.rb	(Revision 7204)
+++ app/helpers/custom_fields_helper.rb	(Arbeitskopie)
@@ -49,7 +49,13 @@
       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_options(custom_value.customized), custom_value.value), :id => field_id)
+      #select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), 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 +98,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 7204)
+++ app/models/custom_field.rb	(Arbeitskopie)
@@ -33,6 +33,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/issue.rb
===================================================================
--- app/models/issue.rb	(Revision 7204)
+++ app/models/issue.rb	(Arbeitskopie)
@@ -266,6 +266,7 @@
     'estimated_hours',
     'custom_field_values',
     'custom_fields',
+	'custom_multi_values',
     'lock_version',
     :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
 
@@ -393,7 +394,8 @@
     @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 }
+    #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
@@ -883,10 +885,10 @@
         @current_journal.details << JournalDetail.new(:property => 'attr',
                                                       :prop_key => c,
                                                       :old_value => @issue_before_change.send(c),
-                                                      :value => send(c))
+                                                      :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 7204)
+++ app/models/journal_detail.rb	(Arbeitskopie)
@@ -22,6 +22,8 @@
   private
   
   def normalize_values
+    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 = normalize(value)
     self.old_value = normalize(old_value)
   end
Index: app/models/custom_value.rb
===================================================================
--- app/models/custom_value.rb	(Revision 7204)
+++ app/models/custom_value.rb	(Arbeitskopie)
@@ -69,3 +69,51 @@
     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/controllers/issues_controller.rb
===================================================================
--- app/controllers/issues_controller.rb	(Revision 7204)
+++ app/controllers/issues_controller.rb	(Arbeitskopie)
@@ -118,6 +118,7 @@
     @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
     @priorities = IssuePriority.all
     @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
+	@custom_values = @issue.custom_field_values
     respond_to do |format|
       format.html { render :template => 'issues/show.rhtml' }
       format.api
@@ -284,6 +285,7 @@
     @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
     @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
     @time_entry.attributes = params[:time_entry]
+	@custom_values = @issue.custom_field_values
 
     @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
     @issue.init_journal(User.current, @notes)
Index: app/views/custom_fields/_form.rhtml
===================================================================
--- app/views/custom_fields/_form.rhtml	(Revision 7204)
+++ app/views/custom_fields/_form.rhtml	(Arbeitskopie)
@@ -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.parentNode);
+	  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.parentNode);
+	  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.parentNode);
+      Element.hide(p_multi.parentNode);
       break;
     case "float":
     case "int":
@@ -39,6 +43,7 @@
       Element.show(p_regexp.parentNode);
       if (p_searchable) Element.hide(p_searchable.parentNode);
       Element.hide(p_values.parentNode);
+	  Element.hide(p_multi.parentNode);
       break;
 		case "user":
     case "version":
@@ -91,6 +96,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/projects/show.rhtml
===================================================================
--- app/views/projects/show.rhtml	(Revision 7204)
+++ app/views/projects/show.rhtml	(Arbeitskopie)
@@ -16,7 +16,7 @@
  	<li><%=l(:label_subproject_plural)%>:
 	    <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
   <% end %>
-	<% @project.visible_custom_field_values.each do |custom_value| %>
+	<% @project.custom_field_values.each do |custom_value| %>
 	<% if !custom_value.value.blank? %>
 	   <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
 	<% end %>
Index: app/views/issues/_form_custom_fields.rhtml
===================================================================
--- app/views/issues/_form_custom_fields.rhtml	(Revision 7204)
+++ app/views/issues/_form_custom_fields.rhtml	(Arbeitskopie)
@@ -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: 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

Eigenschaftsänderungen: db/migrate/20100512172200_add_custom_fields_multi.rb
___________________________________________________________________
Hinzugefügt: svn:executable
   + *

Index: vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb
===================================================================
--- vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb	(Revision 7204)
+++ vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb	(Arbeitskopie)
@@ -69,17 +69,64 @@
           @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)
           self.custom_values = custom_field_values
         end
         
         def custom_field_values
-          @custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:customized => self, :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 visible_custom_field_values
-          custom_field_values.select(&:visible?)
+        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?
@@ -93,6 +140,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
@@ -109,4 +157,4 @@
       end
     end
   end
-end
+end
\ No newline at end of file
Index: config/locales/en.yml
===================================================================
--- config/locales/en.yml	(Revision 7204)
+++ config/locales/en.yml	(Arbeitskopie)
@@ -292,6 +292,7 @@
   field_time_entries: Log time
   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/de.yml
===================================================================
--- config/locales/de.yml	(Revision 7204)
+++ config/locales/de.yml	(Arbeitskopie)
@@ -308,6 +308,7 @@
   field_time_entries: Logzeit
   field_time_zone: Zeitzone
   field_searchable: Durchsuchbar
+  field_allow_multi: Erlaube mehrere Werte
   field_default_value: Standardwert
   field_comments_sorting: Kommentare anzeigen
   field_parent_title: Übergeordnete Seite
