]> source.dussan.org Git - redmine.git/commitdiff
Custom fields refactoring: most of code moved from controllers to models (using new...
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Fri, 27 Jun 2008 20:13:56 +0000 (20:13 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Fri, 27 Jun 2008 20:13:56 +0000 (20:13 +0000)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1592 e93f8b46-1217-0410-a6f0-8f06a7374b81

24 files changed:
app/controllers/account_controller.rb
app/controllers/issues_controller.rb
app/controllers/projects_controller.rb
app/controllers/timelog_controller.rb
app/controllers/users_controller.rb
app/helpers/custom_fields_helper.rb
app/helpers/issues_helper.rb
app/models/issue.rb
app/models/project.rb
app/models/query.rb
app/models/user.rb
app/views/account/register.rhtml
app/views/issues/_form_custom_fields.rhtml
app/views/issues/show.rhtml
app/views/projects/_form.rhtml
app/views/projects/show.rhtml
app/views/users/_form.rhtml
test/fixtures/custom_fields.yml
test/functional/issues_controller_test.rb
test/functional/projects_controller_test.rb
test/integration/admin_test.rb
test/unit/issue_test.rb
vendor/plugins/acts_as_customizable/init.rb [new file with mode: 0644]
vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb [new file with mode: 0644]

index 3fbbc89123cd4481fe2da0a63adf84f08ce5c55f..a9b8a1b82107ddd925a92af66f9fcea15868c830 100644 (file)
@@ -110,17 +110,12 @@ class AccountController < ApplicationController
     redirect_to(home_url) && return unless Setting.self_registration?
     if request.get?
       @user = User.new(:language => Setting.default_language)
-      @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
     else
       @user = User.new(params[:user])
       @user.admin = false
       @user.login = params[:user][:login]
       @user.status = User::STATUS_REGISTERED
       @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
-      @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, 
-                                                                                :customized => @user, 
-                                                                                :value => (params["custom_fields"] ? params["custom_fields"][x.id.to_s] : nil)) }
-      @user.custom_values = @custom_values
       case Setting.self_registration
       when '1'
         # Email activation
index 69c8e7932cd27df1c7da0765c1d283e5965c0ba4..49b443238c4978b53f4673a1a1ff00297ab680b4 100644 (file)
@@ -94,7 +94,6 @@ class IssuesController < ApplicationController
   end
   
   def show
-    @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
     @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
     @journals.each_with_index {|j,i| j.indice = i+1}
     @journals.reverse! if User.current.wants_comments_in_reverse_order?
@@ -113,15 +112,17 @@ class IssuesController < ApplicationController
   # Add a new issue
   # The new issue will be created from an existing one if copy_from parameter is given
   def new
-    @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
+    @issue = Issue.new
+    @issue.copy_from(params[:copy_from]) if params[:copy_from]
     @issue.project = @project
-    @issue.author = User.current
     @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
     if @issue.tracker.nil?
       flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
       render :nothing => true, :layout => true
       return
     end
+    @issue.attributes = params[:issue]
+    @issue.author = User.current
     
     default_status = IssueStatus.default
     unless default_status
@@ -134,17 +135,10 @@ class IssuesController < ApplicationController
     
     if request.get? || request.xhr?
       @issue.start_date ||= Date.today
-      @custom_values = @issue.custom_values.empty? ?
-        @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
-        @issue.custom_values
     else
       requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
       # Check that the user is allowed to apply the requested status
       @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
-      @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, 
-                                                                                                       :customized => @issue,
-                                                                                                       :value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) }
-      @issue.custom_values = @custom_values
       if @issue.save
         attach_files(@issue, params[:attachments])
         flash[:notice] = l(:notice_successful_create)
@@ -165,7 +159,6 @@ class IssuesController < ApplicationController
     @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
     @activities = Enumeration::get_values('ACTI')
     @priorities = Enumeration::get_values('IPRI')
-    @custom_values = []
     @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
     
     @notes = params[:notes]
@@ -178,14 +171,7 @@ class IssuesController < ApplicationController
       @issue.attributes = attrs
     end
 
-    if request.get?
-      @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
-    else
-      # Update custom fields if user has :edit permission
-      if @edit_allowed && params[:custom_fields]
-        @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
-        @issue.custom_values = @custom_values
-      end
+    if request.post?
       @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
       @time_entry.attributes = params[:time_entry]
       attachments = attach_files(@issue, params[:attachments])
index c9a55088b482d6e4595be2087b2271d0a5a4c8aa..a37ae09a8c16d73153c277ba4c1084e466bb7d71 100644 (file)
@@ -63,21 +63,17 @@ class ProjectsController < ApplicationController
   
   # Add a new project
   def add
-    @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
+    @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
     @trackers = Tracker.all
     @root_projects = Project.find(:all,
                                   :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
                                   :order => 'name')
     @project = Project.new(params[:project])
     if request.get?
-      @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
       @project.trackers = Tracker.all
       @project.is_public = Setting.default_projects_public?
       @project.enabled_module_names = Redmine::AccessControl.available_project_modules
     else
-      @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
-      @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
-      @project.custom_values = @custom_values
       @project.enabled_module_names = params[:enabled_modules]
       if @project.save
         flash[:notice] = l(:notice_successful_create)
@@ -88,7 +84,6 @@ class ProjectsController < ApplicationController
        
   # Show @project
   def show
-    @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
     @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
     @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
     @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@@ -115,11 +110,10 @@ class ProjectsController < ApplicationController
     @root_projects = Project.find(:all,
                                   :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
                                   :order => 'name')
-    @custom_fields = IssueCustomField.find(:all)
+    @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
     @issue_category ||= IssueCategory.new
     @member ||= @project.members.new
     @trackers = Tracker.all
-    @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
     @repository ||= @project.repository
     @wiki ||= @project.wiki
   end
@@ -127,10 +121,6 @@ class ProjectsController < ApplicationController
   # Edit @project
   def edit
     if request.post?
-      if params[:custom_fields]
-        @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
-        @project.custom_values = @custom_values
-      end
       @project.attributes = params[:project]
       if @project.save
         flash[:notice] = l(:notice_successful_update)
index 2e257c5aa40b3e5ab5e45e4827978a0f120ffbaf..cf1c844df463ec408947a2f1b974e4dc14ff2249 100644 (file)
@@ -54,7 +54,7 @@ class TimelogController < ApplicationController
                            }
     
     # Add list and boolean custom fields as available criterias
-    @project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
+    @project.all_issue_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
       @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)",
                                              :format => cf.field_format,
                                              :label => cf.name}
index c37709661655a11daa620c1ec91f2bf3e0685dad..eb8aa7bac70741fba3e7e51a5c7fc359e16256d0 100644 (file)
@@ -52,14 +52,11 @@ class UsersController < ApplicationController
   def add
     if request.get?
       @user = User.new(:language => Setting.default_language)
-      @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
     else
       @user = User.new(params[:user])
       @user.admin = params[:user][:admin] || false
       @user.login = params[:user][:login]
       @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
-      @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
-      @user.custom_values = @custom_values                     
       if @user.save
         Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
         flash[:notice] = l(:notice_successful_create)
@@ -71,16 +68,10 @@ class UsersController < ApplicationController
 
   def edit
     @user = User.find(params[:id])
-    if request.get?
-      @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
-    else
+    if request.post?
       @user.admin = params[:user][:admin] if params[:user][:admin]
       @user.login = params[:user][:login] if params[:user][:login]
       @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
-      if params[:custom_fields]
-        @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
-        @user.custom_values = @custom_values
-      end
       if @user.update_attributes(params[:user])
         flash[:notice] = l(:notice_successful_update)
         # Give a string to redirect_to otherwise it would use status param as the response code
index 61c8d6b3603127eca442512d3173df63b1e6e116..540c6b4a1af03413af8d40cd5f1ad51977cf3af3 100644 (file)
@@ -25,37 +25,40 @@ module CustomFieldsHelper
   end
   
   # Return custom field html tag corresponding to its format
-  def custom_field_tag(custom_value)   
+  def custom_field_tag(name, custom_value)     
     custom_field = custom_value.custom_field
-    field_name = "custom_fields[#{custom_field.id}]"
-    field_id = "custom_fields_#{custom_field.id}"
+    field_name = "#{name}[custom_field_values][#{custom_field.id}]"
+    field_id = "#{name}_custom_field_values_#{custom_field.id}"
     
     case custom_field.field_format
     when "date"
-      text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) + 
+      text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) + 
       calendar_for(field_id)
     when "text"
-      text_area 'custom_value', 'value', :name => field_name, :id => field_id, :rows => 3, :style => 'width:99%'
+      text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
     when "bool"
-      check_box 'custom_value', 'value', :name => field_name, :id => field_id
+      check_box_tag(field_name, custom_value.value, :id => field_id)
     when "list"
-      select 'custom_value', 'value', custom_field.possible_values, { :include_blank => true }, :name => field_name, :id => field_id
+      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)
     else
-      text_field 'custom_value', 'value', :name => field_name, :id => field_id
+      text_field_tag(field_name, custom_value.value, :id => field_id)
     end
   end
   
   # Return custom field label tag
-  def custom_field_label_tag(custom_value)
+  def custom_field_label_tag(name, custom_value)
     content_tag "label", custom_value.custom_field.name +
        (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
-       :for => "custom_fields_#{custom_value.custom_field.id}",
+       :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
        :class => (custom_value.errors.empty? ? nil : "error" )
   end
   
   # Return custom field tag with its label tag
-  def custom_field_tag_with_label(custom_value)
-    custom_field_label_tag(custom_value) + custom_field_tag(custom_value)
+  def custom_field_tag_with_label(name, custom_value)
+    custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
   end
 
   # Return a string used to display a custom value
index f42002ec8b0c8f4b481af65b87ab78010d2caf08..40369717851066ce32a524a3f17f8b568a282863 100644 (file)
@@ -149,7 +149,7 @@ module IssuesHelper
                   ]
       # Export project custom fields if project is given
       # otherwise export custom fields marked as "For all projects"
-      custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
+      custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
       custom_fields.each {|f| headers << f.name}
       # Description in the last column
       headers << l(:field_description)
index d83b2ab029fbfc2a82695a3ce2692740162346ed..326e234b069de4859ddc22a7751226844513e662 100644 (file)
@@ -28,13 +28,12 @@ class Issue < ActiveRecord::Base
   has_many :journals, :as => :journalized, :dependent => :destroy
   has_many :attachments, :as => :container, :dependent => :destroy
   has_many :time_entries, :dependent => :delete_all
-  has_many :custom_values, :dependent => :delete_all, :as => :customized
-  has_many :custom_fields, :through => :custom_values
   has_and_belongs_to_many :changesets, :order => "revision ASC"
   
   has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
   has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
   
+  acts_as_customizable
   acts_as_watchable
   acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue}
   acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
@@ -44,7 +43,6 @@ class Issue < ActiveRecord::Base
   validates_length_of :subject, :maximum => 255
   validates_inclusion_of :done_ratio, :in => 0..100
   validates_numericality_of :estimated_hours, :allow_nil => true
-  validates_associated :custom_values, :on => :update
 
   def after_initialize
     if new_record?
@@ -54,6 +52,11 @@ class Issue < ActiveRecord::Base
     end
   end
   
+  # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
+  def available_custom_fields
+    (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
+  end
+  
   def copy_from(arg)
     issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
     self.attributes = issue.attributes.dup
@@ -168,11 +171,6 @@ class Issue < ActiveRecord::Base
     end
   end
   
-  def custom_value_for(custom_field)
-    self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
-    return nil
-  end
-  
   def init_journal(user, notes = "")
     @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
     @issue_before_change = self.clone
index f05ccb2afd1e0af4007e46114db46a0fcd3e3ff2..a5ba246b186bceae2cb5becc8d1768f82185fa06 100644 (file)
@@ -22,7 +22,6 @@ class Project < ActiveRecord::Base
   
   has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
   has_many :users, :through => :members
-  has_many :custom_values, :dependent => :delete_all, :as => :customized
   has_many :enabled_modules, :dependent => :delete_all
   has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
   has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
@@ -38,7 +37,7 @@ class Project < ActiveRecord::Base
   has_many :changesets, :through => :repository
   has_one :wiki, :dependent => :destroy
   # Custom field for the project issues
-  has_and_belongs_to_many :custom_fields, 
+  has_and_belongs_to_many :issue_custom_fields, 
                           :class_name => 'IssueCustomField',
                           :order => "#{CustomField.table_name}.position",
                           :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
@@ -46,6 +45,7 @@ class Project < ActiveRecord::Base
                           
   acts_as_tree :order => "name", :counter_cache => true
 
+  acts_as_customizable
   acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
   acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
                 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
@@ -54,7 +54,6 @@ class Project < ActiveRecord::Base
   
   validates_presence_of :name, :identifier
   validates_uniqueness_of :name, :identifier
-  validates_associated :custom_values, :on => :update
   validates_associated :repository, :wiki
   validates_length_of :name, :maximum => 30
   validates_length_of :homepage, :maximum => 255
@@ -195,12 +194,8 @@ class Project < ActiveRecord::Base
   
   # Returns an array of all custom fields enabled for project issues
   # (explictly associated custom fields and custom fields enabled for all projects)
-  def custom_fields_for_issues(tracker)
-    all_custom_fields.select {|c| tracker.custom_fields.include? c }
-  end
-  
-  def all_custom_fields
-    @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
+  def all_issue_custom_fields
+    @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq
   end
   
   def project
index 4c72e23f22da28e6137954bea37a72378d8b5c33..27ab882c6a083a26840aeef4451c2586262d5d01 100644 (file)
@@ -176,7 +176,7 @@ class Query < ActiveRecord::Base
       unless @project.active_children.empty?
         @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
       end
-      add_custom_fields_filters(@project.all_custom_fields)
+      add_custom_fields_filters(@project.all_issue_custom_fields)
     else
       # global filters for cross project issue list
       add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
@@ -226,7 +226,7 @@ class Query < ActiveRecord::Base
     return @available_columns if @available_columns
     @available_columns = Query.available_columns
     @available_columns += (project ? 
-                            project.all_custom_fields :
+                            project.all_issue_custom_fields :
                             IssueCustomField.find(:all, :conditions => {:is_for_all => true})
                            ).collect {|cf| QueryCustomFieldColumn.new(cf) }      
   end
index 5568027d577791a28bb8c03e4a13a3461e813c32..a34b968617e560902073ae6ffbdbc0a4608c0183 100644 (file)
@@ -37,12 +37,13 @@ class User < ActiveRecord::Base
 
   has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all
   has_many :projects, :through => :memberships
-  has_many :custom_values, :dependent => :delete_all, :as => :customized
   has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
   has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
   has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
   belongs_to :auth_source
   
+  acts_as_customizable
+  
   attr_accessor :password, :password_confirmation
   attr_accessor :last_before_login_on
   # Prevents unauthorized assignments
@@ -60,7 +61,6 @@ class User < ActiveRecord::Base
   validates_length_of :mail, :maximum => 60, :allow_nil => true
   validates_length_of :password, :minimum => 4, :allow_nil => true
   validates_confirmation_of :password, :allow_nil => true
-  validates_associated :custom_values, :on => :update
 
   def before_create
     self.mail_notification = false
@@ -280,6 +280,10 @@ class AnonymousUser < User
     errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
   end
   
+  def available_custom_fields
+    []
+  end
+  
   # Overrides a few properties
   def logged?; false end
   def admin; false end
index 7cf4b6da3c19462ada17a4af91afc8dba5bff50a..4e2b5adf2ace87c5ed0077886b1258188c9c9381 100644 (file)
@@ -27,8 +27,8 @@
 <p><label for="user_language"><%=l(:field_language)%></label>
 <%= select("user", "language", lang_options_for_select) %></p>
 
-<% for @custom_value in @custom_values %>
-       <p><%= custom_field_tag_with_label @custom_value %></p>
+<% @user.custom_field_values.each do |value| %>
+       <p><%= custom_field_tag_with_label :user, value %></p>
 <% end %>
 <!--[eoform:user]-->
 </div>
index 1268bb1f9d8ccd5765103ad6c2af32b57a81caea..ebd4c3219a944ae4a004d29cc72e8a8f4dca4d39 100644 (file)
@@ -1,11 +1,12 @@
 <div class="splitcontentleft">
 <% i = 1 %>
-<% for @custom_value in values %>
-       <p><%= custom_field_tag_with_label @custom_value %></p>
-    <% if i == values.size / 2 %>
+<% split_on = @issue.custom_field_values.size / 2 %>
+<% @issue.custom_field_values.each do |value| %>
+       <p><%= custom_field_tag_with_label :issue, value %></p>
+<% if i == split_on -%>
 </div><div class="splitcontentright">
-    <% end %>
-    <% i += 1 %>
-<% end %>
+<% end -%>
+<% i += 1 -%>
+<% end -%>
 </div>
 <div style="clear:both;"> </div>
index 57fbd05c7ac25fceae5dcf396e3a0eb3a68d29c0..a3b26be12a4c17a40aa7bd9c135b4eeb5f77fd41 100644 (file)
@@ -43,9 +43,9 @@
     <% end %>
 </tr>
 <tr>
-<% n = 0
-for custom_value in @custom_values %>
-    <td valign="top"><b><%= custom_value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(custom_value))) %></td>
+<% n = 0 -%>
+<% @issue.custom_values.each do |value| -%>
+    <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
 <% n = n + 1
    if (n > 1) 
         n = 0 %>
index 774e739774610ce18637d428d3e1e2939c1340ca..11f7e39332344cd4e6cc98f6407016d4a0911cd8 100644 (file)
@@ -17,8 +17,8 @@
 <p><%= f.check_box :is_public %></p>
 <%= wikitoolbar_for 'project_description' %>
 
-<% for @custom_value in @custom_values %>
-       <p><%= custom_field_tag_with_label @custom_value %></p>
+<% @project.custom_field_values.each do |value| %>
+       <p><%= custom_field_tag_with_label :project, value %></p>
 <% end %>
 </div>
 
 </fieldset>
 <% end %>
 
-<% unless @custom_fields.empty? %>
+<% unless @issue_custom_fields.empty? %>
 <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
-<% for custom_field in @custom_fields %>
+<% @issue_custom_fields.each do |custom_field| %>
     <label class="floating">
-       <%= check_box_tag 'project[custom_field_ids][]', custom_field.id, ((@project.custom_fields.include? custom_field) or custom_field.is_for_all?), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
+       <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
        <%= custom_field.name %>
        </label>
 <% end %>
-<%= hidden_field_tag 'project[custom_field_ids][]', '' %>
+<%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
 </fieldset>
 <% end %>
 <!--[eoform:project]-->
index 62b911937a5ce2a46923c6c893671f3882354e0a..6c82c80b4dd71f9d0e49ef08a891160fb12f1367 100644 (file)
@@ -10,7 +10,7 @@
        <% if @project.parent %>
        <li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li>
        <% end %>
-       <% for custom_value in @custom_values %>
+       <% @project.custom_values.each do |custom_value| %>
        <% if !custom_value.value.empty? %>
           <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
        <% end %>
index 09a798468e84c537d3215f6fecf87f212ec1b5bb..799ebde476b275e8b8f3d0cb30b2f593b57c29e5 100644 (file)
@@ -8,9 +8,9 @@
 <p><%= f.text_field :mail, :required => true %></p>
 <p><%= f.select :language, lang_options_for_select %></p>
 
-<% for @custom_value in @custom_values %>
-       <p><%= custom_field_tag_with_label @custom_value %></p>
-<% end if @custom_values%>
+<% @user.custom_field_values.each do |value| %>
+       <p><%= custom_field_tag_with_label :user, value %></p>
+<% end %>
 
 <p><%= f.check_box :admin, :disabled => (@user == User.current) %></p>
 </div>
index 9d88bc6fbe643fcf57d686e5e69b59d328091695..1005edae4792e918e2ae2ea5118f5fb10aa614fe 100644 (file)
@@ -7,7 +7,10 @@ custom_fields_001:
   is_filter: true\r
   type: IssueCustomField\r
   max_length: 0\r
-  possible_values: MySQL|PostgreSQL|Oracle\r
+  possible_values: \r
+  - MySQL\r
+  - PostgreSQL\r
+  - Oracle\r
   id: 1\r
   is_required: false\r
   field_format: list\r
@@ -33,7 +36,11 @@ custom_fields_003:
   is_filter: true\r
   type: ProjectCustomField\r
   max_length: 0\r
-  possible_values: Stable|Beta|Alpha|Planning\r
+  possible_values: \r
+  - Stable\r
+  - Beta\r
+  - Alpha\r
+  - Planning\r
   id: 3\r
   is_required: true\r
   field_format: list\r
index 32d2a7f41d3c777f5ebaea19d043408422d11ccf..ac1f4f83435219a77046619df0bb2493b3a8a33c 100644 (file)
@@ -170,7 +170,7 @@ class IssuesControllerTest < Test::Unit::TestCase
     assert_response :success
     assert_template 'new'
     
-    assert_tag :tag => 'input', :attributes => { :name => 'custom_fields[2]',
+    assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
                                                  :value => 'Default string' }
   end
 
@@ -203,8 +203,8 @@ class IssuesControllerTest < Test::Unit::TestCase
                           :subject => 'This is the test_new issue',
                           :description => 'This is the description',
                           :priority_id => 5,
-                          :estimated_hours => ''},
-               :custom_fields => {'2' => 'Value for field 2'}
+                          :estimated_hours => '',
+                          :custom_field_values => {'2' => 'Value for field 2'}}
     assert_redirected_to 'issues/show'
     
     issue = Issue.find_by_subject('This is the test_new issue')
@@ -226,6 +226,50 @@ class IssuesControllerTest < Test::Unit::TestCase
     assert_redirected_to 'issues/show'
   end
   
+  def test_post_new_with_required_custom_field_and_without_custom_fields_param
+    field = IssueCustomField.find_by_name('Database')
+    field.update_attribute(:is_required, true)
+
+    @request.session[:user_id] = 2
+    post :new, :project_id => 1, 
+               :issue => {:tracker_id => 1,
+                          :subject => 'This is the test_new issue',
+                          :description => 'This is the description',
+                          :priority_id => 5}
+    assert_response :success
+    assert_template 'new'
+    issue = assigns(:issue)
+    assert_not_nil issue
+    assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
+  end
+  
+  def test_post_should_preserve_fields_values_on_validation_failure
+    @request.session[:user_id] = 2
+    post :new, :project_id => 1, 
+               :issue => {:tracker_id => 1,
+                          :subject => 'This is the test_new issue',
+                          # empty description
+                          :description => '',
+                          :priority_id => 6,
+                          :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
+    assert_response :success
+    assert_template 'new'
+    
+    assert_tag :input, :attributes => { :name => 'issue[subject]',
+                                        :value => 'This is the test_new issue' }
+    assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
+                        :child => { :tag => 'option', :attributes => { :selected => 'selected',
+                                                                       :value => '6' },
+                                                      :content => 'High' }  
+    # Custom fields
+    assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
+                        :child => { :tag => 'option', :attributes => { :selected => 'selected',
+                                                                       :value => 'Oracle' },
+                                                      :content => 'Oracle' }  
+    assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
+                                        :value => 'Value for field 2'}
+  end
+  
   def test_copy_issue
     @request.session[:user_id] = 2
     get :new, :project_id => 1, :copy_from => 1
@@ -280,18 +324,28 @@ class IssuesControllerTest < Test::Unit::TestCase
     assert_select_rjs :show, "update"
   end
 
-  def test_post_edit
+  def test_post_edit_without_custom_fields_param
     @request.session[:user_id] = 2
     ActionMailer::Base.deliveries.clear
     
     issue = Issue.find(1)
+    assert_equal '125', issue.custom_value_for(2).value
     old_subject = issue.subject
     new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
     
-    post :edit, :id => 1, :issue => {:subject => new_subject}
+    assert_difference('Journal.count') do
+      assert_difference('JournalDetail.count', 2) do
+        post :edit, :id => 1, :issue => {:subject => new_subject,
+                                         :priority_id => '6',
+                                         :category_id => '1' # no change
+                                        }
+      end
+    end
     assert_redirected_to 'issues/show/1'
     issue.reload
     assert_equal new_subject, issue.subject
+    # Make sure custom fields were not cleared
+    assert_equal '125', issue.custom_value_for(2).value
     
     mail = ActionMailer::Base.deliveries.last
     assert_kind_of TMail::Mail, mail
@@ -299,6 +353,29 @@ class IssuesControllerTest < Test::Unit::TestCase
     assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
   end
   
+  def test_post_edit_with_custom_field_change
+    @request.session[:user_id] = 2
+    issue = Issue.find(1)
+    assert_equal '125', issue.custom_value_for(2).value
+    
+    assert_difference('Journal.count') do
+      assert_difference('JournalDetail.count', 3) do
+        post :edit, :id => 1, :issue => {:subject => 'Custom field change',
+                                         :priority_id => '6',
+                                         :category_id => '1', # no change
+                                         :custom_field_values => { '2' => 'New custom value' }
+                                        }
+      end
+    end
+    assert_redirected_to 'issues/show/1'
+    issue.reload
+    assert_equal 'New custom value', issue.custom_value_for(2).value
+    
+    mail = ActionMailer::Base.deliveries.last
+    assert_kind_of TMail::Mail, mail
+    assert mail.body.include?("Searchable field changed from 125 to New custom value")
+  end
+  
   def test_post_edit_with_status_and_assignee_change
     issue = Issue.find(1)
     assert_equal 1, issue.status_id
index 5b7c29b182d6b8db8e49c2ef80a8680b0c7a5ae8..88c0319ba24011474a1fc03f9d7fb2d2996ec466 100644 (file)
@@ -83,7 +83,7 @@ class ProjectsControllerTest < Test::Unit::TestCase
   def test_edit
     @request.session[:user_id] = 2 # manager
     post :edit, :id => 1, :project => {:name => 'Test changed name',
-                                       :custom_field_ids => ['']}
+                                       :issue_custom_field_ids => ['']}
     assert_redirected_to 'projects/settings/ecookbook'
     project = Project.find(1)
     assert_equal 'Test changed name', project.name
index a424247ccc893e15a0441857d3b7b48787ae121c..6e385873e2d31a50ad860a5b20b1a942e7b0c0d3 100644 (file)
@@ -48,8 +48,9 @@ class AdminTest < ActionController::IntegrationTest
     post "projects/add", :project => { :name => "blog", 
                                        :description => "weblog",
                                        :identifier => "blog",
-                                       :is_public => 1 },
-                         'custom_fields[3]' => 'Beta'
+                                       :is_public => 1,
+                                       :custom_field_values => { '3' => 'Beta' }
+                                       }
     assert_redirected_to "admin/projects"
     assert_equal 'Successful creation.', flash[:notice]
     
index 999c4480d286d4f0ad3257b313f5fa3759ead204..0d98f89d25321cbd3b6c75a083b31e51c890bb1c 100644 (file)
 require File.dirname(__FILE__) + '/../test_helper'
 
 class IssueTest < Test::Unit::TestCase
-  fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries
+  fixtures :projects, :users, :members,
+           :trackers, :projects_trackers,
+           :issue_statuses, :issue_categories,
+           :enumerations,
+           :issues,
+           :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
+           :time_entries
 
   def test_create
     issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
@@ -27,6 +33,76 @@ class IssueTest < Test::Unit::TestCase
     assert_equal 1.5, issue.estimated_hours
   end
   
+  def test_create_with_required_custom_field
+    field = IssueCustomField.find_by_name('Database')
+    field.update_attribute(:is_required, true)
+    
+    issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
+    assert issue.available_custom_fields.include?(field)
+    # No value for the custom field
+    assert !issue.save
+    assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
+    # Blank value
+    issue.custom_field_values = { field.id => '' }
+    assert !issue.save
+    assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
+    # Invalid value
+    issue.custom_field_values = { field.id => 'SQLServer' }
+    assert !issue.save
+    assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
+    # Valid value
+    issue.custom_field_values = { field.id => 'PostgreSQL' }
+    assert issue.save
+    issue.reload
+    assert_equal 'PostgreSQL', issue.custom_value_for(field).value
+  end
+  
+  def test_update_issue_with_required_custom_field
+    field = IssueCustomField.find_by_name('Database')
+    field.update_attribute(:is_required, true)
+    
+    issue = Issue.find(1)
+    assert_nil issue.custom_value_for(field)
+    assert issue.available_custom_fields.include?(field)
+    # No change to custom values, issue can be saved
+    assert issue.save
+    # Blank value
+    issue.custom_field_values = { field.id => '' }
+    assert !issue.save
+    # Valid value
+    issue.custom_field_values = { field.id => 'PostgreSQL' }
+    assert issue.save
+    issue.reload
+    assert_equal 'PostgreSQL', issue.custom_value_for(field).value
+  end
+  
+  def test_should_not_update_attributes_if_custom_fields_validation_fails
+    issue = Issue.find(1)
+    field = IssueCustomField.find_by_name('Database')
+    assert issue.available_custom_fields.include?(field)
+    
+    issue.custom_field_values = { field.id => 'Invalid' }
+    issue.subject = 'Should be not be saved'
+    assert !issue.save
+    
+    issue.reload
+    assert_equal "Can't print recipes", issue.subject
+  end
+  
+  def test_should_not_recreate_custom_values_objects_on_update
+    field = IssueCustomField.find_by_name('Database')
+    
+    issue = Issue.find(1)
+    issue.custom_field_values = { field.id => 'PostgreSQL' }
+    assert issue.save
+    custom_value = issue.custom_value_for(field)
+    issue.reload
+    issue.custom_field_values = { field.id => 'MySQL' }
+    assert issue.save
+    issue.reload
+    assert_equal custom_value.id, issue.custom_value_for(field).id
+  end
+  
   def test_category_based_assignment
     issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
     assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
diff --git a/vendor/plugins/acts_as_customizable/init.rb b/vendor/plugins/acts_as_customizable/init.rb
new file mode 100644 (file)
index 0000000..9036aa5
--- /dev/null
@@ -0,0 +1,2 @@
+require File.dirname(__FILE__) + '/lib/acts_as_customizable'
+ActiveRecord::Base.send(:include, Redmine::Acts::Customizable)
diff --git a/vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb b/vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb
new file mode 100644 (file)
index 0000000..05857d0
--- /dev/null
@@ -0,0 +1,82 @@
+# redMine - project management software
+# Copyright (C) 2006-2008  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.
+
+module Redmine
+  module Acts
+    module Customizable
+      def self.included(base)
+        base.extend ClassMethods
+      end
+
+      module ClassMethods
+        def acts_as_customizable(options = {})
+          return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
+          cattr_accessor :customizable_options
+          self.customizable_options = options
+          has_many :custom_values, :dependent => :delete_all, :as => :customized
+          before_validation_on_create { |customized| customized.custom_field_values }
+          # Trigger validation only if custom values were changed
+          validates_associated :custom_values, :on => :update, :if => Proc.new { |customized| customized.custom_field_values_changed? }
+          send :include, Redmine::Acts::Customizable::InstanceMethods
+          # Save custom values when saving the customized object
+          after_save :save_custom_field_values
+        end
+      end
+
+      module InstanceMethods
+        def self.included(base)
+          base.extend ClassMethods
+        end
+        
+        def available_custom_fields
+          CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
+                                 :order => 'position')
+        end
+        
+        def custom_field_values=(values)
+          @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)
+          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) }
+        end
+        
+        def custom_field_values_changed?
+          @custom_field_values_changed == true
+        end
+        
+        def custom_value_for(c)
+          field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
+          custom_values.detect {|v| v.custom_field_id == field_id }
+        end
+        
+        def save_custom_field_values
+          custom_field_values.each(&:save)
+          @custom_field_values_changed = false
+          @custom_field_values = nil
+        end
+        
+        module ClassMethods
+        end
+      end
+    end
+  end
+end