##// END OF EJS Templates
Performance improvement on calendar and gantt (about 45% on gantt for large number of issues)....
Jean-Philippe Lang -
r783:20b4e226fe06
parent child
Show More
@@ -1,628 +1,629
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 require 'csv'
19 19
20 20 class ProjectsController < ApplicationController
21 21 layout 'base'
22 22 before_filter :find_project, :except => [ :index, :list, :add ]
23 23 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
24 24 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
25 25 accept_key_auth :activity, :calendar
26 26
27 27 cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
28 28 cache_sweeper :issue_sweeper, :only => [ :add_issue ]
29 29 cache_sweeper :version_sweeper, :only => [ :add_version ]
30 30
31 31 helper :sort
32 32 include SortHelper
33 33 helper :custom_fields
34 34 include CustomFieldsHelper
35 35 helper :ifpdf
36 36 include IfpdfHelper
37 helper :issues
37 38 helper IssuesHelper
38 39 helper :queries
39 40 include QueriesHelper
40 41 helper :repositories
41 42 include RepositoriesHelper
42 43 include ProjectsHelper
43 44
44 45 def index
45 46 list
46 47 render :action => 'list' unless request.xhr?
47 48 end
48 49
49 50 # Lists visible projects
50 51 def list
51 52 projects = Project.find :all,
52 53 :conditions => Project.visible_by(logged_in_user),
53 54 :include => :parent
54 55 @project_tree = projects.group_by {|p| p.parent || p}
55 56 @project_tree.each_key {|p| @project_tree[p] -= [p]}
56 57 end
57 58
58 59 # Add a new project
59 60 def add
60 61 @custom_fields = IssueCustomField.find(:all)
61 62 @root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}")
62 63 @project = Project.new(params[:project])
63 64 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
64 65 if request.get?
65 66 @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
66 67 else
67 68 @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
68 69 @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
69 70 @project.custom_values = @custom_values
70 71 if @project.save
71 72 @project.enabled_module_names = params[:enabled_modules]
72 73 flash[:notice] = l(:notice_successful_create)
73 74 redirect_to :controller => 'admin', :action => 'projects'
74 75 end
75 76 end
76 77 end
77 78
78 79 # Show @project
79 80 def show
80 81 @custom_values = @project.custom_values.find(:all, :include => :custom_field)
81 82 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
82 83 @subprojects = @project.active_children
83 84 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
84 85 @trackers = Tracker.find(:all, :order => 'position')
85 86 @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
86 87 @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
87 88 @total_hours = @project.time_entries.sum(:hours)
88 89 @key = User.current.rss_key
89 90 end
90 91
91 92 def settings
92 93 @root_projects = Project::find(:all, :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id])
93 94 @custom_fields = IssueCustomField.find(:all)
94 95 @issue_category ||= IssueCategory.new
95 96 @member ||= @project.members.new
96 97 @custom_values ||= ProjectCustomField.find(:all).collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
97 98 @repository ||= @project.repository
98 99 @wiki ||= @project.wiki
99 100 end
100 101
101 102 # Edit @project
102 103 def edit
103 104 if request.post?
104 105 @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
105 106 if params[:custom_fields]
106 107 @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
107 108 @project.custom_values = @custom_values
108 109 end
109 110 @project.attributes = params[:project]
110 111 if @project.save
111 112 flash[:notice] = l(:notice_successful_update)
112 113 redirect_to :action => 'settings', :id => @project
113 114 else
114 115 settings
115 116 render :action => 'settings'
116 117 end
117 118 end
118 119 end
119 120
120 121 def modules
121 122 @project.enabled_module_names = params[:enabled_modules]
122 123 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
123 124 end
124 125
125 126 def archive
126 127 @project.archive if request.post? && @project.active?
127 128 redirect_to :controller => 'admin', :action => 'projects'
128 129 end
129 130
130 131 def unarchive
131 132 @project.unarchive if request.post? && !@project.active?
132 133 redirect_to :controller => 'admin', :action => 'projects'
133 134 end
134 135
135 136 # Delete @project
136 137 def destroy
137 138 @project_to_destroy = @project
138 139 if request.post? and params[:confirm]
139 140 @project_to_destroy.destroy
140 141 redirect_to :controller => 'admin', :action => 'projects'
141 142 end
142 143 # hide project in layout
143 144 @project = nil
144 145 end
145 146
146 147 # Add a new issue category to @project
147 148 def add_issue_category
148 149 @category = @project.issue_categories.build(params[:category])
149 150 if request.post? and @category.save
150 151 respond_to do |format|
151 152 format.html do
152 153 flash[:notice] = l(:notice_successful_create)
153 154 redirect_to :action => 'settings', :tab => 'categories', :id => @project
154 155 end
155 156 format.js do
156 157 # IE doesn't support the replace_html rjs method for select box options
157 158 render(:update) {|page| page.replace "issue_category_id",
158 159 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
159 160 }
160 161 end
161 162 end
162 163 end
163 164 end
164 165
165 166 # Add a new version to @project
166 167 def add_version
167 168 @version = @project.versions.build(params[:version])
168 169 if request.post? and @version.save
169 170 flash[:notice] = l(:notice_successful_create)
170 171 redirect_to :action => 'settings', :tab => 'versions', :id => @project
171 172 end
172 173 end
173 174
174 175 # Add a new document to @project
175 176 def add_document
176 177 @categories = Enumeration::get_values('DCAT')
177 178 @document = @project.documents.build(params[:document])
178 179 if request.post? and @document.save
179 180 # Save the attachments
180 181 params[:attachments].each { |a|
181 182 Attachment.create(:container => @document, :file => a, :author => logged_in_user) unless a.size == 0
182 183 } if params[:attachments] and params[:attachments].is_a? Array
183 184 flash[:notice] = l(:notice_successful_create)
184 185 Mailer.deliver_document_add(@document) if Setting.notified_events.include?('document_added')
185 186 redirect_to :action => 'list_documents', :id => @project
186 187 end
187 188 end
188 189
189 190 # Show documents list of @project
190 191 def list_documents
191 192 @documents = @project.documents.find :all, :include => :category
192 193 end
193 194
194 195 # Add a new issue to @project
195 196 def add_issue
196 197 @tracker = Tracker.find(params[:tracker_id])
197 198 @priorities = Enumeration::get_values('IPRI')
198 199
199 200 default_status = IssueStatus.default
200 201 unless default_status
201 202 flash.now[:error] = 'No default issue status defined. Please check your configuration.'
202 203 render :nothing => true, :layout => true
203 204 return
204 205 end
205 206 @issue = Issue.new(:project => @project, :tracker => @tracker)
206 207 @issue.status = default_status
207 208 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker))if logged_in_user
208 209 if request.get?
209 210 @issue.start_date = Date.today
210 211 @custom_values = @project.custom_fields_for_issues(@tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) }
211 212 else
212 213 @issue.attributes = params[:issue]
213 214
214 215 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
215 216 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
216 217
217 218 @issue.author_id = self.logged_in_user.id if self.logged_in_user
218 219 # Multiple file upload
219 220 @attachments = []
220 221 params[:attachments].each { |a|
221 222 @attachments << Attachment.new(:container => @issue, :file => a, :author => logged_in_user) unless a.size == 0
222 223 } if params[:attachments] and params[:attachments].is_a? Array
223 224 @custom_values = @project.custom_fields_for_issues(@tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
224 225 @issue.custom_values = @custom_values
225 226 if @issue.save
226 227 @attachments.each(&:save)
227 228 flash[:notice] = l(:notice_successful_create)
228 229 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
229 230 redirect_to :action => 'list_issues', :id => @project
230 231 end
231 232 end
232 233 end
233 234
234 235 # Show filtered/sorted issues list of @project
235 236 def list_issues
236 237 sort_init "#{Issue.table_name}.id", "desc"
237 238 sort_update
238 239
239 240 retrieve_query
240 241
241 242 @results_per_page_options = [ 15, 25, 50, 100 ]
242 243 if params[:per_page] and @results_per_page_options.include? params[:per_page].to_i
243 244 @results_per_page = params[:per_page].to_i
244 245 session[:results_per_page] = @results_per_page
245 246 else
246 247 @results_per_page = session[:results_per_page] || 25
247 248 end
248 249
249 250 if @query.valid?
250 251 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
251 252 @issue_pages = Paginator.new self, @issue_count, @results_per_page, params['page']
252 253 @issues = Issue.find :all, :order => sort_clause,
253 254 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category ],
254 255 :conditions => @query.statement,
255 256 :limit => @issue_pages.items_per_page,
256 257 :offset => @issue_pages.current.offset
257 258 end
258 259
259 260 render :layout => false if request.xhr?
260 261 end
261 262
262 263 # Export filtered/sorted issues list to CSV
263 264 def export_issues_csv
264 265 sort_init "#{Issue.table_name}.id", "desc"
265 266 sort_update
266 267
267 268 retrieve_query
268 269 render :action => 'list_issues' and return unless @query.valid?
269 270
270 271 @issues = Issue.find :all, :order => sort_clause,
271 272 :include => [ :assigned_to, :author, :status, :tracker, :priority, :project, {:custom_values => :custom_field} ],
272 273 :conditions => @query.statement,
273 274 :limit => Setting.issues_export_limit.to_i
274 275
275 276 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
276 277 export = StringIO.new
277 278 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
278 279 # csv header fields
279 280 headers = [ "#", l(:field_status),
280 281 l(:field_project),
281 282 l(:field_tracker),
282 283 l(:field_priority),
283 284 l(:field_subject),
284 285 l(:field_assigned_to),
285 286 l(:field_author),
286 287 l(:field_start_date),
287 288 l(:field_due_date),
288 289 l(:field_done_ratio),
289 290 l(:field_created_on),
290 291 l(:field_updated_on)
291 292 ]
292 293 for custom_field in @project.all_custom_fields
293 294 headers << custom_field.name
294 295 end
295 296 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
296 297 # csv lines
297 298 @issues.each do |issue|
298 299 fields = [issue.id, issue.status.name,
299 300 issue.project.name,
300 301 issue.tracker.name,
301 302 issue.priority.name,
302 303 issue.subject,
303 304 (issue.assigned_to ? issue.assigned_to.name : ""),
304 305 issue.author.name,
305 306 issue.start_date ? l_date(issue.start_date) : nil,
306 307 issue.due_date ? l_date(issue.due_date) : nil,
307 308 issue.done_ratio,
308 309 l_datetime(issue.created_on),
309 310 l_datetime(issue.updated_on)
310 311 ]
311 312 for custom_field in @project.all_custom_fields
312 313 fields << (show_value issue.custom_value_for(custom_field))
313 314 end
314 315 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
315 316 end
316 317 end
317 318 export.rewind
318 319 send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
319 320 end
320 321
321 322 # Export filtered/sorted issues to PDF
322 323 def export_issues_pdf
323 324 sort_init "#{Issue.table_name}.id", "desc"
324 325 sort_update
325 326
326 327 retrieve_query
327 328 render :action => 'list_issues' and return unless @query.valid?
328 329
329 330 @issues = Issue.find :all, :order => sort_clause,
330 331 :include => [ :author, :status, :tracker, :priority, :project ],
331 332 :conditions => @query.statement,
332 333 :limit => Setting.issues_export_limit.to_i
333 334
334 335 @options_for_rfpdf ||= {}
335 336 @options_for_rfpdf[:file_name] = "export.pdf"
336 337 render :layout => false
337 338 end
338 339
339 340 def move_issues
340 341 @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
341 342 redirect_to :action => 'list_issues', :id => @project and return unless @issues
342 343 @projects = []
343 344 # find projects to which the user is allowed to move the issue
344 345 User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:controller => 'projects', :action => 'move_issues')}
345 346 # issue can be moved to any tracker
346 347 @trackers = Tracker.find(:all)
347 348 if request.post? and params[:new_project_id] and params[:new_tracker_id]
348 349 new_project = Project.find_by_id(params[:new_project_id])
349 350 new_tracker = Tracker.find_by_id(params[:new_tracker_id])
350 351 @issues.each do |i|
351 352 if new_project && i.project_id != new_project.id
352 353 # issue is moved to another project
353 354 i.category = nil
354 355 i.fixed_version = nil
355 356 # delete issue relations
356 357 i.relations_from.clear
357 358 i.relations_to.clear
358 359 i.project = new_project
359 360 end
360 361 if new_tracker
361 362 i.tracker = new_tracker
362 363 end
363 364 i.save
364 365 end
365 366 flash[:notice] = l(:notice_successful_update)
366 367 redirect_to :action => 'list_issues', :id => @project
367 368 end
368 369 end
369 370
370 371 # Add a news to @project
371 372 def add_news
372 373 @news = News.new(:project => @project)
373 374 if request.post?
374 375 @news.attributes = params[:news]
375 376 @news.author_id = self.logged_in_user.id if self.logged_in_user
376 377 if @news.save
377 378 flash[:notice] = l(:notice_successful_create)
378 379 Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
379 380 redirect_to :action => 'list_news', :id => @project
380 381 end
381 382 end
382 383 end
383 384
384 385 # Show news list of @project
385 386 def list_news
386 387 @news_pages, @newss = paginate :news, :per_page => 10, :conditions => ["project_id=?", @project.id], :include => :author, :order => "#{News.table_name}.created_on DESC"
387 388
388 389 respond_to do |format|
389 390 format.html { render :layout => false if request.xhr? }
390 391 format.atom { render_feed(@newss, :title => "#{@project.name}: #{l(:label_news_plural)}") }
391 392 end
392 393 end
393 394
394 395 def add_file
395 396 if request.post?
396 397 @version = @project.versions.find_by_id(params[:version_id])
397 398 # Save the attachments
398 399 @attachments = []
399 400 params[:attachments].each { |file|
400 401 next unless file.size > 0
401 402 a = Attachment.create(:container => @version, :file => file, :author => logged_in_user)
402 403 @attachments << a unless a.new_record?
403 404 } if params[:attachments] and params[:attachments].is_a? Array
404 405 Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? && Setting.notified_events.include?('file_added')
405 406 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
406 407 end
407 408 @versions = @project.versions.sort
408 409 end
409 410
410 411 def list_files
411 412 @versions = @project.versions.sort
412 413 end
413 414
414 415 # Show changelog for @project
415 416 def changelog
416 417 @trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
417 418 retrieve_selected_tracker_ids(@trackers)
418 419 @versions = @project.versions.sort
419 420 end
420 421
421 422 def roadmap
422 423 @trackers = Tracker.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position')
423 424 retrieve_selected_tracker_ids(@trackers)
424 425 @versions = @project.versions.sort
425 426 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
426 427 end
427 428
428 429 def activity
429 430 if params[:year] and params[:year].to_i > 1900
430 431 @year = params[:year].to_i
431 432 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
432 433 @month = params[:month].to_i
433 434 end
434 435 end
435 436 @year ||= Date.today.year
436 437 @month ||= Date.today.month
437 438
438 439 case params[:format]
439 440 when 'atom'
440 441 # 30 last days
441 442 @date_from = Date.today - 30
442 443 @date_to = Date.today + 1
443 444 else
444 445 # current month
445 446 @date_from = Date.civil(@year, @month, 1)
446 447 @date_to = @date_from >> 1
447 448 end
448 449
449 450 @event_types = %w(issues news files documents wiki_pages changesets)
450 451 @event_types.delete('wiki_pages') unless @project.wiki
451 452 @event_types.delete('changesets') unless @project.repository
452 453 # only show what the user is allowed to view
453 454 @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
454 455
455 456 @scope = @event_types.select {|t| params["show_#{t}"]}
456 457 # default events if none is specified in parameters
457 458 @scope = (@event_types - %w(wiki_pages))if @scope.empty?
458 459
459 460 @events = []
460 461
461 462 if @scope.include?('issues')
462 463 @events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] )
463 464 end
464 465
465 466 if @scope.include?('news')
466 467 @events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author )
467 468 end
468 469
469 470 if @scope.include?('files')
470 471 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
471 472 end
472 473
473 474 if @scope.include?('documents')
474 475 @events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] )
475 476 @events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
476 477 end
477 478
478 479 if @scope.include?('wiki_pages')
479 480 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
480 481 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
481 482 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
482 483 "#{WikiContent.versioned_table_name}.id"
483 484 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
484 485 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
485 486 conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
486 487 @project.id, @date_from, @date_to]
487 488
488 489 @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
489 490 end
490 491
491 492 if @scope.include?('changesets')
492 493 @events += @project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
493 494 end
494 495
495 496 @events_by_day = @events.group_by(&:event_date)
496 497
497 498 respond_to do |format|
498 499 format.html { render :layout => false if request.xhr? }
499 500 format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") }
500 501 end
501 502 end
502 503
503 504 def calendar
504 505 @trackers = Tracker.find(:all, :order => 'position')
505 506 retrieve_selected_tracker_ids(@trackers)
506 507
507 508 if params[:year] and params[:year].to_i > 1900
508 509 @year = params[:year].to_i
509 510 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
510 511 @month = params[:month].to_i
511 512 end
512 513 end
513 514 @year ||= Date.today.year
514 515 @month ||= Date.today.month
515 516
516 517 @date_from = Date.civil(@year, @month, 1)
517 518 @date_to = (@date_from >> 1)-1
518 519 # start on monday
519 520 @date_from = @date_from - (@date_from.cwday-1)
520 521 # finish on sunday
521 522 @date_to = @date_to + (7-@date_to.cwday)
522 523
523 524 @events = []
524 525 @project.issues_with_subprojects(params[:with_subprojects]) do
525 526 @events += Issue.find(:all,
526 527 :include => [:tracker, :status, :assigned_to, :priority, :project],
527 528 :conditions => ["((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?)) and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')})", @date_from, @date_to, @date_from, @date_to]
528 529 ) unless @selected_tracker_ids.empty?
529 530 end
530 531 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
531 532
532 533 @ending_events_by_days = @events.group_by {|event| event.due_date}
533 534 @starting_events_by_days = @events.group_by {|event| event.start_date}
534 535
535 536 render :layout => false if request.xhr?
536 537 end
537 538
538 539 def gantt
539 540 @trackers = Tracker.find(:all, :order => 'position')
540 541 retrieve_selected_tracker_ids(@trackers)
541 542
542 543 if params[:year] and params[:year].to_i >0
543 544 @year_from = params[:year].to_i
544 545 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
545 546 @month_from = params[:month].to_i
546 547 else
547 548 @month_from = 1
548 549 end
549 550 else
550 551 @month_from ||= (Date.today << 1).month
551 552 @year_from ||= (Date.today << 1).year
552 553 end
553 554
554 555 @zoom = (params[:zoom].to_i > 0 and params[:zoom].to_i < 5) ? params[:zoom].to_i : 2
555 556 @months = (params[:months].to_i > 0 and params[:months].to_i < 25) ? params[:months].to_i : 6
556 557
557 558 @date_from = Date.civil(@year_from, @month_from, 1)
558 559 @date_to = (@date_from >> @months) - 1
559 560
560 561 @events = []
561 562 @project.issues_with_subprojects(params[:with_subprojects]) do
562 563 @events += Issue.find(:all,
563 564 :order => "start_date, due_date",
564 565 :include => [:tracker, :status, :assigned_to, :priority, :project],
565 566 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
566 567 ) unless @selected_tracker_ids.empty?
567 568 end
568 569 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
569 570 @events.sort! {|x,y| x.start_date <=> y.start_date }
570 571
571 572 if params[:format]=='pdf'
572 573 @options_for_rfpdf ||= {}
573 574 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
574 575 render :template => "projects/gantt.rfpdf", :layout => false
575 576 elsif params[:format]=='png' && respond_to?('gantt_image')
576 577 image = gantt_image(@events, @date_from, @months, @zoom)
577 578 image.format = 'PNG'
578 579 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
579 580 else
580 581 render :template => "projects/gantt.rhtml"
581 582 end
582 583 end
583 584
584 585 private
585 586 # Find project of id params[:id]
586 587 # if not found, redirect to project list
587 588 # Used as a before_filter
588 589 def find_project
589 590 @project = Project.find(params[:id])
590 591 rescue ActiveRecord::RecordNotFound
591 592 render_404
592 593 end
593 594
594 595 def retrieve_selected_tracker_ids(selectable_trackers)
595 596 if ids = params[:tracker_ids]
596 597 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
597 598 else
598 599 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
599 600 end
600 601 end
601 602
602 603 # Retrieve query from session or build a new query
603 604 def retrieve_query
604 605 if params[:query_id]
605 606 @query = @project.queries.find(params[:query_id])
606 607 @query.executed_by = logged_in_user
607 608 session[:query] = @query
608 609 else
609 610 if params[:set_filter] or !session[:query] or session[:query].project_id != @project.id
610 611 # Give it a name, required to be valid
611 612 @query = Query.new(:name => "_", :executed_by => logged_in_user)
612 613 @query.project = @project
613 614 if params[:fields] and params[:fields].is_a? Array
614 615 params[:fields].each do |field|
615 616 @query.add_filter(field, params[:operators][field], params[:values][field])
616 617 end
617 618 else
618 619 @query.available_filters.keys.each do |field|
619 620 @query.add_short_filter(field, params[field]) if params[field]
620 621 end
621 622 end
622 623 session[:query] = @query
623 624 else
624 625 @query = session[:query]
625 626 end
626 627 end
627 628 end
628 629 end
@@ -1,171 +1,172
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 TimelogController < ApplicationController
19 19 layout 'base'
20 20 before_filter :find_project, :authorize
21 21
22 22 helper :sort
23 23 include SortHelper
24 helper :issues
24 25
25 26 def report
26 27 @available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
27 28 :values => @project.versions,
28 29 :label => :label_version},
29 30 'category' => {:sql => "#{Issue.table_name}.category_id",
30 31 :values => @project.issue_categories,
31 32 :label => :field_category},
32 33 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
33 34 :values => @project.users,
34 35 :label => :label_member},
35 36 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
36 37 :values => Tracker.find(:all),
37 38 :label => :label_tracker},
38 39 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
39 40 :values => Enumeration::get_values('ACTI'),
40 41 :label => :label_activity}
41 42 }
42 43
43 44 @criterias = params[:criterias] || []
44 45 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
45 46 @criterias.uniq!
46 47
47 48 @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month'
48 49
49 50 if params[:date_from]
50 51 begin; @date_from = params[:date_from].to_date; rescue; end
51 52 end
52 53 if params[:date_to]
53 54 begin; @date_to = params[:date_to].to_date; rescue; end
54 55 end
55 56 @date_from ||= Date.civil(Date.today.year, 1, 1)
56 57 @date_to ||= Date.civil(Date.today.year, Date.today.month+1, 1) - 1
57 58
58 59 unless @criterias.empty?
59 60 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
60 61 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
61 62
62 63 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
63 64 sql << " FROM #{TimeEntry.table_name} LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
64 65 sql << " WHERE #{TimeEntry.table_name}.project_id = %s" % @project.id
65 66 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)]
66 67 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
67 68
68 69 @hours = ActiveRecord::Base.connection.select_all(sql)
69 70
70 71 @hours.each do |row|
71 72 case @columns
72 73 when 'year'
73 74 row['year'] = row['tyear']
74 75 when 'month'
75 76 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
76 77 when 'week'
77 78 row['week'] = "#{row['tyear']}-#{row['tweek']}"
78 79 end
79 80 end
80 81 end
81 82
82 83 @periods = []
83 84 date_from = @date_from
84 85 # 100 columns max
85 86 while date_from < @date_to && @periods.length < 100
86 87 case @columns
87 88 when 'year'
88 89 @periods << "#{date_from.year}"
89 90 date_from = date_from >> 12
90 91 when 'month'
91 92 @periods << "#{date_from.year}-#{date_from.month}"
92 93 date_from = date_from >> 1
93 94 when 'week'
94 95 @periods << "#{date_from.year}-#{date_from.cweek}"
95 96 date_from = date_from + 7
96 97 end
97 98 end
98 99
99 100 render :layout => false if request.xhr?
100 101 end
101 102
102 103 def details
103 104 sort_init 'spent_on', 'desc'
104 105 sort_update
105 106
106 107 @entries = (@issue ? @issue : @project).time_entries.find(:all, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :order => sort_clause)
107 108
108 109 @total_hours = @entries.inject(0) { |sum,entry| sum + entry.hours }
109 110 @owner_id = logged_in_user ? logged_in_user.id : 0
110 111
111 112 send_csv and return if 'csv' == params[:export]
112 113 render :action => 'details', :layout => false if request.xhr?
113 114 end
114 115
115 116 def edit
116 117 render_404 and return if @time_entry && @time_entry.user != logged_in_user
117 118 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today)
118 119 @time_entry.attributes = params[:time_entry]
119 120 if request.post? and @time_entry.save
120 121 flash[:notice] = l(:notice_successful_update)
121 122 redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue
122 123 return
123 124 end
124 125 @activities = Enumeration::get_values('ACTI')
125 126 end
126 127
127 128 private
128 129 def find_project
129 130 if params[:id]
130 131 @time_entry = TimeEntry.find(params[:id])
131 132 @project = @time_entry.project
132 133 elsif params[:issue_id]
133 134 @issue = Issue.find(params[:issue_id])
134 135 @project = @issue.project
135 136 elsif params[:project_id]
136 137 @project = Project.find(params[:project_id])
137 138 else
138 139 render_404
139 140 return false
140 141 end
141 142 end
142 143
143 144 def send_csv
144 145 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
145 146 export = StringIO.new
146 147 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
147 148 # csv header fields
148 149 headers = [l(:field_spent_on),
149 150 l(:field_user),
150 151 l(:field_activity),
151 152 l(:field_issue),
152 153 l(:field_hours),
153 154 l(:field_comments)
154 155 ]
155 156 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
156 157 # csv lines
157 158 @entries.each do |entry|
158 159 fields = [l_date(entry.spent_on),
159 160 entry.user.name,
160 161 entry.activity.name,
161 162 (entry.issue ? entry.issue.id : nil),
162 163 entry.hours,
163 164 entry.comments
164 165 ]
165 166 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
166 167 end
167 168 end
168 169 export.rewind
169 170 send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
170 171 end
171 172 end
@@ -1,343 +1,343
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 module ApplicationHelper
19 19
20 20 def current_role
21 21 @current_role ||= User.current.role_for_project(@project)
22 22 end
23 23
24 24 # Return true if user is authorized for controller/action, otherwise false
25 25 def authorize_for(controller, action)
26 26 User.current.allowed_to?({:controller => controller, :action => action}, @project)
27 27 end
28 28
29 29 # Display a link if user is authorized
30 30 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
31 31 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
32 32 end
33 33
34 34 # Display a link to user's account page
35 35 def link_to_user(user)
36 36 link_to user.name, :controller => 'account', :action => 'show', :id => user
37 37 end
38 38
39 39 def link_to_issue(issue)
40 40 link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
41 41 end
42 42
43 43 def toggle_link(name, id, options={})
44 44 onclick = "Element.toggle('#{id}'); "
45 45 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
46 46 onclick << "return false;"
47 47 link_to(name, "#", :onclick => onclick)
48 48 end
49 49
50 50 def show_and_goto_link(name, id, options={})
51 51 onclick = "Element.show('#{id}'); "
52 52 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
53 53 onclick << "location.href='##{id}-anchor'; "
54 54 onclick << "return false;"
55 55 link_to(name, "#", options.merge(:onclick => onclick))
56 56 end
57 57
58 58 def image_to_function(name, function, html_options = {})
59 59 html_options.symbolize_keys!
60 60 tag(:input, html_options.merge({
61 61 :type => "image", :src => image_path(name),
62 62 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
63 63 }))
64 64 end
65 65
66 66 def prompt_to_remote(name, text, param, url, html_options = {})
67 67 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
68 68 link_to name, {}, html_options
69 69 end
70 70
71 71 def format_date(date)
72 72 return nil unless date
73 @date_format_setting ||= Setting.date_format.to_i
74 @date_format_setting == 0 ? l_date(date) : date.strftime("%Y-%m-%d")
73 @date_format ||= (Setting.date_format.to_i == 0 ? l(:general_fmt_date) : date.strftime("%Y-%m-%d"))
74 date.strftime(@date_format)
75 75 end
76 76
77 77 def format_time(time)
78 78 return nil unless time
79 79 @date_format_setting ||= Setting.date_format.to_i
80 80 time = time.to_time if time.is_a?(String)
81 81 @date_format_setting == 0 ? l_datetime(time) : (time.strftime("%Y-%m-%d") + ' ' + l_time(time))
82 82 end
83 83
84 84 def authoring(created, author)
85 85 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
86 86 l(:label_added_time_by, author.name, time_tag)
87 87 end
88 88
89 89 def day_name(day)
90 90 l(:general_day_names).split(',')[day-1]
91 91 end
92 92
93 93 def month_name(month)
94 94 l(:actionview_datehelper_select_month_names).split(',')[month-1]
95 95 end
96 96
97 97 def pagination_links_full(paginator, options={}, html_options={})
98 98 page_param = options.delete(:page_param) || :page
99 99
100 100 html = ''
101 101 html << link_to_remote(('&#171; ' + l(:label_previous)),
102 102 {:update => "content", :url => options.merge(page_param => paginator.current.previous)},
103 103 {:href => url_for(:params => options.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
104 104
105 105 html << (pagination_links_each(paginator, options) do |n|
106 106 link_to_remote(n.to_s,
107 107 {:url => {:params => options.merge(page_param => n)}, :update => 'content'},
108 108 {:href => url_for(:params => options.merge(page_param => n))})
109 109 end || '')
110 110
111 111 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
112 112 {:update => "content", :url => options.merge(page_param => paginator.current.next)},
113 113 {:href => url_for(:params => options.merge(page_param => paginator.current.next))}) if paginator.current.next
114 114 html
115 115 end
116 116
117 117 def set_html_title(text)
118 118 @html_header_title = text
119 119 end
120 120
121 121 def html_title
122 122 title = []
123 123 title << @project.name if @project
124 124 title << @html_header_title
125 125 title << Setting.app_title
126 126 title.compact.join(' - ')
127 127 end
128 128
129 129 # format text according to system settings
130 130 def textilizable(text, options = {})
131 131 return "" if text.blank?
132 132
133 133 # when using an image link, try to use an attachment, if possible
134 134 attachments = options[:attachments]
135 135 if attachments
136 136 text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
137 137 align = $1
138 138 filename = $2
139 139 rf = Regexp.new(filename, Regexp::IGNORECASE)
140 140 # search for the picture in attachments
141 141 if found = attachments.detect { |att| att.filename =~ rf }
142 142 image_url = url_for :controller => 'attachments', :action => 'download', :id => found.id
143 143 "!#{align}#{image_url}!"
144 144 else
145 145 "!#{align}#{filename}!"
146 146 end
147 147 end
148 148 end
149 149
150 150 text = (Setting.text_formatting == 'textile') ?
151 151 Redmine::WikiFormatting.to_html(text) : simple_format(auto_link(h(text)))
152 152
153 153 # different methods for formatting wiki links
154 154 case options[:wiki_links]
155 155 when :local
156 156 # used for local links to html files
157 157 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
158 158 when :anchor
159 159 # used for single-file wiki export
160 160 format_wiki_link = Proc.new {|project, title| "##{title}" }
161 161 else
162 162 format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title }
163 163 end
164 164
165 165 project = options[:project] || @project
166 166
167 167 # turn wiki links into html links
168 168 # example:
169 169 # [[mypage]]
170 170 # [[mypage|mytext]]
171 171 # wiki links can refer other project wikis, using project name or identifier:
172 172 # [[project:]] -> wiki starting page
173 173 # [[project:|mytext]]
174 174 # [[project:mypage]]
175 175 # [[project:mypage|mytext]]
176 176 text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) do |m|
177 177 link_project = project
178 178 page = $1
179 179 title = $3
180 180 if page =~ /^([^\:]+)\:(.*)$/
181 181 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
182 182 page = title || $2
183 183 title = $1 if page.blank?
184 184 end
185 185
186 186 if link_project && link_project.wiki
187 187 # check if page exists
188 188 wiki_page = link_project.wiki.find_page(page)
189 189 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
190 190 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
191 191 else
192 192 # project or wiki doesn't exist
193 193 title || page
194 194 end
195 195 end
196 196
197 197 # turn issue and revision ids into links
198 198 # example:
199 199 # #52 -> <a href="/issues/show/52">#52</a>
200 200 # r52 -> <a href="/repositories/revision/6?rev=52">r52</a> (project.id is 6)
201 201 text = text.gsub(%r{([\s,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m|
202 202 leading, otype, oid = $1, $2, $3
203 203 link = nil
204 204 if otype == 'r'
205 205 if project && (changeset = project.changesets.find_by_revision(oid))
206 206 link = link_to("r#{oid}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid}, :class => 'changeset',
207 207 :title => truncate(changeset.comments, 100))
208 208 end
209 209 else
210 210 if issue = Issue.find_by_id(oid.to_i, :include => [:project, :status], :conditions => Project.visible_by(User.current))
211 211 link = link_to("##{oid}", {:controller => 'issues', :action => 'show', :id => oid}, :class => 'issue',
212 212 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
213 213 link = content_tag('del', link) if issue.closed?
214 214 end
215 215 end
216 216 leading + (link || "#{otype}#{oid}")
217 217 end
218 218
219 219 text
220 220 end
221 221
222 222 # Same as Rails' simple_format helper without using paragraphs
223 223 def simple_format_without_paragraph(text)
224 224 text.to_s.
225 225 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
226 226 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
227 227 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
228 228 end
229 229
230 230 def error_messages_for(object_name, options = {})
231 231 options = options.symbolize_keys
232 232 object = instance_variable_get("@#{object_name}")
233 233 if object && !object.errors.empty?
234 234 # build full_messages here with controller current language
235 235 full_messages = []
236 236 object.errors.each do |attr, msg|
237 237 next if msg.nil?
238 238 msg = msg.first if msg.is_a? Array
239 239 if attr == "base"
240 240 full_messages << l(msg)
241 241 else
242 242 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
243 243 end
244 244 end
245 245 # retrieve custom values error messages
246 246 if object.errors[:custom_values]
247 247 object.custom_values.each do |v|
248 248 v.errors.each do |attr, msg|
249 249 next if msg.nil?
250 250 msg = msg.first if msg.is_a? Array
251 251 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
252 252 end
253 253 end
254 254 end
255 255 content_tag("div",
256 256 content_tag(
257 257 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
258 258 ) +
259 259 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
260 260 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
261 261 )
262 262 else
263 263 ""
264 264 end
265 265 end
266 266
267 267 def lang_options_for_select(blank=true)
268 268 (blank ? [["(auto)", ""]] : []) +
269 269 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.first <=> y.first }
270 270 end
271 271
272 272 def label_tag_for(name, option_tags = nil, options = {})
273 273 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
274 274 content_tag("label", label_text)
275 275 end
276 276
277 277 def labelled_tabular_form_for(name, object, options, &proc)
278 278 options[:html] ||= {}
279 279 options[:html].store :class, "tabular"
280 280 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
281 281 end
282 282
283 283 def check_all_links(form_name)
284 284 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
285 285 " | " +
286 286 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
287 287 end
288 288
289 289 def calendar_for(field_id)
290 290 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
291 291 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
292 292 end
293 293
294 294 def wikitoolbar_for(field_id)
295 295 return '' unless Setting.text_formatting == 'textile'
296 296 javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
297 297 end
298 298
299 299 def content_for(name, content = nil, &block)
300 300 @has_content ||= {}
301 301 @has_content[name] = true
302 302 super(name, content, &block)
303 303 end
304 304
305 305 def has_content?(name)
306 306 (@has_content && @has_content[name]) || false
307 307 end
308 308 end
309 309
310 310 class TabularFormBuilder < ActionView::Helpers::FormBuilder
311 311 include GLoc
312 312
313 313 def initialize(object_name, object, template, options, proc)
314 314 set_language_if_valid options.delete(:lang)
315 315 @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
316 316 end
317 317
318 318 (field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
319 319 src = <<-END_SRC
320 320 def #{selector}(field, options = {})
321 321 return super if options.delete :no_label
322 322 label_text = l(options[:label]) if options[:label]
323 323 label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym)
324 324 label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required)
325 325 label = @template.content_tag("label", label_text,
326 326 :class => (@object && @object.errors[field] ? "error" : nil),
327 327 :for => (@object_name.to_s + "_" + field.to_s))
328 328 label + super
329 329 end
330 330 END_SRC
331 331 class_eval src, __FILE__, __LINE__
332 332 end
333 333
334 334 def select(field, choices, options = {}, html_options = {})
335 335 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
336 336 label = @template.content_tag("label", label_text,
337 337 :class => (@object && @object.errors[field] ? "error" : nil),
338 338 :for => (@object_name.to_s + "_" + field.to_s))
339 339 label + super
340 340 end
341 341
342 342 end
343 343
@@ -1,91 +1,104
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 module IssuesHelper
19 19
20 def render_issue_tooltip(issue)
21 @cached_label_start_date ||= l(:field_start_date)
22 @cached_label_due_date ||= l(:field_due_date)
23 @cached_label_assigned_to ||= l(:field_assigned_to)
24 @cached_label_priority ||= l(:field_priority)
25
26 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
27 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
28 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
29 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
30 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
31 end
32
20 33 def show_detail(detail, no_html=false)
21 34 case detail.property
22 35 when 'attr'
23 36 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
24 37 case detail.prop_key
25 38 when 'due_date', 'start_date'
26 39 value = format_date(detail.value.to_date) if detail.value
27 40 old_value = format_date(detail.old_value.to_date) if detail.old_value
28 41 when 'status_id'
29 42 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
30 43 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
31 44 when 'assigned_to_id'
32 45 u = User.find_by_id(detail.value) and value = u.name if detail.value
33 46 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
34 47 when 'priority_id'
35 48 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
36 49 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
37 50 when 'category_id'
38 51 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
39 52 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
40 53 when 'fixed_version_id'
41 54 v = Version.find_by_id(detail.value) and value = v.name if detail.value
42 55 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
43 56 end
44 57 when 'cf'
45 58 custom_field = CustomField.find_by_id(detail.prop_key)
46 59 if custom_field
47 60 label = custom_field.name
48 61 value = format_value(detail.value, custom_field.field_format) if detail.value
49 62 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
50 63 end
51 64 when 'attachment'
52 65 label = l(:label_attachment)
53 66 end
54 67
55 68 label ||= detail.prop_key
56 69 value ||= detail.value
57 70 old_value ||= detail.old_value
58 71
59 72 unless no_html
60 73 label = content_tag('strong', label)
61 74 old_value = content_tag("i", h(old_value)) if detail.old_value
62 75 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
63 76 if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
64 77 # Link to the attachment if it has not been removed
65 78 value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key)
66 79 else
67 80 value = content_tag("i", h(value)) if value
68 81 end
69 82 end
70 83
71 84 if !detail.value.blank?
72 85 case detail.property
73 86 when 'attr', 'cf'
74 87 if !detail.old_value.blank?
75 88 label + " " + l(:text_journal_changed, old_value, value)
76 89 else
77 90 label + " " + l(:text_journal_set_to, value)
78 91 end
79 92 when 'attachment'
80 93 "#{label} #{value} #{l(:label_added)}"
81 94 end
82 95 else
83 96 case detail.property
84 97 when 'attr', 'cf'
85 98 label + " " + l(:text_journal_deleted) + " (#{old_value})"
86 99 when 'attachment'
87 100 "#{label} #{old_value} #{l(:label_deleted)}"
88 101 end
89 102 end
90 103 end
91 104 end
@@ -1,88 +1,88
1 1 <% cache(:year => @year, :month => @month, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %>
2 2 <h2><%= l(:label_calendar) %>: <%= "#{month_name(@month).downcase} #{@year}" %></h2>
3 3
4 4 <table width="100%">
5 5 <tr><td align="left">
6 6 <%= link_to_remote ('&#171; ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")),
7 7 {:update => "content", :url => { :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] }},
8 8 {:href => url_for(:action => 'calendar', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects])}
9 9 %>
10 10 </td><td align="right">
11 11 <%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' &#187;'),
12 12 {:update => "content", :url => { :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] }},
13 13 {:href => url_for(:action => 'calendar', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects])}
14 14 %>
15 15 </td></tr>
16 16 </table>
17 17
18 18 <table class="cal">
19 19 <thead>
20 20 <tr>
21 21 <td></td>
22 22 <% 1.upto(7) do |d| %>
23 23 <th style="width:14%"><%= day_name(d) %></th>
24 24 <% end %>
25 25 </tr>
26 26 </thead>
27 27 <tbody>
28 28 <tr style="height:100px">
29 29 <% day = @date_from
30 30 while day <= @date_to
31 31 if day.cwday == 1 %>
32 32 <th><%= day.cweek %></th>
33 33 <% end %>
34 34 <td valign="top" class="<%= day.month==@month ? "even" : "odd" %> <%= Date.today == day ? 'today' : '' %>" style="width:14%;">
35 35 <p class="textright"><%= day==Date.today ? "<b>#{day.day}</b>" : day.day %></p>
36 36 <% ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq.each do |i| %>
37 37 <% if i.is_a? Issue %>
38 38 <div class="tooltip">
39 39 <%= if day == i.start_date and day == i.due_date
40 40 image_tag('arrow_bw.png')
41 41 elsif day == i.start_date
42 42 image_tag('arrow_from.png')
43 43 elsif day == i.due_date
44 44 image_tag('arrow_to.png')
45 45 end %>
46 46 <small>
47 47 <%= h("#{i.project.name} -") unless @project && @project == i.project %>
48 48 <%= link_to_issue i %>:
49 49 <%= h(truncate(i.subject, 30)) %>
50 50 </small>
51 51 <span class="tip">
52 <%= render :partial => "issues/tooltip", :locals => { :issue => i }%>
52 <%= render_issue_tooltip i %>
53 53 </span>
54 54 </div>
55 55 <% else %>
56 56 <small><%= link_to_version i, :class => "icon icon-package" %></small>
57 57 <% end %>
58 58 <% end %>
59 59 </td>
60 60 <%= '</tr><tr style="height:100px">' if day.cwday >= 7 and day!=@date_to %>
61 61 <%
62 62 day = day + 1
63 63 end %>
64 64 </tr>
65 65 </tbody>
66 66 </table>
67 67
68 68 <%= image_tag 'arrow_from.png' %>&nbsp;&nbsp;<%= l(:text_tip_task_begin_day) %><br />
69 69 <%= image_tag 'arrow_to.png' %>&nbsp;&nbsp;<%= l(:text_tip_task_end_day) %><br />
70 70 <%= image_tag 'arrow_bw.png' %>&nbsp;&nbsp;<%= l(:text_tip_task_begin_end_day) %><br />
71 71 <% end %>
72 72
73 73 <% content_for :sidebar do %>
74 74 <h3><%= l(:label_calendar) %></h3>
75 75
76 76 <% form_tag() do %>
77 77 <p><%= select_month(@month, :prefix => "month", :discard_type => true) %>
78 78 <%= select_year(@year, :prefix => "year", :discard_type => true) %></p>
79 79
80 80 <% @trackers.each do |tracker| %>
81 81 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %> <%= tracker.name %></label><br />
82 82 <% end %>
83 83 <% if @project.active_children.any? %>
84 84 <br /><label><%= check_box_tag "with_subprojects", 1, params[:with_subprojects] %> <%=l(:label_subproject_plural)%></label>
85 85 <% end %>
86 86 <p><%= submit_tag l(:button_apply), :class => 'button-small' %></p>
87 87 <% end %>
88 88 <% end %>
@@ -1,247 +1,247
1 1 <% zoom = 1
2 2 @zoom.times { zoom = zoom * 2 }
3 3
4 4 subject_width = 330
5 5 header_heigth = 18
6 6
7 7 headers_height = header_heigth
8 8 show_weeks = false
9 9 show_days = false
10 10
11 11 if @zoom >1
12 12 show_weeks = true
13 13 headers_height = 2*header_heigth
14 14 if @zoom > 2
15 15 show_days = true
16 16 headers_height = 3*header_heigth
17 17 end
18 18 end
19 19
20 20 g_width = (@date_to - @date_from + 1)*zoom
21 21 g_height = [(20 * @events.length + 6)+150, 206].max
22 22 t_height = g_height + headers_height
23 23 %>
24 24
25 25 <div class="contextual">
26 26 </div>
27 27
28 28 <h2><%= l(:label_gantt) %></h2>
29 29
30 30 <% form_tag(params.merge(:month => nil, :year => nil, :years => nil)) do %>
31 31 <table width="100%">
32 32 <tr>
33 33 <td align="left">
34 34 <input type="text" name="months" size="2" value="<%= @months %>" />
35 35 <%= l(:label_months_from) %>
36 36 <%= select_month(@month_from, :prefix => "month", :discard_type => true) %>
37 37 <%= select_year(@year_from, :prefix => "year", :discard_type => true) %>
38 38 <%= hidden_field_tag 'zoom', @zoom %>
39 39 <%= submit_tag l(:button_submit), :class => "button-small" %>
40 40 </td>
41 41
42 42 <td align="right">
43 43 <%= if @zoom < 4
44 44 link_to image_tag('zoom_in.png'), {:zoom => (@zoom+1), :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects]}
45 45 else
46 46 image_tag 'zoom_in_g.png'
47 47 end %>
48 48 <%= if @zoom > 1
49 49 link_to image_tag('zoom_out.png'),{:zoom => (@zoom-1), :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects]}
50 50 else
51 51 image_tag 'zoom_out_g.png'
52 52 end %>
53 53 </td>
54 54 </tr>
55 55 </table>
56 56 <% end %>
57 57
58 58 <% cache(:year => @year_from, :month => @month_from, :months => @months, :zoom => @zoom, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %>
59 59
60 60 <table width="100%" style="border:0; border-collapse: collapse;">
61 61 <tr>
62 62 <td style="width:<%= subject_width %>px;">
63 63
64 64 <div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
65 65 <div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
66 66 <div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
67 67 <%
68 68 #
69 69 # Tasks subjects
70 70 #
71 71 top = headers_height + 8
72 72 @events.each do |i| %>
73 73 <div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:4px;overflow:hidden;"><small>
74 74 <% if i.is_a? Issue %>
75 75 <%= link_to_issue i %><%= " (#{i.project.name})" unless @project && @project == i.project %>:
76 76 <%=h i.subject %>
77 77 <% else %>
78 78 <%= link_to_version i, :class => "icon icon-package" %>
79 79 <% end %>
80 80 </small></div>
81 81 <% top = top + 20
82 82 end %>
83 83 </div>
84 84 </td>
85 85 <td>
86 86
87 87 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
88 88 <div style="width:<%= g_width-1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr">&nbsp;</div>
89 89 <%
90 90 #
91 91 # Months headers
92 92 #
93 93 month_f = @date_from
94 94 left = 0
95 95 height = (show_weeks ? header_heigth : header_heigth + g_height)
96 96 @months.times do
97 97 width = ((month_f >> 1) - month_f) * zoom - 1
98 98 %>
99 99 <div style="left:<%= left %>px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
100 100 <%= link_to "#{month_f.year}-#{month_f.month}", { :year => month_f.year, :month => month_f.month, :zoom => @zoom, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] }, :title => "#{month_name(month_f.month)} #{month_f.year}"%>
101 101 </div>
102 102 <%
103 103 left = left + width + 1
104 104 month_f = month_f >> 1
105 105 end %>
106 106
107 107 <%
108 108 #
109 109 # Weeks headers
110 110 #
111 111 if show_weeks
112 112 left = 0
113 113 height = (show_days ? header_heigth-1 : header_heigth-1 + g_height)
114 114 if @date_from.cwday == 1
115 115 # @date_from is monday
116 116 week_f = @date_from
117 117 else
118 118 # find next monday after @date_from
119 119 week_f = @date_from + (7 - @date_from.cwday + 1)
120 120 width = (7 - @date_from.cwday + 1) * zoom-1
121 121 %>
122 122 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">&nbsp;</div>
123 123 <%
124 124 left = left + width+1
125 125 end %>
126 126 <%
127 127 while week_f <= @date_to
128 128 width = (week_f + 6 <= @date_to) ? 7 * zoom -1 : (@date_to - week_f + 1) * zoom-1
129 129 %>
130 130 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
131 131 <small><%= week_f.cweek if width >= 16 %></small>
132 132 </div>
133 133 <%
134 134 left = left + width+1
135 135 week_f = week_f+7
136 136 end
137 137 end %>
138 138
139 139 <%
140 140 #
141 141 # Days headers
142 142 #
143 143 if show_days
144 144 left = 0
145 145 height = g_height + header_heigth - 1
146 146 wday = @date_from.cwday
147 147 (@date_to - @date_from + 1).to_i.times do
148 148 width = zoom - 1
149 149 %>
150 150 <div style="left:<%= left %>px;top:37px;width:<%= width %>px;height:<%= height %>px;font-size:0.7em;<%= "background:#f1f1f1;" if wday > 5 %>" class="gantt_hdr">
151 151 <%= day_name(wday).first %>
152 152 </div>
153 153 <%
154 154 left = left + width+1
155 155 wday = wday + 1
156 156 wday = 1 if wday > 7
157 157 end
158 158 end %>
159 159
160 160 <%
161 161 #
162 162 # Tasks
163 163 #
164 164 top = headers_height + 10
165 165 @events.each do |i|
166 166 if i.is_a? Issue
167 167 i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from )
168 168 i_end_date = (i.due_date <= @date_to ? i.due_date : @date_to )
169 169
170 170 i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
171 171 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
172 172 i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
173 173
174 174 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
175 175
176 176 i_left = ((i_start_date - @date_from)*zoom).floor
177 177 i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders)
178 178 d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width
179 179 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width
180 180 %>
181 181 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="task task_todo">&nbsp;</div>
182 182 <% if l_width > 0 %>
183 183 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="task task_late">&nbsp;</div>
184 184 <% end %>
185 185 <% if d_width > 0 %>
186 186 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="task task_done">&nbsp;</div>
187 187 <% end %>
188 188 <div style="top:<%= top %>px;left:<%= i_left + i_width + 5 %>px;background:#fff;" class="task">
189 189 <%= i.status.name %>
190 190 <%= (i.done_ratio).to_i %>%
191 191 </div>
192 192 <% # === tooltip === %>
193 193 <div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;">
194 194 <span class="tip">
195 <%= render :partial => "issues/tooltip", :locals => { :issue => i }%>
195 <%= render_issue_tooltip i %>
196 196 </span></div>
197 197 <% else
198 198 i_left = ((i.start_date - @date_from)*zoom).floor
199 199 %>
200 200 <div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone">&nbsp;</div>
201 201 <div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task">
202 202 <strong><%= i.name %></strong>
203 203 </div>
204 204 <% end %>
205 205 <% top = top + 20
206 206 end %>
207 207
208 208 <% end # cache
209 209 %>
210 210
211 211 <%
212 212 #
213 213 # Today red line (excluded from cache)
214 214 #
215 215 if Date.today >= @date_from and Date.today <= @date_to %>
216 216 <div style="position: absolute;height:<%= g_height %>px;top:<%= headers_height + 1 %>px;left:<%= ((Date.today-@date_from+1)*zoom).floor()-1 %>px;width:10px;border-left: 1px dashed red;">&nbsp;</div>
217 217 <% end %>
218 218
219 219 </div>
220 220 </td>
221 221 </tr>
222 222 </table>
223 223
224 224 <table width="100%">
225 225 <tr>
226 226 <td align="left"><%= link_to ('&#171; ' + l(:label_previous)), :year => (@date_from << @months).year, :month => (@date_from << @months).month, :zoom => @zoom, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] %></td>
227 227 <td align="right"><%= link_to (l(:label_next) + ' &#187;'), :year => (@date_from >> @months).year, :month => (@date_from >> @months).month, :zoom => @zoom, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] %></td>
228 228 </tr>
229 229 </table>
230 230
231 231 <div class="contextual"><%= l(:label_export_to) %>
232 232 <%= link_to 'PDF', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'pdf'}, :class => 'icon icon-pdf' %>
233 233 <%= link_to 'PNG', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'png'}, :class => 'icon icon-image' if respond_to?('gantt_image') %>
234 234 </div>
235 235
236 236 <% content_for :sidebar do %>
237 237 <h3><%= l(:label_gantt) %></h3>
238 238 <% form_tag(params.merge(:tracker_ids => nil, :with_subprojects => nil)) do %>
239 239 <% @trackers.each do |tracker| %>
240 240 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %> <%= tracker.name %></label><br />
241 241 <% end %>
242 242 <% if @project.active_children.any? %>
243 243 <br /><label><%= check_box_tag "with_subprojects", 1, params[:with_subprojects] %> <%=l(:label_subproject_plural)%></label>
244 244 <% end %>
245 245 <p><%= submit_tag l(:button_apply), :class => 'button-small' %></p>
246 246 <% end %>
247 247 <% end %>
@@ -1,51 +1,51
1 1 <div class="contextual">
2 2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
3 3 </div>
4 4
5 5 <h2><%= l(:label_spent_time) %></h2>
6 6
7 7 <h3><%= link_to(@project.name, {:action => 'details', :project_id => @project}) if @project %>
8 8 <%= "/ " + link_to_issue(@issue) + h(": #{@issue.subject}") if @issue %></h3>
9 9
10 10 <h3 class="textright"><%= l(:label_total) %>: <%= lwr(:label_f_hour, @total_hours) %></h3>
11 11
12 12 <% unless @entries.empty? %>
13 13 <table class="list">
14 14 <thead>
15 15 <%= sort_header_tag('spent_on', :caption => l(:label_date)) %>
16 16 <%= sort_header_tag('user_id', :caption => l(:label_member)) %>
17 17 <%= sort_header_tag('activity_id', :caption => l(:label_activity)) %>
18 18 <%= sort_header_tag('issue_id', :caption => l(:label_issue)) %>
19 19 <th><%= l(:field_comments) %></th>
20 20 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
21 21 <th></th>
22 22 </thead>
23 23 <tbody>
24 24 <% @entries.each do |entry| %>
25 25 <tr class="<%= cycle("odd", "even") %>">
26 26 <td align="center"><%= format_date(entry.spent_on) %></td>
27 27 <td align="center"><%= entry.user.name %></td>
28 28 <td align="center"><%= entry.activity.name %></td>
29 29 <td align="center">
30 30 <% if entry.issue %>
31 31 <div class="tooltip">
32 <%= link_to "#{entry.issue.tracker.name} ##{entry.issue.id}", {:action => 'details', :issue_id => entry.issue } %>
32 <%= link_to_issue entry.issue %>
33 33 <span class="tip">
34 <%= render :partial => "issues/tooltip", :locals => { :issue => entry.issue }%>
34 <%= render_issue_tooltip entry.issue %>
35 35 </span>
36 36 </div>
37 37 <% end %>
38 38 </td>
39 39 <td><%=h entry.comments %></td>
40 40 <td align="center"><strong><%= entry.hours %></strong></td>
41 41 <td align="center"><%= link_to_if_authorized(l(:button_edit), {:controller => 'timelog', :action => 'edit', :id => entry}, :class => "icon icon-edit") if entry.user_id == @owner_id %></td>
42 42 </tr>
43 43 <% end %>
44 44 </tbdoy>
45 45 </table>
46 46
47 47 <div class="contextual">
48 48 <%= l(:label_export_to) %>
49 49 <%= link_to 'CSV', params.update(:export => 'csv'), :class => 'icon icon-csv' %>
50 50 </div>
51 51 <% end %> No newline at end of file
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now