##// END OF EJS Templates
Merged nested projects branch. Removes limit on subproject nesting (#594)....
Jean-Philippe Lang -
r2302:c9906480d3f2
parent child
Show More
@@ -0,0 +1,11
1 class AddProjectsLftAndRgt < ActiveRecord::Migration
2 def self.up
3 add_column :projects, :lft, :integer
4 add_column :projects, :rgt, :integer
5 end
6
7 def self.down
8 remove_column :projects, :lft
9 remove_column :projects, :rgt
10 end
11 end
@@ -0,0 +1,8
1 class BuildProjectsTree < ActiveRecord::Migration
2 def self.up
3 Project.rebuild!
4 end
5
6 def self.down
7 end
8 end
@@ -0,0 +1,20
1 Copyright (c) 2007 [name of plugin creator]
2
3 Permission is hereby granted, free of charge, to any person obtaining
4 a copy of this software and associated documentation files (the
5 "Software"), to deal in the Software without restriction, including
6 without limitation the rights to use, copy, modify, merge, publish,
7 distribute, sublicense, and/or sell copies of the Software, and to
8 permit persons to whom the Software is furnished to do so, subject to
9 the following conditions:
10
11 The above copyright notice and this permission notice shall be
12 included in all copies or substantial portions of the Software.
13
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,64
1 = AwesomeNestedSet
2
3 Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but awesomer.
4
5 == What makes this so awesome?
6
7 This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support.
8
9 == Installation
10
11 If you are on Rails 2.1 or later:
12
13 script/plugin install git://github.com/collectiveidea/awesome_nested_set.git
14
15 == Usage
16
17 To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id:
18
19 class CreateCategories < ActiveRecord::Migration
20 def self.up
21 create_table :categories do |t|
22 t.string :name
23 t.integer :parent_id
24 t.integer :lft
25 t.integer :rgt
26 end
27 end
28
29 def self.down
30 drop_table :categories
31 end
32 end
33
34 Enable the nested set functionality by declaring acts_as_nested_set on your model
35
36 class Category < ActiveRecord::Base
37 acts_as_nested_set
38 end
39
40 Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet::SingletonMethods for more info.
41
42 == View Helper
43
44 The view helper is called #nested_set_options.
45
46 Example usage:
47
48 <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
49
50 <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
51
52 See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
53
54 == References
55
56 You can learn more about nested sets at:
57
58 http://www.dbmsmag.com/9603d06.html
59 http://threebit.net/tutorials/nestedset/tutorial1.html
60 http://api.rubyonrails.com/classes/ActiveRecord/Acts/NestedSet/ClassMethods.html
61 http://opensource.symetrie.com/trac/better_nested_set/
62
63
64 Copyright (c) 2008 Collective Idea, released under the MIT license No newline at end of file
@@ -0,0 +1,46
1 require 'rake'
2 require 'rake/testtask'
3 require 'rake/rdoctask'
4 require 'rake/gempackagetask'
5 require 'rcov/rcovtask'
6 require "load_multi_rails_rake_tasks"
7
8 spec = eval(File.read("#{File.dirname(__FILE__)}/awesome_nested_set.gemspec"))
9 PKG_NAME = spec.name
10 PKG_VERSION = spec.version
11
12 Rake::GemPackageTask.new(spec) do |pkg|
13 pkg.need_zip = true
14 pkg.need_tar = true
15 end
16
17
18 desc 'Default: run unit tests.'
19 task :default => :test
20
21 desc 'Test the awesome_nested_set plugin.'
22 Rake::TestTask.new(:test) do |t|
23 t.libs << 'lib'
24 t.pattern = 'test/**/*_test.rb'
25 t.verbose = true
26 end
27
28 desc 'Generate documentation for the awesome_nested_set plugin.'
29 Rake::RDocTask.new(:rdoc) do |rdoc|
30 rdoc.rdoc_dir = 'rdoc'
31 rdoc.title = 'AwesomeNestedSet'
32 rdoc.options << '--line-numbers' << '--inline-source'
33 rdoc.rdoc_files.include('README.rdoc')
34 rdoc.rdoc_files.include('lib/**/*.rb')
35 end
36
37 namespace :test do
38 desc "just rcov minus html output"
39 Rcov::RcovTask.new(:coverage) do |t|
40 # t.libs << 'test'
41 t.test_files = FileList['test/**/*_test.rb']
42 t.output_dir = 'coverage'
43 t.verbose = true
44 t.rcov_opts = %w(--exclude test,/usr/lib/ruby,/Library/Ruby,lib/awesome_nested_set/named_scope.rb --sort coverage)
45 end
46 end No newline at end of file
@@ -0,0 +1,20
1 Gem::Specification.new do |s|
2 s.name = "awesome_nested_set"
3 s.version = "1.1.1"
4 s.summary = "An awesome replacement for acts_as_nested_set and better_nested_set."
5 s.description = s.summary
6
7 s.files = %w(init.rb MIT-LICENSE Rakefile README.rdoc lib/awesome_nested_set.rb lib/awesome_nested_set/compatability.rb lib/awesome_nested_set/helper.rb lib/awesome_nested_set/named_scope.rb rails/init.rb test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml)
8
9 s.add_dependency "activerecord", ['>= 1.1']
10
11 s.has_rdoc = true
12 s.extra_rdoc_files = [ "README.rdoc"]
13 s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
14
15 s.test_files = %w(test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml)
16 s.require_path = 'lib'
17 s.author = "Collective Idea"
18 s.email = "info@collectiveidea.com"
19 s.homepage = "http://collectiveidea.com"
20 end
@@ -0,0 +1,1
1 require File.dirname(__FILE__) + "/rails/init"
This diff has been collapsed as it changes many lines, (547 lines changed) Show them Hide them
@@ -0,0 +1,547
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 def self.included(base)
5 base.extend(SingletonMethods)
6 end
7
8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
9 # an _ordered_ tree, with the added feature that you can select the children and all of their
10 # descendants with a single query. The drawback is that insertion or move need some complex
11 # sql queries. But everything is done here by this module!
12 #
13 # Nested sets are appropriate each time you want either an orderd tree (menus,
14 # commercial categories) or an efficient way of querying big trees (threaded posts).
15 #
16 # == API
17 #
18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
19 # by another easier, except for the creation:
20 #
21 # in acts_as_tree:
22 # item.children.create(:name => "child1")
23 #
24 # in acts_as_nested_set:
25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
26 # child = MyClass.new(:name => "child1")
27 # child.save
28 # # now move the item to its right place
29 # child.move_to_child_of my_item
30 #
31 # You can pass an id or an object to:
32 # * <tt>#move_to_child_of</tt>
33 # * <tt>#move_to_right_of</tt>
34 # * <tt>#move_to_left_of</tt>
35 #
36 module SingletonMethods
37 # Configuration options are:
38 #
39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
40 # * +:left_column+ - column name for left boundry data, default "lft"
41 # * +:right_column+ - column name for right boundry data, default "rgt"
42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
43 # (if it hasn't been already) and use that as the foreign key restriction. You
44 # can also pass an array to scope by multiple attributes.
45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
47 # child objects are destroyed alongside this object by calling their destroy
48 # method. If set to :delete_all (default), all the child objects are deleted
49 # without calling their destroy method.
50 #
51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
53 # to acts_as_nested_set models
54 def acts_as_nested_set(options = {})
55 options = {
56 :parent_column => 'parent_id',
57 :left_column => 'lft',
58 :right_column => 'rgt',
59 :order => 'id',
60 :dependent => :delete_all, # or :destroy
61 }.merge(options)
62
63 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
64 options[:scope] = "#{options[:scope]}_id".intern
65 end
66
67 write_inheritable_attribute :acts_as_nested_set_options, options
68 class_inheritable_reader :acts_as_nested_set_options
69
70 include Comparable
71 include Columns
72 include InstanceMethods
73 extend Columns
74 extend ClassMethods
75
76 # no bulk assignment
77 attr_protected left_column_name.intern,
78 right_column_name.intern,
79 parent_column_name.intern
80
81 before_create :set_default_left_and_right
82 before_destroy :prune_from_tree
83
84 # no assignment to structure fields
85 [left_column_name, right_column_name, parent_column_name].each do |column|
86 module_eval <<-"end_eval", __FILE__, __LINE__
87 def #{column}=(x)
88 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
89 end
90 end_eval
91 end
92
93 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
94 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
95 if self.respond_to?(:define_callbacks)
96 define_callbacks("before_move", "after_move")
97 end
98
99
100 end
101
102 end
103
104 module ClassMethods
105
106 # Returns the first root
107 def root
108 roots.find(:first)
109 end
110
111 def valid?
112 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
113 end
114
115 def left_and_rights_valid?
116 count(
117 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
118 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
119 :conditions =>
120 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
121 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
122 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
123 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
124 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
125 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
126 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
127 ) == 0
128 end
129
130 def no_duplicates_for_columns?
131 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
132 connection.quote_column_name(c)
133 end.push(nil).join(", ")
134 [quoted_left_column_name, quoted_right_column_name].all? do |column|
135 # No duplicates
136 find(:first,
137 :select => "#{scope_string}#{column}, COUNT(#{column})",
138 :group => "#{scope_string}#{column}
139 HAVING COUNT(#{column}) > 1").nil?
140 end
141 end
142
143 # Wrapper for each_root_valid? that can deal with scope.
144 def all_roots_valid?
145 if acts_as_nested_set_options[:scope]
146 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
147 each_root_valid?(grouped_roots)
148 end
149 else
150 each_root_valid?(roots)
151 end
152 end
153
154 def each_root_valid?(roots_to_validate)
155 left = right = 0
156 roots_to_validate.all? do |root|
157 returning(root.left > left && root.right > right) do
158 left = root.left
159 right = root.right
160 end
161 end
162 end
163
164 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
165 def rebuild!
166 # Don't rebuild a valid tree.
167 return true if valid?
168
169 scope = lambda{}
170 if acts_as_nested_set_options[:scope]
171 scope = lambda{|node|
172 scope_column_names.inject(""){|str, column_name|
173 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
174 }
175 }
176 end
177 indices = {}
178
179 set_left_and_rights = lambda do |node|
180 # set left
181 node[left_column_name] = indices[scope.call(node)] += 1
182 # find
183 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
184 # set right
185 node[right_column_name] = indices[scope.call(node)] += 1
186 node.save!
187 end
188
189 # Find root node(s)
190 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
191 # setup index for this scope
192 indices[scope.call(root_node)] ||= 0
193 set_left_and_rights.call(root_node)
194 end
195 end
196 end
197
198 # Mixed into both classes and instances to provide easy access to the column names
199 module Columns
200 def left_column_name
201 acts_as_nested_set_options[:left_column]
202 end
203
204 def right_column_name
205 acts_as_nested_set_options[:right_column]
206 end
207
208 def parent_column_name
209 acts_as_nested_set_options[:parent_column]
210 end
211
212 def scope_column_names
213 Array(acts_as_nested_set_options[:scope])
214 end
215
216 def quoted_left_column_name
217 connection.quote_column_name(left_column_name)
218 end
219
220 def quoted_right_column_name
221 connection.quote_column_name(right_column_name)
222 end
223
224 def quoted_parent_column_name
225 connection.quote_column_name(parent_column_name)
226 end
227
228 def quoted_scope_column_names
229 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
230 end
231 end
232
233 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
234 #
235 # category.self_and_descendants.count
236 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
237 module InstanceMethods
238 # Value of the parent column
239 def parent_id
240 self[parent_column_name]
241 end
242
243 # Value of the left column
244 def left
245 self[left_column_name]
246 end
247
248 # Value of the right column
249 def right
250 self[right_column_name]
251 end
252
253 # Returns true if this is a root node.
254 def root?
255 parent_id.nil?
256 end
257
258 def leaf?
259 right - left == 1
260 end
261
262 # Returns true is this is a child node
263 def child?
264 !parent_id.nil?
265 end
266
267 # order by left column
268 def <=>(x)
269 left <=> x.left
270 end
271
272 # Redefine to act like active record
273 def ==(comparison_object)
274 comparison_object.equal?(self) ||
275 (comparison_object.instance_of?(self.class) &&
276 comparison_object.id == id &&
277 !comparison_object.new_record?)
278 end
279
280 # Returns root
281 def root
282 self_and_ancestors.find(:first)
283 end
284
285 # Returns the immediate parent
286 def parent
287 nested_set_scope.find_by_id(parent_id) if parent_id
288 end
289
290 # Returns the array of all parents and self
291 def self_and_ancestors
292 nested_set_scope.scoped :conditions => [
293 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
294 ]
295 end
296
297 # Returns an array of all parents
298 def ancestors
299 without_self self_and_ancestors
300 end
301
302 # Returns the array of all children of the parent, including self
303 def self_and_siblings
304 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
305 end
306
307 # Returns the array of all children of the parent, except self
308 def siblings
309 without_self self_and_siblings
310 end
311
312 # Returns a set of all of its nested children which do not have children
313 def leaves
314 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
315 end
316
317 # Returns the level of this object in the tree
318 # root level is 0
319 def level
320 parent_id.nil? ? 0 : ancestors.count
321 end
322
323 # Returns a set of itself and all of its nested children
324 def self_and_descendants
325 nested_set_scope.scoped :conditions => [
326 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
327 ]
328 end
329
330 # Returns a set of all of its children and nested children
331 def descendants
332 without_self self_and_descendants
333 end
334
335 # Returns a set of only this entry's immediate children
336 def children
337 nested_set_scope.scoped :conditions => {parent_column_name => self}
338 end
339
340 def is_descendant_of?(other)
341 other.left < self.left && self.left < other.right && same_scope?(other)
342 end
343
344 def is_or_is_descendant_of?(other)
345 other.left <= self.left && self.left < other.right && same_scope?(other)
346 end
347
348 def is_ancestor_of?(other)
349 self.left < other.left && other.left < self.right && same_scope?(other)
350 end
351
352 def is_or_is_ancestor_of?(other)
353 self.left <= other.left && other.left < self.right && same_scope?(other)
354 end
355
356 # Check if other model is in the same scope
357 def same_scope?(other)
358 Array(acts_as_nested_set_options[:scope]).all? do |attr|
359 self.send(attr) == other.send(attr)
360 end
361 end
362
363 # Find the first sibling to the left
364 def left_sibling
365 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
366 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
367 end
368
369 # Find the first sibling to the right
370 def right_sibling
371 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
372 end
373
374 # Shorthand method for finding the left sibling and moving to the left of it.
375 def move_left
376 move_to_left_of left_sibling
377 end
378
379 # Shorthand method for finding the right sibling and moving to the right of it.
380 def move_right
381 move_to_right_of right_sibling
382 end
383
384 # Move the node to the left of another node (you can pass id only)
385 def move_to_left_of(node)
386 move_to node, :left
387 end
388
389 # Move the node to the left of another node (you can pass id only)
390 def move_to_right_of(node)
391 move_to node, :right
392 end
393
394 # Move the node to the child of another node (you can pass id only)
395 def move_to_child_of(node)
396 move_to node, :child
397 end
398
399 # Move the node to root nodes
400 def move_to_root
401 move_to nil, :root
402 end
403
404 def move_possible?(target)
405 self != target && # Can't target self
406 same_scope?(target) && # can't be in different scopes
407 # !(left..right).include?(target.left..target.right) # this needs tested more
408 # detect impossible move
409 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
410 end
411
412 def to_text
413 self_and_descendants.map do |node|
414 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
415 end.join("\n")
416 end
417
418 protected
419
420 def without_self(scope)
421 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
422 end
423
424 # All nested set queries should use this nested_set_scope, which performs finds on
425 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
426 # declaration.
427 def nested_set_scope
428 options = {:order => quoted_left_column_name}
429 scopes = Array(acts_as_nested_set_options[:scope])
430 options[:conditions] = scopes.inject({}) do |conditions,attr|
431 conditions.merge attr => self[attr]
432 end unless scopes.empty?
433 self.class.base_class.scoped options
434 end
435
436 # on creation, set automatically lft and rgt to the end of the tree
437 def set_default_left_and_right
438 maxright = nested_set_scope.maximum(right_column_name) || 0
439 # adds the new node to the right of all existing nodes
440 self[left_column_name] = maxright + 1
441 self[right_column_name] = maxright + 2
442 end
443
444 # Prunes a branch off of the tree, shifting all of the elements on the right
445 # back to the left so the counts still work.
446 def prune_from_tree
447 return if right.nil? || left.nil?
448 diff = right - left + 1
449
450 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
451 :destroy_all : :delete_all
452
453 self.class.base_class.transaction do
454 nested_set_scope.send(delete_method,
455 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
456 left, right]
457 )
458 nested_set_scope.update_all(
459 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
460 ["#{quoted_left_column_name} >= ?", right]
461 )
462 nested_set_scope.update_all(
463 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
464 ["#{quoted_right_column_name} >= ?", right]
465 )
466 end
467 end
468
469 # reload left, right, and parent
470 def reload_nested_set
471 reload(:select => "#{quoted_left_column_name}, " +
472 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
473 end
474
475 def move_to(target, position)
476 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
477 return if callback(:before_move) == false
478 transaction do
479 if target.is_a? self.class.base_class
480 target.reload_nested_set
481 elsif position != :root
482 # load object if node is not an object
483 target = nested_set_scope.find(target)
484 end
485 self.reload_nested_set
486
487 unless position == :root || move_possible?(target)
488 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
489 end
490
491 bound = case position
492 when :child; target[right_column_name]
493 when :left; target[left_column_name]
494 when :right; target[right_column_name] + 1
495 when :root; 1
496 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
497 end
498
499 if bound > self[right_column_name]
500 bound = bound - 1
501 other_bound = self[right_column_name] + 1
502 else
503 other_bound = self[left_column_name] - 1
504 end
505
506 # there would be no change
507 return if bound == self[right_column_name] || bound == self[left_column_name]
508
509 # we have defined the boundaries of two non-overlapping intervals,
510 # so sorting puts both the intervals and their boundaries in order
511 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
512
513 new_parent = case position
514 when :child; target.id
515 when :root; nil
516 else target[parent_column_name]
517 end
518
519 self.class.base_class.update_all([
520 "#{quoted_left_column_name} = CASE " +
521 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
522 "THEN #{quoted_left_column_name} + :d - :b " +
523 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
524 "THEN #{quoted_left_column_name} + :a - :c " +
525 "ELSE #{quoted_left_column_name} END, " +
526 "#{quoted_right_column_name} = CASE " +
527 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
528 "THEN #{quoted_right_column_name} + :d - :b " +
529 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
530 "THEN #{quoted_right_column_name} + :a - :c " +
531 "ELSE #{quoted_right_column_name} END, " +
532 "#{quoted_parent_column_name} = CASE " +
533 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
534 "ELSE #{quoted_parent_column_name} END",
535 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
536 ], nested_set_scope.proxy_options[:conditions])
537 end
538 target.reload_nested_set if target
539 self.reload_nested_set
540 callback(:after_move)
541 end
542
543 end
544
545 end
546 end
547 end
@@ -0,0 +1,29
1 # Rails <2.x doesn't define #except
2 class Hash #:nodoc:
3 # Returns a new hash without the given keys.
4 def except(*keys)
5 clone.except!(*keys)
6 end unless method_defined?(:except)
7
8 # Replaces the hash without the given keys.
9 def except!(*keys)
10 keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
11 keys.each { |key| delete(key) }
12 self
13 end unless method_defined?(:except!)
14 end
15
16 # NamedScope is new to Rails 2.1
17 unless defined? ActiveRecord::NamedScope
18 require 'awesome_nested_set/named_scope'
19 ActiveRecord::Base.class_eval do
20 include CollectiveIdea::NamedScope
21 end
22 end
23
24 # Rails 1.2.x doesn't define #quoted_table_name
25 class ActiveRecord::Base #:nodoc:
26 def self.quoted_table_name
27 self.connection.quote_column_name(self.table_name)
28 end unless methods.include?('quoted_table_name')
29 end No newline at end of file
@@ -0,0 +1,40
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 # This module provides some helpers for the model classes using acts_as_nested_set.
5 # It is included by default in all views.
6 #
7 module Helper
8 # Returns options for select.
9 # You can exclude some items from the tree.
10 # You can pass a block receiving an item and returning the string displayed in the select.
11 #
12 # == Params
13 # * +class_or_item+ - Class name or top level times
14 # * +mover+ - The item that is being move, used to exlude impossible moves
15 # * +&block+ - a block that will be used to display: {Β |item| ... item.name }
16 #
17 # == Usage
18 #
19 # <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
20 # "#{'–' * i.level} #{i.name}"
21 # }) %>
22 #
23 def nested_set_options(class_or_item, mover = nil)
24 class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
25 items = Array(class_or_item)
26 result = []
27 items.each do |root|
28 result += root.self_and_descendants.map do |i|
29 if mover.nil? || mover.new_record? || mover.move_possible?(i)
30 [yield(i), i.id]
31 end
32 end.compact
33 end
34 result
35 end
36
37 end
38 end
39 end
40 end No newline at end of file
@@ -0,0 +1,140
1 # Taken from Rails 2.1
2 module CollectiveIdea #:nodoc:
3 module NamedScope #:nodoc:
4 # All subclasses of ActiveRecord::Base have two named_scopes:
5 # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
6 # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly:
7 #
8 # Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)
9 #
10 # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
11 # intermediate values (scopes) around as first-class objects is convenient.
12 def self.included(base)
13 base.class_eval do
14 extend ClassMethods
15 named_scope :scoped, lambda { |scope| scope }
16 end
17 end
18
19 module ClassMethods #:nodoc:
20 def scopes
21 read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
22 end
23
24 # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
25 # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
26 #
27 # class Shirt < ActiveRecord::Base
28 # named_scope :red, :conditions => {:color => 'red'}
29 # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
30 # end
31 #
32 # The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
33 # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
34 #
35 # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
36 # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
37 # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
38 # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
39 # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
40 #
41 # These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
42 # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
43 # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
44 #
45 # All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to
46 # <tt>has_many</tt> associations. If,
47 #
48 # class Person < ActiveRecord::Base
49 # has_many :shirts
50 # end
51 #
52 # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
53 # only shirts.
54 #
55 # Named scopes can also be procedural.
56 #
57 # class Shirt < ActiveRecord::Base
58 # named_scope :colored, lambda { |color|
59 # { :conditions => { :color => color } }
60 # }
61 # end
62 #
63 # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
64 #
65 # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
66 #
67 # class Shirt < ActiveRecord::Base
68 # named_scope :red, :conditions => {:color => 'red'} do
69 # def dom_id
70 # 'red_shirts'
71 # end
72 # end
73 # end
74 #
75 #
76 # For testing complex named scopes, you can examine the scoping options using the
77 # <tt>proxy_options</tt> method on the proxy itself.
78 #
79 # class Shirt < ActiveRecord::Base
80 # named_scope :colored, lambda { |color|
81 # { :conditions => { :color => color } }
82 # }
83 # end
84 #
85 # expected_options = { :conditions => { :colored => 'red' } }
86 # assert_equal expected_options, Shirt.colored('red').proxy_options
87 def named_scope(name, options = {}, &block)
88 scopes[name] = lambda do |parent_scope, *args|
89 Scope.new(parent_scope, case options
90 when Hash
91 options
92 when Proc
93 options.call(*args)
94 end, &block)
95 end
96 (class << self; self end).instance_eval do
97 define_method name do |*args|
98 scopes[name].call(self, *args)
99 end
100 end
101 end
102 end
103
104 class Scope #:nodoc:
105 attr_reader :proxy_scope, :proxy_options
106 [].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ }
107 delegate :scopes, :with_scope, :to => :proxy_scope
108
109 def initialize(proxy_scope, options, &block)
110 [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
111 extend Module.new(&block) if block_given?
112 @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
113 end
114
115 def reload
116 load_found; self
117 end
118
119 protected
120 def proxy_found
121 @found || load_found
122 end
123
124 private
125 def method_missing(method, *args, &block)
126 if scopes.include?(method)
127 scopes[method].call(self, *args)
128 else
129 with_scope :find => proxy_options do
130 proxy_scope.send(method, *args, &block)
131 end
132 end
133 end
134
135 def load_found
136 @found = find(:all)
137 end
138 end
139 end
140 end No newline at end of file
@@ -0,0 +1,13
1 require 'awesome_nested_set/compatability'
2 require 'awesome_nested_set'
3
4 ActiveRecord::Base.class_eval do
5 include CollectiveIdea::Acts::NestedSet
6 end
7
8 if defined?(ActionView)
9 require 'awesome_nested_set/helper'
10 ActionView::Base.class_eval do
11 include CollectiveIdea::Acts::NestedSet::Helper
12 end
13 end No newline at end of file
@@ -0,0 +1,41
1 require File.dirname(__FILE__) + '/../test_helper'
2
3 module CollectiveIdea
4 module Acts #:nodoc:
5 module NestedSet #:nodoc:
6 class AwesomeNestedSetTest < Test::Unit::TestCase
7 include Helper
8 fixtures :categories
9
10 def test_nested_set_options
11 expected = [
12 [" Top Level", 1],
13 ["- Child 1", 2],
14 ['- Child 2', 3],
15 ['-- Child 2.1', 4],
16 ['- Child 3', 5],
17 [" Top Level 2", 6]
18 ]
19 actual = nested_set_options(Category) do |c|
20 "#{'-' * c.level} #{c.name}"
21 end
22 assert_equal expected, actual
23 end
24
25 def test_nested_set_options_with_mover
26 expected = [
27 [" Top Level", 1],
28 ["- Child 1", 2],
29 ['- Child 3', 5],
30 [" Top Level 2", 6]
31 ]
32 actual = nested_set_options(Category, categories(:child_2)) do |c|
33 "#{'-' * c.level} #{c.name}"
34 end
35 assert_equal expected, actual
36 end
37
38 end
39 end
40 end
41 end No newline at end of file
This diff has been collapsed as it changes many lines, (603 lines changed) Show them Hide them
@@ -0,0 +1,603
1 require File.dirname(__FILE__) + '/test_helper'
2
3 class Note < ActiveRecord::Base
4 acts_as_nested_set :scope => [:notable_id, :notable_type]
5 end
6
7 class AwesomeNestedSetTest < Test::Unit::TestCase
8
9 class Default < ActiveRecord::Base
10 acts_as_nested_set
11 set_table_name 'categories'
12 end
13 class Scoped < ActiveRecord::Base
14 acts_as_nested_set :scope => :organization
15 set_table_name 'categories'
16 end
17
18 def test_left_column_default
19 assert_equal 'lft', Default.acts_as_nested_set_options[:left_column]
20 end
21
22 def test_right_column_default
23 assert_equal 'rgt', Default.acts_as_nested_set_options[:right_column]
24 end
25
26 def test_parent_column_default
27 assert_equal 'parent_id', Default.acts_as_nested_set_options[:parent_column]
28 end
29
30 def test_scope_default
31 assert_nil Default.acts_as_nested_set_options[:scope]
32 end
33
34 def test_left_column_name
35 assert_equal 'lft', Default.left_column_name
36 assert_equal 'lft', Default.new.left_column_name
37 end
38
39 def test_right_column_name
40 assert_equal 'rgt', Default.right_column_name
41 assert_equal 'rgt', Default.new.right_column_name
42 end
43
44 def test_parent_column_name
45 assert_equal 'parent_id', Default.parent_column_name
46 assert_equal 'parent_id', Default.new.parent_column_name
47 end
48
49 def test_quoted_left_column_name
50 quoted = Default.connection.quote_column_name('lft')
51 assert_equal quoted, Default.quoted_left_column_name
52 assert_equal quoted, Default.new.quoted_left_column_name
53 end
54
55 def test_quoted_right_column_name
56 quoted = Default.connection.quote_column_name('rgt')
57 assert_equal quoted, Default.quoted_right_column_name
58 assert_equal quoted, Default.new.quoted_right_column_name
59 end
60
61 def test_left_column_protected_from_assignment
62 assert_raises(ActiveRecord::ActiveRecordError) { Category.new.lft = 1 }
63 end
64
65 def test_right_column_protected_from_assignment
66 assert_raises(ActiveRecord::ActiveRecordError) { Category.new.rgt = 1 }
67 end
68
69 def test_parent_column_protected_from_assignment
70 assert_raises(ActiveRecord::ActiveRecordError) { Category.new.parent_id = 1 }
71 end
72
73 def test_colums_protected_on_initialize
74 c = Category.new(:lft => 1, :rgt => 2, :parent_id => 3)
75 assert_nil c.lft
76 assert_nil c.rgt
77 assert_nil c.parent_id
78 end
79
80 def test_scoped_appends_id
81 assert_equal :organization_id, Scoped.acts_as_nested_set_options[:scope]
82 end
83
84 def test_roots_class_method
85 assert_equal Category.find_all_by_parent_id(nil), Category.roots
86 end
87
88 def test_root_class_method
89 assert_equal categories(:top_level), Category.root
90 end
91
92 def test_root
93 assert_equal categories(:top_level), categories(:child_3).root
94 end
95
96 def test_root?
97 assert categories(:top_level).root?
98 assert categories(:top_level_2).root?
99 end
100
101 def test_leaves_class_method
102 assert_equal Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1"), Category.leaves
103 assert_equal Category.leaves.count, 4
104 assert (Category.leaves.include? categories(:child_1))
105 assert (Category.leaves.include? categories(:child_2_1))
106 assert (Category.leaves.include? categories(:child_3))
107 assert (Category.leaves.include? categories(:top_level_2))
108 end
109
110 def test_leaf
111 assert categories(:child_1).leaf?
112 assert categories(:child_2_1).leaf?
113 assert categories(:child_3).leaf?
114 assert categories(:top_level_2).leaf?
115
116 assert !categories(:top_level).leaf?
117 assert !categories(:child_2).leaf?
118 end
119
120 def test_parent
121 assert_equal categories(:child_2), categories(:child_2_1).parent
122 end
123
124 def test_self_and_ancestors
125 child = categories(:child_2_1)
126 self_and_ancestors = [categories(:top_level), categories(:child_2), child]
127 assert_equal self_and_ancestors, child.self_and_ancestors
128 end
129
130 def test_ancestors
131 child = categories(:child_2_1)
132 ancestors = [categories(:top_level), categories(:child_2)]
133 assert_equal ancestors, child.ancestors
134 end
135
136 def test_self_and_siblings
137 child = categories(:child_2)
138 self_and_siblings = [categories(:child_1), child, categories(:child_3)]
139 assert_equal self_and_siblings, child.self_and_siblings
140 assert_nothing_raised do
141 tops = [categories(:top_level), categories(:top_level_2)]
142 assert_equal tops, categories(:top_level).self_and_siblings
143 end
144 end
145
146 def test_siblings
147 child = categories(:child_2)
148 siblings = [categories(:child_1), categories(:child_3)]
149 assert_equal siblings, child.siblings
150 end
151
152 def test_leaves
153 leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3), categories(:top_level_2)]
154 assert categories(:top_level).leaves, leaves
155 end
156
157 def test_level
158 assert_equal 0, categories(:top_level).level
159 assert_equal 1, categories(:child_1).level
160 assert_equal 2, categories(:child_2_1).level
161 end
162
163 def test_has_children?
164 assert categories(:child_2_1).children.empty?
165 assert !categories(:child_2).children.empty?
166 assert !categories(:top_level).children.empty?
167 end
168
169 def test_self_and_descendents
170 parent = categories(:top_level)
171 self_and_descendants = [parent, categories(:child_1), categories(:child_2),
172 categories(:child_2_1), categories(:child_3)]
173 assert_equal self_and_descendants, parent.self_and_descendants
174 assert_equal self_and_descendants, parent.self_and_descendants.count
175 end
176
177 def test_descendents
178 lawyers = Category.create!(:name => "lawyers")
179 us = Category.create!(:name => "United States")
180 us.move_to_child_of(lawyers)
181 patent = Category.create!(:name => "Patent Law")
182 patent.move_to_child_of(us)
183 lawyers.reload
184
185 assert_equal 1, lawyers.children.size
186 assert_equal 1, us.children.size
187 assert_equal 2, lawyers.descendants.size
188 end
189
190 def test_self_and_descendents
191 parent = categories(:top_level)
192 descendants = [categories(:child_1), categories(:child_2),
193 categories(:child_2_1), categories(:child_3)]
194 assert_equal descendants, parent.descendants
195 end
196
197 def test_children
198 category = categories(:top_level)
199 category.children.each {|c| assert_equal category.id, c.parent_id }
200 end
201
202 def test_is_or_is_ancestor_of?
203 assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_1))
204 assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1))
205 assert categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1))
206 assert !categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2))
207 assert !categories(:child_1).is_or_is_ancestor_of?(categories(:child_2))
208 assert categories(:child_1).is_or_is_ancestor_of?(categories(:child_1))
209 end
210
211 def test_is_ancestor_of?
212 assert categories(:top_level).is_ancestor_of?(categories(:child_1))
213 assert categories(:top_level).is_ancestor_of?(categories(:child_2_1))
214 assert categories(:child_2).is_ancestor_of?(categories(:child_2_1))
215 assert !categories(:child_2_1).is_ancestor_of?(categories(:child_2))
216 assert !categories(:child_1).is_ancestor_of?(categories(:child_2))
217 assert !categories(:child_1).is_ancestor_of?(categories(:child_1))
218 end
219
220 def test_is_or_is_ancestor_of_with_scope
221 root = Scoped.root
222 child = root.children.first
223 assert root.is_or_is_ancestor_of?(child)
224 child.update_attribute :organization_id, 'different'
225 assert !root.is_or_is_ancestor_of?(child)
226 end
227
228 def test_is_or_is_descendant_of?
229 assert categories(:child_1).is_or_is_descendant_of?(categories(:top_level))
230 assert categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level))
231 assert categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2))
232 assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1))
233 assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_1))
234 assert categories(:child_1).is_or_is_descendant_of?(categories(:child_1))
235 end
236
237 def test_is_descendant_of?
238 assert categories(:child_1).is_descendant_of?(categories(:top_level))
239 assert categories(:child_2_1).is_descendant_of?(categories(:top_level))
240 assert categories(:child_2_1).is_descendant_of?(categories(:child_2))
241 assert !categories(:child_2).is_descendant_of?(categories(:child_2_1))
242 assert !categories(:child_2).is_descendant_of?(categories(:child_1))
243 assert !categories(:child_1).is_descendant_of?(categories(:child_1))
244 end
245
246 def test_is_or_is_descendant_of_with_scope
247 root = Scoped.root
248 child = root.children.first
249 assert child.is_or_is_descendant_of?(root)
250 child.update_attribute :organization_id, 'different'
251 assert !child.is_or_is_descendant_of?(root)
252 end
253
254 def test_same_scope?
255 root = Scoped.root
256 child = root.children.first
257 assert child.same_scope?(root)
258 child.update_attribute :organization_id, 'different'
259 assert !child.same_scope?(root)
260 end
261
262 def test_left_sibling
263 assert_equal categories(:child_1), categories(:child_2).left_sibling
264 assert_equal categories(:child_2), categories(:child_3).left_sibling
265 end
266
267 def test_left_sibling_of_root
268 assert_nil categories(:top_level).left_sibling
269 end
270
271 def test_left_sibling_without_siblings
272 assert_nil categories(:child_2_1).left_sibling
273 end
274
275 def test_left_sibling_of_leftmost_node
276 assert_nil categories(:child_1).left_sibling
277 end
278
279 def test_right_sibling
280 assert_equal categories(:child_3), categories(:child_2).right_sibling
281 assert_equal categories(:child_2), categories(:child_1).right_sibling
282 end
283
284 def test_right_sibling_of_root
285 assert_equal categories(:top_level_2), categories(:top_level).right_sibling
286 assert_nil categories(:top_level_2).right_sibling
287 end
288
289 def test_right_sibling_without_siblings
290 assert_nil categories(:child_2_1).right_sibling
291 end
292
293 def test_right_sibling_of_rightmost_node
294 assert_nil categories(:child_3).right_sibling
295 end
296
297 def test_move_left
298 categories(:child_2).move_left
299 assert_nil categories(:child_2).left_sibling
300 assert_equal categories(:child_1), categories(:child_2).right_sibling
301 assert Category.valid?
302 end
303
304 def test_move_right
305 categories(:child_2).move_right
306 assert_nil categories(:child_2).right_sibling
307 assert_equal categories(:child_3), categories(:child_2).left_sibling
308 assert Category.valid?
309 end
310
311 def test_move_to_left_of
312 categories(:child_3).move_to_left_of(categories(:child_1))
313 assert_nil categories(:child_3).left_sibling
314 assert_equal categories(:child_1), categories(:child_3).right_sibling
315 assert Category.valid?
316 end
317
318 def test_move_to_right_of
319 categories(:child_1).move_to_right_of(categories(:child_3))
320 assert_nil categories(:child_1).right_sibling
321 assert_equal categories(:child_3), categories(:child_1).left_sibling
322 assert Category.valid?
323 end
324
325 def test_move_to_root
326 categories(:child_2).move_to_root
327 assert_nil categories(:child_2).parent
328 assert_equal 0, categories(:child_2).level
329 assert_equal 1, categories(:child_2_1).level
330 assert_equal 1, categories(:child_2).left
331 assert_equal 4, categories(:child_2).right
332 assert Category.valid?
333 end
334
335 def test_move_to_child_of
336 categories(:child_1).move_to_child_of(categories(:child_3))
337 assert_equal categories(:child_3).id, categories(:child_1).parent_id
338 assert Category.valid?
339 end
340
341 def test_move_to_child_of_appends_to_end
342 child = Category.create! :name => 'New Child'
343 child.move_to_child_of categories(:top_level)
344 assert_equal child, categories(:top_level).children.last
345 end
346
347 def test_subtree_move_to_child_of
348 assert_equal 4, categories(:child_2).left
349 assert_equal 7, categories(:child_2).right
350
351 assert_equal 2, categories(:child_1).left
352 assert_equal 3, categories(:child_1).right
353
354 categories(:child_2).move_to_child_of(categories(:child_1))
355 assert Category.valid?
356 assert_equal categories(:child_1).id, categories(:child_2).parent_id
357
358 assert_equal 3, categories(:child_2).left
359 assert_equal 6, categories(:child_2).right
360 assert_equal 2, categories(:child_1).left
361 assert_equal 7, categories(:child_1).right
362 end
363
364 def test_slightly_difficult_move_to_child_of
365 assert_equal 11, categories(:top_level_2).left
366 assert_equal 12, categories(:top_level_2).right
367
368 # create a new top-level node and move single-node top-level tree inside it.
369 new_top = Category.create(:name => 'New Top')
370 assert_equal 13, new_top.left
371 assert_equal 14, new_top.right
372
373 categories(:top_level_2).move_to_child_of(new_top)
374
375 assert Category.valid?
376 assert_equal new_top.id, categories(:top_level_2).parent_id
377
378 assert_equal 12, categories(:top_level_2).left
379 assert_equal 13, categories(:top_level_2).right
380 assert_equal 11, new_top.left
381 assert_equal 14, new_top.right
382 end
383
384 def test_difficult_move_to_child_of
385 assert_equal 1, categories(:top_level).left
386 assert_equal 10, categories(:top_level).right
387 assert_equal 5, categories(:child_2_1).left
388 assert_equal 6, categories(:child_2_1).right
389
390 # create a new top-level node and move an entire top-level tree inside it.
391 new_top = Category.create(:name => 'New Top')
392 categories(:top_level).move_to_child_of(new_top)
393 categories(:child_2_1).reload
394 assert Category.valid?
395 assert_equal new_top.id, categories(:top_level).parent_id
396
397 assert_equal 4, categories(:top_level).left
398 assert_equal 13, categories(:top_level).right
399 assert_equal 8, categories(:child_2_1).left
400 assert_equal 9, categories(:child_2_1).right
401 end
402
403 #rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
404 def test_move_to_child_more_than_once_per_parent_rebuild
405 root1 = Category.create(:name => 'Root1')
406 root2 = Category.create(:name => 'Root2')
407 root3 = Category.create(:name => 'Root3')
408
409 root2.move_to_child_of root1
410 root3.move_to_child_of root1
411
412 output = Category.roots.last.to_text
413 Category.update_all('lft = null, rgt = null')
414 Category.rebuild!
415
416 assert_equal Category.roots.last.to_text, output
417 end
418
419 # doing move_to_child twice onto same parent from the furthest right first
420 def test_move_to_child_more_than_once_per_parent_outside_in
421 node1 = Category.create(:name => 'Node-1')
422 node2 = Category.create(:name => 'Node-2')
423 node3 = Category.create(:name => 'Node-3')
424
425 node2.move_to_child_of node1
426 node3.move_to_child_of node1
427
428 output = Category.roots.last.to_text
429 Category.update_all('lft = null, rgt = null')
430 Category.rebuild!
431
432 assert_equal Category.roots.last.to_text, output
433 end
434
435
436 def test_valid_with_null_lefts
437 assert Category.valid?
438 Category.update_all('lft = null')
439 assert !Category.valid?
440 end
441
442 def test_valid_with_null_rights
443 assert Category.valid?
444 Category.update_all('rgt = null')
445 assert !Category.valid?
446 end
447
448 def test_valid_with_missing_intermediate_node
449 # Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
450 assert Category.valid?
451 Category.delete(categories(:child_2).id)
452 assert Category.valid?
453 end
454
455 def test_valid_with_overlapping_and_rights
456 assert Category.valid?
457 categories(:top_level_2)['lft'] = 0
458 categories(:top_level_2).save
459 assert !Category.valid?
460 end
461
462 def test_rebuild
463 assert Category.valid?
464 before_text = Category.root.to_text
465 Category.update_all('lft = null, rgt = null')
466 Category.rebuild!
467 assert Category.valid?
468 assert_equal before_text, Category.root.to_text
469 end
470
471 def test_move_possible_for_sibling
472 assert categories(:child_2).move_possible?(categories(:child_1))
473 end
474
475 def test_move_not_possible_to_self
476 assert !categories(:top_level).move_possible?(categories(:top_level))
477 end
478
479 def test_move_not_possible_to_parent
480 categories(:top_level).descendants.each do |descendant|
481 assert !categories(:top_level).move_possible?(descendant)
482 assert descendant.move_possible?(categories(:top_level))
483 end
484 end
485
486 def test_is_or_is_ancestor_of?
487 [:child_1, :child_2, :child_2_1, :child_3].each do |c|
488 assert categories(:top_level).is_or_is_ancestor_of?(categories(c))
489 end
490 assert !categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2))
491 end
492
493 def test_left_and_rights_valid_with_blank_left
494 assert Category.left_and_rights_valid?
495 categories(:child_2)[:lft] = nil
496 categories(:child_2).save(false)
497 assert !Category.left_and_rights_valid?
498 end
499
500 def test_left_and_rights_valid_with_blank_right
501 assert Category.left_and_rights_valid?
502 categories(:child_2)[:rgt] = nil
503 categories(:child_2).save(false)
504 assert !Category.left_and_rights_valid?
505 end
506
507 def test_left_and_rights_valid_with_equal
508 assert Category.left_and_rights_valid?
509 categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
510 categories(:top_level_2).save(false)
511 assert !Category.left_and_rights_valid?
512 end
513
514 def test_left_and_rights_valid_with_left_equal_to_parent
515 assert Category.left_and_rights_valid?
516 categories(:child_2)[:lft] = categories(:top_level)[:lft]
517 categories(:child_2).save(false)
518 assert !Category.left_and_rights_valid?
519 end
520
521 def test_left_and_rights_valid_with_right_equal_to_parent
522 assert Category.left_and_rights_valid?
523 categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
524 categories(:child_2).save(false)
525 assert !Category.left_and_rights_valid?
526 end
527
528 def test_moving_dirty_objects_doesnt_invalidate_tree
529 r1 = Category.create
530 r2 = Category.create
531 r3 = Category.create
532 r4 = Category.create
533 nodes = [r1, r2, r3, r4]
534
535 r2.move_to_child_of(r1)
536 assert Category.valid?
537
538 r3.move_to_child_of(r1)
539 assert Category.valid?
540
541 r4.move_to_child_of(r2)
542 assert Category.valid?
543 end
544
545 def test_multi_scoped_no_duplicates_for_columns?
546 assert_nothing_raised do
547 Note.no_duplicates_for_columns?
548 end
549 end
550
551 def test_multi_scoped_all_roots_valid?
552 assert_nothing_raised do
553 Note.all_roots_valid?
554 end
555 end
556
557 def test_multi_scoped
558 note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
559 note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
560 note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
561
562 assert_equal [note1, note2], note1.self_and_siblings
563 assert_equal [note3], note3.self_and_siblings
564 end
565
566 def test_multi_scoped_rebuild
567 root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
568 child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
569 child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
570
571 child1.move_to_child_of root
572 child2.move_to_child_of root
573
574 Note.update_all('lft = null, rgt = null')
575 Note.rebuild!
576
577 assert_equal Note.roots.find_by_body('A'), root
578 assert_equal [child1, child2], Note.roots.find_by_body('A').children
579 end
580
581 def test_same_scope_with_multi_scopes
582 assert_nothing_raised do
583 notes(:scope1).same_scope?(notes(:child_1))
584 end
585 assert notes(:scope1).same_scope?(notes(:child_1))
586 assert notes(:child_1).same_scope?(notes(:scope1))
587 assert !notes(:scope1).same_scope?(notes(:scope2))
588 end
589
590 def test_quoting_of_multi_scope_column_names
591 assert_equal ["\"notable_id\"", "\"notable_type\""], Note.quoted_scope_column_names
592 end
593
594 def test_equal_in_same_scope
595 assert_equal notes(:scope1), notes(:scope1)
596 assert_not_equal notes(:scope1), notes(:child_1)
597 end
598
599 def test_equal_in_different_scopes
600 assert_not_equal notes(:scope1), notes(:scope2)
601 end
602
603 end
@@ -0,0 +1,18
1 sqlite3:
2 adapter: sqlite3
3 dbfile: awesome_nested_set.sqlite3.db
4 sqlite3mem:
5 :adapter: sqlite3
6 :dbfile: ":memory:"
7 postgresql:
8 :adapter: postgresql
9 :username: postgres
10 :password: postgres
11 :database: awesome_nested_set_plugin_test
12 :min_messages: ERROR
13 mysql:
14 :adapter: mysql
15 :host: localhost
16 :username: root
17 :password:
18 :database: awesome_nested_set_plugin_test No newline at end of file
@@ -0,0 +1,23
1 ActiveRecord::Schema.define(:version => 0) do
2
3 create_table :categories, :force => true do |t|
4 t.column :name, :string
5 t.column :parent_id, :integer
6 t.column :lft, :integer
7 t.column :rgt, :integer
8 t.column :organization_id, :integer
9 end
10
11 create_table :departments, :force => true do |t|
12 t.column :name, :string
13 end
14
15 create_table :notes, :force => true do |t|
16 t.column :body, :text
17 t.column :parent_id, :integer
18 t.column :lft, :integer
19 t.column :rgt, :integer
20 t.column :notable_id, :integer
21 t.column :notable_type, :string
22 end
23 end
@@ -0,0 +1,34
1 top_level:
2 id: 1
3 name: Top Level
4 lft: 1
5 rgt: 10
6 child_1:
7 id: 2
8 name: Child 1
9 parent_id: 1
10 lft: 2
11 rgt: 3
12 child_2:
13 id: 3
14 name: Child 2
15 parent_id: 1
16 lft: 4
17 rgt: 7
18 child_2_1:
19 id: 4
20 name: Child 2.1
21 parent_id: 3
22 lft: 5
23 rgt: 6
24 child_3:
25 id: 5
26 name: Child 3
27 parent_id: 1
28 lft: 8
29 rgt: 9
30 top_level_2:
31 id: 6
32 name: Top Level 2
33 lft: 11
34 rgt: 12
@@ -0,0 +1,15
1 class Category < ActiveRecord::Base
2 acts_as_nested_set
3
4 def to_s
5 name
6 end
7
8 def recurse &block
9 block.call self, lambda{
10 self.children.each do |child|
11 child.recurse &block
12 end
13 }
14 end
15 end No newline at end of file
@@ -0,0 +1,3
1 top:
2 id: 1
3 name: Top No newline at end of file
@@ -0,0 +1,38
1 scope1:
2 id: 1
3 body: Top Level
4 lft: 1
5 rgt: 10
6 notable_id: 1
7 notable_type: Category
8 child_1:
9 id: 2
10 body: Child 1
11 parent_id: 1
12 lft: 2
13 rgt: 3
14 notable_id: 1
15 notable_type: Category
16 child_2:
17 id: 3
18 body: Child 2
19 parent_id: 1
20 lft: 4
21 rgt: 7
22 notable_id: 1
23 notable_type: Category
24 child_3:
25 id: 4
26 body: Child 3
27 parent_id: 1
28 lft: 8
29 rgt: 9
30 notable_id: 1
31 notable_type: Category
32 scope2:
33 id: 5
34 body: Top Level 2
35 lft: 1
36 rgt: 2
37 notable_id: 1
38 notable_type: Departments
@@ -0,0 +1,31
1 $:.unshift(File.dirname(__FILE__) + '/../lib')
2 plugin_test_dir = File.dirname(__FILE__)
3
4 require 'rubygems'
5 require 'test/unit'
6 require 'multi_rails_init'
7 # gem 'activerecord', '>= 2.0'
8 require 'active_record'
9 require 'action_controller'
10 require 'action_view'
11 require 'active_record/fixtures'
12
13 require plugin_test_dir + '/../init.rb'
14
15 ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
16
17 ActiveRecord::Base.configurations = YAML::load(IO.read(plugin_test_dir + "/db/database.yml"))
18 ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3mem")
19 ActiveRecord::Migration.verbose = false
20 load(File.join(plugin_test_dir, "db", "schema.rb"))
21
22 Dir["#{plugin_test_dir}/fixtures/*.rb"].each {|file| require file }
23
24
25 class Test::Unit::TestCase #:nodoc:
26 self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
27 self.use_transactional_fixtures = true
28 self.use_instantiated_fixtures = false
29
30 fixtures :categories, :notes, :departments
31 end No newline at end of file
@@ -1,93 +1,84
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class AdminController < ApplicationController
18 class AdminController < ApplicationController
19 before_filter :require_admin
19 before_filter :require_admin
20
20
21 helper :sort
21 helper :sort
22 include SortHelper
22 include SortHelper
23
23
24 def index
24 def index
25 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
25 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
26 end
26 end
27
27
28 def projects
28 def projects
29 sort_init 'name', 'asc'
30 sort_update %w(name is_public created_on)
31
32 @status = params[:status] ? params[:status].to_i : 1
29 @status = params[:status] ? params[:status].to_i : 1
33 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
30 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
34
31
35 unless params[:name].blank?
32 unless params[:name].blank?
36 name = "%#{params[:name].strip.downcase}%"
33 name = "%#{params[:name].strip.downcase}%"
37 c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
34 c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
38 end
35 end
39
36
40 @project_count = Project.count(:conditions => c.conditions)
37 @projects = Project.find :all, :order => 'lft',
41 @project_pages = Paginator.new self, @project_count,
38 :conditions => c.conditions
42 per_page_option,
43 params['page']
44 @projects = Project.find :all, :order => sort_clause,
45 :conditions => c.conditions,
46 :limit => @project_pages.items_per_page,
47 :offset => @project_pages.current.offset
48
39
49 render :action => "projects", :layout => false if request.xhr?
40 render :action => "projects", :layout => false if request.xhr?
50 end
41 end
51
42
52 def plugins
43 def plugins
53 @plugins = Redmine::Plugin.all
44 @plugins = Redmine::Plugin.all
54 end
45 end
55
46
56 # Loads the default configuration
47 # Loads the default configuration
57 # (roles, trackers, statuses, workflow, enumerations)
48 # (roles, trackers, statuses, workflow, enumerations)
58 def default_configuration
49 def default_configuration
59 if request.post?
50 if request.post?
60 begin
51 begin
61 Redmine::DefaultData::Loader::load(params[:lang])
52 Redmine::DefaultData::Loader::load(params[:lang])
62 flash[:notice] = l(:notice_default_data_loaded)
53 flash[:notice] = l(:notice_default_data_loaded)
63 rescue Exception => e
54 rescue Exception => e
64 flash[:error] = l(:error_can_t_load_default_data, e.message)
55 flash[:error] = l(:error_can_t_load_default_data, e.message)
65 end
56 end
66 end
57 end
67 redirect_to :action => 'index'
58 redirect_to :action => 'index'
68 end
59 end
69
60
70 def test_email
61 def test_email
71 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
62 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
72 # Force ActionMailer to raise delivery errors so we can catch it
63 # Force ActionMailer to raise delivery errors so we can catch it
73 ActionMailer::Base.raise_delivery_errors = true
64 ActionMailer::Base.raise_delivery_errors = true
74 begin
65 begin
75 @test = Mailer.deliver_test(User.current)
66 @test = Mailer.deliver_test(User.current)
76 flash[:notice] = l(:notice_email_sent, User.current.mail)
67 flash[:notice] = l(:notice_email_sent, User.current.mail)
77 rescue Exception => e
68 rescue Exception => e
78 flash[:error] = l(:notice_email_error, e.message)
69 flash[:error] = l(:notice_email_error, e.message)
79 end
70 end
80 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
71 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
81 redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
72 redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
82 end
73 end
83
74
84 def info
75 def info
85 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
76 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
86 @flags = {
77 @flags = {
87 :default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
78 :default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
88 :file_repository_writable => File.writable?(Attachment.storage_path),
79 :file_repository_writable => File.writable?(Attachment.storage_path),
89 :plugin_assets_writable => File.writable?(Engines.public_directory),
80 :plugin_assets_writable => File.writable?(Engines.public_directory),
90 :rmagick_available => Object.const_defined?(:Magick)
81 :rmagick_available => Object.const_defined?(:Magick)
91 }
82 }
92 end
83 end
93 end
84 end
@@ -1,296 +1,290
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :activity, :only => :activity
20 menu_item :activity, :only => :activity
21 menu_item :roadmap, :only => :roadmap
21 menu_item :roadmap, :only => :roadmap
22 menu_item :files, :only => [:list_files, :add_file]
22 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :settings, :only => :settings
23 menu_item :settings, :only => :settings
24 menu_item :issues, :only => [:changelog]
24 menu_item :issues, :only => [:changelog]
25
25
26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
27 before_filter :find_optional_project, :only => :activity
27 before_filter :find_optional_project, :only => :activity
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
30 accept_key_auth :activity
30 accept_key_auth :activity
31
31
32 helper :sort
32 helper :sort
33 include SortHelper
33 include SortHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :issues
36 helper :issues
37 helper IssuesHelper
37 helper IssuesHelper
38 helper :queries
38 helper :queries
39 include QueriesHelper
39 include QueriesHelper
40 helper :repositories
40 helper :repositories
41 include RepositoriesHelper
41 include RepositoriesHelper
42 include ProjectsHelper
42 include ProjectsHelper
43
43
44 # Lists visible projects
44 # Lists visible projects
45 def index
45 def index
46 projects = Project.find :all,
47 :conditions => Project.visible_by(User.current),
48 :include => :parent
49 respond_to do |format|
46 respond_to do |format|
50 format.html {
47 format.html {
51 @project_tree = projects.group_by {|p| p.parent || p}
48 @projects = Project.visible.find(:all, :order => 'lft')
52 @project_tree.keys.each {|p| @project_tree[p] -= [p]}
53 }
49 }
54 format.atom {
50 format.atom {
55 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
51 projects = Project.visible.find(:all, :order => 'created_on DESC',
56 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
52 :limit => Setting.feeds_limit.to_i)
53 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
57 }
54 }
58 end
55 end
59 end
56 end
60
57
61 # Add a new project
58 # Add a new project
62 def add
59 def add
63 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
60 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
64 @trackers = Tracker.all
61 @trackers = Tracker.all
65 @root_projects = Project.find(:all,
66 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
67 :order => 'name')
68 @project = Project.new(params[:project])
62 @project = Project.new(params[:project])
69 if request.get?
63 if request.get?
70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
64 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
71 @project.trackers = Tracker.all
65 @project.trackers = Tracker.all
72 @project.is_public = Setting.default_projects_public?
66 @project.is_public = Setting.default_projects_public?
73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
67 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
74 else
68 else
75 @project.enabled_module_names = params[:enabled_modules]
69 @project.enabled_module_names = params[:enabled_modules]
76 if @project.save
70 if @project.save
71 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
77 flash[:notice] = l(:notice_successful_create)
72 flash[:notice] = l(:notice_successful_create)
78 redirect_to :controller => 'admin', :action => 'projects'
73 redirect_to :controller => 'admin', :action => 'projects'
79 end
74 end
80 end
75 end
81 end
76 end
82
77
83 # Show @project
78 # Show @project
84 def show
79 def show
85 if params[:jump]
80 if params[:jump]
86 # try to redirect to the requested menu item
81 # try to redirect to the requested menu item
87 redirect_to_project_menu_item(@project, params[:jump]) && return
82 redirect_to_project_menu_item(@project, params[:jump]) && return
88 end
83 end
89
84
90 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
85 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
91 @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
86 @subprojects = @project.children.visible
87 @ancestors = @project.ancestors.visible
92 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
88 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
93 @trackers = @project.rolled_up_trackers
89 @trackers = @project.rolled_up_trackers
94
90
95 cond = @project.project_condition(Setting.display_subprojects_issues?)
91 cond = @project.project_condition(Setting.display_subprojects_issues?)
96 Issue.visible_by(User.current) do
92 Issue.visible_by(User.current) do
97 @open_issues_by_tracker = Issue.count(:group => :tracker,
93 @open_issues_by_tracker = Issue.count(:group => :tracker,
98 :include => [:project, :status, :tracker],
94 :include => [:project, :status, :tracker],
99 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
95 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
100 @total_issues_by_tracker = Issue.count(:group => :tracker,
96 @total_issues_by_tracker = Issue.count(:group => :tracker,
101 :include => [:project, :status, :tracker],
97 :include => [:project, :status, :tracker],
102 :conditions => cond)
98 :conditions => cond)
103 end
99 end
104 TimeEntry.visible_by(User.current) do
100 TimeEntry.visible_by(User.current) do
105 @total_hours = TimeEntry.sum(:hours,
101 @total_hours = TimeEntry.sum(:hours,
106 :include => :project,
102 :include => :project,
107 :conditions => cond).to_f
103 :conditions => cond).to_f
108 end
104 end
109 @key = User.current.rss_key
105 @key = User.current.rss_key
110 end
106 end
111
107
112 def settings
108 def settings
113 @root_projects = Project.find(:all,
114 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
115 :order => 'name')
116 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
109 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
117 @issue_category ||= IssueCategory.new
110 @issue_category ||= IssueCategory.new
118 @member ||= @project.members.new
111 @member ||= @project.members.new
119 @trackers = Tracker.all
112 @trackers = Tracker.all
120 @repository ||= @project.repository
113 @repository ||= @project.repository
121 @wiki ||= @project.wiki
114 @wiki ||= @project.wiki
122 end
115 end
123
116
124 # Edit @project
117 # Edit @project
125 def edit
118 def edit
126 if request.post?
119 if request.post?
127 @project.attributes = params[:project]
120 @project.attributes = params[:project]
128 if @project.save
121 if @project.save
122 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
129 flash[:notice] = l(:notice_successful_update)
123 flash[:notice] = l(:notice_successful_update)
130 redirect_to :action => 'settings', :id => @project
124 redirect_to :action => 'settings', :id => @project
131 else
125 else
132 settings
126 settings
133 render :action => 'settings'
127 render :action => 'settings'
134 end
128 end
135 end
129 end
136 end
130 end
137
131
138 def modules
132 def modules
139 @project.enabled_module_names = params[:enabled_modules]
133 @project.enabled_module_names = params[:enabled_modules]
140 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
134 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
141 end
135 end
142
136
143 def archive
137 def archive
144 @project.archive if request.post? && @project.active?
138 @project.archive if request.post? && @project.active?
145 redirect_to :controller => 'admin', :action => 'projects'
139 redirect_to :controller => 'admin', :action => 'projects'
146 end
140 end
147
141
148 def unarchive
142 def unarchive
149 @project.unarchive if request.post? && !@project.active?
143 @project.unarchive if request.post? && !@project.active?
150 redirect_to :controller => 'admin', :action => 'projects'
144 redirect_to :controller => 'admin', :action => 'projects'
151 end
145 end
152
146
153 # Delete @project
147 # Delete @project
154 def destroy
148 def destroy
155 @project_to_destroy = @project
149 @project_to_destroy = @project
156 if request.post? and params[:confirm]
150 if request.post? and params[:confirm]
157 @project_to_destroy.destroy
151 @project_to_destroy.destroy
158 redirect_to :controller => 'admin', :action => 'projects'
152 redirect_to :controller => 'admin', :action => 'projects'
159 end
153 end
160 # hide project in layout
154 # hide project in layout
161 @project = nil
155 @project = nil
162 end
156 end
163
157
164 # Add a new issue category to @project
158 # Add a new issue category to @project
165 def add_issue_category
159 def add_issue_category
166 @category = @project.issue_categories.build(params[:category])
160 @category = @project.issue_categories.build(params[:category])
167 if request.post? and @category.save
161 if request.post? and @category.save
168 respond_to do |format|
162 respond_to do |format|
169 format.html do
163 format.html do
170 flash[:notice] = l(:notice_successful_create)
164 flash[:notice] = l(:notice_successful_create)
171 redirect_to :action => 'settings', :tab => 'categories', :id => @project
165 redirect_to :action => 'settings', :tab => 'categories', :id => @project
172 end
166 end
173 format.js do
167 format.js do
174 # IE doesn't support the replace_html rjs method for select box options
168 # IE doesn't support the replace_html rjs method for select box options
175 render(:update) {|page| page.replace "issue_category_id",
169 render(:update) {|page| page.replace "issue_category_id",
176 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]')
170 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]')
177 }
171 }
178 end
172 end
179 end
173 end
180 end
174 end
181 end
175 end
182
176
183 # Add a new version to @project
177 # Add a new version to @project
184 def add_version
178 def add_version
185 @version = @project.versions.build(params[:version])
179 @version = @project.versions.build(params[:version])
186 if request.post? and @version.save
180 if request.post? and @version.save
187 flash[:notice] = l(:notice_successful_create)
181 flash[:notice] = l(:notice_successful_create)
188 redirect_to :action => 'settings', :tab => 'versions', :id => @project
182 redirect_to :action => 'settings', :tab => 'versions', :id => @project
189 end
183 end
190 end
184 end
191
185
192 def add_file
186 def add_file
193 if request.post?
187 if request.post?
194 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
188 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
195 attachments = attach_files(container, params[:attachments])
189 attachments = attach_files(container, params[:attachments])
196 if !attachments.empty? && Setting.notified_events.include?('file_added')
190 if !attachments.empty? && Setting.notified_events.include?('file_added')
197 Mailer.deliver_attachments_added(attachments)
191 Mailer.deliver_attachments_added(attachments)
198 end
192 end
199 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
193 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
200 return
194 return
201 end
195 end
202 @versions = @project.versions.sort
196 @versions = @project.versions.sort
203 end
197 end
204
198
205 def list_files
199 def list_files
206 sort_init 'filename', 'asc'
200 sort_init 'filename', 'asc'
207 sort_update 'filename' => "#{Attachment.table_name}.filename",
201 sort_update 'filename' => "#{Attachment.table_name}.filename",
208 'created_on' => "#{Attachment.table_name}.created_on",
202 'created_on' => "#{Attachment.table_name}.created_on",
209 'size' => "#{Attachment.table_name}.filesize",
203 'size' => "#{Attachment.table_name}.filesize",
210 'downloads' => "#{Attachment.table_name}.downloads"
204 'downloads' => "#{Attachment.table_name}.downloads"
211
205
212 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
206 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
213 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
207 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
214 render :layout => !request.xhr?
208 render :layout => !request.xhr?
215 end
209 end
216
210
217 # Show changelog for @project
211 # Show changelog for @project
218 def changelog
212 def changelog
219 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
213 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
220 retrieve_selected_tracker_ids(@trackers)
214 retrieve_selected_tracker_ids(@trackers)
221 @versions = @project.versions.sort
215 @versions = @project.versions.sort
222 end
216 end
223
217
224 def roadmap
218 def roadmap
225 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
219 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
226 retrieve_selected_tracker_ids(@trackers)
220 retrieve_selected_tracker_ids(@trackers)
227 @versions = @project.versions.sort
221 @versions = @project.versions.sort
228 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
222 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
229 end
223 end
230
224
231 def activity
225 def activity
232 @days = Setting.activity_days_default.to_i
226 @days = Setting.activity_days_default.to_i
233
227
234 if params[:from]
228 if params[:from]
235 begin; @date_to = params[:from].to_date + 1; rescue; end
229 begin; @date_to = params[:from].to_date + 1; rescue; end
236 end
230 end
237
231
238 @date_to ||= Date.today + 1
232 @date_to ||= Date.today + 1
239 @date_from = @date_to - @days
233 @date_from = @date_to - @days
240 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
234 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
241 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
235 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
242
236
243 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
237 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
244 :with_subprojects => @with_subprojects,
238 :with_subprojects => @with_subprojects,
245 :author => @author)
239 :author => @author)
246 @activity.scope_select {|t| !params["show_#{t}"].nil?}
240 @activity.scope_select {|t| !params["show_#{t}"].nil?}
247 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
241 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
248
242
249 events = @activity.events(@date_from, @date_to)
243 events = @activity.events(@date_from, @date_to)
250
244
251 respond_to do |format|
245 respond_to do |format|
252 format.html {
246 format.html {
253 @events_by_day = events.group_by(&:event_date)
247 @events_by_day = events.group_by(&:event_date)
254 render :layout => false if request.xhr?
248 render :layout => false if request.xhr?
255 }
249 }
256 format.atom {
250 format.atom {
257 title = l(:label_activity)
251 title = l(:label_activity)
258 if @author
252 if @author
259 title = @author.name
253 title = @author.name
260 elsif @activity.scope.size == 1
254 elsif @activity.scope.size == 1
261 title = l("label_#{@activity.scope.first.singularize}_plural")
255 title = l("label_#{@activity.scope.first.singularize}_plural")
262 end
256 end
263 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
257 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
264 }
258 }
265 end
259 end
266
260
267 rescue ActiveRecord::RecordNotFound
261 rescue ActiveRecord::RecordNotFound
268 render_404
262 render_404
269 end
263 end
270
264
271 private
265 private
272 # Find project of id params[:id]
266 # Find project of id params[:id]
273 # if not found, redirect to project list
267 # if not found, redirect to project list
274 # Used as a before_filter
268 # Used as a before_filter
275 def find_project
269 def find_project
276 @project = Project.find(params[:id])
270 @project = Project.find(params[:id])
277 rescue ActiveRecord::RecordNotFound
271 rescue ActiveRecord::RecordNotFound
278 render_404
272 render_404
279 end
273 end
280
274
281 def find_optional_project
275 def find_optional_project
282 return true unless params[:id]
276 return true unless params[:id]
283 @project = Project.find(params[:id])
277 @project = Project.find(params[:id])
284 authorize
278 authorize
285 rescue ActiveRecord::RecordNotFound
279 rescue ActiveRecord::RecordNotFound
286 render_404
280 render_404
287 end
281 end
288
282
289 def retrieve_selected_tracker_ids(selectable_trackers)
283 def retrieve_selected_tracker_ids(selectable_trackers)
290 if ids = params[:tracker_ids]
284 if ids = params[:tracker_ids]
291 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
285 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
292 else
286 else
293 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
287 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
294 end
288 end
295 end
289 end
296 end
290 end
@@ -1,236 +1,236
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ReportsController < ApplicationController
18 class ReportsController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_project, :authorize
20 before_filter :find_project, :authorize
21
21
22 def issue_report
22 def issue_report
23 @statuses = IssueStatus.find(:all, :order => 'position')
23 @statuses = IssueStatus.find(:all, :order => 'position')
24
24
25 case params[:detail]
25 case params[:detail]
26 when "tracker"
26 when "tracker"
27 @field = "tracker_id"
27 @field = "tracker_id"
28 @rows = @project.trackers
28 @rows = @project.trackers
29 @data = issues_by_tracker
29 @data = issues_by_tracker
30 @report_title = l(:field_tracker)
30 @report_title = l(:field_tracker)
31 render :template => "reports/issue_report_details"
31 render :template => "reports/issue_report_details"
32 when "version"
32 when "version"
33 @field = "fixed_version_id"
33 @field = "fixed_version_id"
34 @rows = @project.versions.sort
34 @rows = @project.versions.sort
35 @data = issues_by_version
35 @data = issues_by_version
36 @report_title = l(:field_version)
36 @report_title = l(:field_version)
37 render :template => "reports/issue_report_details"
37 render :template => "reports/issue_report_details"
38 when "priority"
38 when "priority"
39 @field = "priority_id"
39 @field = "priority_id"
40 @rows = Enumeration::get_values('IPRI')
40 @rows = Enumeration::get_values('IPRI')
41 @data = issues_by_priority
41 @data = issues_by_priority
42 @report_title = l(:field_priority)
42 @report_title = l(:field_priority)
43 render :template => "reports/issue_report_details"
43 render :template => "reports/issue_report_details"
44 when "category"
44 when "category"
45 @field = "category_id"
45 @field = "category_id"
46 @rows = @project.issue_categories
46 @rows = @project.issue_categories
47 @data = issues_by_category
47 @data = issues_by_category
48 @report_title = l(:field_category)
48 @report_title = l(:field_category)
49 render :template => "reports/issue_report_details"
49 render :template => "reports/issue_report_details"
50 when "assigned_to"
50 when "assigned_to"
51 @field = "assigned_to_id"
51 @field = "assigned_to_id"
52 @rows = @project.members.collect { |m| m.user }
52 @rows = @project.members.collect { |m| m.user }
53 @data = issues_by_assigned_to
53 @data = issues_by_assigned_to
54 @report_title = l(:field_assigned_to)
54 @report_title = l(:field_assigned_to)
55 render :template => "reports/issue_report_details"
55 render :template => "reports/issue_report_details"
56 when "author"
56 when "author"
57 @field = "author_id"
57 @field = "author_id"
58 @rows = @project.members.collect { |m| m.user }
58 @rows = @project.members.collect { |m| m.user }
59 @data = issues_by_author
59 @data = issues_by_author
60 @report_title = l(:field_author)
60 @report_title = l(:field_author)
61 render :template => "reports/issue_report_details"
61 render :template => "reports/issue_report_details"
62 when "subproject"
62 when "subproject"
63 @field = "project_id"
63 @field = "project_id"
64 @rows = @project.active_children
64 @rows = @project.descendants.active
65 @data = issues_by_subproject
65 @data = issues_by_subproject
66 @report_title = l(:field_subproject)
66 @report_title = l(:field_subproject)
67 render :template => "reports/issue_report_details"
67 render :template => "reports/issue_report_details"
68 else
68 else
69 @trackers = @project.trackers
69 @trackers = @project.trackers
70 @versions = @project.versions.sort
70 @versions = @project.versions.sort
71 @priorities = Enumeration::get_values('IPRI')
71 @priorities = Enumeration::get_values('IPRI')
72 @categories = @project.issue_categories
72 @categories = @project.issue_categories
73 @assignees = @project.members.collect { |m| m.user }
73 @assignees = @project.members.collect { |m| m.user }
74 @authors = @project.members.collect { |m| m.user }
74 @authors = @project.members.collect { |m| m.user }
75 @subprojects = @project.active_children
75 @subprojects = @project.descendants.active
76 issues_by_tracker
76 issues_by_tracker
77 issues_by_version
77 issues_by_version
78 issues_by_priority
78 issues_by_priority
79 issues_by_category
79 issues_by_category
80 issues_by_assigned_to
80 issues_by_assigned_to
81 issues_by_author
81 issues_by_author
82 issues_by_subproject
82 issues_by_subproject
83
83
84 render :template => "reports/issue_report"
84 render :template => "reports/issue_report"
85 end
85 end
86 end
86 end
87
87
88 def delays
88 def delays
89 @trackers = Tracker.find(:all)
89 @trackers = Tracker.find(:all)
90 if request.get?
90 if request.get?
91 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
91 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
92 else
92 else
93 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
93 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
94 end
94 end
95 @selected_tracker_ids ||= []
95 @selected_tracker_ids ||= []
96 @raw =
96 @raw =
97 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
97 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
98 FROM issue_histories a, issue_histories b, issues i
98 FROM issue_histories a, issue_histories b, issues i
99 WHERE a.status_id =5
99 WHERE a.status_id =5
100 AND a.issue_id = b.issue_id
100 AND a.issue_id = b.issue_id
101 AND a.issue_id = i.id
101 AND a.issue_id = i.id
102 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
102 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
103 AND b.id = (
103 AND b.id = (
104 SELECT min( c.id )
104 SELECT min( c.id )
105 FROM issue_histories c
105 FROM issue_histories c
106 WHERE b.issue_id = c.issue_id )
106 WHERE b.issue_id = c.issue_id )
107 GROUP BY delay") unless @selected_tracker_ids.empty?
107 GROUP BY delay") unless @selected_tracker_ids.empty?
108 @raw ||=[]
108 @raw ||=[]
109
109
110 @x_from = 0
110 @x_from = 0
111 @x_to = 0
111 @x_to = 0
112 @y_from = 0
112 @y_from = 0
113 @y_to = 0
113 @y_to = 0
114 @sum_total = 0
114 @sum_total = 0
115 @sum_delay = 0
115 @sum_delay = 0
116 @raw.each do |r|
116 @raw.each do |r|
117 @x_to = [r['delay'].to_i, @x_to].max
117 @x_to = [r['delay'].to_i, @x_to].max
118 @y_to = [r['total'].to_i, @y_to].max
118 @y_to = [r['total'].to_i, @y_to].max
119 @sum_total = @sum_total + r['total'].to_i
119 @sum_total = @sum_total + r['total'].to_i
120 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
120 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
121 end
121 end
122 end
122 end
123
123
124 private
124 private
125 # Find project of id params[:id]
125 # Find project of id params[:id]
126 def find_project
126 def find_project
127 @project = Project.find(params[:id])
127 @project = Project.find(params[:id])
128 rescue ActiveRecord::RecordNotFound
128 rescue ActiveRecord::RecordNotFound
129 render_404
129 render_404
130 end
130 end
131
131
132 def issues_by_tracker
132 def issues_by_tracker
133 @issues_by_tracker ||=
133 @issues_by_tracker ||=
134 ActiveRecord::Base.connection.select_all("select s.id as status_id,
134 ActiveRecord::Base.connection.select_all("select s.id as status_id,
135 s.is_closed as closed,
135 s.is_closed as closed,
136 t.id as tracker_id,
136 t.id as tracker_id,
137 count(i.id) as total
137 count(i.id) as total
138 from
138 from
139 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
139 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
140 where
140 where
141 i.status_id=s.id
141 i.status_id=s.id
142 and i.tracker_id=t.id
142 and i.tracker_id=t.id
143 and i.project_id=#{@project.id}
143 and i.project_id=#{@project.id}
144 group by s.id, s.is_closed, t.id")
144 group by s.id, s.is_closed, t.id")
145 end
145 end
146
146
147 def issues_by_version
147 def issues_by_version
148 @issues_by_version ||=
148 @issues_by_version ||=
149 ActiveRecord::Base.connection.select_all("select s.id as status_id,
149 ActiveRecord::Base.connection.select_all("select s.id as status_id,
150 s.is_closed as closed,
150 s.is_closed as closed,
151 v.id as fixed_version_id,
151 v.id as fixed_version_id,
152 count(i.id) as total
152 count(i.id) as total
153 from
153 from
154 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
154 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
155 where
155 where
156 i.status_id=s.id
156 i.status_id=s.id
157 and i.fixed_version_id=v.id
157 and i.fixed_version_id=v.id
158 and i.project_id=#{@project.id}
158 and i.project_id=#{@project.id}
159 group by s.id, s.is_closed, v.id")
159 group by s.id, s.is_closed, v.id")
160 end
160 end
161
161
162 def issues_by_priority
162 def issues_by_priority
163 @issues_by_priority ||=
163 @issues_by_priority ||=
164 ActiveRecord::Base.connection.select_all("select s.id as status_id,
164 ActiveRecord::Base.connection.select_all("select s.id as status_id,
165 s.is_closed as closed,
165 s.is_closed as closed,
166 p.id as priority_id,
166 p.id as priority_id,
167 count(i.id) as total
167 count(i.id) as total
168 from
168 from
169 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
169 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
170 where
170 where
171 i.status_id=s.id
171 i.status_id=s.id
172 and i.priority_id=p.id
172 and i.priority_id=p.id
173 and i.project_id=#{@project.id}
173 and i.project_id=#{@project.id}
174 group by s.id, s.is_closed, p.id")
174 group by s.id, s.is_closed, p.id")
175 end
175 end
176
176
177 def issues_by_category
177 def issues_by_category
178 @issues_by_category ||=
178 @issues_by_category ||=
179 ActiveRecord::Base.connection.select_all("select s.id as status_id,
179 ActiveRecord::Base.connection.select_all("select s.id as status_id,
180 s.is_closed as closed,
180 s.is_closed as closed,
181 c.id as category_id,
181 c.id as category_id,
182 count(i.id) as total
182 count(i.id) as total
183 from
183 from
184 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
184 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
185 where
185 where
186 i.status_id=s.id
186 i.status_id=s.id
187 and i.category_id=c.id
187 and i.category_id=c.id
188 and i.project_id=#{@project.id}
188 and i.project_id=#{@project.id}
189 group by s.id, s.is_closed, c.id")
189 group by s.id, s.is_closed, c.id")
190 end
190 end
191
191
192 def issues_by_assigned_to
192 def issues_by_assigned_to
193 @issues_by_assigned_to ||=
193 @issues_by_assigned_to ||=
194 ActiveRecord::Base.connection.select_all("select s.id as status_id,
194 ActiveRecord::Base.connection.select_all("select s.id as status_id,
195 s.is_closed as closed,
195 s.is_closed as closed,
196 a.id as assigned_to_id,
196 a.id as assigned_to_id,
197 count(i.id) as total
197 count(i.id) as total
198 from
198 from
199 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
199 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
200 where
200 where
201 i.status_id=s.id
201 i.status_id=s.id
202 and i.assigned_to_id=a.id
202 and i.assigned_to_id=a.id
203 and i.project_id=#{@project.id}
203 and i.project_id=#{@project.id}
204 group by s.id, s.is_closed, a.id")
204 group by s.id, s.is_closed, a.id")
205 end
205 end
206
206
207 def issues_by_author
207 def issues_by_author
208 @issues_by_author ||=
208 @issues_by_author ||=
209 ActiveRecord::Base.connection.select_all("select s.id as status_id,
209 ActiveRecord::Base.connection.select_all("select s.id as status_id,
210 s.is_closed as closed,
210 s.is_closed as closed,
211 a.id as author_id,
211 a.id as author_id,
212 count(i.id) as total
212 count(i.id) as total
213 from
213 from
214 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
214 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
215 where
215 where
216 i.status_id=s.id
216 i.status_id=s.id
217 and i.author_id=a.id
217 and i.author_id=a.id
218 and i.project_id=#{@project.id}
218 and i.project_id=#{@project.id}
219 group by s.id, s.is_closed, a.id")
219 group by s.id, s.is_closed, a.id")
220 end
220 end
221
221
222 def issues_by_subproject
222 def issues_by_subproject
223 @issues_by_subproject ||=
223 @issues_by_subproject ||=
224 ActiveRecord::Base.connection.select_all("select s.id as status_id,
224 ActiveRecord::Base.connection.select_all("select s.id as status_id,
225 s.is_closed as closed,
225 s.is_closed as closed,
226 i.project_id as project_id,
226 i.project_id as project_id,
227 count(i.id) as total
227 count(i.id) as total
228 from
228 from
229 #{Issue.table_name} i, #{IssueStatus.table_name} s
229 #{Issue.table_name} i, #{IssueStatus.table_name} s
230 where
230 where
231 i.status_id=s.id
231 i.status_id=s.id
232 and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')})
232 and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')})
233 group by s.id, s.is_closed, i.project_id") if @project.active_children.any?
233 group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
234 @issues_by_subproject ||= []
234 @issues_by_subproject ||= []
235 end
235 end
236 end
236 end
@@ -1,116 +1,116
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class SearchController < ApplicationController
18 class SearchController < ApplicationController
19 before_filter :find_optional_project
19 before_filter :find_optional_project
20
20
21 helper :messages
21 helper :messages
22 include MessagesHelper
22 include MessagesHelper
23
23
24 def index
24 def index
25 @question = params[:q] || ""
25 @question = params[:q] || ""
26 @question.strip!
26 @question.strip!
27 @all_words = params[:all_words] || (params[:submit] ? false : true)
27 @all_words = params[:all_words] || (params[:submit] ? false : true)
28 @titles_only = !params[:titles_only].nil?
28 @titles_only = !params[:titles_only].nil?
29
29
30 projects_to_search =
30 projects_to_search =
31 case params[:scope]
31 case params[:scope]
32 when 'all'
32 when 'all'
33 nil
33 nil
34 when 'my_projects'
34 when 'my_projects'
35 User.current.memberships.collect(&:project)
35 User.current.memberships.collect(&:project)
36 when 'subprojects'
36 when 'subprojects'
37 @project ? ([ @project ] + @project.active_children) : nil
37 @project ? (@project.self_and_descendants.active) : nil
38 else
38 else
39 @project
39 @project
40 end
40 end
41
41
42 offset = nil
42 offset = nil
43 begin; offset = params[:offset].to_time if params[:offset]; rescue; end
43 begin; offset = params[:offset].to_time if params[:offset]; rescue; end
44
44
45 # quick jump to an issue
45 # quick jump to an issue
46 if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current))
46 if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current))
47 redirect_to :controller => "issues", :action => "show", :id => $1
47 redirect_to :controller => "issues", :action => "show", :id => $1
48 return
48 return
49 end
49 end
50
50
51 @object_types = %w(issues news documents changesets wiki_pages messages projects)
51 @object_types = %w(issues news documents changesets wiki_pages messages projects)
52 if projects_to_search.is_a? Project
52 if projects_to_search.is_a? Project
53 # don't search projects
53 # don't search projects
54 @object_types.delete('projects')
54 @object_types.delete('projects')
55 # only show what the user is allowed to view
55 # only show what the user is allowed to view
56 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
56 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
57 end
57 end
58
58
59 @scope = @object_types.select {|t| params[t]}
59 @scope = @object_types.select {|t| params[t]}
60 @scope = @object_types if @scope.empty?
60 @scope = @object_types if @scope.empty?
61
61
62 # extract tokens from the question
62 # extract tokens from the question
63 # eg. hello "bye bye" => ["hello", "bye bye"]
63 # eg. hello "bye bye" => ["hello", "bye bye"]
64 @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
64 @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
65 # tokens must be at least 3 character long
65 # tokens must be at least 3 character long
66 @tokens = @tokens.uniq.select {|w| w.length > 2 }
66 @tokens = @tokens.uniq.select {|w| w.length > 2 }
67
67
68 if !@tokens.empty?
68 if !@tokens.empty?
69 # no more than 5 tokens to search for
69 # no more than 5 tokens to search for
70 @tokens.slice! 5..-1 if @tokens.size > 5
70 @tokens.slice! 5..-1 if @tokens.size > 5
71 # strings used in sql like statement
71 # strings used in sql like statement
72 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
72 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
73
73
74 @results = []
74 @results = []
75 @results_by_type = Hash.new {|h,k| h[k] = 0}
75 @results_by_type = Hash.new {|h,k| h[k] = 0}
76
76
77 limit = 10
77 limit = 10
78 @scope.each do |s|
78 @scope.each do |s|
79 r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
79 r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
80 :all_words => @all_words,
80 :all_words => @all_words,
81 :titles_only => @titles_only,
81 :titles_only => @titles_only,
82 :limit => (limit+1),
82 :limit => (limit+1),
83 :offset => offset,
83 :offset => offset,
84 :before => params[:previous].nil?)
84 :before => params[:previous].nil?)
85 @results += r
85 @results += r
86 @results_by_type[s] += c
86 @results_by_type[s] += c
87 end
87 end
88 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
88 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
89 if params[:previous].nil?
89 if params[:previous].nil?
90 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
90 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
91 if @results.size > limit
91 if @results.size > limit
92 @pagination_next_date = @results[limit-1].event_datetime
92 @pagination_next_date = @results[limit-1].event_datetime
93 @results = @results[0, limit]
93 @results = @results[0, limit]
94 end
94 end
95 else
95 else
96 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
96 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
97 if @results.size > limit
97 if @results.size > limit
98 @pagination_previous_date = @results[-(limit)].event_datetime
98 @pagination_previous_date = @results[-(limit)].event_datetime
99 @results = @results[-(limit), limit]
99 @results = @results[-(limit), limit]
100 end
100 end
101 end
101 end
102 else
102 else
103 @question = ""
103 @question = ""
104 end
104 end
105 render :layout => false if request.xhr?
105 render :layout => false if request.xhr?
106 end
106 end
107
107
108 private
108 private
109 def find_optional_project
109 def find_optional_project
110 return true unless params[:id]
110 return true unless params[:id]
111 @project = Project.find(params[:id])
111 @project = Project.find(params[:id])
112 check_project_privacy
112 check_project_privacy
113 rescue ActiveRecord::RecordNotFound
113 rescue ActiveRecord::RecordNotFound
114 render_404
114 render_404
115 end
115 end
116 end
116 end
@@ -1,104 +1,104
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class UsersController < ApplicationController
18 class UsersController < ApplicationController
19 before_filter :require_admin
19 before_filter :require_admin
20
20
21 helper :sort
21 helper :sort
22 include SortHelper
22 include SortHelper
23 helper :custom_fields
23 helper :custom_fields
24 include CustomFieldsHelper
24 include CustomFieldsHelper
25
25
26 def index
26 def index
27 list
27 list
28 render :action => 'list' unless request.xhr?
28 render :action => 'list' unless request.xhr?
29 end
29 end
30
30
31 def list
31 def list
32 sort_init 'login', 'asc'
32 sort_init 'login', 'asc'
33 sort_update %w(login firstname lastname mail admin created_on last_login_on)
33 sort_update %w(login firstname lastname mail admin created_on last_login_on)
34
34
35 @status = params[:status] ? params[:status].to_i : 1
35 @status = params[:status] ? params[:status].to_i : 1
36 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
36 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
37
37
38 unless params[:name].blank?
38 unless params[:name].blank?
39 name = "%#{params[:name].strip.downcase}%"
39 name = "%#{params[:name].strip.downcase}%"
40 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
40 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
41 end
41 end
42
42
43 @user_count = User.count(:conditions => c.conditions)
43 @user_count = User.count(:conditions => c.conditions)
44 @user_pages = Paginator.new self, @user_count,
44 @user_pages = Paginator.new self, @user_count,
45 per_page_option,
45 per_page_option,
46 params['page']
46 params['page']
47 @users = User.find :all,:order => sort_clause,
47 @users = User.find :all,:order => sort_clause,
48 :conditions => c.conditions,
48 :conditions => c.conditions,
49 :limit => @user_pages.items_per_page,
49 :limit => @user_pages.items_per_page,
50 :offset => @user_pages.current.offset
50 :offset => @user_pages.current.offset
51
51
52 render :action => "list", :layout => false if request.xhr?
52 render :action => "list", :layout => false if request.xhr?
53 end
53 end
54
54
55 def add
55 def add
56 if request.get?
56 if request.get?
57 @user = User.new(:language => Setting.default_language)
57 @user = User.new(:language => Setting.default_language)
58 else
58 else
59 @user = User.new(params[:user])
59 @user = User.new(params[:user])
60 @user.admin = params[:user][:admin] || false
60 @user.admin = params[:user][:admin] || false
61 @user.login = params[:user][:login]
61 @user.login = params[:user][:login]
62 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
62 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
63 if @user.save
63 if @user.save
64 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
64 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
65 flash[:notice] = l(:notice_successful_create)
65 flash[:notice] = l(:notice_successful_create)
66 redirect_to :action => 'list'
66 redirect_to :action => 'list'
67 end
67 end
68 end
68 end
69 @auth_sources = AuthSource.find(:all)
69 @auth_sources = AuthSource.find(:all)
70 end
70 end
71
71
72 def edit
72 def edit
73 @user = User.find(params[:id])
73 @user = User.find(params[:id])
74 if request.post?
74 if request.post?
75 @user.admin = params[:user][:admin] if params[:user][:admin]
75 @user.admin = params[:user][:admin] if params[:user][:admin]
76 @user.login = params[:user][:login] if params[:user][:login]
76 @user.login = params[:user][:login] if params[:user][:login]
77 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
77 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
78 if @user.update_attributes(params[:user])
78 if @user.update_attributes(params[:user])
79 flash[:notice] = l(:notice_successful_update)
79 flash[:notice] = l(:notice_successful_update)
80 # Give a string to redirect_to otherwise it would use status param as the response code
80 # Give a string to redirect_to otherwise it would use status param as the response code
81 redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
81 redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
82 end
82 end
83 end
83 end
84 @auth_sources = AuthSource.find(:all)
84 @auth_sources = AuthSource.find(:all)
85 @roles = Role.find_all_givable
85 @roles = Role.find_all_givable
86 @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
86 @projects = Project.active.find(:all, :order => 'lft')
87 @membership ||= Member.new
87 @membership ||= Member.new
88 @memberships = @user.memberships
88 @memberships = @user.memberships
89 end
89 end
90
90
91 def edit_membership
91 def edit_membership
92 @user = User.find(params[:id])
92 @user = User.find(params[:id])
93 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
93 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
94 @membership.attributes = params[:membership]
94 @membership.attributes = params[:membership]
95 @membership.save if request.post?
95 @membership.save if request.post?
96 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
96 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
97 end
97 end
98
98
99 def destroy_membership
99 def destroy_membership
100 @user = User.find(params[:id])
100 @user = User.find(params[:id])
101 Member.find(params[:membership_id]).destroy if request.post?
101 Member.find(params[:membership_id]).destroy if request.post?
102 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
102 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
103 end
103 end
104 end
104 end
@@ -1,23 +1,31
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module AdminHelper
18 module AdminHelper
19 def project_status_options_for_select(selected)
19 def project_status_options_for_select(selected)
20 options_for_select([[l(:label_all), ''],
20 options_for_select([[l(:label_all), ''],
21 [l(:status_active), 1]], selected)
21 [l(:status_active), 1]], selected)
22 end
22 end
23
24 def css_project_classes(project)
25 s = 'project'
26 s << ' root' if project.root?
27 s << ' child' if project.child?
28 s << (project.leaf? ? ' leaf' : ' parent')
29 s
30 end
23 end
31 end
@@ -1,628 +1,667
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'coderay'
18 require 'coderay'
19 require 'coderay/helpers/file_type'
19 require 'coderay/helpers/file_type'
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include GravatarHelper::PublicMethods
25 include GravatarHelper::PublicMethods
26
26
27 extend Forwardable
27 extend Forwardable
28 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29
29
30 def current_role
30 def current_role
31 @current_role ||= User.current.role_for_project(@project)
31 @current_role ||= User.current.role_for_project(@project)
32 end
32 end
33
33
34 # Return true if user is authorized for controller/action, otherwise false
34 # Return true if user is authorized for controller/action, otherwise false
35 def authorize_for(controller, action)
35 def authorize_for(controller, action)
36 User.current.allowed_to?({:controller => controller, :action => action}, @project)
36 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 end
37 end
38
38
39 # Display a link if user is authorized
39 # Display a link if user is authorized
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 end
42 end
43
43
44 # Display a link to remote if user is authorized
44 # Display a link to remote if user is authorized
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 url = options[:url] || {}
46 url = options[:url] || {}
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 end
48 end
49
49
50 # Display a link to user's account page
50 # Display a link to user's account page
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
52 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
53 end
53 end
54
54
55 def link_to_issue(issue, options={})
55 def link_to_issue(issue, options={})
56 options[:class] ||= ''
56 options[:class] ||= ''
57 options[:class] << ' issue'
57 options[:class] << ' issue'
58 options[:class] << ' closed' if issue.closed?
58 options[:class] << ' closed' if issue.closed?
59 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
59 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
60 end
60 end
61
61
62 # Generates a link to an attachment.
62 # Generates a link to an attachment.
63 # Options:
63 # Options:
64 # * :text - Link text (default to attachment filename)
64 # * :text - Link text (default to attachment filename)
65 # * :download - Force download (default: false)
65 # * :download - Force download (default: false)
66 def link_to_attachment(attachment, options={})
66 def link_to_attachment(attachment, options={})
67 text = options.delete(:text) || attachment.filename
67 text = options.delete(:text) || attachment.filename
68 action = options.delete(:download) ? 'download' : 'show'
68 action = options.delete(:download) ? 'download' : 'show'
69
69
70 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
70 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
71 end
71 end
72
72
73 def toggle_link(name, id, options={})
73 def toggle_link(name, id, options={})
74 onclick = "Element.toggle('#{id}'); "
74 onclick = "Element.toggle('#{id}'); "
75 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
75 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
76 onclick << "return false;"
76 onclick << "return false;"
77 link_to(name, "#", :onclick => onclick)
77 link_to(name, "#", :onclick => onclick)
78 end
78 end
79
79
80 def image_to_function(name, function, html_options = {})
80 def image_to_function(name, function, html_options = {})
81 html_options.symbolize_keys!
81 html_options.symbolize_keys!
82 tag(:input, html_options.merge({
82 tag(:input, html_options.merge({
83 :type => "image", :src => image_path(name),
83 :type => "image", :src => image_path(name),
84 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
84 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
85 }))
85 }))
86 end
86 end
87
87
88 def prompt_to_remote(name, text, param, url, html_options = {})
88 def prompt_to_remote(name, text, param, url, html_options = {})
89 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
89 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
90 link_to name, {}, html_options
90 link_to name, {}, html_options
91 end
91 end
92
92
93 def format_date(date)
93 def format_date(date)
94 return nil unless date
94 return nil unless date
95 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
95 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
96 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
96 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
97 date.strftime(@date_format)
97 date.strftime(@date_format)
98 end
98 end
99
99
100 def format_time(time, include_date = true)
100 def format_time(time, include_date = true)
101 return nil unless time
101 return nil unless time
102 time = time.to_time if time.is_a?(String)
102 time = time.to_time if time.is_a?(String)
103 zone = User.current.time_zone
103 zone = User.current.time_zone
104 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
104 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
105 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
105 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
106 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
106 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
107 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
107 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
108 end
108 end
109
109
110 def format_activity_title(text)
110 def format_activity_title(text)
111 h(truncate_single_line(text, 100))
111 h(truncate_single_line(text, 100))
112 end
112 end
113
113
114 def format_activity_day(date)
114 def format_activity_day(date)
115 date == Date.today ? l(:label_today).titleize : format_date(date)
115 date == Date.today ? l(:label_today).titleize : format_date(date)
116 end
116 end
117
117
118 def format_activity_description(text)
118 def format_activity_description(text)
119 h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
119 h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
120 end
120 end
121
121
122 def distance_of_date_in_words(from_date, to_date = 0)
122 def distance_of_date_in_words(from_date, to_date = 0)
123 from_date = from_date.to_date if from_date.respond_to?(:to_date)
123 from_date = from_date.to_date if from_date.respond_to?(:to_date)
124 to_date = to_date.to_date if to_date.respond_to?(:to_date)
124 to_date = to_date.to_date if to_date.respond_to?(:to_date)
125 distance_in_days = (to_date - from_date).abs
125 distance_in_days = (to_date - from_date).abs
126 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
126 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
127 end
127 end
128
128
129 def due_date_distance_in_words(date)
129 def due_date_distance_in_words(date)
130 if date
130 if date
131 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
131 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
132 end
132 end
133 end
133 end
134
134
135 def render_page_hierarchy(pages, node=nil)
135 def render_page_hierarchy(pages, node=nil)
136 content = ''
136 content = ''
137 if pages[node]
137 if pages[node]
138 content << "<ul class=\"pages-hierarchy\">\n"
138 content << "<ul class=\"pages-hierarchy\">\n"
139 pages[node].each do |page|
139 pages[node].each do |page|
140 content << "<li>"
140 content << "<li>"
141 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
141 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
142 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
142 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
143 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
143 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
144 content << "</li>\n"
144 content << "</li>\n"
145 end
145 end
146 content << "</ul>\n"
146 content << "</ul>\n"
147 end
147 end
148 content
148 content
149 end
149 end
150
150
151 # Renders flash messages
151 # Renders flash messages
152 def render_flash_messages
152 def render_flash_messages
153 s = ''
153 s = ''
154 flash.each do |k,v|
154 flash.each do |k,v|
155 s << content_tag('div', v, :class => "flash #{k}")
155 s << content_tag('div', v, :class => "flash #{k}")
156 end
156 end
157 s
157 s
158 end
158 end
159
160 # Renders the project quick-jump box
161 def render_project_jump_box
162 # Retrieve them now to avoid a COUNT query
163 projects = User.current.projects.all
164 if projects.any?
165 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
166 "<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
167 '<option disabled="disabled">---</option>'
168 s << project_tree_options_for_select(projects) do |p|
169 { :value => url_for(:controller => 'projects', :action => 'show', :id => p) }
170 end
171 s << '</select>'
172 s
173 end
174 end
175
176 def project_tree_options_for_select(projects, options = {})
177 s = ''
178 project_tree(projects) do |project, level|
179 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
180 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
181 tag_options.merge!(yield(project)) if block_given?
182 s << content_tag('option', name_prefix + h(project), tag_options)
183 end
184 s
185 end
186
187 # Yields the given block for each project with its level in the tree
188 def project_tree(projects, &block)
189 ancestors = []
190 projects.sort_by(&:lft).each do |project|
191 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
192 ancestors.pop
193 end
194 yield project, ancestors.size
195 ancestors << project
196 end
197 end
159
198
160 # Truncates and returns the string as a single line
199 # Truncates and returns the string as a single line
161 def truncate_single_line(string, *args)
200 def truncate_single_line(string, *args)
162 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
201 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
163 end
202 end
164
203
165 def html_hours(text)
204 def html_hours(text)
166 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
205 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
167 end
206 end
168
207
169 def authoring(created, author, options={})
208 def authoring(created, author, options={})
170 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
209 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
171 link_to(distance_of_time_in_words(Time.now, created),
210 link_to(distance_of_time_in_words(Time.now, created),
172 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
211 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
173 :title => format_time(created))
212 :title => format_time(created))
174 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
213 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
175 l(options[:label] || :label_added_time_by, author_tag, time_tag)
214 l(options[:label] || :label_added_time_by, author_tag, time_tag)
176 end
215 end
177
216
178 def l_or_humanize(s, options={})
217 def l_or_humanize(s, options={})
179 k = "#{options[:prefix]}#{s}".to_sym
218 k = "#{options[:prefix]}#{s}".to_sym
180 l_has_string?(k) ? l(k) : s.to_s.humanize
219 l_has_string?(k) ? l(k) : s.to_s.humanize
181 end
220 end
182
221
183 def day_name(day)
222 def day_name(day)
184 l(:general_day_names).split(',')[day-1]
223 l(:general_day_names).split(',')[day-1]
185 end
224 end
186
225
187 def month_name(month)
226 def month_name(month)
188 l(:actionview_datehelper_select_month_names).split(',')[month-1]
227 l(:actionview_datehelper_select_month_names).split(',')[month-1]
189 end
228 end
190
229
191 def syntax_highlight(name, content)
230 def syntax_highlight(name, content)
192 type = CodeRay::FileType[name]
231 type = CodeRay::FileType[name]
193 type ? CodeRay.scan(content, type).html : h(content)
232 type ? CodeRay.scan(content, type).html : h(content)
194 end
233 end
195
234
196 def to_path_param(path)
235 def to_path_param(path)
197 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
236 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
198 end
237 end
199
238
200 def pagination_links_full(paginator, count=nil, options={})
239 def pagination_links_full(paginator, count=nil, options={})
201 page_param = options.delete(:page_param) || :page
240 page_param = options.delete(:page_param) || :page
202 url_param = params.dup
241 url_param = params.dup
203 # don't reuse params if filters are present
242 # don't reuse params if filters are present
204 url_param.clear if url_param.has_key?(:set_filter)
243 url_param.clear if url_param.has_key?(:set_filter)
205
244
206 html = ''
245 html = ''
207 html << link_to_remote(('&#171; ' + l(:label_previous)),
246 html << link_to_remote(('&#171; ' + l(:label_previous)),
208 {:update => 'content',
247 {:update => 'content',
209 :url => url_param.merge(page_param => paginator.current.previous),
248 :url => url_param.merge(page_param => paginator.current.previous),
210 :complete => 'window.scrollTo(0,0)'},
249 :complete => 'window.scrollTo(0,0)'},
211 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
250 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
212
251
213 html << (pagination_links_each(paginator, options) do |n|
252 html << (pagination_links_each(paginator, options) do |n|
214 link_to_remote(n.to_s,
253 link_to_remote(n.to_s,
215 {:url => {:params => url_param.merge(page_param => n)},
254 {:url => {:params => url_param.merge(page_param => n)},
216 :update => 'content',
255 :update => 'content',
217 :complete => 'window.scrollTo(0,0)'},
256 :complete => 'window.scrollTo(0,0)'},
218 {:href => url_for(:params => url_param.merge(page_param => n))})
257 {:href => url_for(:params => url_param.merge(page_param => n))})
219 end || '')
258 end || '')
220
259
221 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
260 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
222 {:update => 'content',
261 {:update => 'content',
223 :url => url_param.merge(page_param => paginator.current.next),
262 :url => url_param.merge(page_param => paginator.current.next),
224 :complete => 'window.scrollTo(0,0)'},
263 :complete => 'window.scrollTo(0,0)'},
225 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
264 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
226
265
227 unless count.nil?
266 unless count.nil?
228 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
267 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
229 end
268 end
230
269
231 html
270 html
232 end
271 end
233
272
234 def per_page_links(selected=nil)
273 def per_page_links(selected=nil)
235 url_param = params.dup
274 url_param = params.dup
236 url_param.clear if url_param.has_key?(:set_filter)
275 url_param.clear if url_param.has_key?(:set_filter)
237
276
238 links = Setting.per_page_options_array.collect do |n|
277 links = Setting.per_page_options_array.collect do |n|
239 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
278 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
240 {:href => url_for(url_param.merge(:per_page => n))})
279 {:href => url_for(url_param.merge(:per_page => n))})
241 end
280 end
242 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
281 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
243 end
282 end
244
283
245 def breadcrumb(*args)
284 def breadcrumb(*args)
246 elements = args.flatten
285 elements = args.flatten
247 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
286 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
248 end
287 end
249
288
250 def html_title(*args)
289 def html_title(*args)
251 if args.empty?
290 if args.empty?
252 title = []
291 title = []
253 title << @project.name if @project
292 title << @project.name if @project
254 title += @html_title if @html_title
293 title += @html_title if @html_title
255 title << Setting.app_title
294 title << Setting.app_title
256 title.compact.join(' - ')
295 title.compact.join(' - ')
257 else
296 else
258 @html_title ||= []
297 @html_title ||= []
259 @html_title += args
298 @html_title += args
260 end
299 end
261 end
300 end
262
301
263 def accesskey(s)
302 def accesskey(s)
264 Redmine::AccessKeys.key_for s
303 Redmine::AccessKeys.key_for s
265 end
304 end
266
305
267 # Formats text according to system settings.
306 # Formats text according to system settings.
268 # 2 ways to call this method:
307 # 2 ways to call this method:
269 # * with a String: textilizable(text, options)
308 # * with a String: textilizable(text, options)
270 # * with an object and one of its attribute: textilizable(issue, :description, options)
309 # * with an object and one of its attribute: textilizable(issue, :description, options)
271 def textilizable(*args)
310 def textilizable(*args)
272 options = args.last.is_a?(Hash) ? args.pop : {}
311 options = args.last.is_a?(Hash) ? args.pop : {}
273 case args.size
312 case args.size
274 when 1
313 when 1
275 obj = options[:object]
314 obj = options[:object]
276 text = args.shift
315 text = args.shift
277 when 2
316 when 2
278 obj = args.shift
317 obj = args.shift
279 text = obj.send(args.shift).to_s
318 text = obj.send(args.shift).to_s
280 else
319 else
281 raise ArgumentError, 'invalid arguments to textilizable'
320 raise ArgumentError, 'invalid arguments to textilizable'
282 end
321 end
283 return '' if text.blank?
322 return '' if text.blank?
284
323
285 only_path = options.delete(:only_path) == false ? false : true
324 only_path = options.delete(:only_path) == false ? false : true
286
325
287 # when using an image link, try to use an attachment, if possible
326 # when using an image link, try to use an attachment, if possible
288 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
327 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
289
328
290 if attachments
329 if attachments
291 attachments = attachments.sort_by(&:created_on).reverse
330 attachments = attachments.sort_by(&:created_on).reverse
292 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
331 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
293 style = $1
332 style = $1
294 filename = $6
333 filename = $6
295 rf = Regexp.new(Regexp.escape(filename), Regexp::IGNORECASE)
334 rf = Regexp.new(Regexp.escape(filename), Regexp::IGNORECASE)
296 # search for the picture in attachments
335 # search for the picture in attachments
297 if found = attachments.detect { |att| att.filename =~ rf }
336 if found = attachments.detect { |att| att.filename =~ rf }
298 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
337 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
299 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
338 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
300 alt = desc.blank? ? nil : "(#{desc})"
339 alt = desc.blank? ? nil : "(#{desc})"
301 "!#{style}#{image_url}#{alt}!"
340 "!#{style}#{image_url}#{alt}!"
302 else
341 else
303 "!#{style}#{filename}!"
342 "!#{style}#{filename}!"
304 end
343 end
305 end
344 end
306 end
345 end
307
346
308 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
347 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
309
348
310 # different methods for formatting wiki links
349 # different methods for formatting wiki links
311 case options[:wiki_links]
350 case options[:wiki_links]
312 when :local
351 when :local
313 # used for local links to html files
352 # used for local links to html files
314 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
353 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
315 when :anchor
354 when :anchor
316 # used for single-file wiki export
355 # used for single-file wiki export
317 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
356 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
318 else
357 else
319 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
358 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
320 end
359 end
321
360
322 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
361 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
323
362
324 # Wiki links
363 # Wiki links
325 #
364 #
326 # Examples:
365 # Examples:
327 # [[mypage]]
366 # [[mypage]]
328 # [[mypage|mytext]]
367 # [[mypage|mytext]]
329 # wiki links can refer other project wikis, using project name or identifier:
368 # wiki links can refer other project wikis, using project name or identifier:
330 # [[project:]] -> wiki starting page
369 # [[project:]] -> wiki starting page
331 # [[project:|mytext]]
370 # [[project:|mytext]]
332 # [[project:mypage]]
371 # [[project:mypage]]
333 # [[project:mypage|mytext]]
372 # [[project:mypage|mytext]]
334 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
373 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
335 link_project = project
374 link_project = project
336 esc, all, page, title = $1, $2, $3, $5
375 esc, all, page, title = $1, $2, $3, $5
337 if esc.nil?
376 if esc.nil?
338 if page =~ /^([^\:]+)\:(.*)$/
377 if page =~ /^([^\:]+)\:(.*)$/
339 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
378 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
340 page = $2
379 page = $2
341 title ||= $1 if page.blank?
380 title ||= $1 if page.blank?
342 end
381 end
343
382
344 if link_project && link_project.wiki
383 if link_project && link_project.wiki
345 # extract anchor
384 # extract anchor
346 anchor = nil
385 anchor = nil
347 if page =~ /^(.+?)\#(.+)$/
386 if page =~ /^(.+?)\#(.+)$/
348 page, anchor = $1, $2
387 page, anchor = $1, $2
349 end
388 end
350 # check if page exists
389 # check if page exists
351 wiki_page = link_project.wiki.find_page(page)
390 wiki_page = link_project.wiki.find_page(page)
352 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
391 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
353 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
392 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
354 else
393 else
355 # project or wiki doesn't exist
394 # project or wiki doesn't exist
356 title || page
395 title || page
357 end
396 end
358 else
397 else
359 all
398 all
360 end
399 end
361 end
400 end
362
401
363 # Redmine links
402 # Redmine links
364 #
403 #
365 # Examples:
404 # Examples:
366 # Issues:
405 # Issues:
367 # #52 -> Link to issue #52
406 # #52 -> Link to issue #52
368 # Changesets:
407 # Changesets:
369 # r52 -> Link to revision 52
408 # r52 -> Link to revision 52
370 # commit:a85130f -> Link to scmid starting with a85130f
409 # commit:a85130f -> Link to scmid starting with a85130f
371 # Documents:
410 # Documents:
372 # document#17 -> Link to document with id 17
411 # document#17 -> Link to document with id 17
373 # document:Greetings -> Link to the document with title "Greetings"
412 # document:Greetings -> Link to the document with title "Greetings"
374 # document:"Some document" -> Link to the document with title "Some document"
413 # document:"Some document" -> Link to the document with title "Some document"
375 # Versions:
414 # Versions:
376 # version#3 -> Link to version with id 3
415 # version#3 -> Link to version with id 3
377 # version:1.0.0 -> Link to version named "1.0.0"
416 # version:1.0.0 -> Link to version named "1.0.0"
378 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
417 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
379 # Attachments:
418 # Attachments:
380 # attachment:file.zip -> Link to the attachment of the current object named file.zip
419 # attachment:file.zip -> Link to the attachment of the current object named file.zip
381 # Source files:
420 # Source files:
382 # source:some/file -> Link to the file located at /some/file in the project's repository
421 # source:some/file -> Link to the file located at /some/file in the project's repository
383 # source:some/file@52 -> Link to the file's revision 52
422 # source:some/file@52 -> Link to the file's revision 52
384 # source:some/file#L120 -> Link to line 120 of the file
423 # source:some/file#L120 -> Link to line 120 of the file
385 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
424 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
386 # export:some/file -> Force the download of the file
425 # export:some/file -> Force the download of the file
387 # Forum messages:
426 # Forum messages:
388 # message#1218 -> Link to message with id 1218
427 # message#1218 -> Link to message with id 1218
389 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
428 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
390 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
429 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
391 link = nil
430 link = nil
392 if esc.nil?
431 if esc.nil?
393 if prefix.nil? && sep == 'r'
432 if prefix.nil? && sep == 'r'
394 if project && (changeset = project.changesets.find_by_revision(oid))
433 if project && (changeset = project.changesets.find_by_revision(oid))
395 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
434 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
396 :class => 'changeset',
435 :class => 'changeset',
397 :title => truncate_single_line(changeset.comments, 100))
436 :title => truncate_single_line(changeset.comments, 100))
398 end
437 end
399 elsif sep == '#'
438 elsif sep == '#'
400 oid = oid.to_i
439 oid = oid.to_i
401 case prefix
440 case prefix
402 when nil
441 when nil
403 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
442 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
404 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
443 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
405 :class => (issue.closed? ? 'issue closed' : 'issue'),
444 :class => (issue.closed? ? 'issue closed' : 'issue'),
406 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
445 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
407 link = content_tag('del', link) if issue.closed?
446 link = content_tag('del', link) if issue.closed?
408 end
447 end
409 when 'document'
448 when 'document'
410 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
449 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
411 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
450 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
412 :class => 'document'
451 :class => 'document'
413 end
452 end
414 when 'version'
453 when 'version'
415 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
454 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
416 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
455 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
417 :class => 'version'
456 :class => 'version'
418 end
457 end
419 when 'message'
458 when 'message'
420 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
459 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
421 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
460 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
422 :controller => 'messages',
461 :controller => 'messages',
423 :action => 'show',
462 :action => 'show',
424 :board_id => message.board,
463 :board_id => message.board,
425 :id => message.root,
464 :id => message.root,
426 :anchor => (message.parent ? "message-#{message.id}" : nil)},
465 :anchor => (message.parent ? "message-#{message.id}" : nil)},
427 :class => 'message'
466 :class => 'message'
428 end
467 end
429 end
468 end
430 elsif sep == ':'
469 elsif sep == ':'
431 # removes the double quotes if any
470 # removes the double quotes if any
432 name = oid.gsub(%r{^"(.*)"$}, "\\1")
471 name = oid.gsub(%r{^"(.*)"$}, "\\1")
433 case prefix
472 case prefix
434 when 'document'
473 when 'document'
435 if project && document = project.documents.find_by_title(name)
474 if project && document = project.documents.find_by_title(name)
436 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
475 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
437 :class => 'document'
476 :class => 'document'
438 end
477 end
439 when 'version'
478 when 'version'
440 if project && version = project.versions.find_by_name(name)
479 if project && version = project.versions.find_by_name(name)
441 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
480 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
442 :class => 'version'
481 :class => 'version'
443 end
482 end
444 when 'commit'
483 when 'commit'
445 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
484 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
446 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
485 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
447 :class => 'changeset',
486 :class => 'changeset',
448 :title => truncate_single_line(changeset.comments, 100)
487 :title => truncate_single_line(changeset.comments, 100)
449 end
488 end
450 when 'source', 'export'
489 when 'source', 'export'
451 if project && project.repository
490 if project && project.repository
452 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
491 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
453 path, rev, anchor = $1, $3, $5
492 path, rev, anchor = $1, $3, $5
454 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
493 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
455 :path => to_path_param(path),
494 :path => to_path_param(path),
456 :rev => rev,
495 :rev => rev,
457 :anchor => anchor,
496 :anchor => anchor,
458 :format => (prefix == 'export' ? 'raw' : nil)},
497 :format => (prefix == 'export' ? 'raw' : nil)},
459 :class => (prefix == 'export' ? 'source download' : 'source')
498 :class => (prefix == 'export' ? 'source download' : 'source')
460 end
499 end
461 when 'attachment'
500 when 'attachment'
462 if attachments && attachment = attachments.detect {|a| a.filename == name }
501 if attachments && attachment = attachments.detect {|a| a.filename == name }
463 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
502 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
464 :class => 'attachment'
503 :class => 'attachment'
465 end
504 end
466 end
505 end
467 end
506 end
468 end
507 end
469 leading + (link || "#{prefix}#{sep}#{oid}")
508 leading + (link || "#{prefix}#{sep}#{oid}")
470 end
509 end
471
510
472 text
511 text
473 end
512 end
474
513
475 # Same as Rails' simple_format helper without using paragraphs
514 # Same as Rails' simple_format helper without using paragraphs
476 def simple_format_without_paragraph(text)
515 def simple_format_without_paragraph(text)
477 text.to_s.
516 text.to_s.
478 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
517 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
479 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
518 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
480 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
519 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
481 end
520 end
482
521
483 def error_messages_for(object_name, options = {})
522 def error_messages_for(object_name, options = {})
484 options = options.symbolize_keys
523 options = options.symbolize_keys
485 object = instance_variable_get("@#{object_name}")
524 object = instance_variable_get("@#{object_name}")
486 if object && !object.errors.empty?
525 if object && !object.errors.empty?
487 # build full_messages here with controller current language
526 # build full_messages here with controller current language
488 full_messages = []
527 full_messages = []
489 object.errors.each do |attr, msg|
528 object.errors.each do |attr, msg|
490 next if msg.nil?
529 next if msg.nil?
491 msg = msg.first if msg.is_a? Array
530 msg = msg.first if msg.is_a? Array
492 if attr == "base"
531 if attr == "base"
493 full_messages << l(msg)
532 full_messages << l(msg)
494 else
533 else
495 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
534 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
496 end
535 end
497 end
536 end
498 # retrieve custom values error messages
537 # retrieve custom values error messages
499 if object.errors[:custom_values]
538 if object.errors[:custom_values]
500 object.custom_values.each do |v|
539 object.custom_values.each do |v|
501 v.errors.each do |attr, msg|
540 v.errors.each do |attr, msg|
502 next if msg.nil?
541 next if msg.nil?
503 msg = msg.first if msg.is_a? Array
542 msg = msg.first if msg.is_a? Array
504 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
543 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
505 end
544 end
506 end
545 end
507 end
546 end
508 content_tag("div",
547 content_tag("div",
509 content_tag(
548 content_tag(
510 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
549 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
511 ) +
550 ) +
512 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
551 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
513 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
552 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
514 )
553 )
515 else
554 else
516 ""
555 ""
517 end
556 end
518 end
557 end
519
558
520 def lang_options_for_select(blank=true)
559 def lang_options_for_select(blank=true)
521 (blank ? [["(auto)", ""]] : []) +
560 (blank ? [["(auto)", ""]] : []) +
522 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
561 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
523 end
562 end
524
563
525 def label_tag_for(name, option_tags = nil, options = {})
564 def label_tag_for(name, option_tags = nil, options = {})
526 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
565 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
527 content_tag("label", label_text)
566 content_tag("label", label_text)
528 end
567 end
529
568
530 def labelled_tabular_form_for(name, object, options, &proc)
569 def labelled_tabular_form_for(name, object, options, &proc)
531 options[:html] ||= {}
570 options[:html] ||= {}
532 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
571 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
533 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
572 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
534 end
573 end
535
574
536 def back_url_hidden_field_tag
575 def back_url_hidden_field_tag
537 back_url = params[:back_url] || request.env['HTTP_REFERER']
576 back_url = params[:back_url] || request.env['HTTP_REFERER']
538 back_url = CGI.unescape(back_url.to_s)
577 back_url = CGI.unescape(back_url.to_s)
539 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
578 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
540 end
579 end
541
580
542 def check_all_links(form_name)
581 def check_all_links(form_name)
543 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
582 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
544 " | " +
583 " | " +
545 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
584 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
546 end
585 end
547
586
548 def progress_bar(pcts, options={})
587 def progress_bar(pcts, options={})
549 pcts = [pcts, pcts] unless pcts.is_a?(Array)
588 pcts = [pcts, pcts] unless pcts.is_a?(Array)
550 pcts[1] = pcts[1] - pcts[0]
589 pcts[1] = pcts[1] - pcts[0]
551 pcts << (100 - pcts[1] - pcts[0])
590 pcts << (100 - pcts[1] - pcts[0])
552 width = options[:width] || '100px;'
591 width = options[:width] || '100px;'
553 legend = options[:legend] || ''
592 legend = options[:legend] || ''
554 content_tag('table',
593 content_tag('table',
555 content_tag('tr',
594 content_tag('tr',
556 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
595 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
557 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
596 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
558 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
597 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
559 ), :class => 'progress', :style => "width: #{width};") +
598 ), :class => 'progress', :style => "width: #{width};") +
560 content_tag('p', legend, :class => 'pourcent')
599 content_tag('p', legend, :class => 'pourcent')
561 end
600 end
562
601
563 def context_menu_link(name, url, options={})
602 def context_menu_link(name, url, options={})
564 options[:class] ||= ''
603 options[:class] ||= ''
565 if options.delete(:selected)
604 if options.delete(:selected)
566 options[:class] << ' icon-checked disabled'
605 options[:class] << ' icon-checked disabled'
567 options[:disabled] = true
606 options[:disabled] = true
568 end
607 end
569 if options.delete(:disabled)
608 if options.delete(:disabled)
570 options.delete(:method)
609 options.delete(:method)
571 options.delete(:confirm)
610 options.delete(:confirm)
572 options.delete(:onclick)
611 options.delete(:onclick)
573 options[:class] << ' disabled'
612 options[:class] << ' disabled'
574 url = '#'
613 url = '#'
575 end
614 end
576 link_to name, url, options
615 link_to name, url, options
577 end
616 end
578
617
579 def calendar_for(field_id)
618 def calendar_for(field_id)
580 include_calendar_headers_tags
619 include_calendar_headers_tags
581 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
620 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
582 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
621 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
583 end
622 end
584
623
585 def include_calendar_headers_tags
624 def include_calendar_headers_tags
586 unless @calendar_headers_tags_included
625 unless @calendar_headers_tags_included
587 @calendar_headers_tags_included = true
626 @calendar_headers_tags_included = true
588 content_for :header_tags do
627 content_for :header_tags do
589 javascript_include_tag('calendar/calendar') +
628 javascript_include_tag('calendar/calendar') +
590 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
629 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
591 javascript_include_tag('calendar/calendar-setup') +
630 javascript_include_tag('calendar/calendar-setup') +
592 stylesheet_link_tag('calendar')
631 stylesheet_link_tag('calendar')
593 end
632 end
594 end
633 end
595 end
634 end
596
635
597 def content_for(name, content = nil, &block)
636 def content_for(name, content = nil, &block)
598 @has_content ||= {}
637 @has_content ||= {}
599 @has_content[name] = true
638 @has_content[name] = true
600 super(name, content, &block)
639 super(name, content, &block)
601 end
640 end
602
641
603 def has_content?(name)
642 def has_content?(name)
604 (@has_content && @has_content[name]) || false
643 (@has_content && @has_content[name]) || false
605 end
644 end
606
645
607 # Returns the avatar image tag for the given +user+ if avatars are enabled
646 # Returns the avatar image tag for the given +user+ if avatars are enabled
608 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
647 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
609 def avatar(user, options = { })
648 def avatar(user, options = { })
610 if Setting.gravatar_enabled?
649 if Setting.gravatar_enabled?
611 email = nil
650 email = nil
612 if user.respond_to?(:mail)
651 if user.respond_to?(:mail)
613 email = user.mail
652 email = user.mail
614 elsif user.to_s =~ %r{<(.+?)>}
653 elsif user.to_s =~ %r{<(.+?)>}
615 email = $1
654 email = $1
616 end
655 end
617 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
656 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
618 end
657 end
619 end
658 end
620
659
621 private
660 private
622
661
623 def wiki_helper
662 def wiki_helper
624 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
663 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
625 extend helper
664 extend helper
626 return self
665 return self
627 end
666 end
628 end
667 end
@@ -1,36 +1,71
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module ProjectsHelper
18 module ProjectsHelper
19 def link_to_version(version, options = {})
19 def link_to_version(version, options = {})
20 return '' unless version && version.is_a?(Version)
20 return '' unless version && version.is_a?(Version)
21 link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
21 link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
22 end
22 end
23
23
24 def project_settings_tabs
24 def project_settings_tabs
25 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
25 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
26 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
26 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
27 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
27 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
28 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
28 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
29 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
29 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
30 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
30 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
31 {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
31 {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
32 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
32 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
33 ]
33 ]
34 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
34 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
35 end
35 end
36
37 def parent_project_select_tag(project)
38 options = '<option></option>' + project_tree_options_for_select(project.possible_parents, :selected => project.parent)
39 content_tag('select', options, :name => 'project[parent_id]')
40 end
41
42 # Renders a tree of projects as a nested set of unordered lists
43 # The given collection may be a subset of the whole project tree
44 # (eg. some intermediate nodes are private and can not be seen)
45 def render_project_hierarchy(projects)
46 s = ''
47 if projects.any?
48 ancestors = []
49 projects.each do |project|
50 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
51 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
52 else
53 ancestors.pop
54 s << "</li>"
55 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
56 ancestors.pop
57 s << "</ul></li>\n"
58 end
59 end
60 classes = (ancestors.empty? ? 'root' : 'child')
61 s << "<li class='#{classes}'><div class='#{classes}'>" +
62 link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
63 s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
64 s << "</div>\n"
65 ancestors << project
66 end
67 s << ("</li></ul>\n" * ancestors.size)
68 end
69 s
70 end
36 end
71 end
@@ -1,63 +1,63
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module SearchHelper
18 module SearchHelper
19 def highlight_tokens(text, tokens)
19 def highlight_tokens(text, tokens)
20 return text unless text && tokens && !tokens.empty?
20 return text unless text && tokens && !tokens.empty?
21 re_tokens = tokens.collect {|t| Regexp.escape(t)}
21 re_tokens = tokens.collect {|t| Regexp.escape(t)}
22 regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
22 regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
23 result = ''
23 result = ''
24 text.split(regexp).each_with_index do |words, i|
24 text.split(regexp).each_with_index do |words, i|
25 if result.length > 1200
25 if result.length > 1200
26 # maximum length of the preview reached
26 # maximum length of the preview reached
27 result << '...'
27 result << '...'
28 break
28 break
29 end
29 end
30 if i.even?
30 if i.even?
31 result << h(words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words)
31 result << h(words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words)
32 else
32 else
33 t = (tokens.index(words.downcase) || 0) % 4
33 t = (tokens.index(words.downcase) || 0) % 4
34 result << content_tag('span', h(words), :class => "highlight token-#{t}")
34 result << content_tag('span', h(words), :class => "highlight token-#{t}")
35 end
35 end
36 end
36 end
37 result
37 result
38 end
38 end
39
39
40 def type_label(t)
40 def type_label(t)
41 l("label_#{t.singularize}_plural")
41 l("label_#{t.singularize}_plural")
42 end
42 end
43
43
44 def project_select_tag
44 def project_select_tag
45 options = [[l(:label_project_all), 'all']]
45 options = [[l(:label_project_all), 'all']]
46 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
46 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
47 options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.active_children.empty?
47 options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
48 options << [@project.name, ''] unless @project.nil?
48 options << [@project.name, ''] unless @project.nil?
49 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
49 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
50 end
50 end
51
51
52 def render_results_by_type(results_by_type)
52 def render_results_by_type(results_by_type)
53 links = []
53 links = []
54 # Sorts types by results count
54 # Sorts types by results count
55 results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
55 results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
56 c = results_by_type[t]
56 c = results_by_type[t]
57 next if c == 0
57 next if c == 0
58 text = "#{type_label(t)} (#{c})"
58 text = "#{type_label(t)} (#{c})"
59 links << link_to(text, :q => params[:q], :titles_only => params[:title_only], :all_words => params[:all_words], :scope => params[:scope], t => 1)
59 links << link_to(text, :q => params[:q], :titles_only => params[:title_only], :all_words => params[:all_words], :scope => params[:scope], t => 1)
60 end
60 end
61 ('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty?
61 ('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty?
62 end
62 end
63 end
63 end
@@ -1,58 +1,53
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module UsersHelper
18 module UsersHelper
19 def users_status_options_for_select(selected)
19 def users_status_options_for_select(selected)
20 user_count_by_status = User.count(:group => 'status').to_hash
20 user_count_by_status = User.count(:group => 'status').to_hash
21 options_for_select([[l(:label_all), ''],
21 options_for_select([[l(:label_all), ''],
22 ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1],
22 ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1],
23 ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2],
23 ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2],
24 ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected)
24 ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected)
25 end
25 end
26
26
27 # Options for the new membership projects combo-box
27 # Options for the new membership projects combo-box
28 def projects_options_for_select(projects)
28 def options_for_membership_project_select(user, projects)
29 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
29 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
30 projects_by_root = projects.group_by(&:root)
30 options << project_tree_options_for_select(projects) do |p|
31 projects_by_root.keys.sort.each do |root|
31 {:disabled => (user.projects.include?(p))}
32 options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root)))
33 projects_by_root[root].sort.each do |project|
34 next if project == root
35 options << content_tag('option', '&#187; ' + h(project.name), :value => project.id)
36 end
37 end
32 end
38 options
33 options
39 end
34 end
40
35
41 def change_status_link(user)
36 def change_status_link(user)
42 url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]}
37 url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]}
43
38
44 if user.locked?
39 if user.locked?
45 link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
40 link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
46 elsif user.registered?
41 elsif user.registered?
47 link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
42 link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
48 elsif user != User.current
43 elsif user != User.current
49 link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
44 link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
50 end
45 end
51 end
46 end
52
47
53 def user_settings_tabs
48 def user_settings_tabs
54 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
49 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
55 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
50 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
56 ]
51 ]
57 end
52 end
58 end
53 end
@@ -1,276 +1,318
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :users, :through => :members
24 has_many :users, :through => :members
25 has_many :enabled_modules, :dependent => :delete_all
25 has_many :enabled_modules, :dependent => :delete_all
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
28 has_many :issue_changes, :through => :issues, :source => :journals
28 has_many :issue_changes, :through => :issues, :source => :journals
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
30 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
32 has_many :documents, :dependent => :destroy
32 has_many :documents, :dependent => :destroy
33 has_many :news, :dependent => :delete_all, :include => :author
33 has_many :news, :dependent => :delete_all, :include => :author
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
36 has_one :repository, :dependent => :destroy
36 has_one :repository, :dependent => :destroy
37 has_many :changesets, :through => :repository
37 has_many :changesets, :through => :repository
38 has_one :wiki, :dependent => :destroy
38 has_one :wiki, :dependent => :destroy
39 # Custom field for the project issues
39 # Custom field for the project issues
40 has_and_belongs_to_many :issue_custom_fields,
40 has_and_belongs_to_many :issue_custom_fields,
41 :class_name => 'IssueCustomField',
41 :class_name => 'IssueCustomField',
42 :order => "#{CustomField.table_name}.position",
42 :order => "#{CustomField.table_name}.position",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 :association_foreign_key => 'custom_field_id'
44 :association_foreign_key => 'custom_field_id'
45
45
46 acts_as_tree :order => "name", :counter_cache => true
46 acts_as_nested_set :order => 'name', :dependent => :destroy
47 acts_as_attachable :view_permission => :view_files,
47 acts_as_attachable :view_permission => :view_files,
48 :delete_permission => :manage_files
48 :delete_permission => :manage_files
49
49
50 acts_as_customizable
50 acts_as_customizable
51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
54 :author => nil
54 :author => nil
55
55
56 attr_protected :status, :enabled_module_names
56 attr_protected :status, :enabled_module_names
57
57
58 validates_presence_of :name, :identifier
58 validates_presence_of :name, :identifier
59 validates_uniqueness_of :name, :identifier
59 validates_uniqueness_of :name, :identifier
60 validates_associated :repository, :wiki
60 validates_associated :repository, :wiki
61 validates_length_of :name, :maximum => 30
61 validates_length_of :name, :maximum => 30
62 validates_length_of :homepage, :maximum => 255
62 validates_length_of :homepage, :maximum => 255
63 validates_length_of :identifier, :in => 2..20
63 validates_length_of :identifier, :in => 2..20
64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
65
65
66 before_destroy :delete_all_members
66 before_destroy :delete_all_members
67
67
68 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
68 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
69 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
70 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
69
71
70 def identifier=(identifier)
72 def identifier=(identifier)
71 super unless identifier_frozen?
73 super unless identifier_frozen?
72 end
74 end
73
75
74 def identifier_frozen?
76 def identifier_frozen?
75 errors[:identifier].nil? && !(new_record? || identifier.blank?)
77 errors[:identifier].nil? && !(new_record? || identifier.blank?)
76 end
78 end
77
79
78 def issues_with_subprojects(include_subprojects=false)
80 def issues_with_subprojects(include_subprojects=false)
79 conditions = nil
81 conditions = nil
80 if include_subprojects
82 if include_subprojects
81 ids = [id] + child_ids
83 ids = [id] + descendants.collect(&:id)
82 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
84 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
83 end
85 end
84 conditions ||= ["#{Project.table_name}.id = ?", id]
86 conditions ||= ["#{Project.table_name}.id = ?", id]
85 # Quick and dirty fix for Rails 2 compatibility
87 # Quick and dirty fix for Rails 2 compatibility
86 Issue.send(:with_scope, :find => { :conditions => conditions }) do
88 Issue.send(:with_scope, :find => { :conditions => conditions }) do
87 Version.send(:with_scope, :find => { :conditions => conditions }) do
89 Version.send(:with_scope, :find => { :conditions => conditions }) do
88 yield
90 yield
89 end
91 end
90 end
92 end
91 end
93 end
92
94
93 # returns latest created projects
95 # returns latest created projects
94 # non public projects will be returned only if user is a member of those
96 # non public projects will be returned only if user is a member of those
95 def self.latest(user=nil, count=5)
97 def self.latest(user=nil, count=5)
96 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
98 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
97 end
99 end
98
100
99 def self.visible_by(user=nil)
101 def self.visible_by(user=nil)
100 user ||= User.current
102 user ||= User.current
101 if user && user.admin?
103 if user && user.admin?
102 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
104 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
103 elsif user && user.memberships.any?
105 elsif user && user.memberships.any?
104 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
105 else
107 else
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
107 end
109 end
108 end
110 end
109
111
110 def self.allowed_to_condition(user, permission, options={})
112 def self.allowed_to_condition(user, permission, options={})
111 statements = []
113 statements = []
112 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
114 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
113 if perm = Redmine::AccessControl.permission(permission)
115 if perm = Redmine::AccessControl.permission(permission)
114 unless perm.project_module.nil?
116 unless perm.project_module.nil?
115 # If the permission belongs to a project module, make sure the module is enabled
117 # If the permission belongs to a project module, make sure the module is enabled
116 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
118 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
117 end
119 end
118 end
120 end
119 if options[:project]
121 if options[:project]
120 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
122 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
121 project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
123 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
122 base_statement = "(#{project_statement}) AND (#{base_statement})"
124 base_statement = "(#{project_statement}) AND (#{base_statement})"
123 end
125 end
124 if user.admin?
126 if user.admin?
125 # no restriction
127 # no restriction
126 else
128 else
127 statements << "1=0"
129 statements << "1=0"
128 if user.logged?
130 if user.logged?
129 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
131 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
130 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
132 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
131 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
133 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
132 elsif Role.anonymous.allowed_to?(permission)
134 elsif Role.anonymous.allowed_to?(permission)
133 # anonymous user allowed on public project
135 # anonymous user allowed on public project
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
136 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 else
137 else
136 # anonymous user is not authorized
138 # anonymous user is not authorized
137 end
139 end
138 end
140 end
139 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
141 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
140 end
142 end
141
143
142 def project_condition(with_subprojects)
144 def project_condition(with_subprojects)
143 cond = "#{Project.table_name}.id = #{id}"
145 cond = "#{Project.table_name}.id = #{id}"
144 cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
146 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
145 cond
147 cond
146 end
148 end
147
149
148 def self.find(*args)
150 def self.find(*args)
149 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
151 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
150 project = find_by_identifier(*args)
152 project = find_by_identifier(*args)
151 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
153 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
152 project
154 project
153 else
155 else
154 super
156 super
155 end
157 end
156 end
158 end
157
159
158 def to_param
160 def to_param
159 # id is used for projects with a numeric identifier (compatibility)
161 # id is used for projects with a numeric identifier (compatibility)
160 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
162 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
161 end
163 end
162
164
163 def active?
165 def active?
164 self.status == STATUS_ACTIVE
166 self.status == STATUS_ACTIVE
165 end
167 end
166
168
169 # Archives the project and its descendants recursively
167 def archive
170 def archive
168 # Archive subprojects if any
171 # Archive subprojects if any
169 children.each do |subproject|
172 children.each do |subproject|
170 subproject.archive
173 subproject.archive
171 end
174 end
172 update_attribute :status, STATUS_ARCHIVED
175 update_attribute :status, STATUS_ARCHIVED
173 end
176 end
174
177
178 # Unarchives the project
179 # All its ancestors must be active
175 def unarchive
180 def unarchive
176 return false if parent && !parent.active?
181 return false if ancestors.detect {|a| !a.active?}
177 update_attribute :status, STATUS_ACTIVE
182 update_attribute :status, STATUS_ACTIVE
178 end
183 end
179
184
180 def active_children
185 # Returns an array of projects the project can be moved to
181 children.select {|child| child.active?}
186 def possible_parents
187 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
188 end
189
190 # Sets the parent of the project
191 # Argument can be either a Project, a String, a Fixnum or nil
192 def set_parent!(p)
193 unless p.nil? || p.is_a?(Project)
194 if p.to_s.blank?
195 p = nil
196 else
197 p = Project.find_by_id(p)
198 return false unless p
199 end
200 end
201 if p == parent && !p.nil?
202 # Nothing to do
203 true
204 elsif p.nil? || (p.active? && move_possible?(p))
205 # Insert the project so that target's children or root projects stay alphabetically sorted
206 sibs = (p.nil? ? self.class.roots : p.children)
207 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
208 if to_be_inserted_before
209 move_to_left_of(to_be_inserted_before)
210 elsif p.nil?
211 if sibs.empty?
212 # move_to_root adds the project in first (ie. left) position
213 move_to_root
214 else
215 move_to_right_of(sibs.last) unless self == sibs.last
216 end
217 else
218 # move_to_child_of adds the project in last (ie.right) position
219 move_to_child_of(p)
220 end
221 true
222 else
223 # Can not move to the given target
224 false
225 end
182 end
226 end
183
227
184 # Returns an array of the trackers used by the project and its sub projects
228 # Returns an array of the trackers used by the project and its sub projects
185 def rolled_up_trackers
229 def rolled_up_trackers
186 @rolled_up_trackers ||=
230 @rolled_up_trackers ||=
187 Tracker.find(:all, :include => :projects,
231 Tracker.find(:all, :include => :projects,
188 :select => "DISTINCT #{Tracker.table_name}.*",
232 :select => "DISTINCT #{Tracker.table_name}.*",
189 :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
233 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt],
190 :order => "#{Tracker.table_name}.position")
234 :order => "#{Tracker.table_name}.position")
191 end
235 end
192
236
193 # Deletes all project's members
237 # Deletes all project's members
194 def delete_all_members
238 def delete_all_members
195 Member.delete_all(['project_id = ?', id])
239 Member.delete_all(['project_id = ?', id])
196 end
240 end
197
241
198 # Users issues can be assigned to
242 # Users issues can be assigned to
199 def assignable_users
243 def assignable_users
200 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
244 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
201 end
245 end
202
246
203 # Returns the mail adresses of users that should be always notified on project events
247 # Returns the mail adresses of users that should be always notified on project events
204 def recipients
248 def recipients
205 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
249 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
206 end
250 end
207
251
208 # Returns an array of all custom fields enabled for project issues
252 # Returns an array of all custom fields enabled for project issues
209 # (explictly associated custom fields and custom fields enabled for all projects)
253 # (explictly associated custom fields and custom fields enabled for all projects)
210 def all_issue_custom_fields
254 def all_issue_custom_fields
211 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
255 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
212 end
256 end
213
257
214 def project
258 def project
215 self
259 self
216 end
260 end
217
261
218 def <=>(project)
262 def <=>(project)
219 name.downcase <=> project.name.downcase
263 name.downcase <=> project.name.downcase
220 end
264 end
221
265
222 def to_s
266 def to_s
223 name
267 name
224 end
268 end
225
269
226 # Returns a short description of the projects (first lines)
270 # Returns a short description of the projects (first lines)
227 def short_description(length = 255)
271 def short_description(length = 255)
228 description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
272 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
229 end
273 end
230
274
231 def allows_to?(action)
275 def allows_to?(action)
232 if action.is_a? Hash
276 if action.is_a? Hash
233 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
277 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
234 else
278 else
235 allowed_permissions.include? action
279 allowed_permissions.include? action
236 end
280 end
237 end
281 end
238
282
239 def module_enabled?(module_name)
283 def module_enabled?(module_name)
240 module_name = module_name.to_s
284 module_name = module_name.to_s
241 enabled_modules.detect {|m| m.name == module_name}
285 enabled_modules.detect {|m| m.name == module_name}
242 end
286 end
243
287
244 def enabled_module_names=(module_names)
288 def enabled_module_names=(module_names)
245 enabled_modules.clear
289 enabled_modules.clear
246 module_names = [] unless module_names && module_names.is_a?(Array)
290 module_names = [] unless module_names && module_names.is_a?(Array)
247 module_names.each do |name|
291 module_names.each do |name|
248 enabled_modules << EnabledModule.new(:name => name.to_s)
292 enabled_modules << EnabledModule.new(:name => name.to_s)
249 end
293 end
250 end
294 end
251
295
252 # Returns an auto-generated project identifier based on the last identifier used
296 # Returns an auto-generated project identifier based on the last identifier used
253 def self.next_identifier
297 def self.next_identifier
254 p = Project.find(:first, :order => 'created_on DESC')
298 p = Project.find(:first, :order => 'created_on DESC')
255 p.nil? ? nil : p.identifier.to_s.succ
299 p.nil? ? nil : p.identifier.to_s.succ
256 end
300 end
257
301
258 protected
302 protected
259 def validate
303 def validate
260 errors.add(parent_id, " must be a root project") if parent and parent.parent
261 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
262 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
304 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
263 end
305 end
264
306
265 private
307 private
266 def allowed_permissions
308 def allowed_permissions
267 @allowed_permissions ||= begin
309 @allowed_permissions ||= begin
268 module_names = enabled_modules.collect {|m| m.name}
310 module_names = enabled_modules.collect {|m| m.name}
269 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
311 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
270 end
312 end
271 end
313 end
272
314
273 def allowed_actions
315 def allowed_actions
274 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
316 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
275 end
317 end
276 end
318 end
@@ -1,406 +1,406
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :default_order
19 attr_accessor :name, :sortable, :default_order
20 include GLoc
20 include GLoc
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.default_order = options[:default_order]
25 self.default_order = options[:default_order]
26 end
26 end
27
27
28 def caption
28 def caption
29 set_language_if_valid(User.current.language)
29 set_language_if_valid(User.current.language)
30 l("field_#{name}")
30 l("field_#{name}")
31 end
31 end
32 end
32 end
33
33
34 class QueryCustomFieldColumn < QueryColumn
34 class QueryCustomFieldColumn < QueryColumn
35
35
36 def initialize(custom_field)
36 def initialize(custom_field)
37 self.name = "cf_#{custom_field.id}".to_sym
37 self.name = "cf_#{custom_field.id}".to_sym
38 self.sortable = custom_field.order_statement || false
38 self.sortable = custom_field.order_statement || false
39 @cf = custom_field
39 @cf = custom_field
40 end
40 end
41
41
42 def caption
42 def caption
43 @cf.name
43 @cf.name
44 end
44 end
45
45
46 def custom_field
46 def custom_field
47 @cf
47 @cf
48 end
48 end
49 end
49 end
50
50
51 class Query < ActiveRecord::Base
51 class Query < ActiveRecord::Base
52 belongs_to :project
52 belongs_to :project
53 belongs_to :user
53 belongs_to :user
54 serialize :filters
54 serialize :filters
55 serialize :column_names
55 serialize :column_names
56
56
57 attr_protected :project_id, :user_id
57 attr_protected :project_id, :user_id
58
58
59 validates_presence_of :name, :on => :save
59 validates_presence_of :name, :on => :save
60 validates_length_of :name, :maximum => 255
60 validates_length_of :name, :maximum => 255
61
61
62 @@operators = { "=" => :label_equals,
62 @@operators = { "=" => :label_equals,
63 "!" => :label_not_equals,
63 "!" => :label_not_equals,
64 "o" => :label_open_issues,
64 "o" => :label_open_issues,
65 "c" => :label_closed_issues,
65 "c" => :label_closed_issues,
66 "!*" => :label_none,
66 "!*" => :label_none,
67 "*" => :label_all,
67 "*" => :label_all,
68 ">=" => '>=',
68 ">=" => '>=',
69 "<=" => '<=',
69 "<=" => '<=',
70 "<t+" => :label_in_less_than,
70 "<t+" => :label_in_less_than,
71 ">t+" => :label_in_more_than,
71 ">t+" => :label_in_more_than,
72 "t+" => :label_in,
72 "t+" => :label_in,
73 "t" => :label_today,
73 "t" => :label_today,
74 "w" => :label_this_week,
74 "w" => :label_this_week,
75 ">t-" => :label_less_than_ago,
75 ">t-" => :label_less_than_ago,
76 "<t-" => :label_more_than_ago,
76 "<t-" => :label_more_than_ago,
77 "t-" => :label_ago,
77 "t-" => :label_ago,
78 "~" => :label_contains,
78 "~" => :label_contains,
79 "!~" => :label_not_contains }
79 "!~" => :label_not_contains }
80
80
81 cattr_reader :operators
81 cattr_reader :operators
82
82
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
86 :list_subprojects => [ "*", "!*", "=" ],
86 :list_subprojects => [ "*", "!*", "=" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
89 :string => [ "=", "~", "!", "!~" ],
89 :string => [ "=", "~", "!", "!~" ],
90 :text => [ "~", "!~" ],
90 :text => [ "~", "!~" ],
91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
92
92
93 cattr_reader :operators_by_filter_type
93 cattr_reader :operators_by_filter_type
94
94
95 @@available_columns = [
95 @@available_columns = [
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
100 QueryColumn.new(:author),
100 QueryColumn.new(:author),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
110 ]
110 ]
111 cattr_reader :available_columns
111 cattr_reader :available_columns
112
112
113 def initialize(attributes = nil)
113 def initialize(attributes = nil)
114 super attributes
114 super attributes
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
116 set_language_if_valid(User.current.language)
116 set_language_if_valid(User.current.language)
117 end
117 end
118
118
119 def after_initialize
119 def after_initialize
120 # Store the fact that project is nil (used in #editable_by?)
120 # Store the fact that project is nil (used in #editable_by?)
121 @is_for_all = project.nil?
121 @is_for_all = project.nil?
122 end
122 end
123
123
124 def validate
124 def validate
125 filters.each_key do |field|
125 filters.each_key do |field|
126 errors.add label_for(field), :activerecord_error_blank unless
126 errors.add label_for(field), :activerecord_error_blank unless
127 # filter requires one or more values
127 # filter requires one or more values
128 (values_for(field) and !values_for(field).first.blank?) or
128 (values_for(field) and !values_for(field).first.blank?) or
129 # filter doesn't require any value
129 # filter doesn't require any value
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
131 end if filters
131 end if filters
132 end
132 end
133
133
134 def editable_by?(user)
134 def editable_by?(user)
135 return false unless user
135 return false unless user
136 # Admin can edit them all and regular users can edit their private queries
136 # Admin can edit them all and regular users can edit their private queries
137 return true if user.admin? || (!is_public && self.user_id == user.id)
137 return true if user.admin? || (!is_public && self.user_id == user.id)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
140 end
140 end
141
141
142 def available_filters
142 def available_filters
143 return @available_filters if @available_filters
143 return @available_filters if @available_filters
144
144
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
146
146
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
150 "subject" => { :type => :text, :order => 8 },
150 "subject" => { :type => :text, :order => 8 },
151 "created_on" => { :type => :date_past, :order => 9 },
151 "created_on" => { :type => :date_past, :order => 9 },
152 "updated_on" => { :type => :date_past, :order => 10 },
152 "updated_on" => { :type => :date_past, :order => 10 },
153 "start_date" => { :type => :date, :order => 11 },
153 "start_date" => { :type => :date, :order => 11 },
154 "due_date" => { :type => :date, :order => 12 },
154 "due_date" => { :type => :date, :order => 12 },
155 "estimated_hours" => { :type => :integer, :order => 13 },
155 "estimated_hours" => { :type => :integer, :order => 13 },
156 "done_ratio" => { :type => :integer, :order => 14 }}
156 "done_ratio" => { :type => :integer, :order => 14 }}
157
157
158 user_values = []
158 user_values = []
159 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
159 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
160 if project
160 if project
161 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
161 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
162 else
162 else
163 # members of the user's projects
163 # members of the user's projects
164 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
164 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
165 end
165 end
166 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
166 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
167 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
167 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
168
168
169 if project
169 if project
170 # project specific filters
170 # project specific filters
171 unless @project.issue_categories.empty?
171 unless @project.issue_categories.empty?
172 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
172 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
173 end
173 end
174 unless @project.versions.empty?
174 unless @project.versions.empty?
175 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
175 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
176 end
176 end
177 unless @project.active_children.empty?
177 unless @project.descendants.active.empty?
178 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
178 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
179 end
179 end
180 add_custom_fields_filters(@project.all_issue_custom_fields)
180 add_custom_fields_filters(@project.all_issue_custom_fields)
181 else
181 else
182 # global filters for cross project issue list
182 # global filters for cross project issue list
183 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
183 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
184 end
184 end
185 @available_filters
185 @available_filters
186 end
186 end
187
187
188 def add_filter(field, operator, values)
188 def add_filter(field, operator, values)
189 # values must be an array
189 # values must be an array
190 return unless values and values.is_a? Array # and !values.first.empty?
190 return unless values and values.is_a? Array # and !values.first.empty?
191 # check if field is defined as an available filter
191 # check if field is defined as an available filter
192 if available_filters.has_key? field
192 if available_filters.has_key? field
193 filter_options = available_filters[field]
193 filter_options = available_filters[field]
194 # check if operator is allowed for that filter
194 # check if operator is allowed for that filter
195 #if @@operators_by_filter_type[filter_options[:type]].include? operator
195 #if @@operators_by_filter_type[filter_options[:type]].include? operator
196 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
196 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
197 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
197 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
198 #end
198 #end
199 filters[field] = {:operator => operator, :values => values }
199 filters[field] = {:operator => operator, :values => values }
200 end
200 end
201 end
201 end
202
202
203 def add_short_filter(field, expression)
203 def add_short_filter(field, expression)
204 return unless expression
204 return unless expression
205 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
205 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
206 add_filter field, (parms[0] || "="), [parms[1] || ""]
206 add_filter field, (parms[0] || "="), [parms[1] || ""]
207 end
207 end
208
208
209 def has_filter?(field)
209 def has_filter?(field)
210 filters and filters[field]
210 filters and filters[field]
211 end
211 end
212
212
213 def operator_for(field)
213 def operator_for(field)
214 has_filter?(field) ? filters[field][:operator] : nil
214 has_filter?(field) ? filters[field][:operator] : nil
215 end
215 end
216
216
217 def values_for(field)
217 def values_for(field)
218 has_filter?(field) ? filters[field][:values] : nil
218 has_filter?(field) ? filters[field][:values] : nil
219 end
219 end
220
220
221 def label_for(field)
221 def label_for(field)
222 label = available_filters[field][:name] if available_filters.has_key?(field)
222 label = available_filters[field][:name] if available_filters.has_key?(field)
223 label ||= field.gsub(/\_id$/, "")
223 label ||= field.gsub(/\_id$/, "")
224 end
224 end
225
225
226 def available_columns
226 def available_columns
227 return @available_columns if @available_columns
227 return @available_columns if @available_columns
228 @available_columns = Query.available_columns
228 @available_columns = Query.available_columns
229 @available_columns += (project ?
229 @available_columns += (project ?
230 project.all_issue_custom_fields :
230 project.all_issue_custom_fields :
231 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
231 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
232 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
232 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
233 end
233 end
234
234
235 def columns
235 def columns
236 if has_default_columns?
236 if has_default_columns?
237 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
237 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
238 else
238 else
239 # preserve the column_names order
239 # preserve the column_names order
240 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
240 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
241 end
241 end
242 end
242 end
243
243
244 def column_names=(names)
244 def column_names=(names)
245 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
245 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
246 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
246 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
247 write_attribute(:column_names, names)
247 write_attribute(:column_names, names)
248 end
248 end
249
249
250 def has_column?(column)
250 def has_column?(column)
251 column_names && column_names.include?(column.name)
251 column_names && column_names.include?(column.name)
252 end
252 end
253
253
254 def has_default_columns?
254 def has_default_columns?
255 column_names.nil? || column_names.empty?
255 column_names.nil? || column_names.empty?
256 end
256 end
257
257
258 def project_statement
258 def project_statement
259 project_clauses = []
259 project_clauses = []
260 if project && !@project.active_children.empty?
260 if project && !@project.descendants.active.empty?
261 ids = [project.id]
261 ids = [project.id]
262 if has_filter?("subproject_id")
262 if has_filter?("subproject_id")
263 case operator_for("subproject_id")
263 case operator_for("subproject_id")
264 when '='
264 when '='
265 # include the selected subprojects
265 # include the selected subprojects
266 ids += values_for("subproject_id").each(&:to_i)
266 ids += values_for("subproject_id").each(&:to_i)
267 when '!*'
267 when '!*'
268 # main project only
268 # main project only
269 else
269 else
270 # all subprojects
270 # all subprojects
271 ids += project.child_ids
271 ids += project.descendants.collect(&:id)
272 end
272 end
273 elsif Setting.display_subprojects_issues?
273 elsif Setting.display_subprojects_issues?
274 ids += project.child_ids
274 ids += project.descendants.collect(&:id)
275 end
275 end
276 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
276 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
277 elsif project
277 elsif project
278 project_clauses << "#{Project.table_name}.id = %d" % project.id
278 project_clauses << "#{Project.table_name}.id = %d" % project.id
279 end
279 end
280 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
280 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
281 project_clauses.join(' AND ')
281 project_clauses.join(' AND ')
282 end
282 end
283
283
284 def statement
284 def statement
285 # filters clauses
285 # filters clauses
286 filters_clauses = []
286 filters_clauses = []
287 filters.each_key do |field|
287 filters.each_key do |field|
288 next if field == "subproject_id"
288 next if field == "subproject_id"
289 v = values_for(field).clone
289 v = values_for(field).clone
290 next unless v and !v.empty?
290 next unless v and !v.empty?
291
291
292 sql = ''
292 sql = ''
293 is_custom_filter = false
293 is_custom_filter = false
294 if field =~ /^cf_(\d+)$/
294 if field =~ /^cf_(\d+)$/
295 # custom field
295 # custom field
296 db_table = CustomValue.table_name
296 db_table = CustomValue.table_name
297 db_field = 'value'
297 db_field = 'value'
298 is_custom_filter = true
298 is_custom_filter = true
299 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
299 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
300 else
300 else
301 # regular field
301 # regular field
302 db_table = Issue.table_name
302 db_table = Issue.table_name
303 db_field = field
303 db_field = field
304 sql << '('
304 sql << '('
305 end
305 end
306
306
307 # "me" value subsitution
307 # "me" value subsitution
308 if %w(assigned_to_id author_id).include?(field)
308 if %w(assigned_to_id author_id).include?(field)
309 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
309 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
310 end
310 end
311
311
312 sql = sql + sql_for_field(field, v, db_table, db_field, is_custom_filter)
312 sql = sql + sql_for_field(field, v, db_table, db_field, is_custom_filter)
313
313
314 sql << ')'
314 sql << ')'
315 filters_clauses << sql
315 filters_clauses << sql
316 end if filters and valid?
316 end if filters and valid?
317
317
318 (filters_clauses << project_statement).join(' AND ')
318 (filters_clauses << project_statement).join(' AND ')
319 end
319 end
320
320
321 private
321 private
322
322
323 # Helper method to generate the WHERE sql for a +field+ with a +value+
323 # Helper method to generate the WHERE sql for a +field+ with a +value+
324 def sql_for_field(field, value, db_table, db_field, is_custom_filter)
324 def sql_for_field(field, value, db_table, db_field, is_custom_filter)
325 sql = ''
325 sql = ''
326 case operator_for field
326 case operator_for field
327 when "="
327 when "="
328 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
328 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
329 when "!"
329 when "!"
330 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
330 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
331 when "!*"
331 when "!*"
332 sql = "#{db_table}.#{db_field} IS NULL"
332 sql = "#{db_table}.#{db_field} IS NULL"
333 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
333 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
334 when "*"
334 when "*"
335 sql = "#{db_table}.#{db_field} IS NOT NULL"
335 sql = "#{db_table}.#{db_field} IS NOT NULL"
336 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
336 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
337 when ">="
337 when ">="
338 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
338 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
339 when "<="
339 when "<="
340 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
340 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
341 when "o"
341 when "o"
342 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
342 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
343 when "c"
343 when "c"
344 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
344 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
345 when ">t-"
345 when ">t-"
346 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
346 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
347 when "<t-"
347 when "<t-"
348 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
348 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
349 when "t-"
349 when "t-"
350 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
350 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
351 when ">t+"
351 when ">t+"
352 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
352 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
353 when "<t+"
353 when "<t+"
354 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
354 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
355 when "t+"
355 when "t+"
356 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
356 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
357 when "t"
357 when "t"
358 sql = date_range_clause(db_table, db_field, 0, 0)
358 sql = date_range_clause(db_table, db_field, 0, 0)
359 when "w"
359 when "w"
360 from = l(:general_first_day_of_week) == '7' ?
360 from = l(:general_first_day_of_week) == '7' ?
361 # week starts on sunday
361 # week starts on sunday
362 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
362 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
363 # week starts on monday (Rails default)
363 # week starts on monday (Rails default)
364 Time.now.at_beginning_of_week
364 Time.now.at_beginning_of_week
365 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
365 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
366 when "~"
366 when "~"
367 sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
367 sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
368 when "!~"
368 when "!~"
369 sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
369 sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
370 end
370 end
371
371
372 return sql
372 return sql
373 end
373 end
374
374
375 def add_custom_fields_filters(custom_fields)
375 def add_custom_fields_filters(custom_fields)
376 @available_filters ||= {}
376 @available_filters ||= {}
377
377
378 custom_fields.select(&:is_filter?).each do |field|
378 custom_fields.select(&:is_filter?).each do |field|
379 case field.field_format
379 case field.field_format
380 when "text"
380 when "text"
381 options = { :type => :text, :order => 20 }
381 options = { :type => :text, :order => 20 }
382 when "list"
382 when "list"
383 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
383 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
384 when "date"
384 when "date"
385 options = { :type => :date, :order => 20 }
385 options = { :type => :date, :order => 20 }
386 when "bool"
386 when "bool"
387 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
387 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
388 else
388 else
389 options = { :type => :string, :order => 20 }
389 options = { :type => :string, :order => 20 }
390 end
390 end
391 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
391 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
392 end
392 end
393 end
393 end
394
394
395 # Returns a SQL clause for a date or datetime field.
395 # Returns a SQL clause for a date or datetime field.
396 def date_range_clause(table, field, from, to)
396 def date_range_clause(table, field, from, to)
397 s = []
397 s = []
398 if from
398 if from
399 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
399 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
400 end
400 end
401 if to
401 if to
402 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
402 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
403 end
403 end
404 s.join(' AND ')
404 s.join(' AND ')
405 end
405 end
406 end
406 end
@@ -1,52 +1,48
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %>
2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_project_plural)%></h2>
5 <h2><%=l(:label_project_plural)%></h2>
6
6
7 <% form_tag({}, :method => :get) do %>
7 <% form_tag({}, :method => :get) do %>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 <label><%= l(:field_status) %> :</label>
9 <label><%= l(:field_status) %> :</label>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11 <label><%= l(:label_project) %>:</label>
11 <label><%= l(:label_project) %>:</label>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
14 </fieldset>
14 </fieldset>
15 <% end %>
15 <% end %>
16 &nbsp;
16 &nbsp;
17
17
18 <table class="list">
18 <table class="list">
19 <thead><tr>
19 <thead><tr>
20 <%= sort_header_tag('name', :caption => l(:label_project)) %>
20 <th><%=l(:label_project)%></th>
21 <th><%=l(:field_description)%></th>
21 <th><%=l(:field_description)%></th>
22 <th><%=l(:label_subproject_plural)%></th>
22 <th><%=l(:field_is_public)%></th>
23 <%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %>
23 <th><%=l(:field_created_on)%></th>
24 <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
25 <th></th>
24 <th></th>
26 <th></th>
25 <th></th>
27 </tr></thead>
26 </tr></thead>
28 <tbody>
27 <tbody>
29 <% for project in @projects %>
28 <% for project in @projects %>
30 <tr class="<%= cycle("odd", "even") %>">
29 <tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %>">
31 <td><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %>
30 <td class="name" style="padding-left: <%= project.level %>em;"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td>
32 <td><%= textilizable project.short_description, :project => project %>
31 <td><%= textilizable project.short_description, :project => project %></td>
33 <td align="center"><%= project.children.size %>
32 <td align="center"><%= image_tag 'true.png' if project.is_public? %></td>
34 <td align="center"><%= image_tag 'true.png' if project.is_public? %>
33 <td align="center"><%= format_date(project.created_on) %></td>
35 <td align="center"><%= format_date(project.created_on) %>
36 <td align="center" style="width:10%">
34 <td align="center" style="width:10%">
37 <small>
35 <small>
38 <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
36 <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
39 <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
37 <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
40 </small>
38 </small>
41 </td>
39 </td>
42 <td align="center" style="width:10%">
40 <td align="center" style="width:10%">
43 <small><%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %></small>
41 <small><%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %></small>
44 </td>
42 </td>
45 </tr>
43 </tr>
46 <% end %>
44 <% end %>
47 </tbody>
45 </tbody>
48 </table>
46 </table>
49
47
50 <p class="pagination"><%= pagination_links_full @project_pages, @project_count %></p>
51
52 <% html_title(l(:label_project_plural)) -%>
48 <% html_title(l(:label_project_plural)) -%>
@@ -1,66 +1,66
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 <head>
3 <head>
4 <title><%=h html_title %></title>
4 <title><%=h html_title %></title>
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 <meta name="keywords" content="issue,bug,tracker" />
7 <meta name="keywords" content="issue,bug,tracker" />
8 <%= stylesheet_link_tag 'application', :media => 'all' %>
8 <%= stylesheet_link_tag 'application', :media => 'all' %>
9 <%= javascript_include_tag :defaults %>
9 <%= javascript_include_tag :defaults %>
10 <%= heads_for_wiki_formatter %>
10 <%= heads_for_wiki_formatter %>
11 <!--[if IE]>
11 <!--[if IE]>
12 <style type="text/css">
12 <style type="text/css">
13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
15 </style>
15 </style>
16 <![endif]-->
16 <![endif]-->
17 <%= call_hook :view_layouts_base_html_head %>
17 <%= call_hook :view_layouts_base_html_head %>
18 <!-- page specific tags -->
18 <!-- page specific tags -->
19 <%= yield :header_tags -%>
19 <%= yield :header_tags -%>
20 </head>
20 </head>
21 <body>
21 <body>
22 <div id="wrapper">
22 <div id="wrapper">
23 <div id="top-menu">
23 <div id="top-menu">
24 <div id="account">
24 <div id="account">
25 <%= render_menu :account_menu -%>
25 <%= render_menu :account_menu -%>
26 </div>
26 </div>
27 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
27 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
28 <%= render_menu :top_menu -%>
28 <%= render_menu :top_menu -%>
29 </div>
29 </div>
30
30
31 <div id="header">
31 <div id="header">
32 <div id="quick-search">
32 <div id="quick-search">
33 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
33 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
34 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
34 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
35 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
35 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
36 <% end %>
36 <% end %>
37 <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
37 <%= render_project_jump_box %>
38 </div>
38 </div>
39
39
40 <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1>
40 <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1>
41
41
42 <div id="main-menu">
42 <div id="main-menu">
43 <%= render_main_menu(@project) %>
43 <%= render_main_menu(@project) %>
44 </div>
44 </div>
45 </div>
45 </div>
46
46
47 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
47 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
48 <div id="sidebar">
48 <div id="sidebar">
49 <%= yield :sidebar %>
49 <%= yield :sidebar %>
50 </div>
50 </div>
51
51
52 <div id="content">
52 <div id="content">
53 <%= render_flash_messages %>
53 <%= render_flash_messages %>
54 <%= yield %>
54 <%= yield %>
55 </div>
55 </div>
56 </div>
56 </div>
57
57
58 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
58 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
59
59
60 <div id="footer">
60 <div id="footer">
61 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2008 Jean-Philippe Lang
61 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2008 Jean-Philippe Lang
62 </div>
62 </div>
63 </div>
63 </div>
64 <%= call_hook :view_layouts_base_body_bottom %>
64 <%= call_hook :view_layouts_base_body_bottom %>
65 </body>
65 </body>
66 </html>
66 </html>
@@ -1,49 +1,49
1 <%= error_messages_for 'project' %>
1 <%= error_messages_for 'project' %>
2
2
3 <div class="box">
3 <div class="box">
4 <!--[form:project]-->
4 <!--[form:project]-->
5 <p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
5 <p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
6
6
7 <% if User.current.admin? and !@root_projects.empty? %>
7 <% if User.current.admin? && !@project.possible_parents.empty? %>
8 <p><%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %></p>
8 <p><label><%= l(:field_parent) %></label><%= parent_project_select_tag(@project) %></p>
9 <% end %>
9 <% end %>
10
10
11 <p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
11 <p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
12 <p><%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %>
12 <p><%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %>
13 <% unless @project.identifier_frozen? %>
13 <% unless @project.identifier_frozen? %>
14 <br /><em><%= l(:text_length_between, 2, 20) %> <%= l(:text_project_identifier_info) %></em>
14 <br /><em><%= l(:text_length_between, 2, 20) %> <%= l(:text_project_identifier_info) %></em>
15 <% end %></p>
15 <% end %></p>
16 <p><%= f.text_field :homepage, :size => 60 %></p>
16 <p><%= f.text_field :homepage, :size => 60 %></p>
17 <p><%= f.check_box :is_public %></p>
17 <p><%= f.check_box :is_public %></p>
18 <%= wikitoolbar_for 'project_description' %>
18 <%= wikitoolbar_for 'project_description' %>
19
19
20 <% @project.custom_field_values.each do |value| %>
20 <% @project.custom_field_values.each do |value| %>
21 <p><%= custom_field_tag_with_label :project, value %></p>
21 <p><%= custom_field_tag_with_label :project, value %></p>
22 <% end %>
22 <% end %>
23 <%= call_hook(:view_projects_form, :project => @project, :form => f) %>
23 <%= call_hook(:view_projects_form, :project => @project, :form => f) %>
24 </div>
24 </div>
25
25
26 <% unless @trackers.empty? %>
26 <% unless @trackers.empty? %>
27 <fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
27 <fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
28 <% @trackers.each do |tracker| %>
28 <% @trackers.each do |tracker| %>
29 <label class="floating">
29 <label class="floating">
30 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
30 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
31 <%= tracker %>
31 <%= tracker %>
32 </label>
32 </label>
33 <% end %>
33 <% end %>
34 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
34 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
35 </fieldset>
35 </fieldset>
36 <% end %>
36 <% end %>
37
37
38 <% unless @issue_custom_fields.empty? %>
38 <% unless @issue_custom_fields.empty? %>
39 <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
39 <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
40 <% @issue_custom_fields.each do |custom_field| %>
40 <% @issue_custom_fields.each do |custom_field| %>
41 <label class="floating">
41 <label class="floating">
42 <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
42 <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
43 <%= custom_field.name %>
43 <%= custom_field.name %>
44 </label>
44 </label>
45 <% end %>
45 <% end %>
46 <%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
46 <%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
47 </fieldset>
47 </fieldset>
48 <% end %>
48 <% end %>
49 <!--[eoform:project]-->
49 <!--[eoform:project]-->
@@ -1,60 +1,60
1 <h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2>
1 <h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2>
2 <p class="subtitle"><%= "#{l(:label_date_from)} #{format_date(@date_to - @days)} #{l(:label_date_to).downcase} #{format_date(@date_to-1)}" %></p>
2 <p class="subtitle"><%= "#{l(:label_date_from)} #{format_date(@date_to - @days)} #{l(:label_date_to).downcase} #{format_date(@date_to-1)}" %></p>
3
3
4 <div id="activity">
4 <div id="activity">
5 <% @events_by_day.keys.sort.reverse.each do |day| %>
5 <% @events_by_day.keys.sort.reverse.each do |day| %>
6 <h3><%= format_activity_day(day) %></h3>
6 <h3><%= format_activity_day(day) %></h3>
7 <dl>
7 <dl>
8 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
8 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
9 <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
9 <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
10 <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %>
10 <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %>
11 <span class="time"><%= format_time(e.event_datetime, false) %></span>
11 <span class="time"><%= format_time(e.event_datetime, false) %></span>
12 <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
12 <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
13 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
13 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
14 <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
14 <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
15 <span class="author"><%= e.event_author if e.respond_to?(:event_author) %></span></dd>
15 <span class="author"><%= e.event_author if e.respond_to?(:event_author) %></span></dd>
16 <% end -%>
16 <% end -%>
17 </dl>
17 </dl>
18 <% end -%>
18 <% end -%>
19 </div>
19 </div>
20
20
21 <%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
21 <%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
22
22
23 <div style="float:left;">
23 <div style="float:left;">
24 <%= link_to_remote(('&#171; ' + l(:label_previous)),
24 <%= link_to_remote(('&#171; ' + l(:label_previous)),
25 {:update => "content", :url => params.merge(:from => @date_to - @days - 1), :complete => 'window.scrollTo(0,0)'},
25 {:update => "content", :url => params.merge(:from => @date_to - @days - 1), :complete => 'window.scrollTo(0,0)'},
26 {:href => url_for(params.merge(:from => @date_to - @days - 1)),
26 {:href => url_for(params.merge(:from => @date_to - @days - 1)),
27 :title => "#{l(:label_date_from)} #{format_date(@date_to - 2*@days)} #{l(:label_date_to).downcase} #{format_date(@date_to - @days - 1)}"}) %>
27 :title => "#{l(:label_date_from)} #{format_date(@date_to - 2*@days)} #{l(:label_date_to).downcase} #{format_date(@date_to - @days - 1)}"}) %>
28 </div>
28 </div>
29 <div style="float:right;">
29 <div style="float:right;">
30 <%= link_to_remote((l(:label_next) + ' &#187;'),
30 <%= link_to_remote((l(:label_next) + ' &#187;'),
31 {:update => "content", :url => params.merge(:from => @date_to + @days - 1), :complete => 'window.scrollTo(0,0)'},
31 {:update => "content", :url => params.merge(:from => @date_to + @days - 1), :complete => 'window.scrollTo(0,0)'},
32 {:href => url_for(params.merge(:from => @date_to + @days - 1)),
32 {:href => url_for(params.merge(:from => @date_to + @days - 1)),
33 :title => "#{l(:label_date_from)} #{format_date(@date_to)} #{l(:label_date_to).downcase} #{format_date(@date_to + @days - 1)}"}) unless @date_to >= Date.today %>
33 :title => "#{l(:label_date_from)} #{format_date(@date_to)} #{l(:label_date_to).downcase} #{format_date(@date_to + @days - 1)}"}) unless @date_to >= Date.today %>
34 </div>
34 </div>
35 &nbsp;
35 &nbsp;
36 <p class="other-formats">
36 <p class="other-formats">
37 <%= l(:label_export_to) %>
37 <%= l(:label_export_to) %>
38 <%= link_to 'Atom', params.merge(:format => :atom, :from => nil, :key => User.current.rss_key), :class => 'feed' %>
38 <%= link_to 'Atom', params.merge(:format => :atom, :from => nil, :key => User.current.rss_key), :class => 'feed' %>
39 </p>
39 </p>
40
40
41 <% content_for :header_tags do %>
41 <% content_for :header_tags do %>
42 <%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %>
42 <%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %>
43 <% end %>
43 <% end %>
44
44
45 <% content_for :sidebar do %>
45 <% content_for :sidebar do %>
46 <% form_tag({}, :method => :get) do %>
46 <% form_tag({}, :method => :get) do %>
47 <h3><%= l(:label_activity) %></h3>
47 <h3><%= l(:label_activity) %></h3>
48 <p><% @activity.event_types.each do |t| %>
48 <p><% @activity.event_types.each do |t| %>
49 <label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br />
49 <label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br />
50 <% end %></p>
50 <% end %></p>
51 <% if @project && @project.active_children.any? %>
51 <% if @project && @project.descendants.active.any? %>
52 <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
52 <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
53 <%= hidden_field_tag 'with_subprojects', 0 %>
53 <%= hidden_field_tag 'with_subprojects', 0 %>
54 <% end %>
54 <% end %>
55 <%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
55 <%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
56 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
56 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
57 <% end %>
57 <% end %>
58 <% end %>
58 <% end %>
59
59
60 <% html_title(l(:label_activity), @author) -%>
60 <% html_title(l(:label_activity), @author) -%>
@@ -1,16 +1,16
1 <h2><%=l(:label_confirmation)%></h2>
1 <h2><%=l(:label_confirmation)%></h2>
2 <div class="warning">
2 <div class="warning">
3 <p><strong><%=h @project_to_destroy %></strong><br />
3 <p><strong><%=h @project_to_destroy %></strong><br />
4 <%=l(:text_project_destroy_confirmation)%>
4 <%=l(:text_project_destroy_confirmation)%>
5
5
6 <% if @project_to_destroy.children.any? %>
6 <% if @project_to_destroy.descendants.any? %>
7 <br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.children.sort.collect{|p| p.to_s}.join(', ')))) %>
7 <br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.descendants.collect{|p| p.to_s}.join(', ')))) %>
8 <% end %>
8 <% end %>
9 </p>
9 </p>
10 <p>
10 <p>
11 <% form_tag({:controller => 'projects', :action => 'destroy', :id => @project_to_destroy}) do %>
11 <% form_tag({:controller => 'projects', :action => 'destroy', :id => @project_to_destroy}) do %>
12 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
12 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
13 <%= submit_tag l(:button_delete) %>
13 <%= submit_tag l(:button_delete) %>
14 <% end %>
14 <% end %>
15 </p>
15 </p>
16 </div>
16 </div>
@@ -1,31 +1,22
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.admin? %>
2 <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.admin? %>
3 <%= link_to l(:label_issue_view_all), { :controller => 'issues' } %> |
3 <%= link_to l(:label_issue_view_all), { :controller => 'issues' } %> |
4 <%= link_to l(:label_overall_activity), { :controller => 'projects', :action => 'activity' }%>
4 <%= link_to l(:label_overall_activity), { :controller => 'projects', :action => 'activity' }%>
5 </div>
5 </div>
6
6
7 <h2><%=l(:label_project_plural)%></h2>
7 <h2><%=l(:label_project_plural)%></h2>
8
8
9 <% @project_tree.keys.sort.each do |project| %>
9 <%= render_project_hierarchy(@projects)%>
10 <h3><%= link_to h(project.name), {:action => 'show', :id => project}, :class => (User.current.member_of?(project) ? "icon icon-fav" : "") %></h3>
11 <%= textilizable(project.short_description, :project => project) %>
12
13 <% if @project_tree[project].any? %>
14 <p><%= l(:label_subproject_plural) %>:
15 <%= @project_tree[project].sort.collect {|subproject|
16 link_to(h(subproject.name), {:action => 'show', :id => subproject}, :class => (User.current.member_of?(subproject) ? "icon icon-fav" : ""))}.join(', ') %></p>
17 <% end %>
18 <% end %>
19
10
20 <% if User.current.logged? %>
11 <% if User.current.logged? %>
21 <p style="text-align:right;">
12 <p style="text-align:right;">
22 <span class="icon icon-fav"><%= l(:label_my_projects) %></span>
13 <span class="my-project"><%= l(:label_my_projects) %></span>
23 </p>
14 </p>
24 <% end %>
15 <% end %>
25
16
26 <p class="other-formats">
17 <p class="other-formats">
27 <%= l(:label_export_to) %>
18 <%= l(:label_export_to) %>
28 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
19 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
29 </p>
20 </p>
30
21
31 <% html_title(l(:label_project_plural)) -%>
22 <% html_title(l(:label_project_plural)) -%>
@@ -1,80 +1,82
1 <h2><%=l(:label_overview)%></h2>
1 <h2><%=l(:label_overview)%></h2>
2
2
3 <div class="splitcontentleft">
3 <div class="splitcontentleft">
4 <%= textilizable @project.description %>
4 <%= textilizable @project.description %>
5 <ul>
5 <ul>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
7 <% if @subprojects.any? %>
7 <% if @subprojects.any? %>
8 <li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li>
8 <li><%=l(:label_subproject_plural)%>:
9 <% end %>
9 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
10 <% if @project.parent %>
10 <% end %>
11 <li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li>
11 <% if @ancestors.any? %>
12 <li><%=l(:field_parent)%>:
13 <%= @ancestors.collect {|p| link_to(h(p), :action => 'show', :id => p)}.join(" &#187; ") %></li>
12 <% end %>
14 <% end %>
13 <% @project.custom_values.each do |custom_value| %>
15 <% @project.custom_values.each do |custom_value| %>
14 <% if !custom_value.value.empty? %>
16 <% if !custom_value.value.empty? %>
15 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
17 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
16 <% end %>
18 <% end %>
17 <% end %>
19 <% end %>
18 </ul>
20 </ul>
19
21
20 <% if User.current.allowed_to?(:view_issues, @project) %>
22 <% if User.current.allowed_to?(:view_issues, @project) %>
21 <div class="box">
23 <div class="box">
22 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
24 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
23 <ul>
25 <ul>
24 <% for tracker in @trackers %>
26 <% for tracker in @trackers %>
25 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
27 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
26 :set_filter => 1,
28 :set_filter => 1,
27 "tracker_id" => tracker.id %>:
29 "tracker_id" => tracker.id %>:
28 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
30 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
29 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
31 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
30 <% end %>
32 <% end %>
31 </ul>
33 </ul>
32 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
34 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
33 </div>
35 </div>
34 <% end %>
36 <% end %>
35 </div>
37 </div>
36
38
37 <div class="splitcontentright">
39 <div class="splitcontentright">
38 <% if @members_by_role.any? %>
40 <% if @members_by_role.any? %>
39 <div class="box">
41 <div class="box">
40 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
42 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
41 <p><% @members_by_role.keys.sort.each do |role| %>
43 <p><% @members_by_role.keys.sort.each do |role| %>
42 <%= role.name %>:
44 <%= role.name %>:
43 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
45 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
44 <br />
46 <br />
45 <% end %></p>
47 <% end %></p>
46 </div>
48 </div>
47 <% end %>
49 <% end %>
48
50
49 <% if @news.any? && authorize_for('news', 'index') %>
51 <% if @news.any? && authorize_for('news', 'index') %>
50 <div class="box">
52 <div class="box">
51 <h3><%=l(:label_news_latest)%></h3>
53 <h3><%=l(:label_news_latest)%></h3>
52 <%= render :partial => 'news/news', :collection => @news %>
54 <%= render :partial => 'news/news', :collection => @news %>
53 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
55 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
54 </div>
56 </div>
55 <% end %>
57 <% end %>
56 </div>
58 </div>
57
59
58 <% content_for :sidebar do %>
60 <% content_for :sidebar do %>
59 <% planning_links = []
61 <% planning_links = []
60 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
62 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
61 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
63 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
62 planning_links.compact!
64 planning_links.compact!
63 unless planning_links.empty? %>
65 unless planning_links.empty? %>
64 <h3><%= l(:label_planning) %></h3>
66 <h3><%= l(:label_planning) %></h3>
65 <p><%= planning_links.join(' | ') %></p>
67 <p><%= planning_links.join(' | ') %></p>
66 <% end %>
68 <% end %>
67
69
68 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
70 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
69 <h3><%= l(:label_spent_time) %></h3>
71 <h3><%= l(:label_spent_time) %></h3>
70 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
72 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
71 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
73 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
72 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
74 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
73 <% end %>
75 <% end %>
74 <% end %>
76 <% end %>
75
77
76 <% content_for :header_tags do %>
78 <% content_for :header_tags do %>
77 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
79 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
78 <% end %>
80 <% end %>
79
81
80 <% html_title(l(:label_overview)) -%>
82 <% html_title(l(:label_overview)) -%>
@@ -1,40 +1,40
1 <% if @memberships.any? %>
1 <% if @memberships.any? %>
2 <table class="list memberships">
2 <table class="list memberships">
3 <thead>
3 <thead>
4 <th><%= l(:label_project) %></th>
4 <th><%= l(:label_project) %></th>
5 <th><%= l(:label_role) %></th>
5 <th><%= l(:label_role) %></th>
6 <th style="width:15%"></th>
6 <th style="width:15%"></th>
7 </thead>
7 </thead>
8 <tbody>
8 <tbody>
9 <% @memberships.each do |membership| %>
9 <% @memberships.each do |membership| %>
10 <% next if membership.new_record? %>
10 <% next if membership.new_record? %>
11 <tr class="<%= cycle 'odd', 'even' %>">
11 <tr class="<%= cycle 'odd', 'even' %>">
12 <td><%=h membership.project %></td>
12 <td><%=h membership.project %></td>
13 <td align="center">
13 <td align="center">
14 <% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }) do %>
14 <% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }) do %>
15 <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name", membership.role_id) %>
15 <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name", membership.role_id) %>
16 <%= submit_tag l(:button_change), :class => "small" %>
16 <%= submit_tag l(:button_change), :class => "small" %>
17 <% end %>
17 <% end %>
18 </td>
18 </td>
19 <td align="center">
19 <td align="center">
20 <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :method => :post, :class => 'icon icon-del' %>
20 <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :method => :post, :class => 'icon icon-del' %>
21 </td>
21 </td>
22 </tr>
22 </tr>
23 </tbody>
23 </tbody>
24 <% end; reset_cycle %>
24 <% end; reset_cycle %>
25 </table>
25 </table>
26 <% else %>
26 <% else %>
27 <p class="nodata"><%= l(:label_no_data) %></p>
27 <p class="nodata"><%= l(:label_no_data) %></p>
28 <% end %>
28 <% end %>
29
29
30 <% if @projects.any? %>
30 <% if @projects.any? %>
31 <p>
31 <p>
32 <label><%=l(:label_project_new)%></label><br/>
32 <label><%=l(:label_project_new)%></label><br/>
33 <% form_tag({ :action => 'edit_membership', :id => @user }) do %>
33 <% form_tag({ :action => 'edit_membership', :id => @user }) do %>
34 <%= select_tag 'membership[project_id]', projects_options_for_select(@projects) %>
34 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, @projects) %>
35 <%= l(:label_role) %>:
35 <%= l(:label_role) %>:
36 <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %>
36 <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %>
37 <%= submit_tag l(:button_add) %>
37 <%= submit_tag l(:button_add) %>
38 <% end %>
38 <% end %>
39 </p>
39 </p>
40 <% end %>
40 <% end %>
@@ -1,689 +1,701
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2
2
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 h1 {margin:0; padding:0; font-size: 24px;}
4 h1 {margin:0; padding:0; font-size: 24px;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8
8
9 /***** Layout *****/
9 /***** Layout *****/
10 #wrapper {background: white;}
10 #wrapper {background: white;}
11
11
12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 #top-menu ul {margin: 0; padding: 0;}
13 #top-menu ul {margin: 0; padding: 0;}
14 #top-menu li {
14 #top-menu li {
15 float:left;
15 float:left;
16 list-style-type:none;
16 list-style-type:none;
17 margin: 0px 0px 0px 0px;
17 margin: 0px 0px 0px 0px;
18 padding: 0px 0px 0px 0px;
18 padding: 0px 0px 0px 0px;
19 white-space:nowrap;
19 white-space:nowrap;
20 }
20 }
21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23
23
24 #account {float:right;}
24 #account {float:right;}
25
25
26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 #header a {color:#f8f8f8;}
27 #header a {color:#f8f8f8;}
28 #quick-search {float:right;}
28 #quick-search {float:right;}
29
29
30 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
30 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
31 #main-menu ul {margin: 0; padding: 0;}
31 #main-menu ul {margin: 0; padding: 0;}
32 #main-menu li {
32 #main-menu li {
33 float:left;
33 float:left;
34 list-style-type:none;
34 list-style-type:none;
35 margin: 0px 2px 0px 0px;
35 margin: 0px 2px 0px 0px;
36 padding: 0px 0px 0px 0px;
36 padding: 0px 0px 0px 0px;
37 white-space:nowrap;
37 white-space:nowrap;
38 }
38 }
39 #main-menu li a {
39 #main-menu li a {
40 display: block;
40 display: block;
41 color: #fff;
41 color: #fff;
42 text-decoration: none;
42 text-decoration: none;
43 font-weight: bold;
43 font-weight: bold;
44 margin: 0;
44 margin: 0;
45 padding: 4px 10px 4px 10px;
45 padding: 4px 10px 4px 10px;
46 }
46 }
47 #main-menu li a:hover {background:#759FCF; color:#fff;}
47 #main-menu li a:hover {background:#759FCF; color:#fff;}
48 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
48 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
49
49
50 #main {background-color:#EEEEEE;}
50 #main {background-color:#EEEEEE;}
51
51
52 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
52 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
53 * html #sidebar{ width: 17%; }
53 * html #sidebar{ width: 17%; }
54 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
54 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
55 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
55 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
56 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
56 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
57
57
58 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
58 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
59 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
59 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
60 html>body #content { min-height: 600px; }
60 html>body #content { min-height: 600px; }
61 * html body #content { height: 600px; } /* IE */
61 * html body #content { height: 600px; } /* IE */
62
62
63 #main.nosidebar #sidebar{ display: none; }
63 #main.nosidebar #sidebar{ display: none; }
64 #main.nosidebar #content{ width: auto; border-right: 0; }
64 #main.nosidebar #content{ width: auto; border-right: 0; }
65
65
66 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
66 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
67
67
68 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
68 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
69 #login-form table td {padding: 6px;}
69 #login-form table td {padding: 6px;}
70 #login-form label {font-weight: bold;}
70 #login-form label {font-weight: bold;}
71
71
72 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
72 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
73
73
74 /***** Links *****/
74 /***** Links *****/
75 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
75 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
76 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
76 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
77 a img{ border: 0; }
77 a img{ border: 0; }
78
78
79 a.issue.closed { text-decoration: line-through; }
79 a.issue.closed { text-decoration: line-through; }
80
80
81 /***** Tables *****/
81 /***** Tables *****/
82 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
82 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
83 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
83 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
84 table.list td { vertical-align: top; }
84 table.list td { vertical-align: top; }
85 table.list td.id { width: 2%; text-align: center;}
85 table.list td.id { width: 2%; text-align: center;}
86 table.list td.checkbox { width: 15px; padding: 0px;}
86 table.list td.checkbox { width: 15px; padding: 0px;}
87
87
88 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
89 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
90
88 tr.issue { text-align: center; white-space: nowrap; }
91 tr.issue { text-align: center; white-space: nowrap; }
89 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
92 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
90 tr.issue td.subject { text-align: left; }
93 tr.issue td.subject { text-align: left; }
91 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
94 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
92
95
93 tr.entry { border: 1px solid #f8f8f8; }
96 tr.entry { border: 1px solid #f8f8f8; }
94 tr.entry td { white-space: nowrap; }
97 tr.entry td { white-space: nowrap; }
95 tr.entry td.filename { width: 30%; }
98 tr.entry td.filename { width: 30%; }
96 tr.entry td.size { text-align: right; font-size: 90%; }
99 tr.entry td.size { text-align: right; font-size: 90%; }
97 tr.entry td.revision, tr.entry td.author { text-align: center; }
100 tr.entry td.revision, tr.entry td.author { text-align: center; }
98 tr.entry td.age { text-align: right; }
101 tr.entry td.age { text-align: right; }
99
102
100 tr.entry span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
103 tr.entry span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
101 tr.entry.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
104 tr.entry.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
102 tr.entry.file td.filename a { margin-left: 16px; }
105 tr.entry.file td.filename a { margin-left: 16px; }
103
106
104 tr.changeset td.author { text-align: center; width: 15%; }
107 tr.changeset td.author { text-align: center; width: 15%; }
105 tr.changeset td.committed_on { text-align: center; width: 15%; }
108 tr.changeset td.committed_on { text-align: center; width: 15%; }
106
109
107 tr.message { height: 2.6em; }
110 tr.message { height: 2.6em; }
108 tr.message td.last_message { font-size: 80%; }
111 tr.message td.last_message { font-size: 80%; }
109 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
112 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
110 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
113 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
111
114
112 tr.user td { width:13%; }
115 tr.user td { width:13%; }
113 tr.user td.email { width:18%; }
116 tr.user td.email { width:18%; }
114 tr.user td { white-space: nowrap; }
117 tr.user td { white-space: nowrap; }
115 tr.user.locked, tr.user.registered { color: #aaa; }
118 tr.user.locked, tr.user.registered { color: #aaa; }
116 tr.user.locked a, tr.user.registered a { color: #aaa; }
119 tr.user.locked a, tr.user.registered a { color: #aaa; }
117
120
118 tr.time-entry { text-align: center; white-space: nowrap; }
121 tr.time-entry { text-align: center; white-space: nowrap; }
119 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
122 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
120 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
123 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
121 td.hours .hours-dec { font-size: 0.9em; }
124 td.hours .hours-dec { font-size: 0.9em; }
122
125
123 table.plugins td { vertical-align: middle; }
126 table.plugins td { vertical-align: middle; }
124 table.plugins td.configure { text-align: right; padding-right: 1em; }
127 table.plugins td.configure { text-align: right; padding-right: 1em; }
125 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
128 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
126 table.plugins span.description { display: block; font-size: 0.9em; }
129 table.plugins span.description { display: block; font-size: 0.9em; }
127 table.plugins span.url { display: block; font-size: 0.9em; }
130 table.plugins span.url { display: block; font-size: 0.9em; }
128
131
129 table.list tbody tr:hover { background-color:#ffffdd; }
132 table.list tbody tr:hover { background-color:#ffffdd; }
130 table td {padding:2px;}
133 table td {padding:2px;}
131 table p {margin:0;}
134 table p {margin:0;}
132 .odd {background-color:#f6f7f8;}
135 .odd {background-color:#f6f7f8;}
133 .even {background-color: #fff;}
136 .even {background-color: #fff;}
134
137
135 .highlight { background-color: #FCFD8D;}
138 .highlight { background-color: #FCFD8D;}
136 .highlight.token-1 { background-color: #faa;}
139 .highlight.token-1 { background-color: #faa;}
137 .highlight.token-2 { background-color: #afa;}
140 .highlight.token-2 { background-color: #afa;}
138 .highlight.token-3 { background-color: #aaf;}
141 .highlight.token-3 { background-color: #aaf;}
139
142
140 .box{
143 .box{
141 padding:6px;
144 padding:6px;
142 margin-bottom: 10px;
145 margin-bottom: 10px;
143 background-color:#f6f6f6;
146 background-color:#f6f6f6;
144 color:#505050;
147 color:#505050;
145 line-height:1.5em;
148 line-height:1.5em;
146 border: 1px solid #e4e4e4;
149 border: 1px solid #e4e4e4;
147 }
150 }
148
151
149 div.square {
152 div.square {
150 border: 1px solid #999;
153 border: 1px solid #999;
151 float: left;
154 float: left;
152 margin: .3em .4em 0 .4em;
155 margin: .3em .4em 0 .4em;
153 overflow: hidden;
156 overflow: hidden;
154 width: .6em; height: .6em;
157 width: .6em; height: .6em;
155 }
158 }
156 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
159 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
157 .contextual input {font-size:0.9em;}
160 .contextual input {font-size:0.9em;}
158
161
159 .splitcontentleft{float:left; width:49%;}
162 .splitcontentleft{float:left; width:49%;}
160 .splitcontentright{float:right; width:49%;}
163 .splitcontentright{float:right; width:49%;}
161 form {display: inline;}
164 form {display: inline;}
162 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
165 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
163 fieldset {border: 1px solid #e4e4e4; margin:0;}
166 fieldset {border: 1px solid #e4e4e4; margin:0;}
164 legend {color: #484848;}
167 legend {color: #484848;}
165 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
168 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
166 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
169 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
167 blockquote blockquote { margin-left: 0;}
170 blockquote blockquote { margin-left: 0;}
168 textarea.wiki-edit { width: 99%; }
171 textarea.wiki-edit { width: 99%; }
169 li p {margin-top: 0;}
172 li p {margin-top: 0;}
170 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
173 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
171 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
174 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
172 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
175 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
173 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
176 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
174
177
175 fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; }
178 fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; }
176 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
179 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
177 fieldset#filters table { border-collapse: collapse; }
180 fieldset#filters table { border-collapse: collapse; }
178 fieldset#filters table td { padding: 0; vertical-align: middle; }
181 fieldset#filters table td { padding: 0; vertical-align: middle; }
179 fieldset#filters tr.filter { height: 2em; }
182 fieldset#filters tr.filter { height: 2em; }
180 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
183 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
181 .buttons { font-size: 0.9em; }
184 .buttons { font-size: 0.9em; }
182
185
183 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
186 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
184 div#issue-changesets .changeset { padding: 4px;}
187 div#issue-changesets .changeset { padding: 4px;}
185 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
188 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
186 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
189 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
187
190
188 div#activity dl, #search-results { margin-left: 2em; }
191 div#activity dl, #search-results { margin-left: 2em; }
189 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
192 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
190 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
193 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
191 div#activity dt.me .time { border-bottom: 1px solid #999; }
194 div#activity dt.me .time { border-bottom: 1px solid #999; }
192 div#activity dt .time { color: #777; font-size: 80%; }
195 div#activity dt .time { color: #777; font-size: 80%; }
193 div#activity dd .description, #search-results dd .description { font-style: italic; }
196 div#activity dd .description, #search-results dd .description { font-style: italic; }
194 div#activity span.project:after, #search-results span.project:after { content: " -"; }
197 div#activity span.project:after, #search-results span.project:after { content: " -"; }
195 div#activity dd span.description, #search-results dd span.description { display:block; }
198 div#activity dd span.description, #search-results dd span.description { display:block; }
196
199
197 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
200 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
198
201
199 div#search-results-counts {float:right;}
202 div#search-results-counts {float:right;}
200 div#search-results-counts ul { margin-top: 0.5em; }
203 div#search-results-counts ul { margin-top: 0.5em; }
201 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
204 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
202
205
203 dt.issue { background-image: url(../images/ticket.png); }
206 dt.issue { background-image: url(../images/ticket.png); }
204 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
207 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
205 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
208 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
206 dt.issue-note { background-image: url(../images/ticket_note.png); }
209 dt.issue-note { background-image: url(../images/ticket_note.png); }
207 dt.changeset { background-image: url(../images/changeset.png); }
210 dt.changeset { background-image: url(../images/changeset.png); }
208 dt.news { background-image: url(../images/news.png); }
211 dt.news { background-image: url(../images/news.png); }
209 dt.message { background-image: url(../images/message.png); }
212 dt.message { background-image: url(../images/message.png); }
210 dt.reply { background-image: url(../images/comments.png); }
213 dt.reply { background-image: url(../images/comments.png); }
211 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
214 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
212 dt.attachment { background-image: url(../images/attachment.png); }
215 dt.attachment { background-image: url(../images/attachment.png); }
213 dt.document { background-image: url(../images/document.png); }
216 dt.document { background-image: url(../images/document.png); }
214 dt.project { background-image: url(../images/projects.png); }
217 dt.project { background-image: url(../images/projects.png); }
215
218
216 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
219 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
217
220
218 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
221 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
219 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
222 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
220 div#roadmap .wiki h1:first-child { display: none; }
223 div#roadmap .wiki h1:first-child { display: none; }
221 div#roadmap .wiki h1 { font-size: 120%; }
224 div#roadmap .wiki h1 { font-size: 120%; }
222 div#roadmap .wiki h2 { font-size: 110%; }
225 div#roadmap .wiki h2 { font-size: 110%; }
223
226
224 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
227 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
225 div#version-summary fieldset { margin-bottom: 1em; }
228 div#version-summary fieldset { margin-bottom: 1em; }
226 div#version-summary .total-hours { text-align: right; }
229 div#version-summary .total-hours { text-align: right; }
227
230
228 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
231 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
229 table#time-report tbody tr { font-style: italic; color: #777; }
232 table#time-report tbody tr { font-style: italic; color: #777; }
230 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
233 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
231 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
234 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
232 table#time-report .hours-dec { font-size: 0.9em; }
235 table#time-report .hours-dec { font-size: 0.9em; }
233
236
234 form#issue-form .attributes { margin-bottom: 8px; }
237 form#issue-form .attributes { margin-bottom: 8px; }
235 form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; }
238 form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; }
236 form#issue-form .attributes select { min-width: 30%; }
239 form#issue-form .attributes select { min-width: 30%; }
237
240
241 ul.projects { margin: 0; padding-left: 1em; }
242 ul.projects.root { margin: 0; padding: 0; }
243 ul.projects ul { border-left: 3px solid #e0e0e0; }
244 ul.projects li { list-style-type:none; }
245 ul.projects li.root { margin-bottom: 1em; }
246 ul.projects li.child { margin-top: 1em;}
247 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
248 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
249
238 ul.properties {padding:0; font-size: 0.9em; color: #777;}
250 ul.properties {padding:0; font-size: 0.9em; color: #777;}
239 ul.properties li {list-style-type:none;}
251 ul.properties li {list-style-type:none;}
240 ul.properties li span {font-style:italic;}
252 ul.properties li span {font-style:italic;}
241
253
242 .total-hours { font-size: 110%; font-weight: bold; }
254 .total-hours { font-size: 110%; font-weight: bold; }
243 .total-hours span.hours-int { font-size: 120%; }
255 .total-hours span.hours-int { font-size: 120%; }
244
256
245 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
257 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
246 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
258 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
247
259
248 .pagination {font-size: 90%}
260 .pagination {font-size: 90%}
249 p.pagination {margin-top:8px;}
261 p.pagination {margin-top:8px;}
250
262
251 /***** Tabular forms ******/
263 /***** Tabular forms ******/
252 .tabular p{
264 .tabular p{
253 margin: 0;
265 margin: 0;
254 padding: 5px 0 8px 0;
266 padding: 5px 0 8px 0;
255 padding-left: 180px; /*width of left column containing the label elements*/
267 padding-left: 180px; /*width of left column containing the label elements*/
256 height: 1%;
268 height: 1%;
257 clear:left;
269 clear:left;
258 }
270 }
259
271
260 html>body .tabular p {overflow:hidden;}
272 html>body .tabular p {overflow:hidden;}
261
273
262 .tabular label{
274 .tabular label{
263 font-weight: bold;
275 font-weight: bold;
264 float: left;
276 float: left;
265 text-align: right;
277 text-align: right;
266 margin-left: -180px; /*width of left column*/
278 margin-left: -180px; /*width of left column*/
267 width: 175px; /*width of labels. Should be smaller than left column to create some right
279 width: 175px; /*width of labels. Should be smaller than left column to create some right
268 margin*/
280 margin*/
269 }
281 }
270
282
271 .tabular label.floating{
283 .tabular label.floating{
272 font-weight: normal;
284 font-weight: normal;
273 margin-left: 0px;
285 margin-left: 0px;
274 text-align: left;
286 text-align: left;
275 width: 270px;
287 width: 270px;
276 }
288 }
277
289
278 input#time_entry_comments { width: 90%;}
290 input#time_entry_comments { width: 90%;}
279
291
280 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
292 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
281
293
282 .tabular.settings p{ padding-left: 300px; }
294 .tabular.settings p{ padding-left: 300px; }
283 .tabular.settings label{ margin-left: -300px; width: 295px; }
295 .tabular.settings label{ margin-left: -300px; width: 295px; }
284
296
285 .required {color: #bb0000;}
297 .required {color: #bb0000;}
286 .summary {font-style: italic;}
298 .summary {font-style: italic;}
287
299
288 #attachments_fields input[type=text] {margin-left: 8px; }
300 #attachments_fields input[type=text] {margin-left: 8px; }
289
301
290 div.attachments { margin-top: 12px; }
302 div.attachments { margin-top: 12px; }
291 div.attachments p { margin:4px 0 2px 0; }
303 div.attachments p { margin:4px 0 2px 0; }
292 div.attachments img { vertical-align: middle; }
304 div.attachments img { vertical-align: middle; }
293 div.attachments span.author { font-size: 0.9em; color: #888; }
305 div.attachments span.author { font-size: 0.9em; color: #888; }
294
306
295 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
307 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
296 .other-formats span + span:before { content: "| "; }
308 .other-formats span + span:before { content: "| "; }
297
309
298 a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
310 a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
299
311
300 /***** Flash & error messages ****/
312 /***** Flash & error messages ****/
301 #errorExplanation, div.flash, .nodata, .warning {
313 #errorExplanation, div.flash, .nodata, .warning {
302 padding: 4px 4px 4px 30px;
314 padding: 4px 4px 4px 30px;
303 margin-bottom: 12px;
315 margin-bottom: 12px;
304 font-size: 1.1em;
316 font-size: 1.1em;
305 border: 2px solid;
317 border: 2px solid;
306 }
318 }
307
319
308 div.flash {margin-top: 8px;}
320 div.flash {margin-top: 8px;}
309
321
310 div.flash.error, #errorExplanation {
322 div.flash.error, #errorExplanation {
311 background: url(../images/false.png) 8px 5px no-repeat;
323 background: url(../images/false.png) 8px 5px no-repeat;
312 background-color: #ffe3e3;
324 background-color: #ffe3e3;
313 border-color: #dd0000;
325 border-color: #dd0000;
314 color: #550000;
326 color: #550000;
315 }
327 }
316
328
317 div.flash.notice {
329 div.flash.notice {
318 background: url(../images/true.png) 8px 5px no-repeat;
330 background: url(../images/true.png) 8px 5px no-repeat;
319 background-color: #dfffdf;
331 background-color: #dfffdf;
320 border-color: #9fcf9f;
332 border-color: #9fcf9f;
321 color: #005f00;
333 color: #005f00;
322 }
334 }
323
335
324 div.flash.warning {
336 div.flash.warning {
325 background: url(../images/warning.png) 8px 5px no-repeat;
337 background: url(../images/warning.png) 8px 5px no-repeat;
326 background-color: #FFEBC1;
338 background-color: #FFEBC1;
327 border-color: #FDBF3B;
339 border-color: #FDBF3B;
328 color: #A6750C;
340 color: #A6750C;
329 text-align: left;
341 text-align: left;
330 }
342 }
331
343
332 .nodata, .warning {
344 .nodata, .warning {
333 text-align: center;
345 text-align: center;
334 background-color: #FFEBC1;
346 background-color: #FFEBC1;
335 border-color: #FDBF3B;
347 border-color: #FDBF3B;
336 color: #A6750C;
348 color: #A6750C;
337 }
349 }
338
350
339 #errorExplanation ul { font-size: 0.9em;}
351 #errorExplanation ul { font-size: 0.9em;}
340
352
341 /***** Ajax indicator ******/
353 /***** Ajax indicator ******/
342 #ajax-indicator {
354 #ajax-indicator {
343 position: absolute; /* fixed not supported by IE */
355 position: absolute; /* fixed not supported by IE */
344 background-color:#eee;
356 background-color:#eee;
345 border: 1px solid #bbb;
357 border: 1px solid #bbb;
346 top:35%;
358 top:35%;
347 left:40%;
359 left:40%;
348 width:20%;
360 width:20%;
349 font-weight:bold;
361 font-weight:bold;
350 text-align:center;
362 text-align:center;
351 padding:0.6em;
363 padding:0.6em;
352 z-index:100;
364 z-index:100;
353 filter:alpha(opacity=50);
365 filter:alpha(opacity=50);
354 opacity: 0.5;
366 opacity: 0.5;
355 }
367 }
356
368
357 html>body #ajax-indicator { position: fixed; }
369 html>body #ajax-indicator { position: fixed; }
358
370
359 #ajax-indicator span {
371 #ajax-indicator span {
360 background-position: 0% 40%;
372 background-position: 0% 40%;
361 background-repeat: no-repeat;
373 background-repeat: no-repeat;
362 background-image: url(../images/loading.gif);
374 background-image: url(../images/loading.gif);
363 padding-left: 26px;
375 padding-left: 26px;
364 vertical-align: bottom;
376 vertical-align: bottom;
365 }
377 }
366
378
367 /***** Calendar *****/
379 /***** Calendar *****/
368 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
380 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
369 table.cal thead th {width: 14%;}
381 table.cal thead th {width: 14%;}
370 table.cal tbody tr {height: 100px;}
382 table.cal tbody tr {height: 100px;}
371 table.cal th { background-color:#EEEEEE; padding: 4px; }
383 table.cal th { background-color:#EEEEEE; padding: 4px; }
372 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
384 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
373 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
385 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
374 table.cal td.odd p.day-num {color: #bbb;}
386 table.cal td.odd p.day-num {color: #bbb;}
375 table.cal td.today {background:#ffffdd;}
387 table.cal td.today {background:#ffffdd;}
376 table.cal td.today p.day-num {font-weight: bold;}
388 table.cal td.today p.day-num {font-weight: bold;}
377
389
378 /***** Tooltips ******/
390 /***** Tooltips ******/
379 .tooltip{position:relative;z-index:24;}
391 .tooltip{position:relative;z-index:24;}
380 .tooltip:hover{z-index:25;color:#000;}
392 .tooltip:hover{z-index:25;color:#000;}
381 .tooltip span.tip{display: none; text-align:left;}
393 .tooltip span.tip{display: none; text-align:left;}
382
394
383 div.tooltip:hover span.tip{
395 div.tooltip:hover span.tip{
384 display:block;
396 display:block;
385 position:absolute;
397 position:absolute;
386 top:12px; left:24px; width:270px;
398 top:12px; left:24px; width:270px;
387 border:1px solid #555;
399 border:1px solid #555;
388 background-color:#fff;
400 background-color:#fff;
389 padding: 4px;
401 padding: 4px;
390 font-size: 0.8em;
402 font-size: 0.8em;
391 color:#505050;
403 color:#505050;
392 }
404 }
393
405
394 /***** Progress bar *****/
406 /***** Progress bar *****/
395 table.progress {
407 table.progress {
396 border: 1px solid #D7D7D7;
408 border: 1px solid #D7D7D7;
397 border-collapse: collapse;
409 border-collapse: collapse;
398 border-spacing: 0pt;
410 border-spacing: 0pt;
399 empty-cells: show;
411 empty-cells: show;
400 text-align: center;
412 text-align: center;
401 float:left;
413 float:left;
402 margin: 1px 6px 1px 0px;
414 margin: 1px 6px 1px 0px;
403 }
415 }
404
416
405 table.progress td { height: 0.9em; }
417 table.progress td { height: 0.9em; }
406 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
418 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
407 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
419 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
408 table.progress td.open { background: #FFF none repeat scroll 0%; }
420 table.progress td.open { background: #FFF none repeat scroll 0%; }
409 p.pourcent {font-size: 80%;}
421 p.pourcent {font-size: 80%;}
410 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
422 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
411
423
412 /***** Tabs *****/
424 /***** Tabs *****/
413 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
425 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
414 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
426 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
415 #content .tabs>ul { bottom:-1px; } /* others */
427 #content .tabs>ul { bottom:-1px; } /* others */
416 #content .tabs ul li {
428 #content .tabs ul li {
417 float:left;
429 float:left;
418 list-style-type:none;
430 list-style-type:none;
419 white-space:nowrap;
431 white-space:nowrap;
420 margin-right:8px;
432 margin-right:8px;
421 background:#fff;
433 background:#fff;
422 }
434 }
423 #content .tabs ul li a{
435 #content .tabs ul li a{
424 display:block;
436 display:block;
425 font-size: 0.9em;
437 font-size: 0.9em;
426 text-decoration:none;
438 text-decoration:none;
427 line-height:1.3em;
439 line-height:1.3em;
428 padding:4px 6px 4px 6px;
440 padding:4px 6px 4px 6px;
429 border: 1px solid #ccc;
441 border: 1px solid #ccc;
430 border-bottom: 1px solid #bbbbbb;
442 border-bottom: 1px solid #bbbbbb;
431 background-color: #eeeeee;
443 background-color: #eeeeee;
432 color:#777;
444 color:#777;
433 font-weight:bold;
445 font-weight:bold;
434 }
446 }
435
447
436 #content .tabs ul li a:hover {
448 #content .tabs ul li a:hover {
437 background-color: #ffffdd;
449 background-color: #ffffdd;
438 text-decoration:none;
450 text-decoration:none;
439 }
451 }
440
452
441 #content .tabs ul li a.selected {
453 #content .tabs ul li a.selected {
442 background-color: #fff;
454 background-color: #fff;
443 border: 1px solid #bbbbbb;
455 border: 1px solid #bbbbbb;
444 border-bottom: 1px solid #fff;
456 border-bottom: 1px solid #fff;
445 }
457 }
446
458
447 #content .tabs ul li a.selected:hover {
459 #content .tabs ul li a.selected:hover {
448 background-color: #fff;
460 background-color: #fff;
449 }
461 }
450
462
451 /***** Diff *****/
463 /***** Diff *****/
452 .diff_out { background: #fcc; }
464 .diff_out { background: #fcc; }
453 .diff_in { background: #cfc; }
465 .diff_in { background: #cfc; }
454
466
455 /***** Wiki *****/
467 /***** Wiki *****/
456 div.wiki table {
468 div.wiki table {
457 border: 1px solid #505050;
469 border: 1px solid #505050;
458 border-collapse: collapse;
470 border-collapse: collapse;
459 margin-bottom: 1em;
471 margin-bottom: 1em;
460 }
472 }
461
473
462 div.wiki table, div.wiki td, div.wiki th {
474 div.wiki table, div.wiki td, div.wiki th {
463 border: 1px solid #bbb;
475 border: 1px solid #bbb;
464 padding: 4px;
476 padding: 4px;
465 }
477 }
466
478
467 div.wiki .external {
479 div.wiki .external {
468 background-position: 0% 60%;
480 background-position: 0% 60%;
469 background-repeat: no-repeat;
481 background-repeat: no-repeat;
470 padding-left: 12px;
482 padding-left: 12px;
471 background-image: url(../images/external.png);
483 background-image: url(../images/external.png);
472 }
484 }
473
485
474 div.wiki a.new {
486 div.wiki a.new {
475 color: #b73535;
487 color: #b73535;
476 }
488 }
477
489
478 div.wiki pre {
490 div.wiki pre {
479 margin: 1em 1em 1em 1.6em;
491 margin: 1em 1em 1em 1.6em;
480 padding: 2px;
492 padding: 2px;
481 background-color: #fafafa;
493 background-color: #fafafa;
482 border: 1px solid #dadada;
494 border: 1px solid #dadada;
483 width:95%;
495 width:95%;
484 overflow-x: auto;
496 overflow-x: auto;
485 }
497 }
486
498
487 div.wiki ul.toc {
499 div.wiki ul.toc {
488 background-color: #ffffdd;
500 background-color: #ffffdd;
489 border: 1px solid #e4e4e4;
501 border: 1px solid #e4e4e4;
490 padding: 4px;
502 padding: 4px;
491 line-height: 1.2em;
503 line-height: 1.2em;
492 margin-bottom: 12px;
504 margin-bottom: 12px;
493 margin-right: 12px;
505 margin-right: 12px;
494 margin-left: 0;
506 margin-left: 0;
495 display: table
507 display: table
496 }
508 }
497 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
509 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
498
510
499 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
511 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
500 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
512 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
501 div.wiki ul.toc li { list-style-type:none;}
513 div.wiki ul.toc li { list-style-type:none;}
502 div.wiki ul.toc li.heading2 { margin-left: 6px; }
514 div.wiki ul.toc li.heading2 { margin-left: 6px; }
503 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
515 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
504
516
505 div.wiki ul.toc a {
517 div.wiki ul.toc a {
506 font-size: 0.9em;
518 font-size: 0.9em;
507 font-weight: normal;
519 font-weight: normal;
508 text-decoration: none;
520 text-decoration: none;
509 color: #606060;
521 color: #606060;
510 }
522 }
511 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
523 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
512
524
513 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
525 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
514 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
526 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
515 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
527 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
516
528
517 /***** My page layout *****/
529 /***** My page layout *****/
518 .block-receiver {
530 .block-receiver {
519 border:1px dashed #c0c0c0;
531 border:1px dashed #c0c0c0;
520 margin-bottom: 20px;
532 margin-bottom: 20px;
521 padding: 15px 0 15px 0;
533 padding: 15px 0 15px 0;
522 }
534 }
523
535
524 .mypage-box {
536 .mypage-box {
525 margin:0 0 20px 0;
537 margin:0 0 20px 0;
526 color:#505050;
538 color:#505050;
527 line-height:1.5em;
539 line-height:1.5em;
528 }
540 }
529
541
530 .handle {
542 .handle {
531 cursor: move;
543 cursor: move;
532 }
544 }
533
545
534 a.close-icon {
546 a.close-icon {
535 display:block;
547 display:block;
536 margin-top:3px;
548 margin-top:3px;
537 overflow:hidden;
549 overflow:hidden;
538 width:12px;
550 width:12px;
539 height:12px;
551 height:12px;
540 background-repeat: no-repeat;
552 background-repeat: no-repeat;
541 cursor:pointer;
553 cursor:pointer;
542 background-image:url('../images/close.png');
554 background-image:url('../images/close.png');
543 }
555 }
544
556
545 a.close-icon:hover {
557 a.close-icon:hover {
546 background-image:url('../images/close_hl.png');
558 background-image:url('../images/close_hl.png');
547 }
559 }
548
560
549 /***** Gantt chart *****/
561 /***** Gantt chart *****/
550 .gantt_hdr {
562 .gantt_hdr {
551 position:absolute;
563 position:absolute;
552 top:0;
564 top:0;
553 height:16px;
565 height:16px;
554 border-top: 1px solid #c0c0c0;
566 border-top: 1px solid #c0c0c0;
555 border-bottom: 1px solid #c0c0c0;
567 border-bottom: 1px solid #c0c0c0;
556 border-right: 1px solid #c0c0c0;
568 border-right: 1px solid #c0c0c0;
557 text-align: center;
569 text-align: center;
558 overflow: hidden;
570 overflow: hidden;
559 }
571 }
560
572
561 .task {
573 .task {
562 position: absolute;
574 position: absolute;
563 height:8px;
575 height:8px;
564 font-size:0.8em;
576 font-size:0.8em;
565 color:#888;
577 color:#888;
566 padding:0;
578 padding:0;
567 margin:0;
579 margin:0;
568 line-height:0.8em;
580 line-height:0.8em;
569 }
581 }
570
582
571 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
583 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
572 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
584 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
573 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
585 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
574 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
586 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
575
587
576 /***** Icons *****/
588 /***** Icons *****/
577 .icon {
589 .icon {
578 background-position: 0% 40%;
590 background-position: 0% 40%;
579 background-repeat: no-repeat;
591 background-repeat: no-repeat;
580 padding-left: 20px;
592 padding-left: 20px;
581 padding-top: 2px;
593 padding-top: 2px;
582 padding-bottom: 3px;
594 padding-bottom: 3px;
583 }
595 }
584
596
585 .icon22 {
597 .icon22 {
586 background-position: 0% 40%;
598 background-position: 0% 40%;
587 background-repeat: no-repeat;
599 background-repeat: no-repeat;
588 padding-left: 26px;
600 padding-left: 26px;
589 line-height: 22px;
601 line-height: 22px;
590 vertical-align: middle;
602 vertical-align: middle;
591 }
603 }
592
604
593 .icon-add { background-image: url(../images/add.png); }
605 .icon-add { background-image: url(../images/add.png); }
594 .icon-edit { background-image: url(../images/edit.png); }
606 .icon-edit { background-image: url(../images/edit.png); }
595 .icon-copy { background-image: url(../images/copy.png); }
607 .icon-copy { background-image: url(../images/copy.png); }
596 .icon-del { background-image: url(../images/delete.png); }
608 .icon-del { background-image: url(../images/delete.png); }
597 .icon-move { background-image: url(../images/move.png); }
609 .icon-move { background-image: url(../images/move.png); }
598 .icon-save { background-image: url(../images/save.png); }
610 .icon-save { background-image: url(../images/save.png); }
599 .icon-cancel { background-image: url(../images/cancel.png); }
611 .icon-cancel { background-image: url(../images/cancel.png); }
600 .icon-file { background-image: url(../images/file.png); }
612 .icon-file { background-image: url(../images/file.png); }
601 .icon-folder { background-image: url(../images/folder.png); }
613 .icon-folder { background-image: url(../images/folder.png); }
602 .open .icon-folder { background-image: url(../images/folder_open.png); }
614 .open .icon-folder { background-image: url(../images/folder_open.png); }
603 .icon-package { background-image: url(../images/package.png); }
615 .icon-package { background-image: url(../images/package.png); }
604 .icon-home { background-image: url(../images/home.png); }
616 .icon-home { background-image: url(../images/home.png); }
605 .icon-user { background-image: url(../images/user.png); }
617 .icon-user { background-image: url(../images/user.png); }
606 .icon-mypage { background-image: url(../images/user_page.png); }
618 .icon-mypage { background-image: url(../images/user_page.png); }
607 .icon-admin { background-image: url(../images/admin.png); }
619 .icon-admin { background-image: url(../images/admin.png); }
608 .icon-projects { background-image: url(../images/projects.png); }
620 .icon-projects { background-image: url(../images/projects.png); }
609 .icon-help { background-image: url(../images/help.png); }
621 .icon-help { background-image: url(../images/help.png); }
610 .icon-attachment { background-image: url(../images/attachment.png); }
622 .icon-attachment { background-image: url(../images/attachment.png); }
611 .icon-index { background-image: url(../images/index.png); }
623 .icon-index { background-image: url(../images/index.png); }
612 .icon-history { background-image: url(../images/history.png); }
624 .icon-history { background-image: url(../images/history.png); }
613 .icon-time { background-image: url(../images/time.png); }
625 .icon-time { background-image: url(../images/time.png); }
614 .icon-stats { background-image: url(../images/stats.png); }
626 .icon-stats { background-image: url(../images/stats.png); }
615 .icon-warning { background-image: url(../images/warning.png); }
627 .icon-warning { background-image: url(../images/warning.png); }
616 .icon-fav { background-image: url(../images/fav.png); }
628 .icon-fav { background-image: url(../images/fav.png); }
617 .icon-fav-off { background-image: url(../images/fav_off.png); }
629 .icon-fav-off { background-image: url(../images/fav_off.png); }
618 .icon-reload { background-image: url(../images/reload.png); }
630 .icon-reload { background-image: url(../images/reload.png); }
619 .icon-lock { background-image: url(../images/locked.png); }
631 .icon-lock { background-image: url(../images/locked.png); }
620 .icon-unlock { background-image: url(../images/unlock.png); }
632 .icon-unlock { background-image: url(../images/unlock.png); }
621 .icon-checked { background-image: url(../images/true.png); }
633 .icon-checked { background-image: url(../images/true.png); }
622 .icon-details { background-image: url(../images/zoom_in.png); }
634 .icon-details { background-image: url(../images/zoom_in.png); }
623 .icon-report { background-image: url(../images/report.png); }
635 .icon-report { background-image: url(../images/report.png); }
624 .icon-comment { background-image: url(../images/comment.png); }
636 .icon-comment { background-image: url(../images/comment.png); }
625
637
626 .icon22-projects { background-image: url(../images/22x22/projects.png); }
638 .icon22-projects { background-image: url(../images/22x22/projects.png); }
627 .icon22-users { background-image: url(../images/22x22/users.png); }
639 .icon22-users { background-image: url(../images/22x22/users.png); }
628 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
640 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
629 .icon22-role { background-image: url(../images/22x22/role.png); }
641 .icon22-role { background-image: url(../images/22x22/role.png); }
630 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
642 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
631 .icon22-options { background-image: url(../images/22x22/options.png); }
643 .icon22-options { background-image: url(../images/22x22/options.png); }
632 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
644 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
633 .icon22-authent { background-image: url(../images/22x22/authent.png); }
645 .icon22-authent { background-image: url(../images/22x22/authent.png); }
634 .icon22-info { background-image: url(../images/22x22/info.png); }
646 .icon22-info { background-image: url(../images/22x22/info.png); }
635 .icon22-comment { background-image: url(../images/22x22/comment.png); }
647 .icon22-comment { background-image: url(../images/22x22/comment.png); }
636 .icon22-package { background-image: url(../images/22x22/package.png); }
648 .icon22-package { background-image: url(../images/22x22/package.png); }
637 .icon22-settings { background-image: url(../images/22x22/settings.png); }
649 .icon22-settings { background-image: url(../images/22x22/settings.png); }
638 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
650 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
639
651
640 img.gravatar {
652 img.gravatar {
641 padding: 2px;
653 padding: 2px;
642 border: solid 1px #d5d5d5;
654 border: solid 1px #d5d5d5;
643 background: #fff;
655 background: #fff;
644 }
656 }
645
657
646 div.issue img.gravatar {
658 div.issue img.gravatar {
647 float: right;
659 float: right;
648 margin: 0 0 0 1em;
660 margin: 0 0 0 1em;
649 padding: 5px;
661 padding: 5px;
650 }
662 }
651
663
652 div.issue table img.gravatar {
664 div.issue table img.gravatar {
653 height: 14px;
665 height: 14px;
654 width: 14px;
666 width: 14px;
655 padding: 2px;
667 padding: 2px;
656 float: left;
668 float: left;
657 margin: 0 0.5em 0 0;
669 margin: 0 0.5em 0 0;
658 }
670 }
659
671
660 #history img.gravatar {
672 #history img.gravatar {
661 padding: 3px;
673 padding: 3px;
662 margin: 0 1.5em 1em 0;
674 margin: 0 1.5em 1em 0;
663 float: left;
675 float: left;
664 }
676 }
665
677
666 td.username img.gravatar {
678 td.username img.gravatar {
667 float: left;
679 float: left;
668 margin: 0 1em 0 0;
680 margin: 0 1em 0 0;
669 }
681 }
670
682
671 #activity dt img.gravatar {
683 #activity dt img.gravatar {
672 float: left;
684 float: left;
673 margin: 0 1em 1em 0;
685 margin: 0 1em 1em 0;
674 }
686 }
675
687
676 #activity dt,
688 #activity dt,
677 .journal {
689 .journal {
678 clear: left;
690 clear: left;
679 }
691 }
680
692
681 h2 img { vertical-align:middle; }
693 h2 img { vertical-align:middle; }
682
694
683
695
684 /***** Media print specific styles *****/
696 /***** Media print specific styles *****/
685 @media print {
697 @media print {
686 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
698 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
687 #main { background: #fff; }
699 #main { background: #fff; }
688 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
700 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
689 }
701 }
@@ -1,57 +1,80
1 ---
1 ---
2 projects_001:
2 projects_001:
3 created_on: 2006-07-19 19:13:59 +02:00
3 created_on: 2006-07-19 19:13:59 +02:00
4 name: eCookbook
4 name: eCookbook
5 updated_on: 2006-07-19 22:53:01 +02:00
5 updated_on: 2006-07-19 22:53:01 +02:00
6 projects_count: 3
6 projects_count: 3
7 id: 1
7 id: 1
8 description: Recipes management application
8 description: Recipes management application
9 homepage: http://ecookbook.somenet.foo/
9 homepage: http://ecookbook.somenet.foo/
10 is_public: true
10 is_public: true
11 identifier: ecookbook
11 identifier: ecookbook
12 parent_id:
12 parent_id:
13 lft: 1
14 rgt: 10
13 projects_002:
15 projects_002:
14 created_on: 2006-07-19 19:14:19 +02:00
16 created_on: 2006-07-19 19:14:19 +02:00
15 name: OnlineStore
17 name: OnlineStore
16 updated_on: 2006-07-19 19:14:19 +02:00
18 updated_on: 2006-07-19 19:14:19 +02:00
17 projects_count: 0
19 projects_count: 0
18 id: 2
20 id: 2
19 description: E-commerce web site
21 description: E-commerce web site
20 homepage: ""
22 homepage: ""
21 is_public: false
23 is_public: false
22 identifier: onlinestore
24 identifier: onlinestore
23 parent_id:
25 parent_id:
26 lft: 11
27 rgt: 12
24 projects_003:
28 projects_003:
25 created_on: 2006-07-19 19:15:21 +02:00
29 created_on: 2006-07-19 19:15:21 +02:00
26 name: eCookbook Subproject 1
30 name: eCookbook Subproject 1
27 updated_on: 2006-07-19 19:18:12 +02:00
31 updated_on: 2006-07-19 19:18:12 +02:00
28 projects_count: 0
32 projects_count: 0
29 id: 3
33 id: 3
30 description: eCookBook Subproject 1
34 description: eCookBook Subproject 1
31 homepage: ""
35 homepage: ""
32 is_public: true
36 is_public: true
33 identifier: subproject1
37 identifier: subproject1
34 parent_id: 1
38 parent_id: 1
39 lft: 6
40 rgt: 7
35 projects_004:
41 projects_004:
36 created_on: 2006-07-19 19:15:51 +02:00
42 created_on: 2006-07-19 19:15:51 +02:00
37 name: eCookbook Subproject 2
43 name: eCookbook Subproject 2
38 updated_on: 2006-07-19 19:17:07 +02:00
44 updated_on: 2006-07-19 19:17:07 +02:00
39 projects_count: 0
45 projects_count: 0
40 id: 4
46 id: 4
41 description: eCookbook Subproject 2
47 description: eCookbook Subproject 2
42 homepage: ""
48 homepage: ""
43 is_public: true
49 is_public: true
44 identifier: subproject2
50 identifier: subproject2
45 parent_id: 1
51 parent_id: 1
52 lft: 8
53 rgt: 9
46 projects_005:
54 projects_005:
47 created_on: 2006-07-19 19:15:51 +02:00
55 created_on: 2006-07-19 19:15:51 +02:00
48 name: Private child of eCookbook
56 name: Private child of eCookbook
49 updated_on: 2006-07-19 19:17:07 +02:00
57 updated_on: 2006-07-19 19:17:07 +02:00
50 projects_count: 0
58 projects_count: 0
51 id: 5
59 id: 5
52 description: This is a private subproject of a public project
60 description: This is a private subproject of a public project
53 homepage: ""
61 homepage: ""
54 is_public: false
62 is_public: false
55 identifier: private_child
63 identifier: private-child
56 parent_id: 1
64 parent_id: 1
65 lft: 2
66 rgt: 5
67 projects_006:
68 created_on: 2006-07-19 19:15:51 +02:00
69 name: Child of private child
70 updated_on: 2006-07-19 19:17:07 +02:00
71 projects_count: 0
72 id: 6
73 description: This is a public subproject of a private project
74 homepage: ""
75 is_public: true
76 identifier: project6
77 parent_id: 5
78 lft: 3
79 rgt: 4
57 No newline at end of file
80
@@ -1,357 +1,364
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < Test::Unit::TestCase
24 class ProjectsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments
27 :attachments
28
28
29 def setup
29 def setup
30 @controller = ProjectsController.new
30 @controller = ProjectsController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 @request.session[:user_id] = nil
33 @request.session[:user_id] = nil
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 end
35 end
36
36
37 def test_index
37 def test_index
38 get :index
38 get :index
39 assert_response :success
39 assert_response :success
40 assert_template 'index'
40 assert_template 'index'
41 assert_not_nil assigns(:project_tree)
41 assert_not_nil assigns(:projects)
42 # Root project as hash key
42
43 assert assigns(:project_tree).keys.include?(Project.find(1))
43 assert_tag :ul, :child => {:tag => 'li',
44 # Subproject in corresponding value
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
45 assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3))
45 :child => { :tag => 'ul',
46 :descendant => { :tag => 'a',
47 :content => 'Child of private child'
48 }
49 }
50 }
51
52 assert_no_tag :a, :content => /Private child of eCookbook/
46 end
53 end
47
54
48 def test_index_atom
55 def test_index_atom
49 get :index, :format => 'atom'
56 get :index, :format => 'atom'
50 assert_response :success
57 assert_response :success
51 assert_template 'common/feed.atom.rxml'
58 assert_template 'common/feed.atom.rxml'
52 assert_select 'feed>title', :text => 'Redmine: Latest projects'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
53 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
54 end
61 end
55
62
56 def test_show_by_id
63 def test_show_by_id
57 get :show, :id => 1
64 get :show, :id => 1
58 assert_response :success
65 assert_response :success
59 assert_template 'show'
66 assert_template 'show'
60 assert_not_nil assigns(:project)
67 assert_not_nil assigns(:project)
61 end
68 end
62
69
63 def test_show_by_identifier
70 def test_show_by_identifier
64 get :show, :id => 'ecookbook'
71 get :show, :id => 'ecookbook'
65 assert_response :success
72 assert_response :success
66 assert_template 'show'
73 assert_template 'show'
67 assert_not_nil assigns(:project)
74 assert_not_nil assigns(:project)
68 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
75 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
69 end
76 end
70
77
71 def test_private_subprojects_hidden
78 def test_private_subprojects_hidden
72 get :show, :id => 'ecookbook'
79 get :show, :id => 'ecookbook'
73 assert_response :success
80 assert_response :success
74 assert_template 'show'
81 assert_template 'show'
75 assert_no_tag :tag => 'a', :content => /Private child/
82 assert_no_tag :tag => 'a', :content => /Private child/
76 end
83 end
77
84
78 def test_private_subprojects_visible
85 def test_private_subprojects_visible
79 @request.session[:user_id] = 2 # manager who is a member of the private subproject
86 @request.session[:user_id] = 2 # manager who is a member of the private subproject
80 get :show, :id => 'ecookbook'
87 get :show, :id => 'ecookbook'
81 assert_response :success
88 assert_response :success
82 assert_template 'show'
89 assert_template 'show'
83 assert_tag :tag => 'a', :content => /Private child/
90 assert_tag :tag => 'a', :content => /Private child/
84 end
91 end
85
92
86 def test_settings
93 def test_settings
87 @request.session[:user_id] = 2 # manager
94 @request.session[:user_id] = 2 # manager
88 get :settings, :id => 1
95 get :settings, :id => 1
89 assert_response :success
96 assert_response :success
90 assert_template 'settings'
97 assert_template 'settings'
91 end
98 end
92
99
93 def test_edit
100 def test_edit
94 @request.session[:user_id] = 2 # manager
101 @request.session[:user_id] = 2 # manager
95 post :edit, :id => 1, :project => {:name => 'Test changed name',
102 post :edit, :id => 1, :project => {:name => 'Test changed name',
96 :issue_custom_field_ids => ['']}
103 :issue_custom_field_ids => ['']}
97 assert_redirected_to 'projects/settings/ecookbook'
104 assert_redirected_to 'projects/settings/ecookbook'
98 project = Project.find(1)
105 project = Project.find(1)
99 assert_equal 'Test changed name', project.name
106 assert_equal 'Test changed name', project.name
100 end
107 end
101
108
102 def test_get_destroy
109 def test_get_destroy
103 @request.session[:user_id] = 1 # admin
110 @request.session[:user_id] = 1 # admin
104 get :destroy, :id => 1
111 get :destroy, :id => 1
105 assert_response :success
112 assert_response :success
106 assert_template 'destroy'
113 assert_template 'destroy'
107 assert_not_nil Project.find_by_id(1)
114 assert_not_nil Project.find_by_id(1)
108 end
115 end
109
116
110 def test_post_destroy
117 def test_post_destroy
111 @request.session[:user_id] = 1 # admin
118 @request.session[:user_id] = 1 # admin
112 post :destroy, :id => 1, :confirm => 1
119 post :destroy, :id => 1, :confirm => 1
113 assert_redirected_to 'admin/projects'
120 assert_redirected_to 'admin/projects'
114 assert_nil Project.find_by_id(1)
121 assert_nil Project.find_by_id(1)
115 end
122 end
116
123
117 def test_add_file
124 def test_add_file
118 set_tmp_attachments_directory
125 set_tmp_attachments_directory
119 @request.session[:user_id] = 2
126 @request.session[:user_id] = 2
120 Setting.notified_events = ['file_added']
127 Setting.notified_events = ['file_added']
121 ActionMailer::Base.deliveries.clear
128 ActionMailer::Base.deliveries.clear
122
129
123 assert_difference 'Attachment.count' do
130 assert_difference 'Attachment.count' do
124 post :add_file, :id => 1, :version_id => '',
131 post :add_file, :id => 1, :version_id => '',
125 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
132 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
126 end
133 end
127 assert_redirected_to 'projects/list_files/ecookbook'
134 assert_redirected_to 'projects/list_files/ecookbook'
128 a = Attachment.find(:first, :order => 'created_on DESC')
135 a = Attachment.find(:first, :order => 'created_on DESC')
129 assert_equal 'testfile.txt', a.filename
136 assert_equal 'testfile.txt', a.filename
130 assert_equal Project.find(1), a.container
137 assert_equal Project.find(1), a.container
131
138
132 mail = ActionMailer::Base.deliveries.last
139 mail = ActionMailer::Base.deliveries.last
133 assert_kind_of TMail::Mail, mail
140 assert_kind_of TMail::Mail, mail
134 assert_equal "[eCookbook] New file", mail.subject
141 assert_equal "[eCookbook] New file", mail.subject
135 assert mail.body.include?('testfile.txt')
142 assert mail.body.include?('testfile.txt')
136 end
143 end
137
144
138 def test_add_version_file
145 def test_add_version_file
139 set_tmp_attachments_directory
146 set_tmp_attachments_directory
140 @request.session[:user_id] = 2
147 @request.session[:user_id] = 2
141 Setting.notified_events = ['file_added']
148 Setting.notified_events = ['file_added']
142
149
143 assert_difference 'Attachment.count' do
150 assert_difference 'Attachment.count' do
144 post :add_file, :id => 1, :version_id => '2',
151 post :add_file, :id => 1, :version_id => '2',
145 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
152 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
146 end
153 end
147 assert_redirected_to 'projects/list_files/ecookbook'
154 assert_redirected_to 'projects/list_files/ecookbook'
148 a = Attachment.find(:first, :order => 'created_on DESC')
155 a = Attachment.find(:first, :order => 'created_on DESC')
149 assert_equal 'testfile.txt', a.filename
156 assert_equal 'testfile.txt', a.filename
150 assert_equal Version.find(2), a.container
157 assert_equal Version.find(2), a.container
151 end
158 end
152
159
153 def test_list_files
160 def test_list_files
154 get :list_files, :id => 1
161 get :list_files, :id => 1
155 assert_response :success
162 assert_response :success
156 assert_template 'list_files'
163 assert_template 'list_files'
157 assert_not_nil assigns(:containers)
164 assert_not_nil assigns(:containers)
158
165
159 # file attached to the project
166 # file attached to the project
160 assert_tag :a, :content => 'project_file.zip',
167 assert_tag :a, :content => 'project_file.zip',
161 :attributes => { :href => '/attachments/download/8/project_file.zip' }
168 :attributes => { :href => '/attachments/download/8/project_file.zip' }
162
169
163 # file attached to a project's version
170 # file attached to a project's version
164 assert_tag :a, :content => 'version_file.zip',
171 assert_tag :a, :content => 'version_file.zip',
165 :attributes => { :href => '/attachments/download/9/version_file.zip' }
172 :attributes => { :href => '/attachments/download/9/version_file.zip' }
166 end
173 end
167
174
168 def test_changelog
175 def test_changelog
169 get :changelog, :id => 1
176 get :changelog, :id => 1
170 assert_response :success
177 assert_response :success
171 assert_template 'changelog'
178 assert_template 'changelog'
172 assert_not_nil assigns(:versions)
179 assert_not_nil assigns(:versions)
173 end
180 end
174
181
175 def test_roadmap
182 def test_roadmap
176 get :roadmap, :id => 1
183 get :roadmap, :id => 1
177 assert_response :success
184 assert_response :success
178 assert_template 'roadmap'
185 assert_template 'roadmap'
179 assert_not_nil assigns(:versions)
186 assert_not_nil assigns(:versions)
180 # Version with no date set appears
187 # Version with no date set appears
181 assert assigns(:versions).include?(Version.find(3))
188 assert assigns(:versions).include?(Version.find(3))
182 # Completed version doesn't appear
189 # Completed version doesn't appear
183 assert !assigns(:versions).include?(Version.find(1))
190 assert !assigns(:versions).include?(Version.find(1))
184 end
191 end
185
192
186 def test_roadmap_with_completed_versions
193 def test_roadmap_with_completed_versions
187 get :roadmap, :id => 1, :completed => 1
194 get :roadmap, :id => 1, :completed => 1
188 assert_response :success
195 assert_response :success
189 assert_template 'roadmap'
196 assert_template 'roadmap'
190 assert_not_nil assigns(:versions)
197 assert_not_nil assigns(:versions)
191 # Version with no date set appears
198 # Version with no date set appears
192 assert assigns(:versions).include?(Version.find(3))
199 assert assigns(:versions).include?(Version.find(3))
193 # Completed version appears
200 # Completed version appears
194 assert assigns(:versions).include?(Version.find(1))
201 assert assigns(:versions).include?(Version.find(1))
195 end
202 end
196
203
197 def test_project_activity
204 def test_project_activity
198 get :activity, :id => 1, :with_subprojects => 0
205 get :activity, :id => 1, :with_subprojects => 0
199 assert_response :success
206 assert_response :success
200 assert_template 'activity'
207 assert_template 'activity'
201 assert_not_nil assigns(:events_by_day)
208 assert_not_nil assigns(:events_by_day)
202
209
203 assert_tag :tag => "h3",
210 assert_tag :tag => "h3",
204 :content => /#{2.days.ago.to_date.day}/,
211 :content => /#{2.days.ago.to_date.day}/,
205 :sibling => { :tag => "dl",
212 :sibling => { :tag => "dl",
206 :child => { :tag => "dt",
213 :child => { :tag => "dt",
207 :attributes => { :class => /issue-edit/ },
214 :attributes => { :class => /issue-edit/ },
208 :child => { :tag => "a",
215 :child => { :tag => "a",
209 :content => /(#{IssueStatus.find(2).name})/,
216 :content => /(#{IssueStatus.find(2).name})/,
210 }
217 }
211 }
218 }
212 }
219 }
213 end
220 end
214
221
215 def test_previous_project_activity
222 def test_previous_project_activity
216 get :activity, :id => 1, :from => 3.days.ago.to_date
223 get :activity, :id => 1, :from => 3.days.ago.to_date
217 assert_response :success
224 assert_response :success
218 assert_template 'activity'
225 assert_template 'activity'
219 assert_not_nil assigns(:events_by_day)
226 assert_not_nil assigns(:events_by_day)
220
227
221 assert_tag :tag => "h3",
228 assert_tag :tag => "h3",
222 :content => /#{3.day.ago.to_date.day}/,
229 :content => /#{3.day.ago.to_date.day}/,
223 :sibling => { :tag => "dl",
230 :sibling => { :tag => "dl",
224 :child => { :tag => "dt",
231 :child => { :tag => "dt",
225 :attributes => { :class => /issue/ },
232 :attributes => { :class => /issue/ },
226 :child => { :tag => "a",
233 :child => { :tag => "a",
227 :content => /#{Issue.find(1).subject}/,
234 :content => /#{Issue.find(1).subject}/,
228 }
235 }
229 }
236 }
230 }
237 }
231 end
238 end
232
239
233 def test_global_activity
240 def test_global_activity
234 get :activity
241 get :activity
235 assert_response :success
242 assert_response :success
236 assert_template 'activity'
243 assert_template 'activity'
237 assert_not_nil assigns(:events_by_day)
244 assert_not_nil assigns(:events_by_day)
238
245
239 assert_tag :tag => "h3",
246 assert_tag :tag => "h3",
240 :content => /#{5.day.ago.to_date.day}/,
247 :content => /#{5.day.ago.to_date.day}/,
241 :sibling => { :tag => "dl",
248 :sibling => { :tag => "dl",
242 :child => { :tag => "dt",
249 :child => { :tag => "dt",
243 :attributes => { :class => /issue/ },
250 :attributes => { :class => /issue/ },
244 :child => { :tag => "a",
251 :child => { :tag => "a",
245 :content => /#{Issue.find(5).subject}/,
252 :content => /#{Issue.find(5).subject}/,
246 }
253 }
247 }
254 }
248 }
255 }
249 end
256 end
250
257
251 def test_user_activity
258 def test_user_activity
252 get :activity, :user_id => 2
259 get :activity, :user_id => 2
253 assert_response :success
260 assert_response :success
254 assert_template 'activity'
261 assert_template 'activity'
255 assert_not_nil assigns(:events_by_day)
262 assert_not_nil assigns(:events_by_day)
256
263
257 assert_tag :tag => "h3",
264 assert_tag :tag => "h3",
258 :content => /#{3.day.ago.to_date.day}/,
265 :content => /#{3.day.ago.to_date.day}/,
259 :sibling => { :tag => "dl",
266 :sibling => { :tag => "dl",
260 :child => { :tag => "dt",
267 :child => { :tag => "dt",
261 :attributes => { :class => /issue/ },
268 :attributes => { :class => /issue/ },
262 :child => { :tag => "a",
269 :child => { :tag => "a",
263 :content => /#{Issue.find(1).subject}/,
270 :content => /#{Issue.find(1).subject}/,
264 }
271 }
265 }
272 }
266 }
273 }
267 end
274 end
268
275
269 def test_activity_atom_feed
276 def test_activity_atom_feed
270 get :activity, :format => 'atom'
277 get :activity, :format => 'atom'
271 assert_response :success
278 assert_response :success
272 assert_template 'common/feed.atom.rxml'
279 assert_template 'common/feed.atom.rxml'
273 end
280 end
274
281
275 def test_archive
282 def test_archive
276 @request.session[:user_id] = 1 # admin
283 @request.session[:user_id] = 1 # admin
277 post :archive, :id => 1
284 post :archive, :id => 1
278 assert_redirected_to 'admin/projects'
285 assert_redirected_to 'admin/projects'
279 assert !Project.find(1).active?
286 assert !Project.find(1).active?
280 end
287 end
281
288
282 def test_unarchive
289 def test_unarchive
283 @request.session[:user_id] = 1 # admin
290 @request.session[:user_id] = 1 # admin
284 Project.find(1).archive
291 Project.find(1).archive
285 post :unarchive, :id => 1
292 post :unarchive, :id => 1
286 assert_redirected_to 'admin/projects'
293 assert_redirected_to 'admin/projects'
287 assert Project.find(1).active?
294 assert Project.find(1).active?
288 end
295 end
289
296
290 def test_jump_should_redirect_to_active_tab
297 def test_jump_should_redirect_to_active_tab
291 get :show, :id => 1, :jump => 'issues'
298 get :show, :id => 1, :jump => 'issues'
292 assert_redirected_to 'projects/ecookbook/issues'
299 assert_redirected_to 'projects/ecookbook/issues'
293 end
300 end
294
301
295 def test_jump_should_not_redirect_to_inactive_tab
302 def test_jump_should_not_redirect_to_inactive_tab
296 get :show, :id => 3, :jump => 'documents'
303 get :show, :id => 3, :jump => 'documents'
297 assert_response :success
304 assert_response :success
298 assert_template 'show'
305 assert_template 'show'
299 end
306 end
300
307
301 def test_jump_should_not_redirect_to_unknown_tab
308 def test_jump_should_not_redirect_to_unknown_tab
302 get :show, :id => 3, :jump => 'foobar'
309 get :show, :id => 3, :jump => 'foobar'
303 assert_response :success
310 assert_response :success
304 assert_template 'show'
311 assert_template 'show'
305 end
312 end
306
313
307 def test_project_menu
314 def test_project_menu
308 assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
315 assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
309 Redmine::MenuManager.map :project_menu do |menu|
316 Redmine::MenuManager.map :project_menu do |menu|
310 menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
317 menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
311 menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
318 menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
312 menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
319 menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
313 end
320 end
314
321
315 get :show, :id => 1
322 get :show, :id => 1
316 assert_tag :div, :attributes => { :id => 'main-menu' },
323 assert_tag :div, :attributes => { :id => 'main-menu' },
317 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo',
324 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo',
318 :attributes => { :class => 'foo' } } }
325 :attributes => { :class => 'foo' } } }
319
326
320 assert_tag :div, :attributes => { :id => 'main-menu' },
327 assert_tag :div, :attributes => { :id => 'main-menu' },
321 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar',
328 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar',
322 :attributes => { :class => 'bar' } },
329 :attributes => { :class => 'bar' } },
323 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
330 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
324
331
325 assert_tag :div, :attributes => { :id => 'main-menu' },
332 assert_tag :div, :attributes => { :id => 'main-menu' },
326 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK',
333 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK',
327 :attributes => { :class => 'hello' } },
334 :attributes => { :class => 'hello' } },
328 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
335 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
329
336
330 # Remove the menu items
337 # Remove the menu items
331 Redmine::MenuManager.map :project_menu do |menu|
338 Redmine::MenuManager.map :project_menu do |menu|
332 menu.delete :foo
339 menu.delete :foo
333 menu.delete :bar
340 menu.delete :bar
334 menu.delete :hello
341 menu.delete :hello
335 end
342 end
336 end
343 end
337 end
344 end
338
345
339 # A hook that is manually registered later
346 # A hook that is manually registered later
340 class ProjectBasedTemplate < Redmine::Hook::ViewListener
347 class ProjectBasedTemplate < Redmine::Hook::ViewListener
341 def view_layouts_base_html_head(context)
348 def view_layouts_base_html_head(context)
342 # Adds a project stylesheet
349 # Adds a project stylesheet
343 stylesheet_link_tag(context[:project].identifier) if context[:project]
350 stylesheet_link_tag(context[:project].identifier) if context[:project]
344 end
351 end
345 end
352 end
346 # Don't use this hook now
353 # Don't use this hook now
347 Redmine::Hook.clear_listeners
354 Redmine::Hook.clear_listeners
348
355
349 def test_hook_response
356 def test_hook_response
350 Redmine::Hook.add_listener(ProjectBasedTemplate)
357 Redmine::Hook.add_listener(ProjectBasedTemplate)
351 get :show, :id => 1
358 get :show, :id => 1
352 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
359 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
353 :parent => {:tag => 'head'}
360 :parent => {:tag => 'head'}
354
361
355 Redmine::Hook.clear_listeners
362 Redmine::Hook.clear_listeners
356 end
363 end
357 end
364 end
@@ -1,144 +1,211
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class ProjectTest < Test::Unit::TestCase
20 class ProjectTest < Test::Unit::TestCase
21 fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, :users, :members, :roles, :projects_trackers, :trackers, :boards
21 fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, :users, :members, :roles, :projects_trackers, :trackers, :boards
22
22
23 def setup
23 def setup
24 @ecookbook = Project.find(1)
24 @ecookbook = Project.find(1)
25 @ecookbook_sub1 = Project.find(3)
25 @ecookbook_sub1 = Project.find(3)
26 end
26 end
27
27
28 def test_truth
28 def test_truth
29 assert_kind_of Project, @ecookbook
29 assert_kind_of Project, @ecookbook
30 assert_equal "eCookbook", @ecookbook.name
30 assert_equal "eCookbook", @ecookbook.name
31 end
31 end
32
32
33 def test_update
33 def test_update
34 assert_equal "eCookbook", @ecookbook.name
34 assert_equal "eCookbook", @ecookbook.name
35 @ecookbook.name = "eCook"
35 @ecookbook.name = "eCook"
36 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
36 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
37 @ecookbook.reload
37 @ecookbook.reload
38 assert_equal "eCook", @ecookbook.name
38 assert_equal "eCook", @ecookbook.name
39 end
39 end
40
40
41 def test_validate
41 def test_validate
42 @ecookbook.name = ""
42 @ecookbook.name = ""
43 assert !@ecookbook.save
43 assert !@ecookbook.save
44 assert_equal 1, @ecookbook.errors.count
44 assert_equal 1, @ecookbook.errors.count
45 assert_equal "activerecord_error_blank", @ecookbook.errors.on(:name)
45 assert_equal "activerecord_error_blank", @ecookbook.errors.on(:name)
46 end
46 end
47
47
48 def test_public_projects
49 public_projects = Project.find(:all, :conditions => ["is_public=?", true])
50 assert_equal 3, public_projects.length
51 assert_equal true, public_projects[0].is_public?
52 end
53
54 def test_archive
48 def test_archive
55 user = @ecookbook.members.first.user
49 user = @ecookbook.members.first.user
56 @ecookbook.archive
50 @ecookbook.archive
57 @ecookbook.reload
51 @ecookbook.reload
58
52
59 assert !@ecookbook.active?
53 assert !@ecookbook.active?
60 assert !user.projects.include?(@ecookbook)
54 assert !user.projects.include?(@ecookbook)
61 # Subproject are also archived
55 # Subproject are also archived
62 assert !@ecookbook.children.empty?
56 assert !@ecookbook.children.empty?
63 assert @ecookbook.active_children.empty?
57 assert @ecookbook.descendants.active.empty?
64 end
58 end
65
59
66 def test_unarchive
60 def test_unarchive
67 user = @ecookbook.members.first.user
61 user = @ecookbook.members.first.user
68 @ecookbook.archive
62 @ecookbook.archive
69 # A subproject of an archived project can not be unarchived
63 # A subproject of an archived project can not be unarchived
70 assert !@ecookbook_sub1.unarchive
64 assert !@ecookbook_sub1.unarchive
71
65
72 # Unarchive project
66 # Unarchive project
73 assert @ecookbook.unarchive
67 assert @ecookbook.unarchive
74 @ecookbook.reload
68 @ecookbook.reload
75 assert @ecookbook.active?
69 assert @ecookbook.active?
76 assert user.projects.include?(@ecookbook)
70 assert user.projects.include?(@ecookbook)
77 # Subproject can now be unarchived
71 # Subproject can now be unarchived
78 @ecookbook_sub1.reload
72 @ecookbook_sub1.reload
79 assert @ecookbook_sub1.unarchive
73 assert @ecookbook_sub1.unarchive
80 end
74 end
81
75
82 def test_destroy
76 def test_destroy
83 # 2 active members
77 # 2 active members
84 assert_equal 2, @ecookbook.members.size
78 assert_equal 2, @ecookbook.members.size
85 # and 1 is locked
79 # and 1 is locked
86 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
80 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
87 # some boards
81 # some boards
88 assert @ecookbook.boards.any?
82 assert @ecookbook.boards.any?
89
83
90 @ecookbook.destroy
84 @ecookbook.destroy
91 # make sure that the project non longer exists
85 # make sure that the project non longer exists
92 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
86 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
93 # make sure related data was removed
87 # make sure related data was removed
94 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
88 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
95 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
89 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
96 end
90 end
97
91
98 def test_subproject_ok
92 def test_move_an_orphan_project_to_a_root_project
99 sub = Project.find(2)
93 sub = Project.find(2)
100 sub.parent = @ecookbook
94 sub.set_parent! @ecookbook
101 assert sub.save
102 assert_equal @ecookbook.id, sub.parent.id
95 assert_equal @ecookbook.id, sub.parent.id
103 @ecookbook.reload
96 @ecookbook.reload
104 assert_equal 4, @ecookbook.children.size
97 assert_equal 4, @ecookbook.children.size
105 end
98 end
106
99
107 def test_subproject_invalid
100 def test_move_an_orphan_project_to_a_subproject
108 sub = Project.find(2)
101 sub = Project.find(2)
109 sub.parent = @ecookbook_sub1
102 assert sub.set_parent!(@ecookbook_sub1)
110 assert !sub.save
103 end
104
105 def test_move_a_root_project_to_a_project
106 sub = @ecookbook
107 assert sub.set_parent!(Project.find(2))
111 end
108 end
112
109
113 def test_subproject_invalid_2
110 def test_should_not_move_a_project_to_its_children
114 sub = @ecookbook
111 sub = @ecookbook
115 sub.parent = Project.find(2)
112 assert !(sub.set_parent!(Project.find(3)))
116 assert !sub.save
113 end
114
115 def test_set_parent_should_add_roots_in_alphabetical_order
116 ProjectCustomField.delete_all
117 Project.delete_all
118 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
119 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
120 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
121 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
122
123 assert_equal 4, Project.count
124 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
125 end
126
127 def test_set_parent_should_add_children_in_alphabetical_order
128 ProjectCustomField.delete_all
129 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
130 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
131 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
132 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
133 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
134
135 parent.reload
136 assert_equal 4, parent.children.size
137 assert_equal parent.children.sort_by(&:name), parent.children
138 end
139
140 def test_rebuild_should_sort_children_alphabetically
141 ProjectCustomField.delete_all
142 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
143 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
144 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
145 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
146 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
147
148 Project.update_all("lft = NULL, rgt = NULL")
149 Project.rebuild!
150
151 parent.reload
152 assert_equal 4, parent.children.size
153 assert_equal parent.children.sort_by(&:name), parent.children
154 end
155
156 def test_parent
157 p = Project.find(6).parent
158 assert p.is_a?(Project)
159 assert_equal 5, p.id
160 end
161
162 def test_ancestors
163 a = Project.find(6).ancestors
164 assert a.first.is_a?(Project)
165 assert_equal [1, 5], a.collect(&:id)
166 end
167
168 def test_root
169 r = Project.find(6).root
170 assert r.is_a?(Project)
171 assert_equal 1, r.id
172 end
173
174 def test_children
175 c = Project.find(1).children
176 assert c.first.is_a?(Project)
177 assert_equal [5, 3, 4], c.collect(&:id)
178 end
179
180 def test_descendants
181 d = Project.find(1).descendants
182 assert d.first.is_a?(Project)
183 assert_equal [5, 6, 3, 4], d.collect(&:id)
117 end
184 end
118
185
119 def test_rolled_up_trackers
186 def test_rolled_up_trackers
120 parent = Project.find(1)
187 parent = Project.find(1)
121 parent.trackers = Tracker.find([1,2])
188 parent.trackers = Tracker.find([1,2])
122 child = parent.children.find(3)
189 child = parent.children.find(3)
123
190
124 assert_equal [1, 2], parent.tracker_ids
191 assert_equal [1, 2], parent.tracker_ids
125 assert_equal [2, 3], child.tracker_ids
192 assert_equal [2, 3], child.tracker_ids
126
193
127 assert_kind_of Tracker, parent.rolled_up_trackers.first
194 assert_kind_of Tracker, parent.rolled_up_trackers.first
128 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
195 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
129
196
130 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
197 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
131 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
198 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
132 end
199 end
133
200
134 def test_next_identifier
201 def test_next_identifier
135 ProjectCustomField.delete_all
202 ProjectCustomField.delete_all
136 Project.create!(:name => 'last', :identifier => 'p2008040')
203 Project.create!(:name => 'last', :identifier => 'p2008040')
137 assert_equal 'p2008041', Project.next_identifier
204 assert_equal 'p2008041', Project.next_identifier
138 end
205 end
139
206
140 def test_next_identifier_first_project
207 def test_next_identifier_first_project
141 Project.delete_all
208 Project.delete_all
142 assert_nil Project.next_identifier
209 assert_nil Project.next_identifier
143 end
210 end
144 end
211 end
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now