##// END OF EJS Templates
Replaces find(:all) calls....
Jean-Philippe Lang -
r10689:96fca0b08f6d
parent child
Show More
@@ -1,260 +1,260
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :roadmap, :only => :roadmap
20 menu_item :roadmap, :only => :roadmap
21 menu_item :settings, :only => :settings
21 menu_item :settings, :only => :settings
22
22
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 before_filter :authorize_global, :only => [:new, :create]
25 before_filter :authorize_global, :only => [:new, :create]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 accept_rss_auth :index
27 accept_rss_auth :index
28 accept_api_auth :index, :show, :create, :update, :destroy
28 accept_api_auth :index, :show, :create, :update, :destroy
29
29
30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
31 if controller.request.post?
31 if controller.request.post?
32 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
32 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
33 end
33 end
34 end
34 end
35
35
36 helper :sort
36 helper :sort
37 include SortHelper
37 include SortHelper
38 helper :custom_fields
38 helper :custom_fields
39 include CustomFieldsHelper
39 include CustomFieldsHelper
40 helper :issues
40 helper :issues
41 helper :queries
41 helper :queries
42 include QueriesHelper
42 include QueriesHelper
43 helper :repositories
43 helper :repositories
44 include RepositoriesHelper
44 include RepositoriesHelper
45 include ProjectsHelper
45 include ProjectsHelper
46
46
47 # Lists visible projects
47 # Lists visible projects
48 def index
48 def index
49 respond_to do |format|
49 respond_to do |format|
50 format.html {
50 format.html {
51 scope = Project
51 scope = Project
52 unless params[:closed]
52 unless params[:closed]
53 scope = scope.active
53 scope = scope.active
54 end
54 end
55 @projects = scope.visible.order('lft').all
55 @projects = scope.visible.order('lft').all
56 }
56 }
57 format.api {
57 format.api {
58 @offset, @limit = api_offset_and_limit
58 @offset, @limit = api_offset_and_limit
59 @project_count = Project.visible.count
59 @project_count = Project.visible.count
60 @projects = Project.visible.offset(@offset).limit(@limit).order('lft').all
60 @projects = Project.visible.offset(@offset).limit(@limit).order('lft').all
61 }
61 }
62 format.atom {
62 format.atom {
63 projects = Project.visible.order('created_on DESC').limit(Setting.feeds_limit.to_i).all
63 projects = Project.visible.order('created_on DESC').limit(Setting.feeds_limit.to_i).all
64 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
64 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
65 }
65 }
66 end
66 end
67 end
67 end
68
68
69 def new
69 def new
70 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
70 @issue_custom_fields = IssueCustomField.sorted.all
71 @trackers = Tracker.sorted.all
71 @trackers = Tracker.sorted.all
72 @project = Project.new
72 @project = Project.new
73 @project.safe_attributes = params[:project]
73 @project.safe_attributes = params[:project]
74 end
74 end
75
75
76 def create
76 def create
77 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
77 @issue_custom_fields = IssueCustomField.sorted.all
78 @trackers = Tracker.sorted.all
78 @trackers = Tracker.sorted.all
79 @project = Project.new
79 @project = Project.new
80 @project.safe_attributes = params[:project]
80 @project.safe_attributes = params[:project]
81
81
82 if validate_parent_id && @project.save
82 if validate_parent_id && @project.save
83 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
83 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
84 # Add current user as a project member if he is not admin
84 # Add current user as a project member if he is not admin
85 unless User.current.admin?
85 unless User.current.admin?
86 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
86 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
87 m = Member.new(:user => User.current, :roles => [r])
87 m = Member.new(:user => User.current, :roles => [r])
88 @project.members << m
88 @project.members << m
89 end
89 end
90 respond_to do |format|
90 respond_to do |format|
91 format.html {
91 format.html {
92 flash[:notice] = l(:notice_successful_create)
92 flash[:notice] = l(:notice_successful_create)
93 redirect_to(params[:continue] ?
93 redirect_to(params[:continue] ?
94 {:controller => 'projects', :action => 'new', :project => {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}} :
94 {:controller => 'projects', :action => 'new', :project => {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}} :
95 {:controller => 'projects', :action => 'settings', :id => @project}
95 {:controller => 'projects', :action => 'settings', :id => @project}
96 )
96 )
97 }
97 }
98 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
98 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
99 end
99 end
100 else
100 else
101 respond_to do |format|
101 respond_to do |format|
102 format.html { render :action => 'new' }
102 format.html { render :action => 'new' }
103 format.api { render_validation_errors(@project) }
103 format.api { render_validation_errors(@project) }
104 end
104 end
105 end
105 end
106
106
107 end
107 end
108
108
109 def copy
109 def copy
110 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
110 @issue_custom_fields = IssueCustomField.sorted.all
111 @trackers = Tracker.sorted.all
111 @trackers = Tracker.sorted.all
112 @source_project = Project.find(params[:id])
112 @source_project = Project.find(params[:id])
113 if request.get?
113 if request.get?
114 @project = Project.copy_from(@source_project)
114 @project = Project.copy_from(@source_project)
115 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
115 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
116 else
116 else
117 Mailer.with_deliveries(params[:notifications] == '1') do
117 Mailer.with_deliveries(params[:notifications] == '1') do
118 @project = Project.new
118 @project = Project.new
119 @project.safe_attributes = params[:project]
119 @project.safe_attributes = params[:project]
120 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
120 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
121 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
121 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
122 flash[:notice] = l(:notice_successful_create)
122 flash[:notice] = l(:notice_successful_create)
123 redirect_to :controller => 'projects', :action => 'settings', :id => @project
123 redirect_to :controller => 'projects', :action => 'settings', :id => @project
124 elsif !@project.new_record?
124 elsif !@project.new_record?
125 # Project was created
125 # Project was created
126 # But some objects were not copied due to validation failures
126 # But some objects were not copied due to validation failures
127 # (eg. issues from disabled trackers)
127 # (eg. issues from disabled trackers)
128 # TODO: inform about that
128 # TODO: inform about that
129 redirect_to :controller => 'projects', :action => 'settings', :id => @project
129 redirect_to :controller => 'projects', :action => 'settings', :id => @project
130 end
130 end
131 end
131 end
132 end
132 end
133 rescue ActiveRecord::RecordNotFound
133 rescue ActiveRecord::RecordNotFound
134 # source_project not found
134 # source_project not found
135 render_404
135 render_404
136 end
136 end
137
137
138 # Show @project
138 # Show @project
139 def show
139 def show
140 if params[:jump]
140 if params[:jump]
141 # try to redirect to the requested menu item
141 # try to redirect to the requested menu item
142 redirect_to_project_menu_item(@project, params[:jump]) && return
142 redirect_to_project_menu_item(@project, params[:jump]) && return
143 end
143 end
144
144
145 @users_by_role = @project.users_by_role
145 @users_by_role = @project.users_by_role
146 @subprojects = @project.children.visible.all
146 @subprojects = @project.children.visible.all
147 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").all
147 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").all
148 @trackers = @project.rolled_up_trackers
148 @trackers = @project.rolled_up_trackers
149
149
150 cond = @project.project_condition(Setting.display_subprojects_issues?)
150 cond = @project.project_condition(Setting.display_subprojects_issues?)
151
151
152 @open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker)
152 @open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker)
153 @total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker)
153 @total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker)
154
154
155 if User.current.allowed_to?(:view_time_entries, @project)
155 if User.current.allowed_to?(:view_time_entries, @project)
156 @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
156 @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
157 end
157 end
158
158
159 @key = User.current.rss_key
159 @key = User.current.rss_key
160
160
161 respond_to do |format|
161 respond_to do |format|
162 format.html
162 format.html
163 format.api
163 format.api
164 end
164 end
165 end
165 end
166
166
167 def settings
167 def settings
168 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
168 @issue_custom_fields = IssueCustomField.sorted.all
169 @issue_category ||= IssueCategory.new
169 @issue_category ||= IssueCategory.new
170 @member ||= @project.members.new
170 @member ||= @project.members.new
171 @trackers = Tracker.sorted.all
171 @trackers = Tracker.sorted.all
172 @wiki ||= @project.wiki
172 @wiki ||= @project.wiki
173 end
173 end
174
174
175 def edit
175 def edit
176 end
176 end
177
177
178 def update
178 def update
179 @project.safe_attributes = params[:project]
179 @project.safe_attributes = params[:project]
180 if validate_parent_id && @project.save
180 if validate_parent_id && @project.save
181 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
181 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
182 respond_to do |format|
182 respond_to do |format|
183 format.html {
183 format.html {
184 flash[:notice] = l(:notice_successful_update)
184 flash[:notice] = l(:notice_successful_update)
185 redirect_to :action => 'settings', :id => @project
185 redirect_to :action => 'settings', :id => @project
186 }
186 }
187 format.api { render_api_ok }
187 format.api { render_api_ok }
188 end
188 end
189 else
189 else
190 respond_to do |format|
190 respond_to do |format|
191 format.html {
191 format.html {
192 settings
192 settings
193 render :action => 'settings'
193 render :action => 'settings'
194 }
194 }
195 format.api { render_validation_errors(@project) }
195 format.api { render_validation_errors(@project) }
196 end
196 end
197 end
197 end
198 end
198 end
199
199
200 def modules
200 def modules
201 @project.enabled_module_names = params[:enabled_module_names]
201 @project.enabled_module_names = params[:enabled_module_names]
202 flash[:notice] = l(:notice_successful_update)
202 flash[:notice] = l(:notice_successful_update)
203 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
203 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
204 end
204 end
205
205
206 def archive
206 def archive
207 if request.post?
207 if request.post?
208 unless @project.archive
208 unless @project.archive
209 flash[:error] = l(:error_can_not_archive_project)
209 flash[:error] = l(:error_can_not_archive_project)
210 end
210 end
211 end
211 end
212 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
212 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
213 end
213 end
214
214
215 def unarchive
215 def unarchive
216 @project.unarchive if request.post? && !@project.active?
216 @project.unarchive if request.post? && !@project.active?
217 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
217 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
218 end
218 end
219
219
220 def close
220 def close
221 @project.close
221 @project.close
222 redirect_to project_path(@project)
222 redirect_to project_path(@project)
223 end
223 end
224
224
225 def reopen
225 def reopen
226 @project.reopen
226 @project.reopen
227 redirect_to project_path(@project)
227 redirect_to project_path(@project)
228 end
228 end
229
229
230 # Delete @project
230 # Delete @project
231 def destroy
231 def destroy
232 @project_to_destroy = @project
232 @project_to_destroy = @project
233 if api_request? || params[:confirm]
233 if api_request? || params[:confirm]
234 @project_to_destroy.destroy
234 @project_to_destroy.destroy
235 respond_to do |format|
235 respond_to do |format|
236 format.html { redirect_to :controller => 'admin', :action => 'projects' }
236 format.html { redirect_to :controller => 'admin', :action => 'projects' }
237 format.api { render_api_ok }
237 format.api { render_api_ok }
238 end
238 end
239 end
239 end
240 # hide project in layout
240 # hide project in layout
241 @project = nil
241 @project = nil
242 end
242 end
243
243
244 private
244 private
245
245
246 # Validates parent_id param according to user's permissions
246 # Validates parent_id param according to user's permissions
247 # TODO: move it to Project model in a validation that depends on User.current
247 # TODO: move it to Project model in a validation that depends on User.current
248 def validate_parent_id
248 def validate_parent_id
249 return true if User.current.admin?
249 return true if User.current.admin?
250 parent_id = params[:project] && params[:project][:parent_id]
250 parent_id = params[:project] && params[:project][:parent_id]
251 if parent_id || @project.new_record?
251 if parent_id || @project.new_record?
252 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
252 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
253 unless @project.allowed_parents.include?(parent)
253 unless @project.allowed_parents.include?(parent)
254 @project.errors.add :parent_id, :invalid
254 @project.errors.add :parent_id, :invalid
255 return false
255 return false
256 end
256 end
257 end
257 end
258 true
258 true
259 end
259 end
260 end
260 end
@@ -1,297 +1,297
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module RepositoriesHelper
20 module RepositoriesHelper
21 def format_revision(revision)
21 def format_revision(revision)
22 if revision.respond_to? :format_identifier
22 if revision.respond_to? :format_identifier
23 revision.format_identifier
23 revision.format_identifier
24 else
24 else
25 revision.to_s
25 revision.to_s
26 end
26 end
27 end
27 end
28
28
29 def truncate_at_line_break(text, length = 255)
29 def truncate_at_line_break(text, length = 255)
30 if text
30 if text
31 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
31 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
32 end
32 end
33 end
33 end
34
34
35 def render_properties(properties)
35 def render_properties(properties)
36 unless properties.nil? || properties.empty?
36 unless properties.nil? || properties.empty?
37 content = ''
37 content = ''
38 properties.keys.sort.each do |property|
38 properties.keys.sort.each do |property|
39 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
39 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
40 end
40 end
41 content_tag('ul', content.html_safe, :class => 'properties')
41 content_tag('ul', content.html_safe, :class => 'properties')
42 end
42 end
43 end
43 end
44
44
45 def render_changeset_changes
45 def render_changeset_changes
46 changes = @changeset.filechanges.find(:all, :limit => 1000, :order => 'path').collect do |change|
46 changes = @changeset.filechanges.limit(1000).reorder('path').all.collect do |change|
47 case change.action
47 case change.action
48 when 'A'
48 when 'A'
49 # Detects moved/copied files
49 # Detects moved/copied files
50 if !change.from_path.blank?
50 if !change.from_path.blank?
51 change.action =
51 change.action =
52 @changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
52 @changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
53 end
53 end
54 change
54 change
55 when 'D'
55 when 'D'
56 @changeset.filechanges.detect {|c| c.from_path == change.path} ? nil : change
56 @changeset.filechanges.detect {|c| c.from_path == change.path} ? nil : change
57 else
57 else
58 change
58 change
59 end
59 end
60 end.compact
60 end.compact
61
61
62 tree = { }
62 tree = { }
63 changes.each do |change|
63 changes.each do |change|
64 p = tree
64 p = tree
65 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
65 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
66 path = ''
66 path = ''
67 dirs.each do |dir|
67 dirs.each do |dir|
68 path += '/' + dir
68 path += '/' + dir
69 p[:s] ||= {}
69 p[:s] ||= {}
70 p = p[:s]
70 p = p[:s]
71 p[path] ||= {}
71 p[path] ||= {}
72 p = p[path]
72 p = p[path]
73 end
73 end
74 p[:c] = change
74 p[:c] = change
75 end
75 end
76 render_changes_tree(tree[:s])
76 render_changes_tree(tree[:s])
77 end
77 end
78
78
79 def render_changes_tree(tree)
79 def render_changes_tree(tree)
80 return '' if tree.nil?
80 return '' if tree.nil?
81 output = ''
81 output = ''
82 output << '<ul>'
82 output << '<ul>'
83 tree.keys.sort.each do |file|
83 tree.keys.sort.each do |file|
84 style = 'change'
84 style = 'change'
85 text = File.basename(h(file))
85 text = File.basename(h(file))
86 if s = tree[file][:s]
86 if s = tree[file][:s]
87 style << ' folder'
87 style << ' folder'
88 path_param = to_path_param(@repository.relative_path(file))
88 path_param = to_path_param(@repository.relative_path(file))
89 text = link_to(h(text), :controller => 'repositories',
89 text = link_to(h(text), :controller => 'repositories',
90 :action => 'show',
90 :action => 'show',
91 :id => @project,
91 :id => @project,
92 :repository_id => @repository.identifier_param,
92 :repository_id => @repository.identifier_param,
93 :path => path_param,
93 :path => path_param,
94 :rev => @changeset.identifier)
94 :rev => @changeset.identifier)
95 output << "<li class='#{style}'>#{text}"
95 output << "<li class='#{style}'>#{text}"
96 output << render_changes_tree(s)
96 output << render_changes_tree(s)
97 output << "</li>"
97 output << "</li>"
98 elsif c = tree[file][:c]
98 elsif c = tree[file][:c]
99 style << " change-#{c.action}"
99 style << " change-#{c.action}"
100 path_param = to_path_param(@repository.relative_path(c.path))
100 path_param = to_path_param(@repository.relative_path(c.path))
101 text = link_to(h(text), :controller => 'repositories',
101 text = link_to(h(text), :controller => 'repositories',
102 :action => 'entry',
102 :action => 'entry',
103 :id => @project,
103 :id => @project,
104 :repository_id => @repository.identifier_param,
104 :repository_id => @repository.identifier_param,
105 :path => path_param,
105 :path => path_param,
106 :rev => @changeset.identifier) unless c.action == 'D'
106 :rev => @changeset.identifier) unless c.action == 'D'
107 text << " - #{h(c.revision)}" unless c.revision.blank?
107 text << " - #{h(c.revision)}" unless c.revision.blank?
108 text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
108 text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
109 :action => 'diff',
109 :action => 'diff',
110 :id => @project,
110 :id => @project,
111 :repository_id => @repository.identifier_param,
111 :repository_id => @repository.identifier_param,
112 :path => path_param,
112 :path => path_param,
113 :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
113 :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
114 text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
114 text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
115 output << "<li class='#{style}'>#{text}</li>"
115 output << "<li class='#{style}'>#{text}</li>"
116 end
116 end
117 end
117 end
118 output << '</ul>'
118 output << '</ul>'
119 output.html_safe
119 output.html_safe
120 end
120 end
121
121
122 def repository_field_tags(form, repository)
122 def repository_field_tags(form, repository)
123 method = repository.class.name.demodulize.underscore + "_field_tags"
123 method = repository.class.name.demodulize.underscore + "_field_tags"
124 if repository.is_a?(Repository) &&
124 if repository.is_a?(Repository) &&
125 respond_to?(method) && method != 'repository_field_tags'
125 respond_to?(method) && method != 'repository_field_tags'
126 send(method, form, repository)
126 send(method, form, repository)
127 end
127 end
128 end
128 end
129
129
130 def scm_select_tag(repository)
130 def scm_select_tag(repository)
131 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
131 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
132 Redmine::Scm::Base.all.each do |scm|
132 Redmine::Scm::Base.all.each do |scm|
133 if Setting.enabled_scm.include?(scm) ||
133 if Setting.enabled_scm.include?(scm) ||
134 (repository && repository.class.name.demodulize == scm)
134 (repository && repository.class.name.demodulize == scm)
135 scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
135 scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
136 end
136 end
137 end
137 end
138 select_tag('repository_scm',
138 select_tag('repository_scm',
139 options_for_select(scm_options, repository.class.name.demodulize),
139 options_for_select(scm_options, repository.class.name.demodulize),
140 :disabled => (repository && !repository.new_record?),
140 :disabled => (repository && !repository.new_record?),
141 :data => {:remote => true, :method => 'get'})
141 :data => {:remote => true, :method => 'get'})
142 end
142 end
143
143
144 def with_leading_slash(path)
144 def with_leading_slash(path)
145 path.to_s.starts_with?('/') ? path : "/#{path}"
145 path.to_s.starts_with?('/') ? path : "/#{path}"
146 end
146 end
147
147
148 def without_leading_slash(path)
148 def without_leading_slash(path)
149 path.gsub(%r{^/+}, '')
149 path.gsub(%r{^/+}, '')
150 end
150 end
151
151
152 def subversion_field_tags(form, repository)
152 def subversion_field_tags(form, repository)
153 content_tag('p', form.text_field(:url, :size => 60, :required => true,
153 content_tag('p', form.text_field(:url, :size => 60, :required => true,
154 :disabled => !repository.safe_attribute?('url')) +
154 :disabled => !repository.safe_attribute?('url')) +
155 '<br />'.html_safe +
155 '<br />'.html_safe +
156 '(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
156 '(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
157 content_tag('p', form.text_field(:login, :size => 30)) +
157 content_tag('p', form.text_field(:login, :size => 30)) +
158 content_tag('p', form.password_field(
158 content_tag('p', form.password_field(
159 :password, :size => 30, :name => 'ignore',
159 :password, :size => 30, :name => 'ignore',
160 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
160 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
161 :onfocus => "this.value=''; this.name='repository[password]';",
161 :onfocus => "this.value=''; this.name='repository[password]';",
162 :onchange => "this.name='repository[password]';"))
162 :onchange => "this.name='repository[password]';"))
163 end
163 end
164
164
165 def darcs_field_tags(form, repository)
165 def darcs_field_tags(form, repository)
166 content_tag('p', form.text_field(
166 content_tag('p', form.text_field(
167 :url, :label => l(:field_path_to_repository),
167 :url, :label => l(:field_path_to_repository),
168 :size => 60, :required => true,
168 :size => 60, :required => true,
169 :disabled => !repository.safe_attribute?('url'))) +
169 :disabled => !repository.safe_attribute?('url'))) +
170 content_tag('p', form.select(
170 content_tag('p', form.select(
171 :log_encoding, [nil] + Setting::ENCODINGS,
171 :log_encoding, [nil] + Setting::ENCODINGS,
172 :label => l(:field_commit_logs_encoding), :required => true))
172 :label => l(:field_commit_logs_encoding), :required => true))
173 end
173 end
174
174
175 def mercurial_field_tags(form, repository)
175 def mercurial_field_tags(form, repository)
176 content_tag('p', form.text_field(
176 content_tag('p', form.text_field(
177 :url, :label => l(:field_path_to_repository),
177 :url, :label => l(:field_path_to_repository),
178 :size => 60, :required => true,
178 :size => 60, :required => true,
179 :disabled => !repository.safe_attribute?('url')
179 :disabled => !repository.safe_attribute?('url')
180 ) +
180 ) +
181 '<br />'.html_safe + l(:text_mercurial_repository_note)) +
181 '<br />'.html_safe + l(:text_mercurial_repository_note)) +
182 content_tag('p', form.select(
182 content_tag('p', form.select(
183 :path_encoding, [nil] + Setting::ENCODINGS,
183 :path_encoding, [nil] + Setting::ENCODINGS,
184 :label => l(:field_scm_path_encoding)
184 :label => l(:field_scm_path_encoding)
185 ) +
185 ) +
186 '<br />'.html_safe + l(:text_scm_path_encoding_note))
186 '<br />'.html_safe + l(:text_scm_path_encoding_note))
187 end
187 end
188
188
189 def git_field_tags(form, repository)
189 def git_field_tags(form, repository)
190 content_tag('p', form.text_field(
190 content_tag('p', form.text_field(
191 :url, :label => l(:field_path_to_repository),
191 :url, :label => l(:field_path_to_repository),
192 :size => 60, :required => true,
192 :size => 60, :required => true,
193 :disabled => !repository.safe_attribute?('url')
193 :disabled => !repository.safe_attribute?('url')
194 ) +
194 ) +
195 '<br />'.html_safe +
195 '<br />'.html_safe +
196 l(:text_git_repository_note)) +
196 l(:text_git_repository_note)) +
197 content_tag('p', form.select(
197 content_tag('p', form.select(
198 :path_encoding, [nil] + Setting::ENCODINGS,
198 :path_encoding, [nil] + Setting::ENCODINGS,
199 :label => l(:field_scm_path_encoding)
199 :label => l(:field_scm_path_encoding)
200 ) +
200 ) +
201 '<br />'.html_safe + l(:text_scm_path_encoding_note)) +
201 '<br />'.html_safe + l(:text_scm_path_encoding_note)) +
202 content_tag('p', form.check_box(
202 content_tag('p', form.check_box(
203 :extra_report_last_commit,
203 :extra_report_last_commit,
204 :label => l(:label_git_report_last_commit)
204 :label => l(:label_git_report_last_commit)
205 ))
205 ))
206 end
206 end
207
207
208 def cvs_field_tags(form, repository)
208 def cvs_field_tags(form, repository)
209 content_tag('p', form.text_field(
209 content_tag('p', form.text_field(
210 :root_url,
210 :root_url,
211 :label => l(:field_cvsroot),
211 :label => l(:field_cvsroot),
212 :size => 60, :required => true,
212 :size => 60, :required => true,
213 :disabled => !repository.safe_attribute?('root_url'))) +
213 :disabled => !repository.safe_attribute?('root_url'))) +
214 content_tag('p', form.text_field(
214 content_tag('p', form.text_field(
215 :url,
215 :url,
216 :label => l(:field_cvs_module),
216 :label => l(:field_cvs_module),
217 :size => 30, :required => true,
217 :size => 30, :required => true,
218 :disabled => !repository.safe_attribute?('url'))) +
218 :disabled => !repository.safe_attribute?('url'))) +
219 content_tag('p', form.select(
219 content_tag('p', form.select(
220 :log_encoding, [nil] + Setting::ENCODINGS,
220 :log_encoding, [nil] + Setting::ENCODINGS,
221 :label => l(:field_commit_logs_encoding), :required => true)) +
221 :label => l(:field_commit_logs_encoding), :required => true)) +
222 content_tag('p', form.select(
222 content_tag('p', form.select(
223 :path_encoding, [nil] + Setting::ENCODINGS,
223 :path_encoding, [nil] + Setting::ENCODINGS,
224 :label => l(:field_scm_path_encoding)
224 :label => l(:field_scm_path_encoding)
225 ) +
225 ) +
226 '<br />'.html_safe + l(:text_scm_path_encoding_note))
226 '<br />'.html_safe + l(:text_scm_path_encoding_note))
227 end
227 end
228
228
229 def bazaar_field_tags(form, repository)
229 def bazaar_field_tags(form, repository)
230 content_tag('p', form.text_field(
230 content_tag('p', form.text_field(
231 :url, :label => l(:field_path_to_repository),
231 :url, :label => l(:field_path_to_repository),
232 :size => 60, :required => true,
232 :size => 60, :required => true,
233 :disabled => !repository.safe_attribute?('url'))) +
233 :disabled => !repository.safe_attribute?('url'))) +
234 content_tag('p', form.select(
234 content_tag('p', form.select(
235 :log_encoding, [nil] + Setting::ENCODINGS,
235 :log_encoding, [nil] + Setting::ENCODINGS,
236 :label => l(:field_commit_logs_encoding), :required => true))
236 :label => l(:field_commit_logs_encoding), :required => true))
237 end
237 end
238
238
239 def filesystem_field_tags(form, repository)
239 def filesystem_field_tags(form, repository)
240 content_tag('p', form.text_field(
240 content_tag('p', form.text_field(
241 :url, :label => l(:field_root_directory),
241 :url, :label => l(:field_root_directory),
242 :size => 60, :required => true,
242 :size => 60, :required => true,
243 :disabled => !repository.safe_attribute?('url'))) +
243 :disabled => !repository.safe_attribute?('url'))) +
244 content_tag('p', form.select(
244 content_tag('p', form.select(
245 :path_encoding, [nil] + Setting::ENCODINGS,
245 :path_encoding, [nil] + Setting::ENCODINGS,
246 :label => l(:field_scm_path_encoding)
246 :label => l(:field_scm_path_encoding)
247 ) +
247 ) +
248 '<br />'.html_safe + l(:text_scm_path_encoding_note))
248 '<br />'.html_safe + l(:text_scm_path_encoding_note))
249 end
249 end
250
250
251 def index_commits(commits, heads)
251 def index_commits(commits, heads)
252 return nil if commits.nil? or commits.first.parents.nil?
252 return nil if commits.nil? or commits.first.parents.nil?
253 refs_map = {}
253 refs_map = {}
254 heads.each do |head|
254 heads.each do |head|
255 refs_map[head.scmid] ||= []
255 refs_map[head.scmid] ||= []
256 refs_map[head.scmid] << head
256 refs_map[head.scmid] << head
257 end
257 end
258 commits_by_scmid = {}
258 commits_by_scmid = {}
259 commits.reverse.each_with_index do |commit, commit_index|
259 commits.reverse.each_with_index do |commit, commit_index|
260 commits_by_scmid[commit.scmid] = {
260 commits_by_scmid[commit.scmid] = {
261 :parent_scmids => commit.parents.collect { |parent| parent.scmid },
261 :parent_scmids => commit.parents.collect { |parent| parent.scmid },
262 :rdmid => commit_index,
262 :rdmid => commit_index,
263 :refs => refs_map.include?(commit.scmid) ? refs_map[commit.scmid].join(" ") : nil,
263 :refs => refs_map.include?(commit.scmid) ? refs_map[commit.scmid].join(" ") : nil,
264 :scmid => commit.scmid,
264 :scmid => commit.scmid,
265 :href => block_given? ? yield(commit.scmid) : commit.scmid
265 :href => block_given? ? yield(commit.scmid) : commit.scmid
266 }
266 }
267 end
267 end
268 heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
268 heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
269 space = nil
269 space = nil
270 heads.each do |head|
270 heads.each do |head|
271 if commits_by_scmid.include? head.scmid
271 if commits_by_scmid.include? head.scmid
272 space = index_head((space || -1) + 1, head, commits_by_scmid)
272 space = index_head((space || -1) + 1, head, commits_by_scmid)
273 end
273 end
274 end
274 end
275 # when no head matched anything use first commit
275 # when no head matched anything use first commit
276 space ||= index_head(0, commits.first, commits_by_scmid)
276 space ||= index_head(0, commits.first, commits_by_scmid)
277 return commits_by_scmid, space
277 return commits_by_scmid, space
278 end
278 end
279
279
280 def index_head(space, commit, commits_by_scmid)
280 def index_head(space, commit, commits_by_scmid)
281 stack = [[space, commits_by_scmid[commit.scmid]]]
281 stack = [[space, commits_by_scmid[commit.scmid]]]
282 max_space = space
282 max_space = space
283 until stack.empty?
283 until stack.empty?
284 space, commit = stack.pop
284 space, commit = stack.pop
285 commit[:space] = space if commit[:space].nil?
285 commit[:space] = space if commit[:space].nil?
286 space -= 1
286 space -= 1
287 commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
287 commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
288 parent_commit = commits_by_scmid[parent_scmid]
288 parent_commit = commits_by_scmid[parent_scmid]
289 if parent_commit and parent_commit[:space].nil?
289 if parent_commit and parent_commit[:space].nil?
290 stack.unshift [space += 1, parent_commit]
290 stack.unshift [space += 1, parent_commit]
291 end
291 end
292 end
292 end
293 max_space = space if max_space < space
293 max_space = space if max_space < space
294 end
294 end
295 max_space
295 max_space
296 end
296 end
297 end
297 end
@@ -1,197 +1,197
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module TimelogHelper
20 module TimelogHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def render_timelog_breadcrumb
23 def render_timelog_breadcrumb
24 links = []
24 links = []
25 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
25 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
26 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
26 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
27 if @issue
27 if @issue
28 if @issue.visible?
28 if @issue.visible?
29 links << link_to_issue(@issue, :subject => false)
29 links << link_to_issue(@issue, :subject => false)
30 else
30 else
31 links << "##{@issue.id}"
31 links << "##{@issue.id}"
32 end
32 end
33 end
33 end
34 breadcrumb links
34 breadcrumb links
35 end
35 end
36
36
37 # Returns a collection of activities for a select field. time_entry
37 # Returns a collection of activities for a select field. time_entry
38 # is optional and will be used to check if the selected TimeEntryActivity
38 # is optional and will be used to check if the selected TimeEntryActivity
39 # is active.
39 # is active.
40 def activity_collection_for_select_options(time_entry=nil, project=nil)
40 def activity_collection_for_select_options(time_entry=nil, project=nil)
41 project ||= @project
41 project ||= @project
42 if project.nil?
42 if project.nil?
43 activities = TimeEntryActivity.shared.active
43 activities = TimeEntryActivity.shared.active
44 else
44 else
45 activities = project.activities
45 activities = project.activities
46 end
46 end
47
47
48 collection = []
48 collection = []
49 if time_entry && time_entry.activity && !time_entry.activity.active?
49 if time_entry && time_entry.activity && !time_entry.activity.active?
50 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
50 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
51 else
51 else
52 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
52 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
53 end
53 end
54 activities.each { |a| collection << [a.name, a.id] }
54 activities.each { |a| collection << [a.name, a.id] }
55 collection
55 collection
56 end
56 end
57
57
58 def select_hours(data, criteria, value)
58 def select_hours(data, criteria, value)
59 if value.to_s.empty?
59 if value.to_s.empty?
60 data.select {|row| row[criteria].blank? }
60 data.select {|row| row[criteria].blank? }
61 else
61 else
62 data.select {|row| row[criteria].to_s == value.to_s}
62 data.select {|row| row[criteria].to_s == value.to_s}
63 end
63 end
64 end
64 end
65
65
66 def sum_hours(data)
66 def sum_hours(data)
67 sum = 0
67 sum = 0
68 data.each do |row|
68 data.each do |row|
69 sum += row['hours'].to_f
69 sum += row['hours'].to_f
70 end
70 end
71 sum
71 sum
72 end
72 end
73
73
74 def options_for_period_select(value)
74 def options_for_period_select(value)
75 options_for_select([[l(:label_all_time), 'all'],
75 options_for_select([[l(:label_all_time), 'all'],
76 [l(:label_today), 'today'],
76 [l(:label_today), 'today'],
77 [l(:label_yesterday), 'yesterday'],
77 [l(:label_yesterday), 'yesterday'],
78 [l(:label_this_week), 'current_week'],
78 [l(:label_this_week), 'current_week'],
79 [l(:label_last_week), 'last_week'],
79 [l(:label_last_week), 'last_week'],
80 [l(:label_last_n_weeks, 2), 'last_2_weeks'],
80 [l(:label_last_n_weeks, 2), 'last_2_weeks'],
81 [l(:label_last_n_days, 7), '7_days'],
81 [l(:label_last_n_days, 7), '7_days'],
82 [l(:label_this_month), 'current_month'],
82 [l(:label_this_month), 'current_month'],
83 [l(:label_last_month), 'last_month'],
83 [l(:label_last_month), 'last_month'],
84 [l(:label_last_n_days, 30), '30_days'],
84 [l(:label_last_n_days, 30), '30_days'],
85 [l(:label_this_year), 'current_year']],
85 [l(:label_this_year), 'current_year']],
86 value)
86 value)
87 end
87 end
88
88
89 def entries_to_csv(entries)
89 def entries_to_csv(entries)
90 decimal_separator = l(:general_csv_decimal_separator)
90 decimal_separator = l(:general_csv_decimal_separator)
91 custom_fields = TimeEntryCustomField.find(:all)
91 custom_fields = TimeEntryCustomField.all
92 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
92 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
93 # csv header fields
93 # csv header fields
94 headers = [l(:field_spent_on),
94 headers = [l(:field_spent_on),
95 l(:field_user),
95 l(:field_user),
96 l(:field_activity),
96 l(:field_activity),
97 l(:field_project),
97 l(:field_project),
98 l(:field_issue),
98 l(:field_issue),
99 l(:field_tracker),
99 l(:field_tracker),
100 l(:field_subject),
100 l(:field_subject),
101 l(:field_hours),
101 l(:field_hours),
102 l(:field_comments)
102 l(:field_comments)
103 ]
103 ]
104 # Export custom fields
104 # Export custom fields
105 headers += custom_fields.collect(&:name)
105 headers += custom_fields.collect(&:name)
106
106
107 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
107 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
108 c.to_s,
108 c.to_s,
109 l(:general_csv_encoding) ) }
109 l(:general_csv_encoding) ) }
110 # csv lines
110 # csv lines
111 entries.each do |entry|
111 entries.each do |entry|
112 fields = [format_date(entry.spent_on),
112 fields = [format_date(entry.spent_on),
113 entry.user,
113 entry.user,
114 entry.activity,
114 entry.activity,
115 entry.project,
115 entry.project,
116 (entry.issue ? entry.issue.id : nil),
116 (entry.issue ? entry.issue.id : nil),
117 (entry.issue ? entry.issue.tracker : nil),
117 (entry.issue ? entry.issue.tracker : nil),
118 (entry.issue ? entry.issue.subject : nil),
118 (entry.issue ? entry.issue.subject : nil),
119 entry.hours.to_s.gsub('.', decimal_separator),
119 entry.hours.to_s.gsub('.', decimal_separator),
120 entry.comments
120 entry.comments
121 ]
121 ]
122 fields += custom_fields.collect {|f| show_value(entry.custom_field_values.detect {|v| v.custom_field_id == f.id}) }
122 fields += custom_fields.collect {|f| show_value(entry.custom_field_values.detect {|v| v.custom_field_id == f.id}) }
123
123
124 csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(
124 csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(
125 c.to_s,
125 c.to_s,
126 l(:general_csv_encoding) ) }
126 l(:general_csv_encoding) ) }
127 end
127 end
128 end
128 end
129 export
129 export
130 end
130 end
131
131
132 def format_criteria_value(criteria_options, value)
132 def format_criteria_value(criteria_options, value)
133 if value.blank?
133 if value.blank?
134 "[#{l(:label_none)}]"
134 "[#{l(:label_none)}]"
135 elsif k = criteria_options[:klass]
135 elsif k = criteria_options[:klass]
136 obj = k.find_by_id(value.to_i)
136 obj = k.find_by_id(value.to_i)
137 if obj.is_a?(Issue)
137 if obj.is_a?(Issue)
138 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
138 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
139 else
139 else
140 obj
140 obj
141 end
141 end
142 else
142 else
143 format_value(value, criteria_options[:format])
143 format_value(value, criteria_options[:format])
144 end
144 end
145 end
145 end
146
146
147 def report_to_csv(report)
147 def report_to_csv(report)
148 decimal_separator = l(:general_csv_decimal_separator)
148 decimal_separator = l(:general_csv_decimal_separator)
149 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
149 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
150 # Column headers
150 # Column headers
151 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
151 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
152 headers += report.periods
152 headers += report.periods
153 headers << l(:label_total)
153 headers << l(:label_total)
154 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
154 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
155 c.to_s,
155 c.to_s,
156 l(:general_csv_encoding) ) }
156 l(:general_csv_encoding) ) }
157 # Content
157 # Content
158 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
158 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
159 # Total row
159 # Total row
160 str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding))
160 str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding))
161 row = [ str_total ] + [''] * (report.criteria.size - 1)
161 row = [ str_total ] + [''] * (report.criteria.size - 1)
162 total = 0
162 total = 0
163 report.periods.each do |period|
163 report.periods.each do |period|
164 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
164 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
165 total += sum
165 total += sum
166 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
166 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
167 end
167 end
168 row << ("%.2f" % total).gsub('.',decimal_separator)
168 row << ("%.2f" % total).gsub('.',decimal_separator)
169 csv << row
169 csv << row
170 end
170 end
171 export
171 export
172 end
172 end
173
173
174 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
174 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
175 decimal_separator = l(:general_csv_decimal_separator)
175 decimal_separator = l(:general_csv_decimal_separator)
176 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
176 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
177 hours_for_value = select_hours(hours, criteria[level], value)
177 hours_for_value = select_hours(hours, criteria[level], value)
178 next if hours_for_value.empty?
178 next if hours_for_value.empty?
179 row = [''] * level
179 row = [''] * level
180 row << Redmine::CodesetUtil.from_utf8(
180 row << Redmine::CodesetUtil.from_utf8(
181 format_criteria_value(available_criteria[criteria[level]], value).to_s,
181 format_criteria_value(available_criteria[criteria[level]], value).to_s,
182 l(:general_csv_encoding) )
182 l(:general_csv_encoding) )
183 row += [''] * (criteria.length - level - 1)
183 row += [''] * (criteria.length - level - 1)
184 total = 0
184 total = 0
185 periods.each do |period|
185 periods.each do |period|
186 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
186 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
187 total += sum
187 total += sum
188 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
188 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
189 end
189 end
190 row << ("%.2f" % total).gsub('.',decimal_separator)
190 row << ("%.2f" % total).gsub('.',decimal_separator)
191 csv << row
191 csv << row
192 if criteria.length > level + 1
192 if criteria.length > level + 1
193 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
193 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
194 end
194 end
195 end
195 end
196 end
196 end
197 end
197 end
@@ -1,320 +1,322
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 include Redmine::SubclassFactory
19 include Redmine::SubclassFactory
20
20
21 has_many :custom_values, :dependent => :delete_all
21 has_many :custom_values, :dependent => :delete_all
22 acts_as_list :scope => 'type = \'#{self.class}\''
22 acts_as_list :scope => 'type = \'#{self.class}\''
23 serialize :possible_values
23 serialize :possible_values
24
24
25 validates_presence_of :name, :field_format
25 validates_presence_of :name, :field_format
26 validates_uniqueness_of :name, :scope => :type
26 validates_uniqueness_of :name, :scope => :type
27 validates_length_of :name, :maximum => 30
27 validates_length_of :name, :maximum => 30
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29
29
30 validate :validate_custom_field
30 validate :validate_custom_field
31 before_validation :set_searchable
31 before_validation :set_searchable
32
32
33 scope :sorted, order("#{table_name}.position ASC")
34
33 CUSTOM_FIELDS_TABS = [
35 CUSTOM_FIELDS_TABS = [
34 {:name => 'IssueCustomField', :partial => 'custom_fields/index',
36 {:name => 'IssueCustomField', :partial => 'custom_fields/index',
35 :label => :label_issue_plural},
37 :label => :label_issue_plural},
36 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
38 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
37 :label => :label_spent_time},
39 :label => :label_spent_time},
38 {:name => 'ProjectCustomField', :partial => 'custom_fields/index',
40 {:name => 'ProjectCustomField', :partial => 'custom_fields/index',
39 :label => :label_project_plural},
41 :label => :label_project_plural},
40 {:name => 'VersionCustomField', :partial => 'custom_fields/index',
42 {:name => 'VersionCustomField', :partial => 'custom_fields/index',
41 :label => :label_version_plural},
43 :label => :label_version_plural},
42 {:name => 'UserCustomField', :partial => 'custom_fields/index',
44 {:name => 'UserCustomField', :partial => 'custom_fields/index',
43 :label => :label_user_plural},
45 :label => :label_user_plural},
44 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
46 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
45 :label => :label_group_plural},
47 :label => :label_group_plural},
46 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
48 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
47 :label => TimeEntryActivity::OptionName},
49 :label => TimeEntryActivity::OptionName},
48 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
50 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
49 :label => IssuePriority::OptionName},
51 :label => IssuePriority::OptionName},
50 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
52 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
51 :label => DocumentCategory::OptionName}
53 :label => DocumentCategory::OptionName}
52 ]
54 ]
53
55
54 CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]}
56 CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]}
55
57
56 def field_format=(arg)
58 def field_format=(arg)
57 # cannot change format of a saved custom field
59 # cannot change format of a saved custom field
58 super if new_record?
60 super if new_record?
59 end
61 end
60
62
61 def set_searchable
63 def set_searchable
62 # make sure these fields are not searchable
64 # make sure these fields are not searchable
63 self.searchable = false if %w(int float date bool).include?(field_format)
65 self.searchable = false if %w(int float date bool).include?(field_format)
64 # make sure only these fields can have multiple values
66 # make sure only these fields can have multiple values
65 self.multiple = false unless %w(list user version).include?(field_format)
67 self.multiple = false unless %w(list user version).include?(field_format)
66 true
68 true
67 end
69 end
68
70
69 def validate_custom_field
71 def validate_custom_field
70 if self.field_format == "list"
72 if self.field_format == "list"
71 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
73 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
72 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
74 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
73 end
75 end
74
76
75 if regexp.present?
77 if regexp.present?
76 begin
78 begin
77 Regexp.new(regexp)
79 Regexp.new(regexp)
78 rescue
80 rescue
79 errors.add(:regexp, :invalid)
81 errors.add(:regexp, :invalid)
80 end
82 end
81 end
83 end
82
84
83 if default_value.present? && !valid_field_value?(default_value)
85 if default_value.present? && !valid_field_value?(default_value)
84 errors.add(:default_value, :invalid)
86 errors.add(:default_value, :invalid)
85 end
87 end
86 end
88 end
87
89
88 def possible_values_options(obj=nil)
90 def possible_values_options(obj=nil)
89 case field_format
91 case field_format
90 when 'user', 'version'
92 when 'user', 'version'
91 if obj.respond_to?(:project) && obj.project
93 if obj.respond_to?(:project) && obj.project
92 case field_format
94 case field_format
93 when 'user'
95 when 'user'
94 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
96 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
95 when 'version'
97 when 'version'
96 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
98 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
97 end
99 end
98 elsif obj.is_a?(Array)
100 elsif obj.is_a?(Array)
99 obj.collect {|o| possible_values_options(o)}.reduce(:&)
101 obj.collect {|o| possible_values_options(o)}.reduce(:&)
100 else
102 else
101 []
103 []
102 end
104 end
103 when 'bool'
105 when 'bool'
104 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
106 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
105 else
107 else
106 possible_values || []
108 possible_values || []
107 end
109 end
108 end
110 end
109
111
110 def possible_values(obj=nil)
112 def possible_values(obj=nil)
111 case field_format
113 case field_format
112 when 'user', 'version'
114 when 'user', 'version'
113 possible_values_options(obj).collect(&:last)
115 possible_values_options(obj).collect(&:last)
114 when 'bool'
116 when 'bool'
115 ['1', '0']
117 ['1', '0']
116 else
118 else
117 values = super()
119 values = super()
118 if values.is_a?(Array)
120 if values.is_a?(Array)
119 values.each do |value|
121 values.each do |value|
120 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
122 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
121 end
123 end
122 end
124 end
123 values || []
125 values || []
124 end
126 end
125 end
127 end
126
128
127 # Makes possible_values accept a multiline string
129 # Makes possible_values accept a multiline string
128 def possible_values=(arg)
130 def possible_values=(arg)
129 if arg.is_a?(Array)
131 if arg.is_a?(Array)
130 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
132 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
131 else
133 else
132 self.possible_values = arg.to_s.split(/[\n\r]+/)
134 self.possible_values = arg.to_s.split(/[\n\r]+/)
133 end
135 end
134 end
136 end
135
137
136 def cast_value(value)
138 def cast_value(value)
137 casted = nil
139 casted = nil
138 unless value.blank?
140 unless value.blank?
139 case field_format
141 case field_format
140 when 'string', 'text', 'list'
142 when 'string', 'text', 'list'
141 casted = value
143 casted = value
142 when 'date'
144 when 'date'
143 casted = begin; value.to_date; rescue; nil end
145 casted = begin; value.to_date; rescue; nil end
144 when 'bool'
146 when 'bool'
145 casted = (value == '1' ? true : false)
147 casted = (value == '1' ? true : false)
146 when 'int'
148 when 'int'
147 casted = value.to_i
149 casted = value.to_i
148 when 'float'
150 when 'float'
149 casted = value.to_f
151 casted = value.to_f
150 when 'user', 'version'
152 when 'user', 'version'
151 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
153 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
152 end
154 end
153 end
155 end
154 casted
156 casted
155 end
157 end
156
158
157 def value_from_keyword(keyword, customized)
159 def value_from_keyword(keyword, customized)
158 possible_values_options = possible_values_options(customized)
160 possible_values_options = possible_values_options(customized)
159 if possible_values_options.present?
161 if possible_values_options.present?
160 keyword = keyword.to_s.downcase
162 keyword = keyword.to_s.downcase
161 possible_values_options.detect {|text, id| text.downcase == keyword}.try(:last)
163 possible_values_options.detect {|text, id| text.downcase == keyword}.try(:last)
162 else
164 else
163 keyword
165 keyword
164 end
166 end
165 end
167 end
166
168
167 # Returns a ORDER BY clause that can used to sort customized
169 # Returns a ORDER BY clause that can used to sort customized
168 # objects by their value of the custom field.
170 # objects by their value of the custom field.
169 # Returns nil if the custom field can not be used for sorting.
171 # Returns nil if the custom field can not be used for sorting.
170 def order_statement
172 def order_statement
171 return nil if multiple?
173 return nil if multiple?
172 case field_format
174 case field_format
173 when 'string', 'text', 'list', 'date', 'bool'
175 when 'string', 'text', 'list', 'date', 'bool'
174 # COALESCE is here to make sure that blank and NULL values are sorted equally
176 # COALESCE is here to make sure that blank and NULL values are sorted equally
175 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
177 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
176 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
178 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
177 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
179 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
178 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
180 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
179 when 'int', 'float'
181 when 'int', 'float'
180 # Make the database cast values into numeric
182 # Make the database cast values into numeric
181 # Postgresql will raise an error if a value can not be casted!
183 # Postgresql will raise an error if a value can not be casted!
182 # CustomValue validations should ensure that it doesn't occur
184 # CustomValue validations should ensure that it doesn't occur
183 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
185 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
184 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
186 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
185 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
187 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
186 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
188 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
187 when 'user', 'version'
189 when 'user', 'version'
188 value_class.fields_for_order_statement(value_join_alias)
190 value_class.fields_for_order_statement(value_join_alias)
189 else
191 else
190 nil
192 nil
191 end
193 end
192 end
194 end
193
195
194 # Returns a GROUP BY clause that can used to group by custom value
196 # Returns a GROUP BY clause that can used to group by custom value
195 # Returns nil if the custom field can not be used for grouping.
197 # Returns nil if the custom field can not be used for grouping.
196 def group_statement
198 def group_statement
197 return nil if multiple?
199 return nil if multiple?
198 case field_format
200 case field_format
199 when 'list', 'date', 'bool', 'int'
201 when 'list', 'date', 'bool', 'int'
200 order_statement
202 order_statement
201 when 'user', 'version'
203 when 'user', 'version'
202 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
204 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
203 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
205 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
204 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
206 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
205 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
207 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
206 else
208 else
207 nil
209 nil
208 end
210 end
209 end
211 end
210
212
211 def join_for_order_statement
213 def join_for_order_statement
212 case field_format
214 case field_format
213 when 'user', 'version'
215 when 'user', 'version'
214 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
216 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
215 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
217 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
216 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
218 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
217 " AND #{join_alias}.custom_field_id = #{id}" +
219 " AND #{join_alias}.custom_field_id = #{id}" +
218 " AND #{join_alias}.value <> ''" +
220 " AND #{join_alias}.value <> ''" +
219 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
221 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
220 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
222 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
221 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
223 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
222 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
224 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
223 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
225 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
224 " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
226 " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
225 else
227 else
226 nil
228 nil
227 end
229 end
228 end
230 end
229
231
230 def join_alias
232 def join_alias
231 "cf_#{id}"
233 "cf_#{id}"
232 end
234 end
233
235
234 def value_join_alias
236 def value_join_alias
235 join_alias + "_" + field_format
237 join_alias + "_" + field_format
236 end
238 end
237
239
238 def <=>(field)
240 def <=>(field)
239 position <=> field.position
241 position <=> field.position
240 end
242 end
241
243
242 # Returns the class that values represent
244 # Returns the class that values represent
243 def value_class
245 def value_class
244 case field_format
246 case field_format
245 when 'user', 'version'
247 when 'user', 'version'
246 field_format.classify.constantize
248 field_format.classify.constantize
247 else
249 else
248 nil
250 nil
249 end
251 end
250 end
252 end
251
253
252 def self.customized_class
254 def self.customized_class
253 self.name =~ /^(.+)CustomField$/
255 self.name =~ /^(.+)CustomField$/
254 begin; $1.constantize; rescue nil; end
256 begin; $1.constantize; rescue nil; end
255 end
257 end
256
258
257 # to move in project_custom_field
259 # to move in project_custom_field
258 def self.for_all
260 def self.for_all
259 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
261 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
260 end
262 end
261
263
262 def type_name
264 def type_name
263 nil
265 nil
264 end
266 end
265
267
266 # Returns the error messages for the given value
268 # Returns the error messages for the given value
267 # or an empty array if value is a valid value for the custom field
269 # or an empty array if value is a valid value for the custom field
268 def validate_field_value(value)
270 def validate_field_value(value)
269 errs = []
271 errs = []
270 if value.is_a?(Array)
272 if value.is_a?(Array)
271 if !multiple?
273 if !multiple?
272 errs << ::I18n.t('activerecord.errors.messages.invalid')
274 errs << ::I18n.t('activerecord.errors.messages.invalid')
273 end
275 end
274 if is_required? && value.detect(&:present?).nil?
276 if is_required? && value.detect(&:present?).nil?
275 errs << ::I18n.t('activerecord.errors.messages.blank')
277 errs << ::I18n.t('activerecord.errors.messages.blank')
276 end
278 end
277 value.each {|v| errs += validate_field_value_format(v)}
279 value.each {|v| errs += validate_field_value_format(v)}
278 else
280 else
279 if is_required? && value.blank?
281 if is_required? && value.blank?
280 errs << ::I18n.t('activerecord.errors.messages.blank')
282 errs << ::I18n.t('activerecord.errors.messages.blank')
281 end
283 end
282 errs += validate_field_value_format(value)
284 errs += validate_field_value_format(value)
283 end
285 end
284 errs
286 errs
285 end
287 end
286
288
287 # Returns true if value is a valid value for the custom field
289 # Returns true if value is a valid value for the custom field
288 def valid_field_value?(value)
290 def valid_field_value?(value)
289 validate_field_value(value).empty?
291 validate_field_value(value).empty?
290 end
292 end
291
293
292 def format_in?(*args)
294 def format_in?(*args)
293 args.include?(field_format)
295 args.include?(field_format)
294 end
296 end
295
297
296 protected
298 protected
297
299
298 # Returns the error message for the given value regarding its format
300 # Returns the error message for the given value regarding its format
299 def validate_field_value_format(value)
301 def validate_field_value_format(value)
300 errs = []
302 errs = []
301 if value.present?
303 if value.present?
302 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
304 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
303 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
305 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
304 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
306 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
305
307
306 # Format specific validations
308 # Format specific validations
307 case field_format
309 case field_format
308 when 'int'
310 when 'int'
309 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
311 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
310 when 'float'
312 when 'float'
311 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
313 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
312 when 'date'
314 when 'date'
313 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
315 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
314 when 'list'
316 when 'list'
315 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
317 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
316 end
318 end
317 end
319 end
318 errs
320 errs
319 end
321 end
320 end
322 end
General Comments 0
You need to be logged in to leave comments. Login now