##// END OF EJS Templates
Gantt chart can now be exported to a graphic file (png)....
Jean-Philippe Lang -
r660:edba1f692b5a
parent child
Show More
@@ -1,676 +1,681
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
26 26 cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
27 27 cache_sweeper :issue_sweeper, :only => [ :add_issue ]
28 28 cache_sweeper :version_sweeper, :only => [ :add_version ]
29 29
30 30 helper :sort
31 31 include SortHelper
32 32 helper :custom_fields
33 33 include CustomFieldsHelper
34 34 helper :ifpdf
35 35 include IfpdfHelper
36 36 helper IssuesHelper
37 37 helper :queries
38 38 include QueriesHelper
39 39 helper :repositories
40 40 include RepositoriesHelper
41 include ProjectsHelper
41 42
42 43 def index
43 44 list
44 45 render :action => 'list' unless request.xhr?
45 46 end
46 47
47 48 # Lists public projects
48 49 def list
49 50 sort_init "#{Project.table_name}.name", "asc"
50 51 sort_update
51 52 @project_count = Project.count(:all, :conditions => Project.visible_by(logged_in_user))
52 53 @project_pages = Paginator.new self, @project_count,
53 54 15,
54 55 params['page']
55 56 @projects = Project.find :all, :order => sort_clause,
56 57 :conditions => Project.visible_by(logged_in_user),
57 58 :include => :parent,
58 59 :limit => @project_pages.items_per_page,
59 60 :offset => @project_pages.current.offset
60 61
61 62 render :action => "list", :layout => false if request.xhr?
62 63 end
63 64
64 65 # Add a new project
65 66 def add
66 67 @custom_fields = IssueCustomField.find(:all)
67 68 @root_projects = Project.find(:all, :conditions => "parent_id is null")
68 69 @project = Project.new(params[:project])
69 70 if request.get?
70 71 @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
71 72 else
72 73 @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
73 74 @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
74 75 @project.custom_values = @custom_values
75 76 if params[:repository_enabled] && params[:repository_enabled] == "1"
76 77 @project.repository = Repository.factory(params[:repository_scm])
77 78 @project.repository.attributes = params[:repository]
78 79 end
79 80 if "1" == params[:wiki_enabled]
80 81 @project.wiki = Wiki.new
81 82 @project.wiki.attributes = params[:wiki]
82 83 end
83 84 if @project.save
84 85 flash[:notice] = l(:notice_successful_create)
85 86 redirect_to :controller => 'admin', :action => 'projects'
86 87 end
87 88 end
88 89 end
89 90
90 91 # Show @project
91 92 def show
92 93 @custom_values = @project.custom_values.find(:all, :include => :custom_field)
93 94 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
94 95 @subprojects = @project.active_children
95 96 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
96 97 @trackers = Tracker.find(:all, :order => 'position')
97 98 @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])
98 99 @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
99 100
100 101 @key = logged_in_user.get_or_create_rss_key.value if logged_in_user
101 102 end
102 103
103 104 def settings
104 105 @root_projects = Project::find(:all, :conditions => ["parent_id is null and id <> ?", @project.id])
105 106 @custom_fields = IssueCustomField.find(:all)
106 107 @issue_category ||= IssueCategory.new
107 108 @member ||= @project.members.new
108 109 @custom_values ||= ProjectCustomField.find(:all).collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
109 110 end
110 111
111 112 # Edit @project
112 113 def edit
113 114 if request.post?
114 115 @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
115 116 if params[:custom_fields]
116 117 @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
117 118 @project.custom_values = @custom_values
118 119 end
119 120 if params[:repository_enabled]
120 121 case params[:repository_enabled]
121 122 when "0"
122 123 @project.repository = nil
123 124 when "1"
124 125 @project.repository ||= Repository.factory(params[:repository_scm])
125 126 @project.repository.update_attributes params[:repository] if @project.repository
126 127 end
127 128 end
128 129 if params[:wiki_enabled]
129 130 case params[:wiki_enabled]
130 131 when "0"
131 132 @project.wiki.destroy if @project.wiki
132 133 when "1"
133 134 @project.wiki ||= Wiki.new
134 135 @project.wiki.update_attributes params[:wiki]
135 136 end
136 137 end
137 138 @project.attributes = params[:project]
138 139 if @project.save
139 140 flash[:notice] = l(:notice_successful_update)
140 141 redirect_to :action => 'settings', :id => @project
141 142 else
142 143 settings
143 144 render :action => 'settings'
144 145 end
145 146 end
146 147 end
147 148
148 149 def archive
149 150 @project.archive if request.post? && @project.active?
150 151 redirect_to :controller => 'admin', :action => 'projects'
151 152 end
152 153
153 154 def unarchive
154 155 @project.unarchive if request.post? && !@project.active?
155 156 redirect_to :controller => 'admin', :action => 'projects'
156 157 end
157 158
158 159 # Delete @project
159 160 def destroy
160 161 @project_to_destroy = @project
161 162 if request.post? and params[:confirm]
162 163 @project_to_destroy.destroy
163 164 redirect_to :controller => 'admin', :action => 'projects'
164 165 end
165 166 # hide project in layout
166 167 @project = nil
167 168 end
168 169
169 170 # Add a new issue category to @project
170 171 def add_issue_category
171 172 @category = @project.issue_categories.build(params[:category])
172 173 if request.post? and @category.save
173 174 respond_to do |format|
174 175 format.html do
175 176 flash[:notice] = l(:notice_successful_create)
176 177 redirect_to :action => 'settings', :tab => 'categories', :id => @project
177 178 end
178 179 format.js do
179 180 # IE doesn't support the replace_html rjs method for select box options
180 181 render(:update) {|page| page.replace "issue_category_id",
181 182 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]')
182 183 }
183 184 end
184 185 end
185 186 end
186 187 end
187 188
188 189 # Add a new version to @project
189 190 def add_version
190 191 @version = @project.versions.build(params[:version])
191 192 if request.post? and @version.save
192 193 flash[:notice] = l(:notice_successful_create)
193 194 redirect_to :action => 'settings', :tab => 'versions', :id => @project
194 195 end
195 196 end
196 197
197 198 # Add a new member to @project
198 199 def add_member
199 200 @member = @project.members.build(params[:member])
200 201 if request.post? && @member.save
201 202 respond_to do |format|
202 203 format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project }
203 204 format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'members'} }
204 205 end
205 206 else
206 207 settings
207 208 render :action => 'settings'
208 209 end
209 210 end
210 211
211 212 # Show members list of @project
212 213 def list_members
213 214 @members = @project.members.find(:all)
214 215 end
215 216
216 217 # Add a new document to @project
217 218 def add_document
218 219 @categories = Enumeration::get_values('DCAT')
219 220 @document = @project.documents.build(params[:document])
220 221 if request.post? and @document.save
221 222 # Save the attachments
222 223 params[:attachments].each { |a|
223 224 Attachment.create(:container => @document, :file => a, :author => logged_in_user) unless a.size == 0
224 225 } if params[:attachments] and params[:attachments].is_a? Array
225 226 flash[:notice] = l(:notice_successful_create)
226 227 Mailer.deliver_document_add(@document) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
227 228 redirect_to :action => 'list_documents', :id => @project
228 229 end
229 230 end
230 231
231 232 # Show documents list of @project
232 233 def list_documents
233 234 @documents = @project.documents.find :all, :include => :category
234 235 end
235 236
236 237 # Add a new issue to @project
237 238 def add_issue
238 239 @tracker = Tracker.find(params[:tracker_id])
239 240 @priorities = Enumeration::get_values('IPRI')
240 241
241 242 default_status = IssueStatus.default
242 243 unless default_status
243 244 flash.now[:error] = 'No default issue status defined. Please check your configuration.'
244 245 render :nothing => true, :layout => true
245 246 return
246 247 end
247 248 @issue = Issue.new(:project => @project, :tracker => @tracker)
248 249 @issue.status = default_status
249 250 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker))if logged_in_user
250 251 if request.get?
251 252 @issue.start_date = Date.today
252 253 @custom_values = @project.custom_fields_for_issues(@tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) }
253 254 else
254 255 @issue.attributes = params[:issue]
255 256
256 257 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
257 258 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
258 259
259 260 @issue.author_id = self.logged_in_user.id if self.logged_in_user
260 261 # Multiple file upload
261 262 @attachments = []
262 263 params[:attachments].each { |a|
263 264 @attachments << Attachment.new(:container => @issue, :file => a, :author => logged_in_user) unless a.size == 0
264 265 } if params[:attachments] and params[:attachments].is_a? Array
265 266 @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]) }
266 267 @issue.custom_values = @custom_values
267 268 if @issue.save
268 269 @attachments.each(&:save)
269 270 flash[:notice] = l(:notice_successful_create)
270 271 Mailer.deliver_issue_add(@issue) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
271 272 redirect_to :action => 'list_issues', :id => @project
272 273 end
273 274 end
274 275 end
275 276
276 277 # Show filtered/sorted issues list of @project
277 278 def list_issues
278 279 sort_init "#{Issue.table_name}.id", "desc"
279 280 sort_update
280 281
281 282 retrieve_query
282 283
283 284 @results_per_page_options = [ 15, 25, 50, 100 ]
284 285 if params[:per_page] and @results_per_page_options.include? params[:per_page].to_i
285 286 @results_per_page = params[:per_page].to_i
286 287 session[:results_per_page] = @results_per_page
287 288 else
288 289 @results_per_page = session[:results_per_page] || 25
289 290 end
290 291
291 292 if @query.valid?
292 293 @issue_count = Issue.count(:include => [:status, :project, :custom_values], :conditions => @query.statement)
293 294 @issue_pages = Paginator.new self, @issue_count, @results_per_page, params['page']
294 295 @issues = Issue.find :all, :order => sort_clause,
295 296 :include => [ :assigned_to, :status, :tracker, :project, :priority, :custom_values ],
296 297 :conditions => @query.statement,
297 298 :limit => @issue_pages.items_per_page,
298 299 :offset => @issue_pages.current.offset
299 300 end
300 301 render :layout => false if request.xhr?
301 302 end
302 303
303 304 # Export filtered/sorted issues list to CSV
304 305 def export_issues_csv
305 306 sort_init "#{Issue.table_name}.id", "desc"
306 307 sort_update
307 308
308 309 retrieve_query
309 310 render :action => 'list_issues' and return unless @query.valid?
310 311
311 312 @issues = Issue.find :all, :order => sort_clause,
312 313 :include => [ :assigned_to, :author, :status, :tracker, :priority, :project, {:custom_values => :custom_field} ],
313 314 :conditions => @query.statement,
314 315 :limit => Setting.issues_export_limit.to_i
315 316
316 317 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
317 318 export = StringIO.new
318 319 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
319 320 # csv header fields
320 321 headers = [ "#", l(:field_status),
321 322 l(:field_project),
322 323 l(:field_tracker),
323 324 l(:field_priority),
324 325 l(:field_subject),
325 326 l(:field_assigned_to),
326 327 l(:field_author),
327 328 l(:field_start_date),
328 329 l(:field_due_date),
329 330 l(:field_done_ratio),
330 331 l(:field_created_on),
331 332 l(:field_updated_on)
332 333 ]
333 334 for custom_field in @project.all_custom_fields
334 335 headers << custom_field.name
335 336 end
336 337 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
337 338 # csv lines
338 339 @issues.each do |issue|
339 340 fields = [issue.id, issue.status.name,
340 341 issue.project.name,
341 342 issue.tracker.name,
342 343 issue.priority.name,
343 344 issue.subject,
344 345 (issue.assigned_to ? issue.assigned_to.name : ""),
345 346 issue.author.name,
346 347 issue.start_date ? l_date(issue.start_date) : nil,
347 348 issue.due_date ? l_date(issue.due_date) : nil,
348 349 issue.done_ratio,
349 350 l_datetime(issue.created_on),
350 351 l_datetime(issue.updated_on)
351 352 ]
352 353 for custom_field in @project.all_custom_fields
353 354 fields << (show_value issue.custom_value_for(custom_field))
354 355 end
355 356 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
356 357 end
357 358 end
358 359 export.rewind
359 360 send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
360 361 end
361 362
362 363 # Export filtered/sorted issues to PDF
363 364 def export_issues_pdf
364 365 sort_init "#{Issue.table_name}.id", "desc"
365 366 sort_update
366 367
367 368 retrieve_query
368 369 render :action => 'list_issues' and return unless @query.valid?
369 370
370 371 @issues = Issue.find :all, :order => sort_clause,
371 372 :include => [ :author, :status, :tracker, :priority, :project, :custom_values ],
372 373 :conditions => @query.statement,
373 374 :limit => Setting.issues_export_limit.to_i
374 375
375 376 @options_for_rfpdf ||= {}
376 377 @options_for_rfpdf[:file_name] = "export.pdf"
377 378 render :layout => false
378 379 end
379 380
380 381 def move_issues
381 382 @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
382 383 redirect_to :action => 'list_issues', :id => @project and return unless @issues
383 384 @projects = []
384 385 # find projects to which the user is allowed to move the issue
385 386 @logged_in_user.memberships.each {|m| @projects << m.project if Permission.allowed_to_role("projects/move_issues", m.role)}
386 387 # issue can be moved to any tracker
387 388 @trackers = Tracker.find(:all)
388 389 if request.post? and params[:new_project_id] and params[:new_tracker_id]
389 390 new_project = Project.find(params[:new_project_id])
390 391 new_tracker = Tracker.find(params[:new_tracker_id])
391 392 @issues.each { |i|
392 393 # project dependent properties
393 394 unless i.project_id == new_project.id
394 395 i.category = nil
395 396 i.fixed_version = nil
396 397 # delete issue relations
397 398 i.relations_from.clear
398 399 i.relations_to.clear
399 400 end
400 401 # move the issue
401 402 i.project = new_project
402 403 i.tracker = new_tracker
403 404 i.save
404 405 }
405 406 flash[:notice] = l(:notice_successful_update)
406 407 redirect_to :action => 'list_issues', :id => @project
407 408 end
408 409 end
409 410
410 411 # Add a news to @project
411 412 def add_news
412 413 @news = News.new(:project => @project)
413 414 if request.post?
414 415 @news.attributes = params[:news]
415 416 @news.author_id = self.logged_in_user.id if self.logged_in_user
416 417 if @news.save
417 418 flash[:notice] = l(:notice_successful_create)
418 419 redirect_to :action => 'list_news', :id => @project
419 420 end
420 421 end
421 422 end
422 423
423 424 # Show news list of @project
424 425 def list_news
425 426 @news_pages, @news = paginate :news, :per_page => 10, :conditions => ["project_id=?", @project.id], :include => :author, :order => "#{News.table_name}.created_on DESC"
426 427 render :action => "list_news", :layout => false if request.xhr?
427 428 end
428 429
429 430 def add_file
430 431 if request.post?
431 432 @version = @project.versions.find_by_id(params[:version_id])
432 433 # Save the attachments
433 434 @attachments = []
434 435 params[:attachments].each { |file|
435 436 next unless file.size > 0
436 437 a = Attachment.create(:container => @version, :file => file, :author => logged_in_user)
437 438 @attachments << a unless a.new_record?
438 439 } if params[:attachments] and params[:attachments].is_a? Array
439 440 Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
440 441 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
441 442 end
442 443 @versions = @project.versions.sort
443 444 end
444 445
445 446 def list_files
446 447 @versions = @project.versions.sort
447 448 end
448 449
449 450 # Show changelog for @project
450 451 def changelog
451 452 @trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
452 453 retrieve_selected_tracker_ids(@trackers)
453 454 @versions = @project.versions.sort
454 455 end
455 456
456 457 def roadmap
457 458 @trackers = Tracker.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position')
458 459 retrieve_selected_tracker_ids(@trackers)
459 460 @versions = @project.versions.sort
460 461 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
461 462 end
462 463
463 464 def activity
464 465 if params[:year] and params[:year].to_i > 1900
465 466 @year = params[:year].to_i
466 467 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
467 468 @month = params[:month].to_i
468 469 end
469 470 end
470 471 @year ||= Date.today.year
471 472 @month ||= Date.today.month
472 473
473 474 @date_from = Date.civil(@year, @month, 1)
474 475 @date_to = @date_from >> 1
475 476
476 477 @events_by_day = Hash.new { |h,k| h[k] = [] }
477 478
478 479 unless params[:show_issues] == "0"
479 480 @project.issues.find(:all, :include => [:author], :conditions => ["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to] ).each { |i|
480 481 @events_by_day[i.created_on.to_date] << i
481 482 }
482 483 @project.issue_changes.find(:all, :include => :details, :conditions => ["(#{Journal.table_name}.created_on BETWEEN ? AND ?) AND (#{JournalDetail.table_name}.prop_key = 'status_id')", @date_from, @date_to] ).each { |i|
483 484 @events_by_day[i.created_on.to_date] << i
484 485 }
485 486 @show_issues = 1
486 487 end
487 488
488 489 unless params[:show_news] == "0"
489 490 @project.news.find(:all, :conditions => ["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to], :include => :author ).each { |i|
490 491 @events_by_day[i.created_on.to_date] << i
491 492 }
492 493 @show_news = 1
493 494 end
494 495
495 496 unless params[:show_files] == "0"
496 497 Attachment.find(:all, :select => "#{Attachment.table_name}.*",
497 498 :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id",
498 499 :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on BETWEEN ? AND ? ", @project.id, @date_from, @date_to],
499 500 :include => :author ).each { |i|
500 501 @events_by_day[i.created_on.to_date] << i
501 502 }
502 503 @show_files = 1
503 504 end
504 505
505 506 unless params[:show_documents] == "0"
506 507 @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to] ).each { |i|
507 508 @events_by_day[i.created_on.to_date] << i
508 509 }
509 510 Attachment.find(:all, :select => "attachments.*",
510 511 :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id",
511 512 :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on BETWEEN ? AND ? ", @project.id, @date_from, @date_to],
512 513 :include => :author ).each { |i|
513 514 @events_by_day[i.created_on.to_date] << i
514 515 }
515 516 @show_documents = 1
516 517 end
517 518
518 519 unless @project.wiki.nil? || params[:show_wiki_edits] == "0"
519 520 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
520 521 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title"
521 522 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
522 523 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
523 524 conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
524 525 @project.id, @date_from, @date_to]
525 526
526 527 WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions).each { |i|
527 528 # We provide this alias so all events can be treated in the same manner
528 529 def i.created_on
529 530 self.updated_on
530 531 end
531 532 @events_by_day[i.created_on.to_date] << i
532 533 }
533 534 @show_wiki_edits = 1
534 535 end
535 536
536 537 unless @project.repository.nil? || params[:show_changesets] == "0"
537 538 @project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]).each { |i|
538 539 def i.created_on
539 540 self.committed_on
540 541 end
541 542 @events_by_day[i.created_on.to_date] << i
542 543 }
543 544 @show_changesets = 1
544 545 end
545 546
546 547 render :layout => false if request.xhr?
547 548 end
548 549
549 550 def calendar
550 551 @trackers = Tracker.find(:all, :order => 'position')
551 552 retrieve_selected_tracker_ids(@trackers)
552 553
553 554 if params[:year] and params[:year].to_i > 1900
554 555 @year = params[:year].to_i
555 556 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
556 557 @month = params[:month].to_i
557 558 end
558 559 end
559 560 @year ||= Date.today.year
560 561 @month ||= Date.today.month
561 562
562 563 @date_from = Date.civil(@year, @month, 1)
563 564 @date_to = (@date_from >> 1)-1
564 565 # start on monday
565 566 @date_from = @date_from - (@date_from.cwday-1)
566 567 # finish on sunday
567 568 @date_to = @date_to + (7-@date_to.cwday)
568 569
569 570 @events = []
570 571 @project.issues_with_subprojects(params[:with_subprojects]) do
571 572 @events += Issue.find(:all,
572 573 :include => [:tracker, :status, :assigned_to, :priority, :project],
573 574 :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]
574 575 ) unless @selected_tracker_ids.empty?
575 576 end
576 577 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
577 578
578 579 @ending_events_by_days = @events.group_by {|event| event.due_date}
579 580 @starting_events_by_days = @events.group_by {|event| event.start_date}
580 581
581 582 render :layout => false if request.xhr?
582 583 end
583 584
584 585 def gantt
585 586 @trackers = Tracker.find(:all, :order => 'position')
586 587 retrieve_selected_tracker_ids(@trackers)
587 588
588 589 if params[:year] and params[:year].to_i >0
589 590 @year_from = params[:year].to_i
590 591 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
591 592 @month_from = params[:month].to_i
592 593 else
593 594 @month_from = 1
594 595 end
595 596 else
596 597 @month_from ||= (Date.today << 1).month
597 598 @year_from ||= (Date.today << 1).year
598 599 end
599 600
600 601 @zoom = (params[:zoom].to_i > 0 and params[:zoom].to_i < 5) ? params[:zoom].to_i : 2
601 602 @months = (params[:months].to_i > 0 and params[:months].to_i < 25) ? params[:months].to_i : 6
602 603
603 604 @date_from = Date.civil(@year_from, @month_from, 1)
604 605 @date_to = (@date_from >> @months) - 1
605 606
606 607 @events = []
607 608 @project.issues_with_subprojects(params[:with_subprojects]) do
608 609 @events += Issue.find(:all,
609 610 :order => "start_date, due_date",
610 611 :include => [:tracker, :status, :assigned_to, :priority, :project],
611 612 :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]
612 613 ) unless @selected_tracker_ids.empty?
613 614 end
614 615 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
615 616 @events.sort! {|x,y| x.start_date <=> y.start_date }
616 617
617 if params[:output]=='pdf'
618 if params[:format]=='pdf'
618 619 @options_for_rfpdf ||= {}
619 @options_for_rfpdf[:file_name] = "gantt.pdf"
620 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
620 621 render :template => "projects/gantt.rfpdf", :layout => false
622 elsif params[:format]=='png' && respond_to?('gantt_image')
623 image = gantt_image(@events, @date_from, @months, @zoom)
624 image.format = 'PNG'
625 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
621 626 else
622 627 render :template => "projects/gantt.rhtml"
623 628 end
624 629 end
625 630
626 631 def feeds
627 632 @queries = @project.queries.find :all, :conditions => ["is_public=? or user_id=?", true, (logged_in_user ? logged_in_user.id : 0)]
628 633 @key = logged_in_user.get_or_create_rss_key.value if logged_in_user
629 634 end
630 635
631 636 private
632 637 # Find project of id params[:id]
633 638 # if not found, redirect to project list
634 639 # Used as a before_filter
635 640 def find_project
636 641 @project = Project.find(params[:id])
637 642 @html_title = @project.name
638 643 rescue ActiveRecord::RecordNotFound
639 644 render_404
640 645 end
641 646
642 647 def retrieve_selected_tracker_ids(selectable_trackers)
643 648 if ids = params[:tracker_ids]
644 649 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
645 650 else
646 651 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
647 652 end
648 653 end
649 654
650 655 # Retrieve query from session or build a new query
651 656 def retrieve_query
652 657 if params[:query_id]
653 658 @query = @project.queries.find(params[:query_id])
654 659 @query.executed_by = logged_in_user
655 660 session[:query] = @query
656 661 else
657 662 if params[:set_filter] or !session[:query] or session[:query].project_id != @project.id
658 663 # Give it a name, required to be valid
659 664 @query = Query.new(:name => "_", :executed_by => logged_in_user)
660 665 @query.project = @project
661 666 if params[:fields] and params[:fields].is_a? Array
662 667 params[:fields].each do |field|
663 668 @query.add_filter(field, params[:operators][field], params[:values][field])
664 669 end
665 670 else
666 671 @query.available_filters.keys.each do |field|
667 672 @query.add_short_filter(field, params[field]) if params[field]
668 673 end
669 674 end
670 675 session[:query] = @query
671 676 else
672 677 @query = session[:query]
673 678 end
674 679 end
675 680 end
676 681 end
@@ -1,28 +1,178
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 ProjectsHelper
19 19 def link_to_version(version, options = {})
20 20 return '' unless version && version.is_a?(Version)
21 21 link_to version.name, {:controller => 'projects',
22 22 :action => 'roadmap',
23 23 :id => version.project_id,
24 24 :completed => (version.completed? ? 1 : nil),
25 25 :anchor => version.name
26 26 }, options
27 27 end
28
29 # Generates a gantt image
30 # Only defined if RMagick is avalaible
31 def gantt_image(events, date_from, months, zoom)
32 date_to = (date_from >> months)-1
33 show_weeks = zoom > 1
34 show_days = zoom > 2
35
36 subject_width = 320
37 header_heigth = 18
38 # width of one day in pixels
39 zoom = zoom*2
40 g_width = (date_to - date_from + 1)*zoom
41 g_height = 20 * events.length + 20
42 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
43 height = g_height + headers_heigth
44
45 imgl = Magick::ImageList.new
46 imgl.new_image(subject_width+g_width+1, height)
47 gc = Magick::Draw.new
48
49 # Subjects
50 top = headers_heigth + 20
51 gc.fill('black')
52 gc.stroke('transparent')
53 gc.stroke_width(1)
54 events.each do |i|
55 gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name))
56 top = top + 20
57 end
58
59 # Months headers
60 month_f = date_from
61 left = subject_width
62 months.times do
63 width = ((month_f >> 1) - month_f) * zoom
64 gc.fill('white')
65 gc.stroke('grey')
66 gc.stroke_width(1)
67 gc.rectangle(left, 0, left + width, height)
68 gc.fill('black')
69 gc.stroke('transparent')
70 gc.stroke_width(1)
71 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
72 left = left + width
73 month_f = month_f >> 1
74 end
75
76 # Weeks headers
77 if show_weeks
78 left = subject_width
79 height = header_heigth
80 if date_from.cwday == 1
81 # date_from is monday
82 week_f = date_from
83 else
84 # find next monday after date_from
85 week_f = date_from + (7 - date_from.cwday + 1)
86 width = (7 - date_from.cwday + 1) * zoom
87 gc.fill('white')
88 gc.stroke('grey')
89 gc.stroke_width(1)
90 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
91 left = left + width
92 end
93 while week_f <= date_to
94 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
95 gc.fill('white')
96 gc.stroke('grey')
97 gc.stroke_width(1)
98 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
99 gc.fill('black')
100 gc.stroke('transparent')
101 gc.stroke_width(1)
102 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
103 left = left + width
104 week_f = week_f+7
105 end
106 end
107
108 # Days details (week-end in grey)
109 if show_days
110 left = subject_width
111 height = g_height + header_heigth - 1
112 wday = date_from.cwday
113 (date_to - date_from + 1).to_i.times do
114 width = zoom
115 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
116 gc.stroke('grey')
117 gc.stroke_width(1)
118 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
119 left = left + width
120 wday = wday + 1
121 wday = 1 if wday > 7
122 end
123 end
124
125 # border
126 gc.fill('transparent')
127 gc.stroke('grey')
128 gc.stroke_width(1)
129 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
130 gc.stroke('black')
131 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
132
133 # content
134 top = headers_heigth + 20
135 gc.stroke('transparent')
136 events.each do |i|
137 if i.is_a?(Issue)
138 i_start_date = (i.start_date >= date_from ? i.start_date : date_from )
139 i_end_date = (i.due_date <= date_to ? i.due_date : date_to )
140 i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
141 i_done_date = (i_done_date <= date_from ? date_from : i_done_date )
142 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
143 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
144
145 i_left = subject_width + ((i_start_date - date_from)*zoom).floor
146 i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
147 d_width = ((i_done_date - i_start_date)*zoom).floor # done width
148 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
149
150 gc.fill('grey')
151 gc.rectangle(i_left, top, i_left + i_width, top - 6)
152 gc.fill('red')
153 gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
154 gc.fill('blue')
155 gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
156 gc.fill('black')
157 gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
158 else
159 i_left = subject_width + ((i.start_date - date_from)*zoom).floor
160 gc.fill('green')
161 gc.rectangle(i_left, top, i_left + 6, top - 6)
162 gc.fill('black')
163 gc.text(i_left + 11, top + 1, i.name)
164 end
165 top = top + 20
166 end
167
168 # today red line
169 if Date.today >= @date_from and Date.today <= @date_to
170 gc.stroke('red')
171 x = (Date.today-@date_from+1)*zoom + subject_width
172 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
173 end
174
175 gc.draw(imgl)
176 imgl
177 end if Object.const_defined?(:Magick)
28 178 end
@@ -1,244 +1,246
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 <% cache(:year => @year_from, :month => @month_from, :months => @months, :zoom => @zoom, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %>
26 25 <div class="contextual">
27 26 <%= l(:label_export_to) %>
28 <%= link_to 'PDF', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :output => 'pdf'}, :class => 'icon icon-pdf' %>
27 <%= 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' %>
28 <%= 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') %>
29 29 </div>
30 30
31 31 <h2><%= l(:label_gantt) %></h2>
32 32
33 33 <% form_tag do %>
34 34 <table width="100%">
35 35 <tr>
36 36 <td align="left">
37 37 <input type="text" name="months" size="2" value="<%= @months %>" />
38 38 <%= l(:label_months_from) %>
39 39 <%= select_month(@month_from, :prefix => "month", :discard_type => true) %>
40 40 <%= select_year(@year_from, :prefix => "year", :discard_type => true) %>
41 41 <%= hidden_field_tag 'zoom', @zoom %>
42 42 <%= submit_tag l(:button_submit), :class => "button-small" %>
43 43 </td>
44 44 <td>
45 45 <%= toggle_link l(:label_options), "trackerselect" %>
46 46 <div id="trackerselect" class="rightbox overlay" style="width:140px; display: none;">
47 47 <p><strong><%=l(:label_tracker_plural)%></strong></p>
48 48 <% @trackers.each do |tracker| %>
49 49 <%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %>
50 50 <%= tracker.name %><br />
51 51 <% end %>
52 52 <% if @project.active_children.any? %>
53 53 <p><strong><%=l(:label_subproject_plural)%></strong></p>
54 54 <%= check_box_tag "with_subprojects", 1, params[:with_subprojects] %> <%= l(:general_text_Yes) %>
55 55 <% end %>
56 56 <p><center><%= submit_tag l(:button_apply), :class => 'button-small' %></center></p>
57 57 </div>
58 58 </td>
59 59 <td align="right">
60 60 <%= if @zoom < 4
61 61 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]}
62 62 else
63 63 image_tag 'zoom_in_g.png'
64 64 end %>
65 65 <%= if @zoom > 1
66 66 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]}
67 67 else
68 68 image_tag 'zoom_out_g.png'
69 69 end %>
70 70 </td>
71 71 </tr>
72 72 </table>
73 73 <% end %>
74 74
75 <% cache(:year => @year_from, :month => @month_from, :months => @months, :zoom => @zoom, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %>
76
75 77 <table width="100%" style="border:0; border-collapse: collapse;">
76 78 <tr>
77 79 <td style="width:<%= subject_width %>px;">
78 80
79 81 <div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
80 82 <div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
81 83 <div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
82 84 <%
83 85 #
84 86 # Tasks subjects
85 87 #
86 88 top = headers_height + 8
87 89 @events.each do |i| %>
88 90 <div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:4px;overflow:hidden;"><small>
89 91 <% if i.is_a? Issue %>
90 92 <%= link_to_issue i %><%= " (#{i.project.name})" unless @project && @project == i.project %>:
91 93 <%=h i.subject %>
92 94 <% else %>
93 95 <%= link_to_version i, :class => "icon icon-package" %>
94 96 <% end %>
95 97 </small></div>
96 98 <% top = top + 20
97 99 end %>
98 100 </div>
99 101 </td>
100 102 <td>
101 103
102 104 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
103 105 <div style="width:<%= g_width-1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr">&nbsp;</div>
104 106 <%
105 107 #
106 108 # Months headers
107 109 #
108 110 month_f = @date_from
109 111 left = 0
110 112 height = (show_weeks ? header_heigth : header_heigth + g_height)
111 113 @months.times do
112 114 width = ((month_f >> 1) - month_f) * zoom - 1
113 115 %>
114 116 <div style="left:<%= left %>px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
115 117 <%= 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}"%>
116 118 </div>
117 119 <%
118 120 left = left + width + 1
119 121 month_f = month_f >> 1
120 122 end %>
121 123
122 124 <%
123 125 #
124 126 # Weeks headers
125 127 #
126 128 if show_weeks
127 129 left = 0
128 130 height = (show_days ? header_heigth-1 : header_heigth-1 + g_height)
129 131 if @date_from.cwday == 1
130 132 # @date_from is monday
131 133 week_f = @date_from
132 134 else
133 135 # find next monday after @date_from
134 136 week_f = @date_from + (7 - @date_from.cwday + 1)
135 137 width = (7 - @date_from.cwday + 1) * zoom-1
136 138 %>
137 139 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">&nbsp;</div>
138 140 <%
139 141 left = left + width+1
140 142 end %>
141 143 <%
142 144 while week_f <= @date_to
143 145 width = (week_f + 6 <= @date_to) ? 7 * zoom -1 : (@date_to - week_f + 1) * zoom-1
144 146 %>
145 147 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
146 148 <small><%= week_f.cweek if width >= 16 %></small>
147 149 </div>
148 150 <%
149 151 left = left + width+1
150 152 week_f = week_f+7
151 153 end
152 154 end %>
153 155
154 156 <%
155 157 #
156 158 # Days headers
157 159 #
158 160 if show_days
159 161 left = 0
160 162 height = g_height + header_heigth - 1
161 163 wday = @date_from.cwday
162 164 (@date_to - @date_from + 1).to_i.times do
163 165 width = zoom - 1
164 166 %>
165 167 <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">
166 168 <%= day_name(wday).first %>
167 169 </div>
168 170 <%
169 171 left = left + width+1
170 172 wday = wday + 1
171 173 wday = 1 if wday > 7
172 174 end
173 175 end %>
174 176
175 177 <%
176 178 #
177 179 # Tasks
178 180 #
179 181 top = headers_height + 10
180 182 @events.each do |i|
181 183 if i.is_a? Issue
182 184 i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from )
183 185 i_end_date = (i.due_date <= @date_to ? i.due_date : @date_to )
184 186
185 187 i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
186 188 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
187 189 i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
188 190
189 191 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
190 192
191 193 i_left = ((i_start_date - @date_from)*zoom).floor
192 194 i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders)
193 195 d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width
194 196 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width
195 197 %>
196 198 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="task task_todo">&nbsp;</div>
197 199 <% if l_width > 0 %>
198 200 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="task task_late">&nbsp;</div>
199 201 <% end %>
200 202 <% if d_width > 0 %>
201 203 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="task task_done">&nbsp;</div>
202 204 <% end %>
203 205 <div style="top:<%= top %>px;left:<%= i_left + i_width + 5 %>px;background:#fff;" class="task">
204 206 <%= i.status.name %>
205 207 <%= (i.done_ratio).to_i %>%
206 208 </div>
207 209 <% # === tooltip === %>
208 210 <div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;">
209 211 <span class="tip">
210 212 <%= render :partial => "issues/tooltip", :locals => { :issue => i }%>
211 213 </span></div>
212 214 <% else
213 215 i_left = ((i.start_date - @date_from)*zoom).floor
214 216 %>
215 217 <div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone">&nbsp;</div>
216 218 <div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task">
217 219 <strong><%= i.name %></strong>
218 220 </div>
219 221 <% end %>
220 222 <% top = top + 20
221 223 end %>
222 224
223 225 <% end # cache
224 226 %>
225 227
226 228 <%
227 229 #
228 230 # Today red line (excluded from cache)
229 231 #
230 232 if Date.today >= @date_from and Date.today <= @date_to %>
231 233 <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>
232 234 <% end %>
233 235
234 236 </div>
235 237 </td>
236 238 </tr>
237 239 </table>
238 240
239 241 <table width="100%">
240 242 <tr>
241 243 <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>
242 244 <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>
243 245 </tr>
244 246 </table>
@@ -1,5 +1,11
1 1 require 'redmine/version'
2 2 require 'redmine/mime_type'
3 3 require 'redmine/acts_as_watchable/init'
4 4
5 begin
6 require_library_or_gem 'rmagick' unless Object.const_defined?(:Magick)
7 rescue LoadError
8 # RMagick is not available
9 end
10
5 11 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs )
@@ -1,694 +1,695
1 1 /* andreas08 - an open source xhtml/css website layout by Andreas Viklund - http://andreasviklund.com . Free to use in any way and for any purpose as long as the proper credits are given to the original designer. Version: 1.0, November 28, 2005 */
2 2 /* Edited by Jean-Philippe Lang *>
3 3 /**************** Body and tag styles ****************/
4 4
5 5 #header * {margin:0; padding:0;}
6 6 p, ul, ol, li {margin:0; padding:0;}
7 7
8 8 body{
9 9 font:76% Verdana,Tahoma,Arial,sans-serif;
10 10 line-height:1.4em;
11 11 text-align:center;
12 12 color:#303030;
13 13 background:#e8eaec;
14 14 margin:0;
15 15 }
16 16
17 17 a{color:#467aa7;font-weight:bold;text-decoration:none;background-color:inherit;}
18 18 a:hover{color:#2a5a8a; text-decoration:none; background-color:inherit;}
19 19 a img{border:none;}
20 20
21 21 p{margin:0 0 1em 0;}
22 22 p form{margin-top:0; margin-bottom:20px;}
23 23
24 24 img.left,img.center,img.right{padding:4px; border:1px solid #a0a0a0;}
25 25 img.left{float:left; margin:0 12px 5px 0;}
26 26 img.center{display:block; margin:0 auto 5px auto;}
27 27 img.right{float:right; margin:0 0 5px 12px;}
28 28
29 29 /**************** Header and navigation styles ****************/
30 30
31 31 #container{
32 32 width:100%;
33 33 min-width: 800px;
34 34 margin:0;
35 35 padding:0;
36 36 text-align:left;
37 37 background:#ffffff;
38 38 color:#303030;
39 39 }
40 40
41 41 #header{
42 42 height:4.5em;
43 43 margin:0;
44 44 background:#467aa7;
45 45 color:#ffffff;
46 46 margin-bottom:1px;
47 47 }
48 48
49 49 #header h1{
50 50 padding:10px 0 0 20px;
51 51 font-size:2em;
52 52 background-color:inherit;
53 53 color:#fff;
54 54 letter-spacing:-1px;
55 55 font-weight:bold;
56 56 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
57 57 }
58 58
59 59 #header h2{
60 60 margin:3px 0 0 40px;
61 61 font-size:1.5em;
62 62 background-color:inherit;
63 63 color:#f0f2f4;
64 64 letter-spacing:-1px;
65 65 font-weight:normal;
66 66 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
67 67 }
68 68
69 69 #header a {color:#fff;}
70 70
71 71 #navigation{
72 72 height:2.2em;
73 73 line-height:2.2em;
74 74 margin:0;
75 75 background:#578bb8;
76 76 color:#ffffff;
77 77 }
78 78
79 79 #navigation li{
80 80 float:left;
81 81 list-style-type:none;
82 82 border-right:1px solid #ffffff;
83 83 white-space:nowrap;
84 84 }
85 85
86 86 #navigation li.right {
87 87 float:right;
88 88 list-style-type:none;
89 89 border-right:0;
90 90 border-left:1px solid #ffffff;
91 91 white-space:nowrap;
92 92 }
93 93
94 94 #navigation li a{
95 95 display:block;
96 96 padding:0px 10px 0px 22px;
97 97 font-size:0.8em;
98 98 font-weight:normal;
99 99 text-decoration:none;
100 100 background-color:inherit;
101 101 color: #ffffff;
102 102 }
103 103
104 104 #navigation li.submenu {background:url(../images/arrow_down.png) 96% 80% no-repeat;}
105 105 #navigation li.submenu a {padding:0px 16px 0px 22px;}
106 106 * html #navigation a {width:1%;}
107 107
108 108 #navigation .selected,#navigation a:hover{
109 109 color:#ffffff;
110 110 text-decoration:none;
111 111 background-color: #80b0da;
112 112 }
113 113
114 114 /**************** Icons *******************/
115 115 .icon {
116 116 background-position: 0% 40%;
117 117 background-repeat: no-repeat;
118 118 padding-left: 20px;
119 119 padding-top: 2px;
120 120 padding-bottom: 3px;
121 121 vertical-align: middle;
122 122 }
123 123
124 124 #navigation .icon {
125 125 background-position: 4px 50%;
126 126 }
127 127
128 128 .icon22 {
129 129 background-position: 0% 40%;
130 130 background-repeat: no-repeat;
131 131 padding-left: 26px;
132 132 line-height: 22px;
133 133 vertical-align: middle;
134 134 }
135 135
136 136 .icon-add { background-image: url(../images/add.png); }
137 137 .icon-edit { background-image: url(../images/edit.png); }
138 138 .icon-del { background-image: url(../images/delete.png); }
139 139 .icon-move { background-image: url(../images/move.png); }
140 140 .icon-save { background-image: url(../images/save.png); }
141 141 .icon-cancel { background-image: url(../images/cancel.png); }
142 142 .icon-pdf { background-image: url(../images/pdf.png); }
143 143 .icon-csv { background-image: url(../images/csv.png); }
144 144 .icon-html { background-image: url(../images/html.png); }
145 .icon-image { background-image: url(../images/image.png); }
145 146 .icon-txt { background-image: url(../images/txt.png); }
146 147 .icon-file { background-image: url(../images/file.png); }
147 148 .icon-folder { background-image: url(../images/folder.png); }
148 149 .icon-package { background-image: url(../images/package.png); }
149 150 .icon-home { background-image: url(../images/home.png); }
150 151 .icon-user { background-image: url(../images/user.png); }
151 152 .icon-mypage { background-image: url(../images/user_page.png); }
152 153 .icon-admin { background-image: url(../images/admin.png); }
153 154 .icon-projects { background-image: url(../images/projects.png); }
154 155 .icon-logout { background-image: url(../images/logout.png); }
155 156 .icon-help { background-image: url(../images/help.png); }
156 157 .icon-attachment { background-image: url(../images/attachment.png); }
157 158 .icon-index { background-image: url(../images/index.png); }
158 159 .icon-history { background-image: url(../images/history.png); }
159 160 .icon-feed { background-image: url(../images/feed.png); }
160 161 .icon-time { background-image: url(../images/time.png); }
161 162 .icon-stats { background-image: url(../images/stats.png); }
162 163 .icon-warning { background-image: url(../images/warning.png); }
163 164 .icon-fav { background-image: url(../images/fav.png); }
164 165 .icon-fav-off { background-image: url(../images/fav_off.png); }
165 166 .icon-reload { background-image: url(../images/reload.png); }
166 167 .icon-lock { background-image: url(../images/locked.png); }
167 168 .icon-unlock { background-image: url(../images/unlock.png); }
168 169
169 170 .icon22-projects { background-image: url(../images/22x22/projects.png); }
170 171 .icon22-users { background-image: url(../images/22x22/users.png); }
171 172 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
172 173 .icon22-role { background-image: url(../images/22x22/role.png); }
173 174 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
174 175 .icon22-options { background-image: url(../images/22x22/options.png); }
175 176 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
176 177 .icon22-authent { background-image: url(../images/22x22/authent.png); }
177 178 .icon22-info { background-image: url(../images/22x22/info.png); }
178 179 .icon22-comment { background-image: url(../images/22x22/comment.png); }
179 180 .icon22-package { background-image: url(../images/22x22/package.png); }
180 181 .icon22-settings { background-image: url(../images/22x22/settings.png); }
181 182
182 183 /**************** Content styles ****************/
183 184
184 185 html>body #content {
185 186 height: auto;
186 187 min-height: 500px;
187 188 }
188 189
189 190 #content{
190 191 width: auto;
191 192 height:500px;
192 193 font-size:0.9em;
193 194 padding:20px 10px 10px 20px;
194 195 margin-left: 120px;
195 196 border-left: 1px dashed #c0c0c0;
196 197
197 198 }
198 199
199 200 #content h2, #content div.wiki h1 {
200 201 display:block;
201 202 margin:0 0 16px 0;
202 203 font-size:1.7em;
203 204 font-weight:normal;
204 205 letter-spacing:-1px;
205 206 color:#606060;
206 207 background-color:inherit;
207 208 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
208 209 }
209 210
210 211 #content h2 a{font-weight:normal;}
211 212 #content h3{margin:0 0 12px 0; font-size:1.4em;color:#707070;font-family: Trebuchet MS,Georgia,"Times New Roman",serif;}
212 213 #content h4{font-size: 1em; margin-bottom: 12px; margin-top: 20px; font-weight: normal; border-bottom: dotted 1px #c0c0c0;}
213 214 #content a:hover,#subcontent a:hover{text-decoration:underline;}
214 215 #content ul,#content ol{margin:0 5px 16px 35px;}
215 216 #content dl{margin:0 5px 10px 25px;}
216 217 #content dt{font-weight:bold; margin-bottom:5px;}
217 218 #content dd{margin:0 0 10px 15px;}
218 219
219 220 #content .tabs{height: 2.6em;}
220 221 #content .tabs ul{margin:0;}
221 222 #content .tabs ul li{
222 223 float:left;
223 224 list-style-type:none;
224 225 white-space:nowrap;
225 226 margin-right:8px;
226 227 background:#fff;
227 228 }
228 229 #content .tabs ul li a{
229 230 display:block;
230 231 font-size: 0.9em;
231 232 text-decoration:none;
232 233 line-height:1em;
233 234 padding:4px;
234 235 border: 1px solid #c0c0c0;
235 236 }
236 237
237 238 #content .tabs ul li a.selected, #content .tabs ul li a:hover{
238 239 background-color: #80b0da;
239 240 border: 1px solid #80b0da;
240 241 color: #fff;
241 242 text-decoration:none;
242 243 }
243 244
244 245 /***********************************************/
245 246
246 247 form {display: inline;}
247 248 blockquote {padding-left: 6px; border-left: 2px solid #ccc;}
248 249 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
249 250
250 251 input.button-small {font-size: 0.8em;}
251 252 textarea.wiki-edit { width: 99.5%; }
252 253 .select-small {font-size: 0.8em;}
253 254 label {font-weight: bold; font-size: 1em; color: #505050;}
254 255 fieldset {border:1px solid #c0c0c0; padding: 6px;}
255 256 legend {color: #505050;}
256 257 .required {color: #bb0000;}
257 258 .odd {background-color:#f6f7f8;}
258 259 .even {background-color: #fff;}
259 260 hr { border:0; border-top: dotted 1px #fff; border-bottom: dotted 1px #c0c0c0; }
260 261 table p {margin:0; padding:0;}
261 262
262 263 .highlight { background-color: #FCFD8D;}
263 264
264 265 div.square {
265 266 border: 1px solid #999;
266 267 float: left;
267 268 margin: .4em .5em 0 0;
268 269 overflow: hidden;
269 270 width: .6em; height: .6em;
270 271 }
271 272
272 273 ul.documents {
273 274 list-style-type: none;
274 275 padding: 0;
275 276 margin: 0;
276 277 }
277 278
278 279 ul.documents li {
279 280 background-image: url(../images/32x32/file.png);
280 281 background-repeat: no-repeat;
281 282 background-position: 0 1px;
282 283 padding-left: 36px;
283 284 margin-bottom: 10px;
284 285 margin-left: -37px;
285 286 }
286 287
287 288 /********** Table used to display lists of things ***********/
288 289
289 290 table.list {
290 291 width:100%;
291 292 border-collapse: collapse;
292 293 border: 1px dotted #d0d0d0;
293 294 margin-bottom: 6px;
294 295 }
295 296
296 297 table.with-cells td {
297 298 border: 1px solid #d7d7d7;
298 299 }
299 300
300 301 table.list td {
301 302 padding:2px;
302 303 }
303 304
304 305 table.list thead th {
305 306 text-align: center;
306 307 background: #eee;
307 308 border: 1px solid #d7d7d7;
308 309 color: #777;
309 310 }
310 311
311 312 table.list tbody th {
312 313 font-weight: bold;
313 314 background: #eed;
314 315 border: 1px solid #d7d7d7;
315 316 color: #777;
316 317 }
317 318
318 319 /*========== Drop down menu ==============*/
319 320 div.menu {
320 321 background-color: #FFFFFF;
321 322 border-style: solid;
322 323 border-width: 1px;
323 324 border-color: #7F9DB9;
324 325 position: absolute;
325 326 top: 0px;
326 327 left: 0px;
327 328 padding: 0;
328 329 visibility: hidden;
329 330 z-index: 101;
330 331 }
331 332
332 333 div.menu a.menuItem {
333 334 font-size: 10px;
334 335 font-weight: normal;
335 336 line-height: 2em;
336 337 color: #000000;
337 338 background-color: #FFFFFF;
338 339 cursor: default;
339 340 display: block;
340 341 padding: 0 1em;
341 342 margin: 0;
342 343 border: 0;
343 344 text-decoration: none;
344 345 white-space: nowrap;
345 346 }
346 347
347 348 div.menu a.menuItem:hover, div.menu a.menuItemHighlight {
348 349 background-color: #80b0da;
349 350 color: #ffffff;
350 351 }
351 352
352 353 div.menu a.menuItem span.menuItemText {}
353 354
354 355 div.menu a.menuItem span.menuItemArrow {
355 356 margin-right: -.75em;
356 357 }
357 358
358 359 /**************** Sidebar styles ****************/
359 360
360 361 #subcontent{
361 362 position: absolute;
362 363 left: 0px;
363 364 width:95px;
364 365 padding:20px 20px 10px 5px;
365 366 overflow: hidden;
366 367 }
367 368
368 369 #subcontent h2{
369 370 display:block;
370 371 margin:0 0 5px 0;
371 372 font-size:1.0em;
372 373 font-weight:bold;
373 374 text-align:left;
374 375 color:#606060;
375 376 background-color:inherit;
376 377 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
377 378 }
378 379
379 380 #subcontent p{margin:0 0 16px 0; font-size:0.9em;}
380 381
381 382 /**************** Menublock styles ****************/
382 383
383 384 .menublock{margin:0 0 20px 8px; font-size:0.8em;}
384 385 .menublock li{list-style:none; display:block; padding:1px; margin-bottom:0px;}
385 386 .menublock li a{font-weight:bold; text-decoration:none;}
386 387 .menublock li a:hover{text-decoration:none;}
387 388 .menublock li ul{margin:0; font-size:1em; font-weight:normal;}
388 389 .menublock li ul li{margin-bottom:0;}
389 390 .menublock li ul a{font-weight:normal;}
390 391
391 392 /**************** Footer styles ****************/
392 393
393 394 #footer{
394 395 clear:both;
395 396 padding:5px 0;
396 397 margin:0;
397 398 font-size:0.9em;
398 399 color:#f0f0f0;
399 400 background:#467aa7;
400 401 }
401 402
402 403 #footer p{padding:0; margin:0; text-align:center;}
403 404 #footer a{color:#f0f0f0; background-color:inherit; font-weight:bold;}
404 405 #footer a:hover{color:#ffffff; background-color:inherit; text-decoration: underline;}
405 406
406 407 /**************** Misc classes and styles ****************/
407 408
408 409 .splitcontentleft{float:left; width:49%;}
409 410 .splitcontentright{float:right; width:49%;}
410 411 .clear{clear:both;}
411 412 .small{font-size:0.8em;line-height:1.4em;padding:0 0 0 0;}
412 413 .hide{display:none;}
413 414 .textcenter{text-align:center;}
414 415 .textright{text-align:right;}
415 416 .important{color:#f02025; background-color:inherit; font-weight:bold;}
416 417
417 418 .box{
418 419 margin:0 0 20px 0;
419 420 padding:10px;
420 421 border:1px solid #c0c0c0;
421 422 background-color:#fafbfc;
422 423 color:#505050;
423 424 line-height:1.5em;
424 425 }
425 426
426 427 a.close-icon {
427 428 display:block;
428 429 margin-top:3px;
429 430 overflow:hidden;
430 431 width:12px;
431 432 height:12px;
432 433 background-repeat: no-repeat;
433 434 cursor:pointer;
434 435 background-image:url('../images/close.png');
435 436 }
436 437
437 438 a.close-icon:hover {
438 439 background-image:url('../images/close_hl.png');
439 440 }
440 441
441 442 .rightbox{
442 443 background: #fafbfc;
443 444 border: 1px solid #c0c0c0;
444 445 float: right;
445 446 padding: 8px;
446 447 position: relative;
447 448 margin: 0 5px 5px;
448 449 }
449 450
450 451 div.attachments {padding-left: 6px; border-left: 2px solid #ccc; margin-bottom: 8px;}
451 452 div.attachments p {margin-bottom:2px;}
452 453
453 454 .overlay{
454 455 position: absolute;
455 456 margin-left:0;
456 457 z-index: 50;
457 458 }
458 459
459 460 .layout-active {
460 461 background: #ECF3E1;
461 462 }
462 463
463 464 .block-receiver {
464 465 border:1px dashed #c0c0c0;
465 466 margin-bottom: 20px;
466 467 padding: 15px 0 15px 0;
467 468 }
468 469
469 470 .mypage-box {
470 471 margin:0 0 20px 0;
471 472 color:#505050;
472 473 line-height:1.5em;
473 474 }
474 475
475 476 .handle {
476 477 cursor: move;
477 478 }
478 479
479 480 .login {
480 481 width: 50%;
481 482 text-align: left;
482 483 }
483 484
484 485 img.calendar-trigger {
485 486 cursor: pointer;
486 487 vertical-align: middle;
487 488 margin-left: 4px;
488 489 }
489 490
490 491 #history p {
491 492 margin-left: 34px;
492 493 }
493 494
494 495 .progress {
495 496 border: 1px solid #D7D7D7;
496 497 border-collapse: collapse;
497 498 border-spacing: 0pt;
498 499 empty-cells: show;
499 500 padding: 3px;
500 501 width: 40em;
501 502 text-align: center;
502 503 }
503 504
504 505 .progress td { height: 1em; }
505 506 .progress .closed { background: #BAE0BA none repeat scroll 0%; }
506 507 .progress .open { background: #FFF none repeat scroll 0%; }
507 508
508 509 /***** Contextual links div *****/
509 510 .contextual {
510 511 float: right;
511 512 font-size: 0.8em;
512 513 line-height: 16px;
513 514 padding: 2px;
514 515 }
515 516
516 517 .contextual select, .contextual input {
517 518 font-size: 1em;
518 519 }
519 520
520 521 /***** Gantt chart *****/
521 522 .gantt_hdr {
522 523 position:absolute;
523 524 top:0;
524 525 height:16px;
525 526 border-top: 1px solid #c0c0c0;
526 527 border-bottom: 1px solid #c0c0c0;
527 528 border-right: 1px solid #c0c0c0;
528 529 text-align: center;
529 530 overflow: hidden;
530 531 }
531 532
532 533 .task {
533 534 position: absolute;
534 535 height:8px;
535 536 font-size:0.8em;
536 537 color:#888;
537 538 padding:0;
538 539 margin:0;
539 540 line-height:0.8em;
540 541 }
541 542
542 543 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
543 544 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
544 545 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
545 546 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
546 547
547 548 /***** Tooltips ******/
548 549 .tooltip{position:relative;z-index:24;}
549 550 .tooltip:hover{z-index:25;color:#000;}
550 551 .tooltip span.tip{display: none; text-align:left;}
551 552
552 553 div.tooltip:hover span.tip{
553 554 display:block;
554 555 position:absolute;
555 556 top:12px; left:24px; width:270px;
556 557 border:1px solid #555;
557 558 background-color:#fff;
558 559 padding: 4px;
559 560 font-size: 0.8em;
560 561 color:#505050;
561 562 }
562 563
563 564 /***** CSS FORM ******/
564 565 .tabular p{
565 566 margin: 0;
566 567 padding: 5px 0 8px 0;
567 568 padding-left: 180px; /*width of left column containing the label elements*/
568 569 height: 1%;
569 570 clear:both;
570 571 }
571 572
572 573 .tabular label{
573 574 font-weight: bold;
574 575 float: left;
575 576 margin-left: -180px; /*width of left column*/
576 577 margin-bottom: 10px;
577 578 width: 175px; /*width of labels. Should be smaller than left column to create some right
578 579 margin*/
579 580 }
580 581
581 582 .error {
582 583 color: #cc0000;
583 584 }
584 585
585 586 #settings .tabular p{ padding-left: 300px; }
586 587 #settings .tabular label{ margin-left: -300px; width: 295px; }
587 588
588 589 /*.threepxfix class below:
589 590 Targets IE6- ONLY. Adds 3 pixel indent for multi-line form contents.
590 591 to account for 3 pixel bug: http://www.positioniseverything.net/explorer/threepxtest.html
591 592 */
592 593
593 594 * html .threepxfix{
594 595 margin-left: 3px;
595 596 }
596 597
597 598 /***** Wiki sections ****/
598 599 #content div.wiki { font-size: 110%}
599 600
600 601 #content div.wiki h2, div.wiki h3 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; color:#606060; }
601 602 #content div.wiki h2 { font-size: 1.4em;}
602 603 #content div.wiki h3 { font-size: 1.2em;}
603 604
604 605 div.wiki table {
605 606 border: 1px solid #505050;
606 607 border-collapse: collapse;
607 608 }
608 609
609 610 div.wiki table, div.wiki td {
610 611 border: 1px solid #bbb;
611 612 padding: 4px;
612 613 }
613 614
614 615 div.wiki a {
615 616 background-position: 0% 60%;
616 617 background-repeat: no-repeat;
617 618 padding-left: 12px;
618 619 background-image: url(../images/external.png);
619 620 }
620 621
621 622 div.wiki a.wiki-page, div.wiki a.issue, div.wiki a.changeset {
622 623 padding-left: 0;
623 624 background-image: none;
624 625 }
625 626
626 627 div.wiki code {
627 628 font-size: 1.2em;
628 629 }
629 630
630 631 div.wiki img {
631 632 margin: 6px;
632 633 }
633 634
634 635 .diff_out{
635 636 background: #fcc;
636 637 }
637 638
638 639 .diff_in{
639 640 background: #cfc;
640 641 }
641 642
642 643 #preview .preview { background: #fafbfc url(../images/draft.png); }
643 644
644 645 #ajax-indicator {
645 646 position: absolute; /* fixed not supported by IE */
646 647 background-color:#eee;
647 648 border: 1px solid #bbb;
648 649 top:35%;
649 650 left:40%;
650 651 width:20%;
651 652 font-weight:bold;
652 653 text-align:center;
653 654 padding:0.6em;
654 655 z-index:100;
655 656 filter:alpha(opacity=50);
656 657 -moz-opacity:0.5;
657 658 opacity: 0.5;
658 659 -khtml-opacity: 0.5;
659 660 }
660 661
661 662 html>body #ajax-indicator { position: fixed; }
662 663
663 664 #ajax-indicator span {
664 665 background-position: 0% 40%;
665 666 background-repeat: no-repeat;
666 667 background-image: url(../images/loading.gif);
667 668 padding-left: 26px;
668 669 vertical-align: bottom;
669 670 }
670 671
671 672 /***** Flash & error messages ****/
672 673 #flash div, #errorExplanation {
673 674 padding: 4px 4px 4px 30px;
674 675 margin-bottom: 16px;
675 676 font-size: 1.1em;
676 677 border: 2px solid;
677 678 }
678 679
679 680 #flash div.error, #errorExplanation {
680 681 background: url(../images/false.png) 8px 5px no-repeat;
681 682 background-color: #ffe3e3;
682 683 border-color: #dd0000;
683 684 color: #550000;
684 685 }
685 686
686 687 #flash div.notice {
687 688 background: url(../images/true.png) 8px 5px no-repeat;
688 689 background-color: #dfffdf;
689 690 border-color: #9fcf9f;
690 691 color: #005f00;
691 692 }
692 693
693 694 #errorExplanation ul { margin-bottom: 0px; }
694 695 #errorExplanation ul li { list-style: none; margin-left: -16px;}
General Comments 0
You need to be logged in to leave comments. Login now