##// END OF EJS Templates
Makes models #initialize accept additional arguments....
Jean-Philippe Lang -
r8167:062fbeae8047
parent child
Show More
@@ -1,164 +1,164
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 include Redmine::SubclassFactory
19 include Redmine::SubclassFactory
20
20
21 has_many :custom_values, :dependent => :delete_all
21 has_many :custom_values, :dependent => :delete_all
22 acts_as_list :scope => 'type = \'#{self.class}\''
22 acts_as_list :scope => 'type = \'#{self.class}\''
23 serialize :possible_values
23 serialize :possible_values
24
24
25 validates_presence_of :name, :field_format
25 validates_presence_of :name, :field_format
26 validates_uniqueness_of :name, :scope => :type
26 validates_uniqueness_of :name, :scope => :type
27 validates_length_of :name, :maximum => 30
27 validates_length_of :name, :maximum => 30
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29
29
30 validate :validate_values
30 validate :validate_values
31 before_validation :set_searchable
31 before_validation :set_searchable
32
32
33 def initialize(attributes = nil)
33 def initialize(attributes=nil, *args)
34 super
34 super
35 self.possible_values ||= []
35 self.possible_values ||= []
36 end
36 end
37
37
38 def set_searchable
38 def set_searchable
39 # make sure these fields are not searchable
39 # make sure these fields are not searchable
40 self.searchable = false if %w(int float date bool).include?(field_format)
40 self.searchable = false if %w(int float date bool).include?(field_format)
41 true
41 true
42 end
42 end
43
43
44 def validate_values
44 def validate_values
45 if self.field_format == "list"
45 if self.field_format == "list"
46 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
46 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
47 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
47 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
48 end
48 end
49
49
50 if regexp.present?
50 if regexp.present?
51 begin
51 begin
52 Regexp.new(regexp)
52 Regexp.new(regexp)
53 rescue
53 rescue
54 errors.add(:regexp, :invalid)
54 errors.add(:regexp, :invalid)
55 end
55 end
56 end
56 end
57
57
58 # validate default value
58 # validate default value
59 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
59 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
60 v.custom_field.is_required = false
60 v.custom_field.is_required = false
61 errors.add(:default_value, :invalid) unless v.valid?
61 errors.add(:default_value, :invalid) unless v.valid?
62 end
62 end
63
63
64 def possible_values_options(obj=nil)
64 def possible_values_options(obj=nil)
65 case field_format
65 case field_format
66 when 'user', 'version'
66 when 'user', 'version'
67 if obj.respond_to?(:project) && obj.project
67 if obj.respond_to?(:project) && obj.project
68 case field_format
68 case field_format
69 when 'user'
69 when 'user'
70 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
70 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
71 when 'version'
71 when 'version'
72 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
72 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
73 end
73 end
74 elsif obj.is_a?(Array)
74 elsif obj.is_a?(Array)
75 obj.collect {|o| possible_values_options(o)}.inject {|memo, v| memo & v}
75 obj.collect {|o| possible_values_options(o)}.inject {|memo, v| memo & v}
76 else
76 else
77 []
77 []
78 end
78 end
79 else
79 else
80 read_attribute :possible_values
80 read_attribute :possible_values
81 end
81 end
82 end
82 end
83
83
84 def possible_values(obj=nil)
84 def possible_values(obj=nil)
85 case field_format
85 case field_format
86 when 'user', 'version'
86 when 'user', 'version'
87 possible_values_options(obj).collect(&:last)
87 possible_values_options(obj).collect(&:last)
88 else
88 else
89 read_attribute :possible_values
89 read_attribute :possible_values
90 end
90 end
91 end
91 end
92
92
93 # Makes possible_values accept a multiline string
93 # Makes possible_values accept a multiline string
94 def possible_values=(arg)
94 def possible_values=(arg)
95 if arg.is_a?(Array)
95 if arg.is_a?(Array)
96 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
96 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
97 else
97 else
98 self.possible_values = arg.to_s.split(/[\n\r]+/)
98 self.possible_values = arg.to_s.split(/[\n\r]+/)
99 end
99 end
100 end
100 end
101
101
102 def cast_value(value)
102 def cast_value(value)
103 casted = nil
103 casted = nil
104 unless value.blank?
104 unless value.blank?
105 case field_format
105 case field_format
106 when 'string', 'text', 'list'
106 when 'string', 'text', 'list'
107 casted = value
107 casted = value
108 when 'date'
108 when 'date'
109 casted = begin; value.to_date; rescue; nil end
109 casted = begin; value.to_date; rescue; nil end
110 when 'bool'
110 when 'bool'
111 casted = (value == '1' ? true : false)
111 casted = (value == '1' ? true : false)
112 when 'int'
112 when 'int'
113 casted = value.to_i
113 casted = value.to_i
114 when 'float'
114 when 'float'
115 casted = value.to_f
115 casted = value.to_f
116 when 'user', 'version'
116 when 'user', 'version'
117 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
117 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
118 end
118 end
119 end
119 end
120 casted
120 casted
121 end
121 end
122
122
123 # Returns a ORDER BY clause that can used to sort customized
123 # Returns a ORDER BY clause that can used to sort customized
124 # objects by their value of the custom field.
124 # objects by their value of the custom field.
125 # Returns false, if the custom field can not be used for sorting.
125 # Returns false, if the custom field can not be used for sorting.
126 def order_statement
126 def order_statement
127 case field_format
127 case field_format
128 when 'string', 'text', 'list', 'date', 'bool'
128 when 'string', 'text', 'list', 'date', 'bool'
129 # COALESCE is here to make sure that blank and NULL values are sorted equally
129 # COALESCE is here to make sure that blank and NULL values are sorted equally
130 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
130 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
131 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
131 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
132 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
132 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
133 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
133 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
134 when 'int', 'float'
134 when 'int', 'float'
135 # Make the database cast values into numeric
135 # Make the database cast values into numeric
136 # Postgresql will raise an error if a value can not be casted!
136 # Postgresql will raise an error if a value can not be casted!
137 # CustomValue validations should ensure that it doesn't occur
137 # CustomValue validations should ensure that it doesn't occur
138 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
138 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
139 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
139 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
140 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
140 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
141 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
141 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
142 else
142 else
143 nil
143 nil
144 end
144 end
145 end
145 end
146
146
147 def <=>(field)
147 def <=>(field)
148 position <=> field.position
148 position <=> field.position
149 end
149 end
150
150
151 def self.customized_class
151 def self.customized_class
152 self.name =~ /^(.+)CustomField$/
152 self.name =~ /^(.+)CustomField$/
153 begin; $1.constantize; rescue nil; end
153 begin; $1.constantize; rescue nil; end
154 end
154 end
155
155
156 # to move in project_custom_field
156 # to move in project_custom_field
157 def self.for_all
157 def self.for_all
158 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
158 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
159 end
159 end
160
160
161 def type_name
161 def type_name
162 nil
162 nil
163 end
163 end
164 end
164 end
@@ -1,888 +1,888
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :destroy, :include => :author
46 has_many :news, :dependent => :destroy, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :dependent => :destroy
49 has_one :repository, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_nested_set :order => 'name', :dependent => :destroy
59 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
79 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
81
81
82 before_destroy :delete_all_members
82 before_destroy :delete_all_members
83
83
84 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] } }
84 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] } }
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
86 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
87 named_scope :all_public, { :conditions => { :is_public => true } }
87 named_scope :all_public, { :conditions => { :is_public => true } }
88 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
88 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
89 named_scope :like, lambda {|arg|
89 named_scope :like, lambda {|arg|
90 if arg.blank?
90 if arg.blank?
91 {}
91 {}
92 else
92 else
93 pattern = "%#{arg.to_s.strip.downcase}%"
93 pattern = "%#{arg.to_s.strip.downcase}%"
94 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
94 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
95 end
95 end
96 }
96 }
97
97
98 def initialize(attributes = nil)
98 def initialize(attributes=nil, *args)
99 super
99 super
100
100
101 initialized = (attributes || {}).stringify_keys
101 initialized = (attributes || {}).stringify_keys
102 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
102 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
103 self.identifier = Project.next_identifier
103 self.identifier = Project.next_identifier
104 end
104 end
105 if !initialized.key?('is_public')
105 if !initialized.key?('is_public')
106 self.is_public = Setting.default_projects_public?
106 self.is_public = Setting.default_projects_public?
107 end
107 end
108 if !initialized.key?('enabled_module_names')
108 if !initialized.key?('enabled_module_names')
109 self.enabled_module_names = Setting.default_projects_modules
109 self.enabled_module_names = Setting.default_projects_modules
110 end
110 end
111 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
111 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
112 self.trackers = Tracker.all
112 self.trackers = Tracker.all
113 end
113 end
114 end
114 end
115
115
116 def identifier=(identifier)
116 def identifier=(identifier)
117 super unless identifier_frozen?
117 super unless identifier_frozen?
118 end
118 end
119
119
120 def identifier_frozen?
120 def identifier_frozen?
121 errors[:identifier].nil? && !(new_record? || identifier.blank?)
121 errors[:identifier].nil? && !(new_record? || identifier.blank?)
122 end
122 end
123
123
124 # returns latest created projects
124 # returns latest created projects
125 # non public projects will be returned only if user is a member of those
125 # non public projects will be returned only if user is a member of those
126 def self.latest(user=nil, count=5)
126 def self.latest(user=nil, count=5)
127 visible(user).find(:all, :limit => count, :order => "created_on DESC")
127 visible(user).find(:all, :limit => count, :order => "created_on DESC")
128 end
128 end
129
129
130 # Returns true if the project is visible to +user+ or to the current user.
130 # Returns true if the project is visible to +user+ or to the current user.
131 def visible?(user=User.current)
131 def visible?(user=User.current)
132 user.allowed_to?(:view_project, self)
132 user.allowed_to?(:view_project, self)
133 end
133 end
134
134
135 # Returns a SQL conditions string used to find all projects visible by the specified user.
135 # Returns a SQL conditions string used to find all projects visible by the specified user.
136 #
136 #
137 # Examples:
137 # Examples:
138 # Project.visible_condition(admin) => "projects.status = 1"
138 # Project.visible_condition(admin) => "projects.status = 1"
139 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
139 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
140 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
140 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
141 def self.visible_condition(user, options={})
141 def self.visible_condition(user, options={})
142 allowed_to_condition(user, :view_project, options)
142 allowed_to_condition(user, :view_project, options)
143 end
143 end
144
144
145 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
145 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
146 #
146 #
147 # Valid options:
147 # Valid options:
148 # * :project => limit the condition to project
148 # * :project => limit the condition to project
149 # * :with_subprojects => limit the condition to project and its subprojects
149 # * :with_subprojects => limit the condition to project and its subprojects
150 # * :member => limit the condition to the user projects
150 # * :member => limit the condition to the user projects
151 def self.allowed_to_condition(user, permission, options={})
151 def self.allowed_to_condition(user, permission, options={})
152 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
152 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
153 if perm = Redmine::AccessControl.permission(permission)
153 if perm = Redmine::AccessControl.permission(permission)
154 unless perm.project_module.nil?
154 unless perm.project_module.nil?
155 # If the permission belongs to a project module, make sure the module is enabled
155 # If the permission belongs to a project module, make sure the module is enabled
156 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
156 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
157 end
157 end
158 end
158 end
159 if options[:project]
159 if options[:project]
160 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
160 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
161 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
161 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
162 base_statement = "(#{project_statement}) AND (#{base_statement})"
162 base_statement = "(#{project_statement}) AND (#{base_statement})"
163 end
163 end
164
164
165 if user.admin?
165 if user.admin?
166 base_statement
166 base_statement
167 else
167 else
168 statement_by_role = {}
168 statement_by_role = {}
169 unless options[:member]
169 unless options[:member]
170 role = user.logged? ? Role.non_member : Role.anonymous
170 role = user.logged? ? Role.non_member : Role.anonymous
171 if role.allowed_to?(permission)
171 if role.allowed_to?(permission)
172 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
172 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
173 end
173 end
174 end
174 end
175 if user.logged?
175 if user.logged?
176 user.projects_by_role.each do |role, projects|
176 user.projects_by_role.each do |role, projects|
177 if role.allowed_to?(permission)
177 if role.allowed_to?(permission)
178 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
178 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
179 end
179 end
180 end
180 end
181 end
181 end
182 if statement_by_role.empty?
182 if statement_by_role.empty?
183 "1=0"
183 "1=0"
184 else
184 else
185 if block_given?
185 if block_given?
186 statement_by_role.each do |role, statement|
186 statement_by_role.each do |role, statement|
187 if s = yield(role, user)
187 if s = yield(role, user)
188 statement_by_role[role] = "(#{statement} AND (#{s}))"
188 statement_by_role[role] = "(#{statement} AND (#{s}))"
189 end
189 end
190 end
190 end
191 end
191 end
192 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
192 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
193 end
193 end
194 end
194 end
195 end
195 end
196
196
197 # Returns the Systemwide and project specific activities
197 # Returns the Systemwide and project specific activities
198 def activities(include_inactive=false)
198 def activities(include_inactive=false)
199 if include_inactive
199 if include_inactive
200 return all_activities
200 return all_activities
201 else
201 else
202 return active_activities
202 return active_activities
203 end
203 end
204 end
204 end
205
205
206 # Will create a new Project specific Activity or update an existing one
206 # Will create a new Project specific Activity or update an existing one
207 #
207 #
208 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
208 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
209 # does not successfully save.
209 # does not successfully save.
210 def update_or_create_time_entry_activity(id, activity_hash)
210 def update_or_create_time_entry_activity(id, activity_hash)
211 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
211 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
212 self.create_time_entry_activity_if_needed(activity_hash)
212 self.create_time_entry_activity_if_needed(activity_hash)
213 else
213 else
214 activity = project.time_entry_activities.find_by_id(id.to_i)
214 activity = project.time_entry_activities.find_by_id(id.to_i)
215 activity.update_attributes(activity_hash) if activity
215 activity.update_attributes(activity_hash) if activity
216 end
216 end
217 end
217 end
218
218
219 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
219 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
220 #
220 #
221 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
221 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
222 # does not successfully save.
222 # does not successfully save.
223 def create_time_entry_activity_if_needed(activity)
223 def create_time_entry_activity_if_needed(activity)
224 if activity['parent_id']
224 if activity['parent_id']
225
225
226 parent_activity = TimeEntryActivity.find(activity['parent_id'])
226 parent_activity = TimeEntryActivity.find(activity['parent_id'])
227 activity['name'] = parent_activity.name
227 activity['name'] = parent_activity.name
228 activity['position'] = parent_activity.position
228 activity['position'] = parent_activity.position
229
229
230 if Enumeration.overridding_change?(activity, parent_activity)
230 if Enumeration.overridding_change?(activity, parent_activity)
231 project_activity = self.time_entry_activities.create(activity)
231 project_activity = self.time_entry_activities.create(activity)
232
232
233 if project_activity.new_record?
233 if project_activity.new_record?
234 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
234 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
235 else
235 else
236 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
236 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
237 end
237 end
238 end
238 end
239 end
239 end
240 end
240 end
241
241
242 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
242 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
243 #
243 #
244 # Examples:
244 # Examples:
245 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
245 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
246 # project.project_condition(false) => "projects.id = 1"
246 # project.project_condition(false) => "projects.id = 1"
247 def project_condition(with_subprojects)
247 def project_condition(with_subprojects)
248 cond = "#{Project.table_name}.id = #{id}"
248 cond = "#{Project.table_name}.id = #{id}"
249 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
249 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
250 cond
250 cond
251 end
251 end
252
252
253 def self.find(*args)
253 def self.find(*args)
254 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
254 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
255 project = find_by_identifier(*args)
255 project = find_by_identifier(*args)
256 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
256 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
257 project
257 project
258 else
258 else
259 super
259 super
260 end
260 end
261 end
261 end
262
262
263 def to_param
263 def to_param
264 # id is used for projects with a numeric identifier (compatibility)
264 # id is used for projects with a numeric identifier (compatibility)
265 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
265 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
266 end
266 end
267
267
268 def active?
268 def active?
269 self.status == STATUS_ACTIVE
269 self.status == STATUS_ACTIVE
270 end
270 end
271
271
272 def archived?
272 def archived?
273 self.status == STATUS_ARCHIVED
273 self.status == STATUS_ARCHIVED
274 end
274 end
275
275
276 # Archives the project and its descendants
276 # Archives the project and its descendants
277 def archive
277 def archive
278 # Check that there is no issue of a non descendant project that is assigned
278 # Check that there is no issue of a non descendant project that is assigned
279 # to one of the project or descendant versions
279 # to one of the project or descendant versions
280 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
280 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
281 if v_ids.any? && Issue.find(:first, :include => :project,
281 if v_ids.any? && Issue.find(:first, :include => :project,
282 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
282 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
283 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
283 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
284 return false
284 return false
285 end
285 end
286 Project.transaction do
286 Project.transaction do
287 archive!
287 archive!
288 end
288 end
289 true
289 true
290 end
290 end
291
291
292 # Unarchives the project
292 # Unarchives the project
293 # All its ancestors must be active
293 # All its ancestors must be active
294 def unarchive
294 def unarchive
295 return false if ancestors.detect {|a| !a.active?}
295 return false if ancestors.detect {|a| !a.active?}
296 update_attribute :status, STATUS_ACTIVE
296 update_attribute :status, STATUS_ACTIVE
297 end
297 end
298
298
299 # Returns an array of projects the project can be moved to
299 # Returns an array of projects the project can be moved to
300 # by the current user
300 # by the current user
301 def allowed_parents
301 def allowed_parents
302 return @allowed_parents if @allowed_parents
302 return @allowed_parents if @allowed_parents
303 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
303 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
304 @allowed_parents = @allowed_parents - self_and_descendants
304 @allowed_parents = @allowed_parents - self_and_descendants
305 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
305 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
306 @allowed_parents << nil
306 @allowed_parents << nil
307 end
307 end
308 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
308 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
309 @allowed_parents << parent
309 @allowed_parents << parent
310 end
310 end
311 @allowed_parents
311 @allowed_parents
312 end
312 end
313
313
314 # Sets the parent of the project with authorization check
314 # Sets the parent of the project with authorization check
315 def set_allowed_parent!(p)
315 def set_allowed_parent!(p)
316 unless p.nil? || p.is_a?(Project)
316 unless p.nil? || p.is_a?(Project)
317 if p.to_s.blank?
317 if p.to_s.blank?
318 p = nil
318 p = nil
319 else
319 else
320 p = Project.find_by_id(p)
320 p = Project.find_by_id(p)
321 return false unless p
321 return false unless p
322 end
322 end
323 end
323 end
324 if p.nil?
324 if p.nil?
325 if !new_record? && allowed_parents.empty?
325 if !new_record? && allowed_parents.empty?
326 return false
326 return false
327 end
327 end
328 elsif !allowed_parents.include?(p)
328 elsif !allowed_parents.include?(p)
329 return false
329 return false
330 end
330 end
331 set_parent!(p)
331 set_parent!(p)
332 end
332 end
333
333
334 # Sets the parent of the project
334 # Sets the parent of the project
335 # Argument can be either a Project, a String, a Fixnum or nil
335 # Argument can be either a Project, a String, a Fixnum or nil
336 def set_parent!(p)
336 def set_parent!(p)
337 unless p.nil? || p.is_a?(Project)
337 unless p.nil? || p.is_a?(Project)
338 if p.to_s.blank?
338 if p.to_s.blank?
339 p = nil
339 p = nil
340 else
340 else
341 p = Project.find_by_id(p)
341 p = Project.find_by_id(p)
342 return false unless p
342 return false unless p
343 end
343 end
344 end
344 end
345 if p == parent && !p.nil?
345 if p == parent && !p.nil?
346 # Nothing to do
346 # Nothing to do
347 true
347 true
348 elsif p.nil? || (p.active? && move_possible?(p))
348 elsif p.nil? || (p.active? && move_possible?(p))
349 # Insert the project so that target's children or root projects stay alphabetically sorted
349 # Insert the project so that target's children or root projects stay alphabetically sorted
350 sibs = (p.nil? ? self.class.roots : p.children)
350 sibs = (p.nil? ? self.class.roots : p.children)
351 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
351 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
352 if to_be_inserted_before
352 if to_be_inserted_before
353 move_to_left_of(to_be_inserted_before)
353 move_to_left_of(to_be_inserted_before)
354 elsif p.nil?
354 elsif p.nil?
355 if sibs.empty?
355 if sibs.empty?
356 # move_to_root adds the project in first (ie. left) position
356 # move_to_root adds the project in first (ie. left) position
357 move_to_root
357 move_to_root
358 else
358 else
359 move_to_right_of(sibs.last) unless self == sibs.last
359 move_to_right_of(sibs.last) unless self == sibs.last
360 end
360 end
361 else
361 else
362 # move_to_child_of adds the project in last (ie.right) position
362 # move_to_child_of adds the project in last (ie.right) position
363 move_to_child_of(p)
363 move_to_child_of(p)
364 end
364 end
365 Issue.update_versions_from_hierarchy_change(self)
365 Issue.update_versions_from_hierarchy_change(self)
366 true
366 true
367 else
367 else
368 # Can not move to the given target
368 # Can not move to the given target
369 false
369 false
370 end
370 end
371 end
371 end
372
372
373 # Returns an array of the trackers used by the project and its active sub projects
373 # Returns an array of the trackers used by the project and its active sub projects
374 def rolled_up_trackers
374 def rolled_up_trackers
375 @rolled_up_trackers ||=
375 @rolled_up_trackers ||=
376 Tracker.find(:all, :joins => :projects,
376 Tracker.find(:all, :joins => :projects,
377 :select => "DISTINCT #{Tracker.table_name}.*",
377 :select => "DISTINCT #{Tracker.table_name}.*",
378 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
378 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
379 :order => "#{Tracker.table_name}.position")
379 :order => "#{Tracker.table_name}.position")
380 end
380 end
381
381
382 # Closes open and locked project versions that are completed
382 # Closes open and locked project versions that are completed
383 def close_completed_versions
383 def close_completed_versions
384 Version.transaction do
384 Version.transaction do
385 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
385 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
386 if version.completed?
386 if version.completed?
387 version.update_attribute(:status, 'closed')
387 version.update_attribute(:status, 'closed')
388 end
388 end
389 end
389 end
390 end
390 end
391 end
391 end
392
392
393 # Returns a scope of the Versions on subprojects
393 # Returns a scope of the Versions on subprojects
394 def rolled_up_versions
394 def rolled_up_versions
395 @rolled_up_versions ||=
395 @rolled_up_versions ||=
396 Version.scoped(:include => :project,
396 Version.scoped(:include => :project,
397 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
397 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
398 end
398 end
399
399
400 # Returns a scope of the Versions used by the project
400 # Returns a scope of the Versions used by the project
401 def shared_versions
401 def shared_versions
402 @shared_versions ||= begin
402 @shared_versions ||= begin
403 r = root? ? self : root
403 r = root? ? self : root
404 Version.scoped(:include => :project,
404 Version.scoped(:include => :project,
405 :conditions => "#{Project.table_name}.id = #{id}" +
405 :conditions => "#{Project.table_name}.id = #{id}" +
406 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
406 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
407 " #{Version.table_name}.sharing = 'system'" +
407 " #{Version.table_name}.sharing = 'system'" +
408 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
408 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
409 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
409 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
410 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
410 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
411 "))")
411 "))")
412 end
412 end
413 end
413 end
414
414
415 # Returns a hash of project users grouped by role
415 # Returns a hash of project users grouped by role
416 def users_by_role
416 def users_by_role
417 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
417 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
418 m.roles.each do |r|
418 m.roles.each do |r|
419 h[r] ||= []
419 h[r] ||= []
420 h[r] << m.user
420 h[r] << m.user
421 end
421 end
422 h
422 h
423 end
423 end
424 end
424 end
425
425
426 # Deletes all project's members
426 # Deletes all project's members
427 def delete_all_members
427 def delete_all_members
428 me, mr = Member.table_name, MemberRole.table_name
428 me, mr = Member.table_name, MemberRole.table_name
429 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
429 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
430 Member.delete_all(['project_id = ?', id])
430 Member.delete_all(['project_id = ?', id])
431 end
431 end
432
432
433 # Users/groups issues can be assigned to
433 # Users/groups issues can be assigned to
434 def assignable_users
434 def assignable_users
435 assignable = Setting.issue_group_assignment? ? member_principals : members
435 assignable = Setting.issue_group_assignment? ? member_principals : members
436 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
436 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
437 end
437 end
438
438
439 # Returns the mail adresses of users that should be always notified on project events
439 # Returns the mail adresses of users that should be always notified on project events
440 def recipients
440 def recipients
441 notified_users.collect {|user| user.mail}
441 notified_users.collect {|user| user.mail}
442 end
442 end
443
443
444 # Returns the users that should be notified on project events
444 # Returns the users that should be notified on project events
445 def notified_users
445 def notified_users
446 # TODO: User part should be extracted to User#notify_about?
446 # TODO: User part should be extracted to User#notify_about?
447 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
447 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
448 end
448 end
449
449
450 # Returns an array of all custom fields enabled for project issues
450 # Returns an array of all custom fields enabled for project issues
451 # (explictly associated custom fields and custom fields enabled for all projects)
451 # (explictly associated custom fields and custom fields enabled for all projects)
452 def all_issue_custom_fields
452 def all_issue_custom_fields
453 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
453 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
454 end
454 end
455
455
456 # Returns an array of all custom fields enabled for project time entries
456 # Returns an array of all custom fields enabled for project time entries
457 # (explictly associated custom fields and custom fields enabled for all projects)
457 # (explictly associated custom fields and custom fields enabled for all projects)
458 def all_time_entry_custom_fields
458 def all_time_entry_custom_fields
459 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
459 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
460 end
460 end
461
461
462 def project
462 def project
463 self
463 self
464 end
464 end
465
465
466 def <=>(project)
466 def <=>(project)
467 name.downcase <=> project.name.downcase
467 name.downcase <=> project.name.downcase
468 end
468 end
469
469
470 def to_s
470 def to_s
471 name
471 name
472 end
472 end
473
473
474 # Returns a short description of the projects (first lines)
474 # Returns a short description of the projects (first lines)
475 def short_description(length = 255)
475 def short_description(length = 255)
476 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
476 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
477 end
477 end
478
478
479 def css_classes
479 def css_classes
480 s = 'project'
480 s = 'project'
481 s << ' root' if root?
481 s << ' root' if root?
482 s << ' child' if child?
482 s << ' child' if child?
483 s << (leaf? ? ' leaf' : ' parent')
483 s << (leaf? ? ' leaf' : ' parent')
484 s
484 s
485 end
485 end
486
486
487 # The earliest start date of a project, based on it's issues and versions
487 # The earliest start date of a project, based on it's issues and versions
488 def start_date
488 def start_date
489 [
489 [
490 issues.minimum('start_date'),
490 issues.minimum('start_date'),
491 shared_versions.collect(&:effective_date),
491 shared_versions.collect(&:effective_date),
492 shared_versions.collect(&:start_date)
492 shared_versions.collect(&:start_date)
493 ].flatten.compact.min
493 ].flatten.compact.min
494 end
494 end
495
495
496 # The latest due date of an issue or version
496 # The latest due date of an issue or version
497 def due_date
497 def due_date
498 [
498 [
499 issues.maximum('due_date'),
499 issues.maximum('due_date'),
500 shared_versions.collect(&:effective_date),
500 shared_versions.collect(&:effective_date),
501 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
501 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
502 ].flatten.compact.max
502 ].flatten.compact.max
503 end
503 end
504
504
505 def overdue?
505 def overdue?
506 active? && !due_date.nil? && (due_date < Date.today)
506 active? && !due_date.nil? && (due_date < Date.today)
507 end
507 end
508
508
509 # Returns the percent completed for this project, based on the
509 # Returns the percent completed for this project, based on the
510 # progress on it's versions.
510 # progress on it's versions.
511 def completed_percent(options={:include_subprojects => false})
511 def completed_percent(options={:include_subprojects => false})
512 if options.delete(:include_subprojects)
512 if options.delete(:include_subprojects)
513 total = self_and_descendants.collect(&:completed_percent).sum
513 total = self_and_descendants.collect(&:completed_percent).sum
514
514
515 total / self_and_descendants.count
515 total / self_and_descendants.count
516 else
516 else
517 if versions.count > 0
517 if versions.count > 0
518 total = versions.collect(&:completed_pourcent).sum
518 total = versions.collect(&:completed_pourcent).sum
519
519
520 total / versions.count
520 total / versions.count
521 else
521 else
522 100
522 100
523 end
523 end
524 end
524 end
525 end
525 end
526
526
527 # Return true if this project is allowed to do the specified action.
527 # Return true if this project is allowed to do the specified action.
528 # action can be:
528 # action can be:
529 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
529 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
530 # * a permission Symbol (eg. :edit_project)
530 # * a permission Symbol (eg. :edit_project)
531 def allows_to?(action)
531 def allows_to?(action)
532 if action.is_a? Hash
532 if action.is_a? Hash
533 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
533 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
534 else
534 else
535 allowed_permissions.include? action
535 allowed_permissions.include? action
536 end
536 end
537 end
537 end
538
538
539 def module_enabled?(module_name)
539 def module_enabled?(module_name)
540 module_name = module_name.to_s
540 module_name = module_name.to_s
541 enabled_modules.detect {|m| m.name == module_name}
541 enabled_modules.detect {|m| m.name == module_name}
542 end
542 end
543
543
544 def enabled_module_names=(module_names)
544 def enabled_module_names=(module_names)
545 if module_names && module_names.is_a?(Array)
545 if module_names && module_names.is_a?(Array)
546 module_names = module_names.collect(&:to_s).reject(&:blank?)
546 module_names = module_names.collect(&:to_s).reject(&:blank?)
547 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
547 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
548 else
548 else
549 enabled_modules.clear
549 enabled_modules.clear
550 end
550 end
551 end
551 end
552
552
553 # Returns an array of the enabled modules names
553 # Returns an array of the enabled modules names
554 def enabled_module_names
554 def enabled_module_names
555 enabled_modules.collect(&:name)
555 enabled_modules.collect(&:name)
556 end
556 end
557
557
558 # Enable a specific module
558 # Enable a specific module
559 #
559 #
560 # Examples:
560 # Examples:
561 # project.enable_module!(:issue_tracking)
561 # project.enable_module!(:issue_tracking)
562 # project.enable_module!("issue_tracking")
562 # project.enable_module!("issue_tracking")
563 def enable_module!(name)
563 def enable_module!(name)
564 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
564 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
565 end
565 end
566
566
567 # Disable a module if it exists
567 # Disable a module if it exists
568 #
568 #
569 # Examples:
569 # Examples:
570 # project.disable_module!(:issue_tracking)
570 # project.disable_module!(:issue_tracking)
571 # project.disable_module!("issue_tracking")
571 # project.disable_module!("issue_tracking")
572 # project.disable_module!(project.enabled_modules.first)
572 # project.disable_module!(project.enabled_modules.first)
573 def disable_module!(target)
573 def disable_module!(target)
574 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
574 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
575 target.destroy unless target.blank?
575 target.destroy unless target.blank?
576 end
576 end
577
577
578 safe_attributes 'name',
578 safe_attributes 'name',
579 'description',
579 'description',
580 'homepage',
580 'homepage',
581 'is_public',
581 'is_public',
582 'identifier',
582 'identifier',
583 'custom_field_values',
583 'custom_field_values',
584 'custom_fields',
584 'custom_fields',
585 'tracker_ids',
585 'tracker_ids',
586 'issue_custom_field_ids'
586 'issue_custom_field_ids'
587
587
588 safe_attributes 'enabled_module_names',
588 safe_attributes 'enabled_module_names',
589 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
589 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
590
590
591 # Returns an array of projects that are in this project's hierarchy
591 # Returns an array of projects that are in this project's hierarchy
592 #
592 #
593 # Example: parents, children, siblings
593 # Example: parents, children, siblings
594 def hierarchy
594 def hierarchy
595 parents = project.self_and_ancestors || []
595 parents = project.self_and_ancestors || []
596 descendants = project.descendants || []
596 descendants = project.descendants || []
597 project_hierarchy = parents | descendants # Set union
597 project_hierarchy = parents | descendants # Set union
598 end
598 end
599
599
600 # Returns an auto-generated project identifier based on the last identifier used
600 # Returns an auto-generated project identifier based on the last identifier used
601 def self.next_identifier
601 def self.next_identifier
602 p = Project.find(:first, :order => 'created_on DESC')
602 p = Project.find(:first, :order => 'created_on DESC')
603 p.nil? ? nil : p.identifier.to_s.succ
603 p.nil? ? nil : p.identifier.to_s.succ
604 end
604 end
605
605
606 # Copies and saves the Project instance based on the +project+.
606 # Copies and saves the Project instance based on the +project+.
607 # Duplicates the source project's:
607 # Duplicates the source project's:
608 # * Wiki
608 # * Wiki
609 # * Versions
609 # * Versions
610 # * Categories
610 # * Categories
611 # * Issues
611 # * Issues
612 # * Members
612 # * Members
613 # * Queries
613 # * Queries
614 #
614 #
615 # Accepts an +options+ argument to specify what to copy
615 # Accepts an +options+ argument to specify what to copy
616 #
616 #
617 # Examples:
617 # Examples:
618 # project.copy(1) # => copies everything
618 # project.copy(1) # => copies everything
619 # project.copy(1, :only => 'members') # => copies members only
619 # project.copy(1, :only => 'members') # => copies members only
620 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
620 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
621 def copy(project, options={})
621 def copy(project, options={})
622 project = project.is_a?(Project) ? project : Project.find(project)
622 project = project.is_a?(Project) ? project : Project.find(project)
623
623
624 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
624 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
625 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
625 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
626
626
627 Project.transaction do
627 Project.transaction do
628 if save
628 if save
629 reload
629 reload
630 to_be_copied.each do |name|
630 to_be_copied.each do |name|
631 send "copy_#{name}", project
631 send "copy_#{name}", project
632 end
632 end
633 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
633 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
634 save
634 save
635 end
635 end
636 end
636 end
637 end
637 end
638
638
639
639
640 # Copies +project+ and returns the new instance. This will not save
640 # Copies +project+ and returns the new instance. This will not save
641 # the copy
641 # the copy
642 def self.copy_from(project)
642 def self.copy_from(project)
643 begin
643 begin
644 project = project.is_a?(Project) ? project : Project.find(project)
644 project = project.is_a?(Project) ? project : Project.find(project)
645 if project
645 if project
646 # clear unique attributes
646 # clear unique attributes
647 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
647 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
648 copy = Project.new(attributes)
648 copy = Project.new(attributes)
649 copy.enabled_modules = project.enabled_modules
649 copy.enabled_modules = project.enabled_modules
650 copy.trackers = project.trackers
650 copy.trackers = project.trackers
651 copy.custom_values = project.custom_values.collect {|v| v.clone}
651 copy.custom_values = project.custom_values.collect {|v| v.clone}
652 copy.issue_custom_fields = project.issue_custom_fields
652 copy.issue_custom_fields = project.issue_custom_fields
653 return copy
653 return copy
654 else
654 else
655 return nil
655 return nil
656 end
656 end
657 rescue ActiveRecord::RecordNotFound
657 rescue ActiveRecord::RecordNotFound
658 return nil
658 return nil
659 end
659 end
660 end
660 end
661
661
662 # Yields the given block for each project with its level in the tree
662 # Yields the given block for each project with its level in the tree
663 def self.project_tree(projects, &block)
663 def self.project_tree(projects, &block)
664 ancestors = []
664 ancestors = []
665 projects.sort_by(&:lft).each do |project|
665 projects.sort_by(&:lft).each do |project|
666 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
666 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
667 ancestors.pop
667 ancestors.pop
668 end
668 end
669 yield project, ancestors.size
669 yield project, ancestors.size
670 ancestors << project
670 ancestors << project
671 end
671 end
672 end
672 end
673
673
674 private
674 private
675
675
676 # Copies wiki from +project+
676 # Copies wiki from +project+
677 def copy_wiki(project)
677 def copy_wiki(project)
678 # Check that the source project has a wiki first
678 # Check that the source project has a wiki first
679 unless project.wiki.nil?
679 unless project.wiki.nil?
680 self.wiki ||= Wiki.new
680 self.wiki ||= Wiki.new
681 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
681 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
682 wiki_pages_map = {}
682 wiki_pages_map = {}
683 project.wiki.pages.each do |page|
683 project.wiki.pages.each do |page|
684 # Skip pages without content
684 # Skip pages without content
685 next if page.content.nil?
685 next if page.content.nil?
686 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
686 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
687 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
687 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
688 new_wiki_page.content = new_wiki_content
688 new_wiki_page.content = new_wiki_content
689 wiki.pages << new_wiki_page
689 wiki.pages << new_wiki_page
690 wiki_pages_map[page.id] = new_wiki_page
690 wiki_pages_map[page.id] = new_wiki_page
691 end
691 end
692 wiki.save
692 wiki.save
693 # Reproduce page hierarchy
693 # Reproduce page hierarchy
694 project.wiki.pages.each do |page|
694 project.wiki.pages.each do |page|
695 if page.parent_id && wiki_pages_map[page.id]
695 if page.parent_id && wiki_pages_map[page.id]
696 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
696 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
697 wiki_pages_map[page.id].save
697 wiki_pages_map[page.id].save
698 end
698 end
699 end
699 end
700 end
700 end
701 end
701 end
702
702
703 # Copies versions from +project+
703 # Copies versions from +project+
704 def copy_versions(project)
704 def copy_versions(project)
705 project.versions.each do |version|
705 project.versions.each do |version|
706 new_version = Version.new
706 new_version = Version.new
707 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
707 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
708 self.versions << new_version
708 self.versions << new_version
709 end
709 end
710 end
710 end
711
711
712 # Copies issue categories from +project+
712 # Copies issue categories from +project+
713 def copy_issue_categories(project)
713 def copy_issue_categories(project)
714 project.issue_categories.each do |issue_category|
714 project.issue_categories.each do |issue_category|
715 new_issue_category = IssueCategory.new
715 new_issue_category = IssueCategory.new
716 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
716 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
717 self.issue_categories << new_issue_category
717 self.issue_categories << new_issue_category
718 end
718 end
719 end
719 end
720
720
721 # Copies issues from +project+
721 # Copies issues from +project+
722 # Note: issues assigned to a closed version won't be copied due to validation rules
722 # Note: issues assigned to a closed version won't be copied due to validation rules
723 def copy_issues(project)
723 def copy_issues(project)
724 # Stores the source issue id as a key and the copied issues as the
724 # Stores the source issue id as a key and the copied issues as the
725 # value. Used to map the two togeather for issue relations.
725 # value. Used to map the two togeather for issue relations.
726 issues_map = {}
726 issues_map = {}
727
727
728 # Get issues sorted by root_id, lft so that parent issues
728 # Get issues sorted by root_id, lft so that parent issues
729 # get copied before their children
729 # get copied before their children
730 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
730 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
731 new_issue = Issue.new
731 new_issue = Issue.new
732 new_issue.copy_from(issue)
732 new_issue.copy_from(issue)
733 new_issue.project = self
733 new_issue.project = self
734 # Reassign fixed_versions by name, since names are unique per
734 # Reassign fixed_versions by name, since names are unique per
735 # project and the versions for self are not yet saved
735 # project and the versions for self are not yet saved
736 if issue.fixed_version
736 if issue.fixed_version
737 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
737 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
738 end
738 end
739 # Reassign the category by name, since names are unique per
739 # Reassign the category by name, since names are unique per
740 # project and the categories for self are not yet saved
740 # project and the categories for self are not yet saved
741 if issue.category
741 if issue.category
742 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
742 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
743 end
743 end
744 # Parent issue
744 # Parent issue
745 if issue.parent_id
745 if issue.parent_id
746 if copied_parent = issues_map[issue.parent_id]
746 if copied_parent = issues_map[issue.parent_id]
747 new_issue.parent_issue_id = copied_parent.id
747 new_issue.parent_issue_id = copied_parent.id
748 end
748 end
749 end
749 end
750
750
751 self.issues << new_issue
751 self.issues << new_issue
752 if new_issue.new_record?
752 if new_issue.new_record?
753 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
753 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
754 else
754 else
755 issues_map[issue.id] = new_issue unless new_issue.new_record?
755 issues_map[issue.id] = new_issue unless new_issue.new_record?
756 end
756 end
757 end
757 end
758
758
759 # Relations after in case issues related each other
759 # Relations after in case issues related each other
760 project.issues.each do |issue|
760 project.issues.each do |issue|
761 new_issue = issues_map[issue.id]
761 new_issue = issues_map[issue.id]
762 unless new_issue
762 unless new_issue
763 # Issue was not copied
763 # Issue was not copied
764 next
764 next
765 end
765 end
766
766
767 # Relations
767 # Relations
768 issue.relations_from.each do |source_relation|
768 issue.relations_from.each do |source_relation|
769 new_issue_relation = IssueRelation.new
769 new_issue_relation = IssueRelation.new
770 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
770 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
771 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
771 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
772 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
772 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
773 new_issue_relation.issue_to = source_relation.issue_to
773 new_issue_relation.issue_to = source_relation.issue_to
774 end
774 end
775 new_issue.relations_from << new_issue_relation
775 new_issue.relations_from << new_issue_relation
776 end
776 end
777
777
778 issue.relations_to.each do |source_relation|
778 issue.relations_to.each do |source_relation|
779 new_issue_relation = IssueRelation.new
779 new_issue_relation = IssueRelation.new
780 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
780 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
781 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
781 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
782 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
782 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
783 new_issue_relation.issue_from = source_relation.issue_from
783 new_issue_relation.issue_from = source_relation.issue_from
784 end
784 end
785 new_issue.relations_to << new_issue_relation
785 new_issue.relations_to << new_issue_relation
786 end
786 end
787 end
787 end
788 end
788 end
789
789
790 # Copies members from +project+
790 # Copies members from +project+
791 def copy_members(project)
791 def copy_members(project)
792 # Copy users first, then groups to handle members with inherited and given roles
792 # Copy users first, then groups to handle members with inherited and given roles
793 members_to_copy = []
793 members_to_copy = []
794 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
794 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
795 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
795 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
796
796
797 members_to_copy.each do |member|
797 members_to_copy.each do |member|
798 new_member = Member.new
798 new_member = Member.new
799 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
799 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
800 # only copy non inherited roles
800 # only copy non inherited roles
801 # inherited roles will be added when copying the group membership
801 # inherited roles will be added when copying the group membership
802 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
802 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
803 next if role_ids.empty?
803 next if role_ids.empty?
804 new_member.role_ids = role_ids
804 new_member.role_ids = role_ids
805 new_member.project = self
805 new_member.project = self
806 self.members << new_member
806 self.members << new_member
807 end
807 end
808 end
808 end
809
809
810 # Copies queries from +project+
810 # Copies queries from +project+
811 def copy_queries(project)
811 def copy_queries(project)
812 project.queries.each do |query|
812 project.queries.each do |query|
813 new_query = Query.new
813 new_query = Query.new
814 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
814 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
815 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
815 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
816 new_query.project = self
816 new_query.project = self
817 new_query.user_id = query.user_id
817 new_query.user_id = query.user_id
818 self.queries << new_query
818 self.queries << new_query
819 end
819 end
820 end
820 end
821
821
822 # Copies boards from +project+
822 # Copies boards from +project+
823 def copy_boards(project)
823 def copy_boards(project)
824 project.boards.each do |board|
824 project.boards.each do |board|
825 new_board = Board.new
825 new_board = Board.new
826 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
826 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
827 new_board.project = self
827 new_board.project = self
828 self.boards << new_board
828 self.boards << new_board
829 end
829 end
830 end
830 end
831
831
832 def allowed_permissions
832 def allowed_permissions
833 @allowed_permissions ||= begin
833 @allowed_permissions ||= begin
834 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
834 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
835 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
835 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
836 end
836 end
837 end
837 end
838
838
839 def allowed_actions
839 def allowed_actions
840 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
840 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
841 end
841 end
842
842
843 # Returns all the active Systemwide and project specific activities
843 # Returns all the active Systemwide and project specific activities
844 def active_activities
844 def active_activities
845 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
845 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
846
846
847 if overridden_activity_ids.empty?
847 if overridden_activity_ids.empty?
848 return TimeEntryActivity.shared.active
848 return TimeEntryActivity.shared.active
849 else
849 else
850 return system_activities_and_project_overrides
850 return system_activities_and_project_overrides
851 end
851 end
852 end
852 end
853
853
854 # Returns all the Systemwide and project specific activities
854 # Returns all the Systemwide and project specific activities
855 # (inactive and active)
855 # (inactive and active)
856 def all_activities
856 def all_activities
857 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
857 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
858
858
859 if overridden_activity_ids.empty?
859 if overridden_activity_ids.empty?
860 return TimeEntryActivity.shared
860 return TimeEntryActivity.shared
861 else
861 else
862 return system_activities_and_project_overrides(true)
862 return system_activities_and_project_overrides(true)
863 end
863 end
864 end
864 end
865
865
866 # Returns the systemwide active activities merged with the project specific overrides
866 # Returns the systemwide active activities merged with the project specific overrides
867 def system_activities_and_project_overrides(include_inactive=false)
867 def system_activities_and_project_overrides(include_inactive=false)
868 if include_inactive
868 if include_inactive
869 return TimeEntryActivity.shared.
869 return TimeEntryActivity.shared.
870 find(:all,
870 find(:all,
871 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
871 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
872 self.time_entry_activities
872 self.time_entry_activities
873 else
873 else
874 return TimeEntryActivity.shared.active.
874 return TimeEntryActivity.shared.active.
875 find(:all,
875 find(:all,
876 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
876 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
877 self.time_entry_activities.active
877 self.time_entry_activities.active
878 end
878 end
879 end
879 end
880
880
881 # Archives subprojects recursively
881 # Archives subprojects recursively
882 def archive!
882 def archive!
883 children.each do |subproject|
883 children.each do |subproject|
884 subproject.send :archive!
884 subproject.send :archive!
885 end
885 end
886 update_attribute :status, STATUS_ARCHIVED
886 update_attribute :status, STATUS_ARCHIVED
887 end
887 end
888 end
888 end
@@ -1,819 +1,819
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
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.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !@sortable.nil?
39 !@sortable.nil?
40 end
40 end
41
41
42 def sortable
42 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 end
44 end
45
45
46 def value(issue)
46 def value(issue)
47 issue.send name
47 issue.send name
48 end
48 end
49
49
50 def css_classes
50 def css_classes
51 name
51 name
52 end
52 end
53 end
53 end
54
54
55 class QueryCustomFieldColumn < QueryColumn
55 class QueryCustomFieldColumn < QueryColumn
56
56
57 def initialize(custom_field)
57 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
58 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
59 self.sortable = custom_field.order_statement || false
60 if %w(list date bool int).include?(custom_field.field_format)
60 if %w(list date bool int).include?(custom_field.field_format)
61 self.groupable = custom_field.order_statement
61 self.groupable = custom_field.order_statement
62 end
62 end
63 self.groupable ||= false
63 self.groupable ||= false
64 @cf = custom_field
64 @cf = custom_field
65 end
65 end
66
66
67 def caption
67 def caption
68 @cf.name
68 @cf.name
69 end
69 end
70
70
71 def custom_field
71 def custom_field
72 @cf
72 @cf
73 end
73 end
74
74
75 def value(issue)
75 def value(issue)
76 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
76 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
77 cv && @cf.cast_value(cv.value)
77 cv && @cf.cast_value(cv.value)
78 end
78 end
79
79
80 def css_classes
80 def css_classes
81 @css_classes ||= "#{name} #{@cf.field_format}"
81 @css_classes ||= "#{name} #{@cf.field_format}"
82 end
82 end
83 end
83 end
84
84
85 class Query < ActiveRecord::Base
85 class Query < ActiveRecord::Base
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
87 end
87 end
88
88
89 belongs_to :project
89 belongs_to :project
90 belongs_to :user
90 belongs_to :user
91 serialize :filters
91 serialize :filters
92 serialize :column_names
92 serialize :column_names
93 serialize :sort_criteria, Array
93 serialize :sort_criteria, Array
94
94
95 attr_protected :project_id, :user_id
95 attr_protected :project_id, :user_id
96
96
97 validates_presence_of :name, :on => :save
97 validates_presence_of :name, :on => :save
98 validates_length_of :name, :maximum => 255
98 validates_length_of :name, :maximum => 255
99 validate :validate_query_filters
99 validate :validate_query_filters
100
100
101 @@operators = { "=" => :label_equals,
101 @@operators = { "=" => :label_equals,
102 "!" => :label_not_equals,
102 "!" => :label_not_equals,
103 "o" => :label_open_issues,
103 "o" => :label_open_issues,
104 "c" => :label_closed_issues,
104 "c" => :label_closed_issues,
105 "!*" => :label_none,
105 "!*" => :label_none,
106 "*" => :label_all,
106 "*" => :label_all,
107 ">=" => :label_greater_or_equal,
107 ">=" => :label_greater_or_equal,
108 "<=" => :label_less_or_equal,
108 "<=" => :label_less_or_equal,
109 "><" => :label_between,
109 "><" => :label_between,
110 "<t+" => :label_in_less_than,
110 "<t+" => :label_in_less_than,
111 ">t+" => :label_in_more_than,
111 ">t+" => :label_in_more_than,
112 "t+" => :label_in,
112 "t+" => :label_in,
113 "t" => :label_today,
113 "t" => :label_today,
114 "w" => :label_this_week,
114 "w" => :label_this_week,
115 ">t-" => :label_less_than_ago,
115 ">t-" => :label_less_than_ago,
116 "<t-" => :label_more_than_ago,
116 "<t-" => :label_more_than_ago,
117 "t-" => :label_ago,
117 "t-" => :label_ago,
118 "~" => :label_contains,
118 "~" => :label_contains,
119 "!~" => :label_not_contains }
119 "!~" => :label_not_contains }
120
120
121 cattr_reader :operators
121 cattr_reader :operators
122
122
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
126 :list_subprojects => [ "*", "!*", "=" ],
126 :list_subprojects => [ "*", "!*", "=" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
129 :string => [ "=", "~", "!", "!~" ],
129 :string => [ "=", "~", "!", "!~" ],
130 :text => [ "~", "!~" ],
130 :text => [ "~", "!~" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
133
133
134 cattr_reader :operators_by_filter_type
134 cattr_reader :operators_by_filter_type
135
135
136 @@available_columns = [
136 @@available_columns = [
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
153 ]
153 ]
154 cattr_reader :available_columns
154 cattr_reader :available_columns
155
155
156 named_scope :visible, lambda {|*args|
156 named_scope :visible, lambda {|*args|
157 user = args.shift || User.current
157 user = args.shift || User.current
158 base = Project.allowed_to_condition(user, :view_issues, *args)
158 base = Project.allowed_to_condition(user, :view_issues, *args)
159 user_id = user.logged? ? user.id : 0
159 user_id = user.logged? ? user.id : 0
160 {
160 {
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
162 :include => :project
162 :include => :project
163 }
163 }
164 }
164 }
165
165
166 def initialize(attributes = nil)
166 def initialize(attributes=nil, *args)
167 super attributes
167 super attributes
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
169 end
169 end
170
170
171 def after_initialize
171 def after_initialize
172 # Store the fact that project is nil (used in #editable_by?)
172 # Store the fact that project is nil (used in #editable_by?)
173 @is_for_all = project.nil?
173 @is_for_all = project.nil?
174 end
174 end
175
175
176 def validate_query_filters
176 def validate_query_filters
177 filters.each_key do |field|
177 filters.each_key do |field|
178 if values_for(field)
178 if values_for(field)
179 case type_for(field)
179 case type_for(field)
180 when :integer
180 when :integer
181 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
181 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
182 when :float
182 when :float
183 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
183 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
184 when :date, :date_past
184 when :date, :date_past
185 case operator_for(field)
185 case operator_for(field)
186 when "=", ">=", "<=", "><"
186 when "=", ">=", "<=", "><"
187 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
187 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
188 when ">t-", "<t-", "t-"
188 when ">t-", "<t-", "t-"
189 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
189 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
190 end
190 end
191 end
191 end
192 end
192 end
193
193
194 errors.add label_for(field), :blank unless
194 errors.add label_for(field), :blank unless
195 # filter requires one or more values
195 # filter requires one or more values
196 (values_for(field) and !values_for(field).first.blank?) or
196 (values_for(field) and !values_for(field).first.blank?) or
197 # filter doesn't require any value
197 # filter doesn't require any value
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
199 end if filters
199 end if filters
200 end
200 end
201
201
202 # Returns true if the query is visible to +user+ or the current user.
202 # Returns true if the query is visible to +user+ or the current user.
203 def visible?(user=User.current)
203 def visible?(user=User.current)
204 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
204 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
205 end
205 end
206
206
207 def editable_by?(user)
207 def editable_by?(user)
208 return false unless user
208 return false unless user
209 # Admin can edit them all and regular users can edit their private queries
209 # Admin can edit them all and regular users can edit their private queries
210 return true if user.admin? || (!is_public && self.user_id == user.id)
210 return true if user.admin? || (!is_public && self.user_id == user.id)
211 # Members can not edit public queries that are for all project (only admin is allowed to)
211 # Members can not edit public queries that are for all project (only admin is allowed to)
212 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
212 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
213 end
213 end
214
214
215 def available_filters
215 def available_filters
216 return @available_filters if @available_filters
216 return @available_filters if @available_filters
217
217
218 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
218 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
219
219
220 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
220 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
221 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
221 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
222 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
222 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
223 "subject" => { :type => :text, :order => 8 },
223 "subject" => { :type => :text, :order => 8 },
224 "created_on" => { :type => :date_past, :order => 9 },
224 "created_on" => { :type => :date_past, :order => 9 },
225 "updated_on" => { :type => :date_past, :order => 10 },
225 "updated_on" => { :type => :date_past, :order => 10 },
226 "start_date" => { :type => :date, :order => 11 },
226 "start_date" => { :type => :date, :order => 11 },
227 "due_date" => { :type => :date, :order => 12 },
227 "due_date" => { :type => :date, :order => 12 },
228 "estimated_hours" => { :type => :float, :order => 13 },
228 "estimated_hours" => { :type => :float, :order => 13 },
229 "done_ratio" => { :type => :integer, :order => 14 }}
229 "done_ratio" => { :type => :integer, :order => 14 }}
230
230
231 principals = []
231 principals = []
232 if project
232 if project
233 principals += project.principals.sort
233 principals += project.principals.sort
234 else
234 else
235 all_projects = Project.visible.all
235 all_projects = Project.visible.all
236 if all_projects.any?
236 if all_projects.any?
237 # members of visible projects
237 # members of visible projects
238 principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort
238 principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort
239
239
240 # project filter
240 # project filter
241 project_values = []
241 project_values = []
242 Project.project_tree(all_projects) do |p, level|
242 Project.project_tree(all_projects) do |p, level|
243 prefix = (level > 0 ? ('--' * level + ' ') : '')
243 prefix = (level > 0 ? ('--' * level + ' ') : '')
244 project_values << ["#{prefix}#{p.name}", p.id.to_s]
244 project_values << ["#{prefix}#{p.name}", p.id.to_s]
245 end
245 end
246 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
246 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
247 end
247 end
248 end
248 end
249 users = principals.select {|p| p.is_a?(User)}
249 users = principals.select {|p| p.is_a?(User)}
250
250
251 assigned_to_values = []
251 assigned_to_values = []
252 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
252 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
253 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
253 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
254 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
254 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
255
255
256 author_values = []
256 author_values = []
257 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
257 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
258 author_values += users.collect{|s| [s.name, s.id.to_s] }
258 author_values += users.collect{|s| [s.name, s.id.to_s] }
259 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
259 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
260
260
261 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
261 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
262 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
262 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
263
263
264 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
264 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
265 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
265 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
266
266
267 if User.current.logged?
267 if User.current.logged?
268 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
268 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
269 end
269 end
270
270
271 if project
271 if project
272 # project specific filters
272 # project specific filters
273 categories = project.issue_categories.all
273 categories = project.issue_categories.all
274 unless categories.empty?
274 unless categories.empty?
275 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
275 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
276 end
276 end
277 versions = project.shared_versions.all
277 versions = project.shared_versions.all
278 unless versions.empty?
278 unless versions.empty?
279 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
279 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
280 end
280 end
281 unless project.leaf?
281 unless project.leaf?
282 subprojects = project.descendants.visible.all
282 subprojects = project.descendants.visible.all
283 unless subprojects.empty?
283 unless subprojects.empty?
284 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
284 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
285 end
285 end
286 end
286 end
287 add_custom_fields_filters(project.all_issue_custom_fields)
287 add_custom_fields_filters(project.all_issue_custom_fields)
288 else
288 else
289 # global filters for cross project issue list
289 # global filters for cross project issue list
290 system_shared_versions = Version.visible.find_all_by_sharing('system')
290 system_shared_versions = Version.visible.find_all_by_sharing('system')
291 unless system_shared_versions.empty?
291 unless system_shared_versions.empty?
292 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
292 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
293 end
293 end
294 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
294 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
295 end
295 end
296 @available_filters
296 @available_filters
297 end
297 end
298
298
299 def add_filter(field, operator, values)
299 def add_filter(field, operator, values)
300 # values must be an array
300 # values must be an array
301 return unless values.nil? || values.is_a?(Array)
301 return unless values.nil? || values.is_a?(Array)
302 # check if field is defined as an available filter
302 # check if field is defined as an available filter
303 if available_filters.has_key? field
303 if available_filters.has_key? field
304 filter_options = available_filters[field]
304 filter_options = available_filters[field]
305 # check if operator is allowed for that filter
305 # check if operator is allowed for that filter
306 #if @@operators_by_filter_type[filter_options[:type]].include? operator
306 #if @@operators_by_filter_type[filter_options[:type]].include? operator
307 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
307 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
308 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
308 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
309 #end
309 #end
310 filters[field] = {:operator => operator, :values => (values || [''])}
310 filters[field] = {:operator => operator, :values => (values || [''])}
311 end
311 end
312 end
312 end
313
313
314 def add_short_filter(field, expression)
314 def add_short_filter(field, expression)
315 return unless expression && available_filters.has_key?(field)
315 return unless expression && available_filters.has_key?(field)
316 field_type = available_filters[field][:type]
316 field_type = available_filters[field][:type]
317 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
317 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
318 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
318 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
319 add_filter field, operator, $1.present? ? $1.split('|') : ['']
319 add_filter field, operator, $1.present? ? $1.split('|') : ['']
320 end || add_filter(field, '=', expression.split('|'))
320 end || add_filter(field, '=', expression.split('|'))
321 end
321 end
322
322
323 # Add multiple filters using +add_filter+
323 # Add multiple filters using +add_filter+
324 def add_filters(fields, operators, values)
324 def add_filters(fields, operators, values)
325 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
325 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
326 fields.each do |field|
326 fields.each do |field|
327 add_filter(field, operators[field], values && values[field])
327 add_filter(field, operators[field], values && values[field])
328 end
328 end
329 end
329 end
330 end
330 end
331
331
332 def has_filter?(field)
332 def has_filter?(field)
333 filters and filters[field]
333 filters and filters[field]
334 end
334 end
335
335
336 def type_for(field)
336 def type_for(field)
337 available_filters[field][:type] if available_filters.has_key?(field)
337 available_filters[field][:type] if available_filters.has_key?(field)
338 end
338 end
339
339
340 def operator_for(field)
340 def operator_for(field)
341 has_filter?(field) ? filters[field][:operator] : nil
341 has_filter?(field) ? filters[field][:operator] : nil
342 end
342 end
343
343
344 def values_for(field)
344 def values_for(field)
345 has_filter?(field) ? filters[field][:values] : nil
345 has_filter?(field) ? filters[field][:values] : nil
346 end
346 end
347
347
348 def value_for(field, index=0)
348 def value_for(field, index=0)
349 (values_for(field) || [])[index]
349 (values_for(field) || [])[index]
350 end
350 end
351
351
352 def label_for(field)
352 def label_for(field)
353 label = available_filters[field][:name] if available_filters.has_key?(field)
353 label = available_filters[field][:name] if available_filters.has_key?(field)
354 label ||= field.gsub(/\_id$/, "")
354 label ||= field.gsub(/\_id$/, "")
355 end
355 end
356
356
357 def available_columns
357 def available_columns
358 return @available_columns if @available_columns
358 return @available_columns if @available_columns
359 @available_columns = ::Query.available_columns.dup
359 @available_columns = ::Query.available_columns.dup
360 @available_columns += (project ?
360 @available_columns += (project ?
361 project.all_issue_custom_fields :
361 project.all_issue_custom_fields :
362 IssueCustomField.find(:all)
362 IssueCustomField.find(:all)
363 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
363 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
364
364
365 if User.current.allowed_to?(:view_time_entries, project, :global => true)
365 if User.current.allowed_to?(:view_time_entries, project, :global => true)
366 index = nil
366 index = nil
367 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
367 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
368 index = (index ? index + 1 : -1)
368 index = (index ? index + 1 : -1)
369 # insert the column after estimated_hours or at the end
369 # insert the column after estimated_hours or at the end
370 @available_columns.insert index, QueryColumn.new(:spent_hours,
370 @available_columns.insert index, QueryColumn.new(:spent_hours,
371 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
371 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
372 :default_order => 'desc',
372 :default_order => 'desc',
373 :caption => :label_spent_time
373 :caption => :label_spent_time
374 )
374 )
375 end
375 end
376 @available_columns
376 @available_columns
377 end
377 end
378
378
379 def self.available_columns=(v)
379 def self.available_columns=(v)
380 self.available_columns = (v)
380 self.available_columns = (v)
381 end
381 end
382
382
383 def self.add_available_column(column)
383 def self.add_available_column(column)
384 self.available_columns << (column) if column.is_a?(QueryColumn)
384 self.available_columns << (column) if column.is_a?(QueryColumn)
385 end
385 end
386
386
387 # Returns an array of columns that can be used to group the results
387 # Returns an array of columns that can be used to group the results
388 def groupable_columns
388 def groupable_columns
389 available_columns.select {|c| c.groupable}
389 available_columns.select {|c| c.groupable}
390 end
390 end
391
391
392 # Returns a Hash of columns and the key for sorting
392 # Returns a Hash of columns and the key for sorting
393 def sortable_columns
393 def sortable_columns
394 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
394 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
395 h[column.name.to_s] = column.sortable
395 h[column.name.to_s] = column.sortable
396 h
396 h
397 })
397 })
398 end
398 end
399
399
400 def columns
400 def columns
401 # preserve the column_names order
401 # preserve the column_names order
402 (has_default_columns? ? default_columns_names : column_names).collect do |name|
402 (has_default_columns? ? default_columns_names : column_names).collect do |name|
403 available_columns.find { |col| col.name == name }
403 available_columns.find { |col| col.name == name }
404 end.compact
404 end.compact
405 end
405 end
406
406
407 def default_columns_names
407 def default_columns_names
408 @default_columns_names ||= begin
408 @default_columns_names ||= begin
409 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
409 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
410
410
411 project.present? ? default_columns : [:project] | default_columns
411 project.present? ? default_columns : [:project] | default_columns
412 end
412 end
413 end
413 end
414
414
415 def column_names=(names)
415 def column_names=(names)
416 if names
416 if names
417 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
417 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
418 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
418 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
419 # Set column_names to nil if default columns
419 # Set column_names to nil if default columns
420 if names == default_columns_names
420 if names == default_columns_names
421 names = nil
421 names = nil
422 end
422 end
423 end
423 end
424 write_attribute(:column_names, names)
424 write_attribute(:column_names, names)
425 end
425 end
426
426
427 def has_column?(column)
427 def has_column?(column)
428 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
428 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
429 end
429 end
430
430
431 def has_default_columns?
431 def has_default_columns?
432 column_names.nil? || column_names.empty?
432 column_names.nil? || column_names.empty?
433 end
433 end
434
434
435 def sort_criteria=(arg)
435 def sort_criteria=(arg)
436 c = []
436 c = []
437 if arg.is_a?(Hash)
437 if arg.is_a?(Hash)
438 arg = arg.keys.sort.collect {|k| arg[k]}
438 arg = arg.keys.sort.collect {|k| arg[k]}
439 end
439 end
440 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
440 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
441 write_attribute(:sort_criteria, c)
441 write_attribute(:sort_criteria, c)
442 end
442 end
443
443
444 def sort_criteria
444 def sort_criteria
445 read_attribute(:sort_criteria) || []
445 read_attribute(:sort_criteria) || []
446 end
446 end
447
447
448 def sort_criteria_key(arg)
448 def sort_criteria_key(arg)
449 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
449 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
450 end
450 end
451
451
452 def sort_criteria_order(arg)
452 def sort_criteria_order(arg)
453 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
453 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
454 end
454 end
455
455
456 # Returns the SQL sort order that should be prepended for grouping
456 # Returns the SQL sort order that should be prepended for grouping
457 def group_by_sort_order
457 def group_by_sort_order
458 if grouped? && (column = group_by_column)
458 if grouped? && (column = group_by_column)
459 column.sortable.is_a?(Array) ?
459 column.sortable.is_a?(Array) ?
460 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
460 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
461 "#{column.sortable} #{column.default_order}"
461 "#{column.sortable} #{column.default_order}"
462 end
462 end
463 end
463 end
464
464
465 # Returns true if the query is a grouped query
465 # Returns true if the query is a grouped query
466 def grouped?
466 def grouped?
467 !group_by_column.nil?
467 !group_by_column.nil?
468 end
468 end
469
469
470 def group_by_column
470 def group_by_column
471 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
471 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
472 end
472 end
473
473
474 def group_by_statement
474 def group_by_statement
475 group_by_column.try(:groupable)
475 group_by_column.try(:groupable)
476 end
476 end
477
477
478 def project_statement
478 def project_statement
479 project_clauses = []
479 project_clauses = []
480 if project && !project.descendants.active.empty?
480 if project && !project.descendants.active.empty?
481 ids = [project.id]
481 ids = [project.id]
482 if has_filter?("subproject_id")
482 if has_filter?("subproject_id")
483 case operator_for("subproject_id")
483 case operator_for("subproject_id")
484 when '='
484 when '='
485 # include the selected subprojects
485 # include the selected subprojects
486 ids += values_for("subproject_id").each(&:to_i)
486 ids += values_for("subproject_id").each(&:to_i)
487 when '!*'
487 when '!*'
488 # main project only
488 # main project only
489 else
489 else
490 # all subprojects
490 # all subprojects
491 ids += project.descendants.collect(&:id)
491 ids += project.descendants.collect(&:id)
492 end
492 end
493 elsif Setting.display_subprojects_issues?
493 elsif Setting.display_subprojects_issues?
494 ids += project.descendants.collect(&:id)
494 ids += project.descendants.collect(&:id)
495 end
495 end
496 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
496 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
497 elsif project
497 elsif project
498 project_clauses << "#{Project.table_name}.id = %d" % project.id
498 project_clauses << "#{Project.table_name}.id = %d" % project.id
499 end
499 end
500 project_clauses.any? ? project_clauses.join(' AND ') : nil
500 project_clauses.any? ? project_clauses.join(' AND ') : nil
501 end
501 end
502
502
503 def statement
503 def statement
504 # filters clauses
504 # filters clauses
505 filters_clauses = []
505 filters_clauses = []
506 filters.each_key do |field|
506 filters.each_key do |field|
507 next if field == "subproject_id"
507 next if field == "subproject_id"
508 v = values_for(field).clone
508 v = values_for(field).clone
509 next unless v and !v.empty?
509 next unless v and !v.empty?
510 operator = operator_for(field)
510 operator = operator_for(field)
511
511
512 # "me" value subsitution
512 # "me" value subsitution
513 if %w(assigned_to_id author_id watcher_id).include?(field)
513 if %w(assigned_to_id author_id watcher_id).include?(field)
514 if v.delete("me")
514 if v.delete("me")
515 if User.current.logged?
515 if User.current.logged?
516 v.push(User.current.id.to_s)
516 v.push(User.current.id.to_s)
517 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
517 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
518 else
518 else
519 v.push("0")
519 v.push("0")
520 end
520 end
521 end
521 end
522 end
522 end
523
523
524 if field =~ /^cf_(\d+)$/
524 if field =~ /^cf_(\d+)$/
525 # custom field
525 # custom field
526 filters_clauses << sql_for_custom_field(field, operator, v, $1)
526 filters_clauses << sql_for_custom_field(field, operator, v, $1)
527 elsif respond_to?("sql_for_#{field}_field")
527 elsif respond_to?("sql_for_#{field}_field")
528 # specific statement
528 # specific statement
529 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
529 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
530 else
530 else
531 # regular field
531 # regular field
532 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
532 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
533 end
533 end
534 end if filters and valid?
534 end if filters and valid?
535
535
536 filters_clauses << project_statement
536 filters_clauses << project_statement
537 filters_clauses.reject!(&:blank?)
537 filters_clauses.reject!(&:blank?)
538
538
539 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
539 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
540 end
540 end
541
541
542 # Returns the issue count
542 # Returns the issue count
543 def issue_count
543 def issue_count
544 Issue.visible.count(:include => [:status, :project], :conditions => statement)
544 Issue.visible.count(:include => [:status, :project], :conditions => statement)
545 rescue ::ActiveRecord::StatementInvalid => e
545 rescue ::ActiveRecord::StatementInvalid => e
546 raise StatementInvalid.new(e.message)
546 raise StatementInvalid.new(e.message)
547 end
547 end
548
548
549 # Returns the issue count by group or nil if query is not grouped
549 # Returns the issue count by group or nil if query is not grouped
550 def issue_count_by_group
550 def issue_count_by_group
551 r = nil
551 r = nil
552 if grouped?
552 if grouped?
553 begin
553 begin
554 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
554 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
555 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
555 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
556 rescue ActiveRecord::RecordNotFound
556 rescue ActiveRecord::RecordNotFound
557 r = {nil => issue_count}
557 r = {nil => issue_count}
558 end
558 end
559 c = group_by_column
559 c = group_by_column
560 if c.is_a?(QueryCustomFieldColumn)
560 if c.is_a?(QueryCustomFieldColumn)
561 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
561 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
562 end
562 end
563 end
563 end
564 r
564 r
565 rescue ::ActiveRecord::StatementInvalid => e
565 rescue ::ActiveRecord::StatementInvalid => e
566 raise StatementInvalid.new(e.message)
566 raise StatementInvalid.new(e.message)
567 end
567 end
568
568
569 # Returns the issues
569 # Returns the issues
570 # Valid options are :order, :offset, :limit, :include, :conditions
570 # Valid options are :order, :offset, :limit, :include, :conditions
571 def issues(options={})
571 def issues(options={})
572 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
572 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
573 order_option = nil if order_option.blank?
573 order_option = nil if order_option.blank?
574
574
575 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
575 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
576
576
577 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
577 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
578 :conditions => statement,
578 :conditions => statement,
579 :order => order_option,
579 :order => order_option,
580 :joins => joins,
580 :joins => joins,
581 :limit => options[:limit],
581 :limit => options[:limit],
582 :offset => options[:offset]
582 :offset => options[:offset]
583
583
584 if has_column?(:spent_hours)
584 if has_column?(:spent_hours)
585 Issue.load_visible_spent_hours(issues)
585 Issue.load_visible_spent_hours(issues)
586 end
586 end
587 issues
587 issues
588 rescue ::ActiveRecord::StatementInvalid => e
588 rescue ::ActiveRecord::StatementInvalid => e
589 raise StatementInvalid.new(e.message)
589 raise StatementInvalid.new(e.message)
590 end
590 end
591
591
592 # Returns the journals
592 # Returns the journals
593 # Valid options are :order, :offset, :limit
593 # Valid options are :order, :offset, :limit
594 def journals(options={})
594 def journals(options={})
595 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
595 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
596 :conditions => statement,
596 :conditions => statement,
597 :order => options[:order],
597 :order => options[:order],
598 :limit => options[:limit],
598 :limit => options[:limit],
599 :offset => options[:offset]
599 :offset => options[:offset]
600 rescue ::ActiveRecord::StatementInvalid => e
600 rescue ::ActiveRecord::StatementInvalid => e
601 raise StatementInvalid.new(e.message)
601 raise StatementInvalid.new(e.message)
602 end
602 end
603
603
604 # Returns the versions
604 # Returns the versions
605 # Valid options are :conditions
605 # Valid options are :conditions
606 def versions(options={})
606 def versions(options={})
607 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
607 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
608 rescue ::ActiveRecord::StatementInvalid => e
608 rescue ::ActiveRecord::StatementInvalid => e
609 raise StatementInvalid.new(e.message)
609 raise StatementInvalid.new(e.message)
610 end
610 end
611
611
612 def sql_for_watcher_id_field(field, operator, value)
612 def sql_for_watcher_id_field(field, operator, value)
613 db_table = Watcher.table_name
613 db_table = Watcher.table_name
614 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
614 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
615 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
615 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
616 end
616 end
617
617
618 def sql_for_member_of_group_field(field, operator, value)
618 def sql_for_member_of_group_field(field, operator, value)
619 if operator == '*' # Any group
619 if operator == '*' # Any group
620 groups = Group.all
620 groups = Group.all
621 operator = '=' # Override the operator since we want to find by assigned_to
621 operator = '=' # Override the operator since we want to find by assigned_to
622 elsif operator == "!*"
622 elsif operator == "!*"
623 groups = Group.all
623 groups = Group.all
624 operator = '!' # Override the operator since we want to find by assigned_to
624 operator = '!' # Override the operator since we want to find by assigned_to
625 else
625 else
626 groups = Group.find_all_by_id(value)
626 groups = Group.find_all_by_id(value)
627 end
627 end
628 groups ||= []
628 groups ||= []
629
629
630 members_of_groups = groups.inject([]) {|user_ids, group|
630 members_of_groups = groups.inject([]) {|user_ids, group|
631 if group && group.user_ids.present?
631 if group && group.user_ids.present?
632 user_ids << group.user_ids
632 user_ids << group.user_ids
633 end
633 end
634 user_ids.flatten.uniq.compact
634 user_ids.flatten.uniq.compact
635 }.sort.collect(&:to_s)
635 }.sort.collect(&:to_s)
636
636
637 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
637 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
638 end
638 end
639
639
640 def sql_for_assigned_to_role_field(field, operator, value)
640 def sql_for_assigned_to_role_field(field, operator, value)
641 case operator
641 case operator
642 when "*", "!*" # Member / Not member
642 when "*", "!*" # Member / Not member
643 sw = operator == "!*" ? 'NOT' : ''
643 sw = operator == "!*" ? 'NOT' : ''
644 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
644 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
645 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
645 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
646 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
646 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
647 when "=", "!"
647 when "=", "!"
648 role_cond = value.any? ?
648 role_cond = value.any? ?
649 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
649 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
650 "1=0"
650 "1=0"
651
651
652 sw = operator == "!" ? 'NOT' : ''
652 sw = operator == "!" ? 'NOT' : ''
653 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
653 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
654 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
654 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
655 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
655 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
656 end
656 end
657 end
657 end
658
658
659 private
659 private
660
660
661 def sql_for_custom_field(field, operator, value, custom_field_id)
661 def sql_for_custom_field(field, operator, value, custom_field_id)
662 db_table = CustomValue.table_name
662 db_table = CustomValue.table_name
663 db_field = 'value'
663 db_field = 'value'
664 "#{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=#{custom_field_id} WHERE " +
664 "#{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=#{custom_field_id} WHERE " +
665 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
665 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
666 end
666 end
667
667
668 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
668 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
669 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
669 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
670 sql = ''
670 sql = ''
671 case operator
671 case operator
672 when "="
672 when "="
673 if value.any?
673 if value.any?
674 case type_for(field)
674 case type_for(field)
675 when :date, :date_past
675 when :date, :date_past
676 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
676 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
677 when :integer
677 when :integer
678 if is_custom_filter
678 if is_custom_filter
679 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
679 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
680 else
680 else
681 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
681 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
682 end
682 end
683 when :float
683 when :float
684 if is_custom_filter
684 if is_custom_filter
685 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
685 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
686 else
686 else
687 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
687 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
688 end
688 end
689 else
689 else
690 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
690 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
691 end
691 end
692 else
692 else
693 # IN an empty set
693 # IN an empty set
694 sql = "1=0"
694 sql = "1=0"
695 end
695 end
696 when "!"
696 when "!"
697 if value.any?
697 if value.any?
698 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
698 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
699 else
699 else
700 # NOT IN an empty set
700 # NOT IN an empty set
701 sql = "1=1"
701 sql = "1=1"
702 end
702 end
703 when "!*"
703 when "!*"
704 sql = "#{db_table}.#{db_field} IS NULL"
704 sql = "#{db_table}.#{db_field} IS NULL"
705 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
705 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
706 when "*"
706 when "*"
707 sql = "#{db_table}.#{db_field} IS NOT NULL"
707 sql = "#{db_table}.#{db_field} IS NOT NULL"
708 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
708 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
709 when ">="
709 when ">="
710 if [:date, :date_past].include?(type_for(field))
710 if [:date, :date_past].include?(type_for(field))
711 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
711 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
712 else
712 else
713 if is_custom_filter
713 if is_custom_filter
714 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
714 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
715 else
715 else
716 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
716 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
717 end
717 end
718 end
718 end
719 when "<="
719 when "<="
720 if [:date, :date_past].include?(type_for(field))
720 if [:date, :date_past].include?(type_for(field))
721 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
721 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
722 else
722 else
723 if is_custom_filter
723 if is_custom_filter
724 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
724 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
725 else
725 else
726 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
726 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
727 end
727 end
728 end
728 end
729 when "><"
729 when "><"
730 if [:date, :date_past].include?(type_for(field))
730 if [:date, :date_past].include?(type_for(field))
731 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
731 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
732 else
732 else
733 if is_custom_filter
733 if is_custom_filter
734 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
734 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
735 else
735 else
736 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
736 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
737 end
737 end
738 end
738 end
739 when "o"
739 when "o"
740 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
740 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
741 when "c"
741 when "c"
742 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
742 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
743 when ">t-"
743 when ">t-"
744 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
744 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
745 when "<t-"
745 when "<t-"
746 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
746 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
747 when "t-"
747 when "t-"
748 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
748 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
749 when ">t+"
749 when ">t+"
750 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
750 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
751 when "<t+"
751 when "<t+"
752 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
752 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
753 when "t+"
753 when "t+"
754 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
754 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
755 when "t"
755 when "t"
756 sql = relative_date_clause(db_table, db_field, 0, 0)
756 sql = relative_date_clause(db_table, db_field, 0, 0)
757 when "w"
757 when "w"
758 first_day_of_week = l(:general_first_day_of_week).to_i
758 first_day_of_week = l(:general_first_day_of_week).to_i
759 day_of_week = Date.today.cwday
759 day_of_week = Date.today.cwday
760 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
760 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
761 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
761 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
762 when "~"
762 when "~"
763 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
763 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
764 when "!~"
764 when "!~"
765 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
765 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
766 else
766 else
767 raise "Unknown query operator #{operator}"
767 raise "Unknown query operator #{operator}"
768 end
768 end
769
769
770 return sql
770 return sql
771 end
771 end
772
772
773 def add_custom_fields_filters(custom_fields)
773 def add_custom_fields_filters(custom_fields)
774 @available_filters ||= {}
774 @available_filters ||= {}
775
775
776 custom_fields.select(&:is_filter?).each do |field|
776 custom_fields.select(&:is_filter?).each do |field|
777 case field.field_format
777 case field.field_format
778 when "text"
778 when "text"
779 options = { :type => :text, :order => 20 }
779 options = { :type => :text, :order => 20 }
780 when "list"
780 when "list"
781 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
781 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
782 when "date"
782 when "date"
783 options = { :type => :date, :order => 20 }
783 options = { :type => :date, :order => 20 }
784 when "bool"
784 when "bool"
785 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
785 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
786 when "int"
786 when "int"
787 options = { :type => :integer, :order => 20 }
787 options = { :type => :integer, :order => 20 }
788 when "float"
788 when "float"
789 options = { :type => :float, :order => 20 }
789 options = { :type => :float, :order => 20 }
790 when "user", "version"
790 when "user", "version"
791 next unless project
791 next unless project
792 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
792 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
793 else
793 else
794 options = { :type => :string, :order => 20 }
794 options = { :type => :string, :order => 20 }
795 end
795 end
796 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
796 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
797 end
797 end
798 end
798 end
799
799
800 # Returns a SQL clause for a date or datetime field.
800 # Returns a SQL clause for a date or datetime field.
801 def date_clause(table, field, from, to)
801 def date_clause(table, field, from, to)
802 s = []
802 s = []
803 if from
803 if from
804 from_yesterday = from - 1
804 from_yesterday = from - 1
805 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
805 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
806 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
806 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
807 end
807 end
808 if to
808 if to
809 to_utc = Time.gm(to.year, to.month, to.day)
809 to_utc = Time.gm(to.year, to.month, to.day)
810 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
810 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
811 end
811 end
812 s.join(' AND ')
812 s.join(' AND ')
813 end
813 end
814
814
815 # Returns a SQL clause for a date or datetime field using relative dates.
815 # Returns a SQL clause for a date or datetime field using relative dates.
816 def relative_date_clause(table, field, days_from, days_to)
816 def relative_date_clause(table, field, days_from, days_to)
817 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
817 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
818 end
818 end
819 end
819 end
@@ -1,59 +1,59
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 UserPreference < ActiveRecord::Base
18 class UserPreference < ActiveRecord::Base
19 belongs_to :user
19 belongs_to :user
20 serialize :others
20 serialize :others
21
21
22 attr_protected :others
22 attr_protected :others
23
23
24 before_save :set_others_hash
24 before_save :set_others_hash
25
25
26 def initialize(attributes = nil)
26 def initialize(attributes=nil, *args)
27 super
27 super
28 self.others ||= {}
28 self.others ||= {}
29 end
29 end
30
30
31 def set_others_hash
31 def set_others_hash
32 self.others ||= {}
32 self.others ||= {}
33 end
33 end
34
34
35 def [](attr_name)
35 def [](attr_name)
36 if attribute_present? attr_name
36 if attribute_present? attr_name
37 super
37 super
38 else
38 else
39 others ? others[attr_name] : nil
39 others ? others[attr_name] : nil
40 end
40 end
41 end
41 end
42
42
43 def []=(attr_name, value)
43 def []=(attr_name, value)
44 if attribute_present? attr_name
44 if attribute_present? attr_name
45 super
45 super
46 else
46 else
47 h = read_attribute(:others).dup || {}
47 h = read_attribute(:others).dup || {}
48 h.update(attr_name => value)
48 h.update(attr_name => value)
49 write_attribute(:others, h)
49 write_attribute(:others, h)
50 value
50 value
51 end
51 end
52 end
52 end
53
53
54 def comments_sorting; self[:comments_sorting] end
54 def comments_sorting; self[:comments_sorting] end
55 def comments_sorting=(order); self[:comments_sorting]=order end
55 def comments_sorting=(order); self[:comments_sorting]=order end
56
56
57 def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end
57 def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end
58 def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end
58 def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end
59 end
59 end
General Comments 0
You need to be logged in to leave comments. Login now