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