Revision 1298:4f746d8966dd app/models

View differences:

app/models/attachment.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
require "digest/md5"
19
require "fileutils"
19 20

  
20 21
class Attachment < ActiveRecord::Base
21 22
  belongs_to :container, :polymorphic => true
......
92 93

  
93 94
  def filename=(arg)
94 95
    write_attribute :filename, sanitize_filename(arg.to_s)
95
    if new_record? && disk_filename.blank?
96
      self.disk_filename = Attachment.disk_filename(filename)
97
    end
98 96
    filename
99 97
  end
100 98

  
......
102 100
  # and computes its MD5 hash
103 101
  def files_to_final_location
104 102
    if @temp_file && (@temp_file.size > 0)
103
      self.disk_directory = target_directory
104
      self.disk_filename = Attachment.disk_filename(filename, disk_directory)
105 105
      logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
106
      path = File.dirname(diskfile)
107
      unless File.directory?(path)
108
        FileUtils.mkdir_p(path)
109
      end
106 110
      md5 = Digest::MD5.new
107 111
      File.open(diskfile, "wb") do |f|
108 112
        if @temp_file.respond_to?(:read)
......
134 138

  
135 139
  # Returns file's location on disk
136 140
  def diskfile
137
    File.join(self.class.storage_path, disk_filename.to_s)
141
    File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
138 142
  end
139 143

  
140 144
  def title
......
154 158
  end
155 159

  
156 160
  def visible?(user=User.current)
157
    container && container.attachments_visible?(user)
161
    if container_id
162
      container && container.attachments_visible?(user)
163
    else
164
      author == user
165
    end
158 166
  end
159 167

  
160 168
  def deletable?(user=User.current)
161
    container && container.attachments_deletable?(user)
169
    if container_id
170
      container && container.attachments_deletable?(user)
171
    else
172
      author == user
173
    end
162 174
  end
163 175

  
164 176
  def image?
......
251 263
    Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
252 264
  end
253 265

  
266
  # Moves an existing attachment to its target directory
267
  def move_to_target_directory!
268
    if !new_record? & readable?
269
      src = diskfile
270
      self.disk_directory = target_directory
271
      dest = diskfile
272
      if src != dest && FileUtils.mkdir_p(File.dirname(dest)) && FileUtils.mv(src, dest)
273
        update_column :disk_directory, disk_directory
274
      end
275
    end
276
  end
277

  
278
  # Moves existing attachments that are stored at the root of the files
279
  # directory (ie. created before Redmine 2.3) to their target subdirectories
280
  def self.move_from_root_to_target_directory
281
    Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
282
      attachment.move_to_target_directory!
283
    end
284
  end
285

  
254 286
  private
255 287

  
256 288
  # Physically deletes the file from the file system
......
268 300
    @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
269 301
  end
270 302

  
271
  # Returns an ASCII or hashed filename
272
  def self.disk_filename(filename)
303
  # Returns the subdirectory in which the attachment will be saved
304
  def target_directory
305
    time = created_on || DateTime.now
306
    time.strftime("%Y/%m")
307
  end
308

  
309
  # Returns an ASCII or hashed filename that do not
310
  # exists yet in the given subdirectory
311
  def self.disk_filename(filename, directory=nil)
273 312
    timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
274 313
    ascii = ''
275 314
    if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
......
279 318
      # keep the extension if any
280 319
      ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
281 320
    end
282
    while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
321
    while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
283 322
      timestamp.succ!
284 323
    end
285 324
    "#{timestamp}_#{ascii}"
app/models/auth_source.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
48 48
    write_ciphered_attribute(:account_password, arg)
49 49
  end
50 50

  
51
  def searchable?
52
    false
53
  end
54

  
55
  def self.search(q)
56
    results = []
57
    AuthSource.all.each do |source|
58
      begin
59
        if source.searchable?
60
          results += source.search(q)
61
        end
62
      rescue AuthSourceException => e
63
        logger.error "Error while searching users in #{source.name}: #{e.message}"
64
      end
65
    end
66
    results
67
  end
68

  
51 69
  def allow_password_changes?
52 70
    self.class.allow_password_changes?
53 71
  end
app/models/auth_source_ldap.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18
require 'iconv'
19 18
require 'net/ldap'
20 19
require 'net/ldap/dn'
21 20
require 'timeout'
......
64 63
    "LDAP"
65 64
  end
66 65

  
66
  # Returns true if this source can be searched for users
67
  def searchable?
68
    !account.to_s.include?("$login") && %w(login firstname lastname mail).all? {|a| send("attr_#{a}?")}
69
  end
70

  
71
  # Searches the source for users and returns an array of results
72
  def search(q)
73
    q = q.to_s.strip
74
    return [] unless searchable? && q.present?
75

  
76
    results = []
77
    search_filter = base_filter & Net::LDAP::Filter.begins(self.attr_login, q)
78
    ldap_con = initialize_ldap_con(self.account, self.account_password)
79
    ldap_con.search(:base => self.base_dn,
80
                    :filter => search_filter,
81
                    :attributes => ['dn', self.attr_login, self.attr_firstname, self.attr_lastname, self.attr_mail],
82
                    :size => 10) do |entry|
83
      attrs = get_user_attributes_from_ldap_entry(entry)
84
      attrs[:login] = AuthSourceLdap.get_attr(entry, self.attr_login)
85
      results << attrs
86
    end
87
    results
88
  rescue Net::LDAP::LdapError => e
89
    raise AuthSourceException.new(e.message)
90
  end
91

  
67 92
  private
68 93

  
69 94
  def with_timeout(&block)
......
84 109
    nil
85 110
  end
86 111

  
112
  def base_filter
113
    filter = Net::LDAP::Filter.eq("objectClass", "*")
114
    if f = ldap_filter
115
      filter = filter & f
116
    end
117
    filter
118
  end
119

  
87 120
  def validate_filter
88 121
    if filter.present? && ldap_filter.nil?
89 122
      errors.add(:filter, :invalid)
......
140 173
    else
141 174
      ldap_con = initialize_ldap_con(self.account, self.account_password)
142 175
    end
143
    login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
144
    object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
145 176
    attrs = {}
146

  
147
    search_filter = object_filter & login_filter
148
    if f = ldap_filter
149
      search_filter = search_filter & f
150
    end
177
    search_filter = base_filter & Net::LDAP::Filter.eq(self.attr_login, login)
151 178

  
152 179
    ldap_con.search( :base => self.base_dn,
153 180
                     :filter => search_filter,
app/models/board.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
30 30
  validates_length_of :description, :maximum => 255
31 31
  validate :validate_board
32 32

  
33
  scope :visible, lambda {|*args| { :include => :project,
34
                                          :conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
33
  scope :visible, lambda {|*args|
34
    includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args))
35
  }
35 36

  
36 37
  safe_attributes 'name', 'description', 'parent_id', 'move_to'
37 38

  
app/models/change.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/changeset.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18
require 'iconv'
19

  
20 18
class Changeset < ActiveRecord::Base
21 19
  belongs_to :repository
22 20
  belongs_to :user
......
49 47
  validates_uniqueness_of :revision, :scope => :repository_id
50 48
  validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
51 49

  
52
  scope :visible,
53
     lambda {|*args| { :include => {:repository => :project},
54
                                          :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
50
  scope :visible, lambda {|*args|
51
    includes(:repository => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
52
  }
55 53

  
56 54
  after_create :scan_for_issues
57 55
  before_create :before_create_cs
app/models/comment.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/comment_observer.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/custom_field.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
29 29

  
30 30
  validate :validate_custom_field
31 31
  before_validation :set_searchable
32
  after_save :handle_multiplicity_change
33

  
34
  scope :sorted, lambda { order("#{table_name}.position ASC") }
32 35

  
33 36
  CUSTOM_FIELDS_TABS = [
34 37
    {:name => 'IssueCustomField', :partial => 'custom_fields/index',
......
169 172
      keyword
170 173
    end
171 174
  end
172
 
175

  
173 176
  # Returns a ORDER BY clause that can used to sort customized
174 177
  # objects by their value of the custom field.
175 178
  # Returns nil if the custom field can not be used for sorting.
......
178 181
    case field_format
179 182
      when 'string', 'text', 'list', 'date', 'bool'
180 183
        # COALESCE is here to make sure that blank and NULL values are sorted equally
181
        "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
182
          " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
183
          " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
184
          " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
184
        "COALESCE(#{join_alias}.value, '')"
185 185
      when 'int', 'float'
186 186
        # Make the database cast values into numeric
187 187
        # Postgresql will raise an error if a value can not be casted!
188 188
        # CustomValue validations should ensure that it doesn't occur
189
        "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
190
          " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
191
          " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
192
          " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
189
        "CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,3))"
193 190
      when 'user', 'version'
194 191
        value_class.fields_for_order_statement(value_join_alias)
195 192
      else
......
199 196

  
200 197
  # Returns a GROUP BY clause that can used to group by custom value
201 198
  # Returns nil if the custom field can not be used for grouping.
202
  def group_statement 
199
  def group_statement
203 200
    return nil if multiple?
204 201
    case field_format
205 202
      when 'list', 'date', 'bool', 'int'
206 203
        order_statement
207 204
      when 'user', 'version'
208
        "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
209
          " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
210
          " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
211
          " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
205
        "COALESCE(#{join_alias}.value, '')"
212 206
      else
213 207
        nil
214 208
    end
......
227 221
            " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
228 222
            " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
229 223
          " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
230
          " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
224
          " ON CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,0)) = #{value_join_alias}.id"
225
      when 'int', 'float'
226
        "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
227
          " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
228
          " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
229
          " AND #{join_alias}.custom_field_id = #{id}" +
230
          " AND #{join_alias}.value <> ''" +
231
          " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
232
            " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
233
            " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
234
            " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)"
235
      when 'string', 'text', 'list', 'date', 'bool'
236
        "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
237
          " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
238
          " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
239
          " AND #{join_alias}.custom_field_id = #{id}" +
240
          " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
241
            " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
242
            " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
243
            " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)"
231 244
      else
232 245
        nil
233 246
    end
......
262 275

  
263 276
  # to move in project_custom_field
264 277
  def self.for_all
265
    find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
278
    where(:is_for_all => true).order('position').all
266 279
  end
267 280

  
268 281
  def type_name
......
323 336
    end
324 337
    errs
325 338
  end
339

  
340
  # Removes multiple values for the custom field after setting the multiple attribute to false
341
  # We kepp the value with the highest id for each customized object
342
  def handle_multiplicity_change
343
    if !new_record? && multiple_was && !multiple
344
      ids = custom_values.
345
        where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
346
          " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
347
          " AND cve.id > #{CustomValue.table_name}.id)").
348
        pluck(:id)
349

  
350
      if ids.any?
351
        custom_values.where(:id => ids).delete_all
352
      end
353
    end
354
  end
326 355
end
app/models/custom_field_value.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/custom_value.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/document.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
19 19
  include Redmine::SafeAttributes
20 20
  belongs_to :project
21 21
  belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
22
  acts_as_attachable :delete_permission => :manage_documents
22
  acts_as_attachable :delete_permission => :delete_documents
23 23

  
24 24
  acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
25 25
  acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
26
                :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
26
                :author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) },
27 27
                :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
28 28
  acts_as_activity_provider :find_options => {:include => :project}
29 29

  
30 30
  validates_presence_of :project, :title, :category
31 31
  validates_length_of :title, :maximum => 60
32 32

  
33
  scope :visible, lambda {|*args| { :include => :project,
34
                                          :conditions => Project.allowed_to_condition(args.shift || User.current, :view_documents, *args) } }
33
  scope :visible, lambda {|*args|
34
    includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args))
35
  }
35 36

  
36 37
  safe_attributes 'category_id', 'title', 'description'
37 38

  
app/models/document_category.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/document_category_custom_field.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/document_observer.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/enabled_module.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/enumeration.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
24 24

  
25 25
  acts_as_list :scope => 'type = \'#{type}\''
26 26
  acts_as_customizable
27
  acts_as_tree :order => 'position ASC'
27
  acts_as_tree :order => "#{Enumeration.table_name}.position ASC"
28 28

  
29 29
  before_destroy :check_integrity
30 30
  before_save    :check_default
......
35 35
  validates_uniqueness_of :name, :scope => [:type, :project_id]
36 36
  validates_length_of :name, :maximum => 30
37 37

  
38
  scope :shared, where(:project_id => nil)
39
  scope :sorted, order("#{table_name}.position ASC")
40
  scope :active, where(:active => true)
38
  scope :shared, lambda { where(:project_id => nil) }
39
  scope :sorted, lambda { order("#{table_name}.position ASC") }
40
  scope :active, lambda { where(:active => true) }
41
  scope :system, lambda { where(:project_id => nil) }
41 42
  scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
42 43

  
43 44
  def self.default
app/models/group.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
25 25

  
26 26
  validates_presence_of :lastname
27 27
  validates_uniqueness_of :lastname, :case_sensitive => false
28
  validates_length_of :lastname, :maximum => 30
28
  validates_length_of :lastname, :maximum => 255
29 29

  
30 30
  before_destroy :remove_references_before_destroy
31 31

  
32
  scope :sorted, order("#{table_name}.lastname ASC")
32
  scope :sorted, lambda { order("#{table_name}.lastname ASC") }
33
  scope :named, lambda {|arg| where("LOWER(#{table_name}.lastname) = LOWER(?)", arg.to_s.strip)}
33 34

  
34 35
  safe_attributes 'name',
35 36
    'user_ids',
......
62 63

  
63 64
  def user_removed(user)
64 65
    members.each do |member|
65
      MemberRole.find(:all, :include => :member,
66
                            :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
66
      MemberRole.
67
        includes(:member).
68
        where("#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids).
69
        all.
70
        each(&:destroy)
67 71
    end
68 72
  end
69 73

  
app/models/group_custom_field.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/issue.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
67 67

  
68 68
  validates_length_of :subject, :maximum => 255
69 69
  validates_inclusion_of :done_ratio, :in => 0..100
70
  validates_numericality_of :estimated_hours, :allow_nil => true
70
  validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
71
  validates :start_date, :date => true
72
  validates :due_date, :date => true
71 73
  validate :validate_issue, :validate_required_fields
72 74

  
73
  scope :visible,
74
        lambda {|*args| { :include => :project,
75
                          :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
75
  scope :visible, lambda {|*args|
76
    includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
77
  }
76 78

  
77 79
  scope :open, lambda {|*args|
78 80
    is_closed = args.size > 0 ? !args.first : false
79
    {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
81
    includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
80 82
  }
81 83

  
82
  scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
83
  scope :on_active_project, :include => [:status, :project, :tracker],
84
                            :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
84
  scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
85
  scope :on_active_project, lambda {
86
    includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
87
  }
88
  scope :fixed_version, lambda {|versions|
89
    ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
90
    ids.any? ? where(:fixed_version_id => ids) : where('1=0')
91
  }
85 92

  
86 93
  before_create :default_assign
87
  before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
94
  before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
88 95
  after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} 
89 96
  after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
90 97
  # Should be after_create but would be called before previous after_save callbacks
......
133 140
    end
134 141
  end
135 142

  
143
  # Returns true if user or current user is allowed to edit or add a note to the issue
144
  def editable?(user=User.current)
145
    user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
146
  end
147

  
136 148
  def initialize(attributes=nil, *args)
137 149
    super
138 150
    if new_record?
......
143 155
    end
144 156
  end
145 157

  
158
  def create_or_update
159
    super
160
  ensure
161
    @status_was = nil
162
  end
163
  private :create_or_update
164

  
146 165
  # AR#Persistence#destroy would raise and RecordNotFound exception
147 166
  # if the issue was already deleted or updated (non matching lock_version).
148 167
  # This is a problem when bulk deleting issues or deleting a project
......
165 184
    super
166 185
  end
167 186

  
187
  alias :base_reload :reload
168 188
  def reload(*args)
169 189
    @workflow_rule_by_attribute = nil
170 190
    @assignable_versions = nil
171
    super
191
    @relations = nil
192
    base_reload(*args)
172 193
  end
173 194

  
174 195
  # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
......
526 547
  end
527 548

  
528 549
  def validate_issue
529
    if due_date.nil? && @attributes['due_date'].present?
530
      errors.add :due_date, :not_a_date
531
    end
532

  
533
    if start_date.nil? && @attributes['start_date'].present?
534
      errors.add :start_date, :not_a_date
535
    end
536

  
537 550
    if due_date && start_date && due_date < start_date
538 551
      errors.add :due_date, :greater_than_start_date
539 552
    end
......
563 576
    elsif @parent_issue
564 577
      if !valid_parent_project?(@parent_issue)
565 578
        errors.add :parent_issue_id, :invalid
579
      elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
580
        errors.add :parent_issue_id, :invalid
566 581
      elsif !new_record?
567 582
        # moving an existing issue
568 583
        if @parent_issue.root_id != root_id
......
633 648
    scope
634 649
  end
635 650

  
651
  # Returns the initial status of the issue
652
  # Returns nil for a new issue
653
  def status_was
654
    if status_id_was && status_id_was.to_i > 0
655
      @status_was ||= IssueStatus.find_by_id(status_id_was)
656
    end
657
  end
658

  
636 659
  # Return true if the issue is closed, otherwise false
637 660
  def closed?
638 661
    self.status.is_closed?
......
653 676
  # Return true if the issue is being closed
654 677
  def closing?
655 678
    if !new_record? && status_id_changed?
656
      status_was = IssueStatus.find_by_id(status_id_was)
657
      status_new = IssueStatus.find_by_id(status_id)
658
      if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
679
      if status_was && status && !status_was.is_closed? && status.is_closed?
659 680
        return true
660 681
      end
661 682
    end
......
785 806
  end
786 807

  
787 808
  def relations
788
    @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort)
809
    @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
789 810
  end
790 811

  
791 812
  # Preloads relations for a collection of issues
......
822 843
          relations_from.select {|relation| relation.issue_from_id == issue.id} +
823 844
          relations_to.select {|relation| relation.issue_to_id == issue.id}
824 845

  
825
        issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort)
846
        issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
826 847
      end
827 848
    end
828 849
  end
......
832 853
    IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
833 854
  end
834 855

  
856
  # Returns all the other issues that depend on the issue
835 857
  def all_dependent_issues(except=[])
836 858
    except << self
837 859
    dependencies = []
838
    relations_from.each do |relation|
839
      if relation.issue_to && !except.include?(relation.issue_to)
840
        dependencies << relation.issue_to
841
        dependencies += relation.issue_to.all_dependent_issues(except)
842
      end
860
    dependencies += relations_from.map(&:issue_to)
861
    dependencies += children unless leaf?
862
    dependencies.compact!
863
    dependencies -= except
864
    dependencies += dependencies.map {|issue| issue.all_dependent_issues(except)}.flatten
865
    if parent
866
      dependencies << parent
867
      dependencies += parent.all_dependent_issues(except + parent.descendants)
843 868
    end
844 869
    dependencies
845 870
  end
......
873 898
    @soonest_start = nil if reload
874 899
    @soonest_start ||= (
875 900
        relations_to(reload).collect{|relation| relation.successor_soonest_start} +
876
        ancestors.collect(&:soonest_start)
901
        [(@parent_issue || parent).try(:soonest_start)]
877 902
      ).compact.max
878 903
  end
879 904

  
......
936 961

  
937 962
  # Returns a string of css classes that apply to the issue
938 963
  def css_classes
939
    s = "issue status-#{status_id} #{priority.try(:css_classes)}"
964
    s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
940 965
    s << ' closed' if closed?
941 966
    s << ' overdue' if overdue?
942 967
    s << ' child' if child?
......
1005 1030
    end
1006 1031
  end
1007 1032

  
1008
	# Returns true if issue's project is a valid
1009
	# parent issue project
1033
  # Returns true if issue's project is a valid
1034
  # parent issue project
1010 1035
  def valid_parent_project?(issue=parent)
1011 1036
    return true if issue.nil? || issue.project_id == project_id
1012 1037

  
......
1128 1153
    end
1129 1154

  
1130 1155
    unless @copied_from.leaf? || @copy_options[:subtasks] == false
1131
      @copied_from.children.each do |child|
1156
      copy_options = (@copy_options || {}).merge(:subtasks => false)
1157
      copied_issue_ids = {@copied_from.id => self.id}
1158
      @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1159
        # Do not copy self when copying an issue as a descendant of the copied issue
1160
        next if child == self
1161
        # Do not copy subtasks of issues that were not copied
1162
        next unless copied_issue_ids[child.parent_id]
1163
        # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1132 1164
        unless child.visible?
1133
          # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1134 1165
          logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1135 1166
          next
1136 1167
        end
1137
        copy = Issue.new.copy_from(child, @copy_options)
1168
        copy = Issue.new.copy_from(child, copy_options)
1138 1169
        copy.author = author
1139 1170
        copy.project = project
1140
        copy.parent_issue_id = id
1141
        # Children subtasks are copied recursively
1171
        copy.parent_issue_id = copied_issue_ids[child.parent_id]
1142 1172
        unless copy.save
1143 1173
          logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1174
          next
1144 1175
        end
1176
        copied_issue_ids[child.id] = copy.id
1145 1177
      end
1146 1178
    end
1147 1179
    @after_create_from_copy_handled = true
......
1303 1335
    end
1304 1336
  end
1305 1337

  
1306
  # Make sure updated_on is updated when adding a note
1338
  # Make sure updated_on is updated when adding a note and set updated_on now
1339
  # so we can set closed_on with the same value on closing
1307 1340
  def force_updated_on_change
1308
    if @current_journal
1341
    if @current_journal || changed?
1309 1342
      self.updated_on = current_time_from_proper_timezone
1343
      if new_record?
1344
        self.created_on = updated_on
1345
      end
1346
    end
1347
  end
1348

  
1349
  # Callback for setting closed_on when the issue is closed.
1350
  # The closed_on attribute stores the time of the last closing
1351
  # and is preserved when the issue is reopened.
1352
  def update_closed_on
1353
    if closing? || (new_record? && closed?)
1354
      self.closed_on = updated_on
1310 1355
    end
1311 1356
  end
1312 1357

  
......
1316 1361
    if @current_journal
1317 1362
      # attributes changes
1318 1363
      if @attributes_before_change
1319
        (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1364
        (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1320 1365
          before = @attributes_before_change[c]
1321 1366
          after = send(c)
1322 1367
          next if before == after || (before.blank? && after.blank?)
app/models/issue_category.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
24 24
  validates_presence_of :name
25 25
  validates_uniqueness_of :name, :scope => [:project_id]
26 26
  validates_length_of :name, :maximum => 30
27
  
27

  
28 28
  safe_attributes 'name', 'assigned_to_id'
29 29

  
30 30
  scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
......
35 35
  # If a category is specified, issues are reassigned to this category
36 36
  def destroy(reassign_to = nil)
37 37
    if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project
38
      Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")
38
      Issue.update_all({:category_id => reassign_to.id}, {:category_id => id})
39 39
    end
40 40
    destroy_without_reassign
41 41
  end
app/models/issue_custom_field.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/issue_observer.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/issue_priority.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/issue_priority_custom_field.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
app/models/issue_query.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
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
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
class IssueQuery < Query
19

  
20
  self.queried_class = Issue
21

  
22
  self.available_columns = [
23
    QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
24
    QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25
    QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
26
    QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
27
    QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
28
    QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
29
    QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
30
    QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
31
    QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
32
    QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
33
    QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
34
    QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
35
    QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
36
    QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
37
    QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
38
    QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
39
    QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
40
    QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
41
    QueryColumn.new(:relations, :caption => :label_related_issues),
42
    QueryColumn.new(:description, :inline => false)
43
  ]
44

  
45
  scope :visible, lambda {|*args|
46
    user = args.shift || User.current
47
    base = Project.allowed_to_condition(user, :view_issues, *args)
48
    user_id = user.logged? ? user.id : 0
49

  
50
    includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
51
  }
52

  
53
  def initialize(attributes=nil, *args)
54
    super attributes
55
    self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
56
  end
57

  
58
  # Returns true if the query is visible to +user+ or the current user.
59
  def visible?(user=User.current)
60
    (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
61
  end
62

  
63
  def initialize_available_filters
64
    principals = []
65
    subprojects = []
66
    versions = []
67
    categories = []
68
    issue_custom_fields = []
69
    
70
    if project
71
      principals += project.principals.sort
72
      unless project.leaf?
73
        subprojects = project.descendants.visible.all
74
        principals += Principal.member_of(subprojects)
75
      end
76
      versions = project.shared_versions.all
77
      categories = project.issue_categories.all
78
      issue_custom_fields = project.all_issue_custom_fields
79
    else
80
      if all_projects.any?
81
        principals += Principal.member_of(all_projects)
82
      end
83
      versions = Version.visible.find_all_by_sharing('system')
84
      issue_custom_fields = IssueCustomField.where(:is_filter => true, :is_for_all => true).all
85
    end
86
    principals.uniq!
87
    principals.sort!
88
    users = principals.select {|p| p.is_a?(User)}
89

  
90

  
91
    add_available_filter "status_id",
92
      :type => :list_status, :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
93

  
94
    if project.nil?
95
      project_values = []
96
      if User.current.logged? && User.current.memberships.any?
97
        project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
98
      end
99
      project_values += all_projects_values
100
      add_available_filter("project_id",
101
        :type => :list, :values => project_values
102
      ) unless project_values.empty?
103
    end
104

  
105
    add_available_filter "tracker_id",
106
      :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
107
    add_available_filter "priority_id",
108
      :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
109

  
110
    author_values = []
111
    author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
112
    author_values += users.collect{|s| [s.name, s.id.to_s] }
113
    add_available_filter("author_id",
114
      :type => :list, :values => author_values
115
    ) unless author_values.empty?
116

  
117
    assigned_to_values = []
118
    assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
119
    assigned_to_values += (Setting.issue_group_assignment? ?
120
                              principals : users).collect{|s| [s.name, s.id.to_s] }
121
    add_available_filter("assigned_to_id",
122
      :type => :list_optional, :values => assigned_to_values
123
    ) unless assigned_to_values.empty?
124

  
125
    group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
126
    add_available_filter("member_of_group",
127
      :type => :list_optional, :values => group_values
128
    ) unless group_values.empty?
129

  
130
    role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
131
    add_available_filter("assigned_to_role",
132
      :type => :list_optional, :values => role_values
133
    ) unless role_values.empty?
134

  
135
    if versions.any?
136
      add_available_filter "fixed_version_id",
137
        :type => :list_optional,
138
        :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
139
    end
140

  
141
    if categories.any?
142
      add_available_filter "category_id",
143
        :type => :list_optional,
144
        :values => categories.collect{|s| [s.name, s.id.to_s] }
145
    end
146

  
147
    add_available_filter "subject", :type => :text
148
    add_available_filter "created_on", :type => :date_past
149
    add_available_filter "updated_on", :type => :date_past
150
    add_available_filter "closed_on", :type => :date_past
151
    add_available_filter "start_date", :type => :date
152
    add_available_filter "due_date", :type => :date
153
    add_available_filter "estimated_hours", :type => :float
154
    add_available_filter "done_ratio", :type => :integer
155

  
156
    if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
157
      User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
158
      add_available_filter "is_private",
159
        :type => :list,
160
        :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
161
    end
162

  
163
    if User.current.logged?
164
      add_available_filter "watcher_id",
165
        :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
166
    end
167

  
168
    if subprojects.any?
169
      add_available_filter "subproject_id",
170
        :type => :list_subprojects,
171
        :values => subprojects.collect{|s| [s.name, s.id.to_s] }
172
    end
173

  
174
    add_custom_fields_filters(issue_custom_fields)
175

  
176
    add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
177

  
178
    IssueRelation::TYPES.each do |relation_type, options|
179
      add_available_filter relation_type, :type => :relation, :label => options[:name]
180
    end
181

  
182
    Tracker.disabled_core_fields(trackers).each {|field|
183
      delete_available_filter field
184
    }
185
  end
186

  
187
  def available_columns
188
    return @available_columns if @available_columns
189
    @available_columns = self.class.available_columns.dup
190
    @available_columns += (project ?
191
                            project.all_issue_custom_fields :
192
                            IssueCustomField.all
193
                           ).collect {|cf| QueryCustomFieldColumn.new(cf) }
194

  
195
    if User.current.allowed_to?(:view_time_entries, project, :global => true)
196
      index = nil
197
      @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
198
      index = (index ? index + 1 : -1)
199
      # insert the column after estimated_hours or at the end
200
      @available_columns.insert index, QueryColumn.new(:spent_hours,
201
        :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
202
        :default_order => 'desc',
203
        :caption => :label_spent_time
204
      )
205
    end
206

  
207
    if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
208
      User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
209
      @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
210
    end
211

  
212
    disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
213
    @available_columns.reject! {|column|
214
      disabled_fields.include?(column.name.to_s)
215
    }
216

  
217
    @available_columns
218
  end
219

  
220
  def default_columns_names
221
    @default_columns_names ||= begin
222
      default_columns = Setting.issue_list_default_columns.map(&:to_sym)
223

  
224
      project.present? ? default_columns : [:project] | default_columns
225
    end
226
  end
227

  
228
  # Returns the issue count
229
  def issue_count
230
    Issue.visible.count(:include => [:status, :project], :conditions => statement)
231
  rescue ::ActiveRecord::StatementInvalid => e
232
    raise StatementInvalid.new(e.message)
233
  end
234

  
235
  # Returns the issue count by group or nil if query is not grouped
236
  def issue_count_by_group
237
    r = nil
238
    if grouped?
239
      begin
240
        # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
241
        r = Issue.visible.count(:joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement)
242
      rescue ActiveRecord::RecordNotFound
243
        r = {nil => issue_count}
244
      end
245
      c = group_by_column
246
      if c.is_a?(QueryCustomFieldColumn)
247
        r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
248
      end
249
    end
250
    r
251
  rescue ::ActiveRecord::StatementInvalid => e
252
    raise StatementInvalid.new(e.message)
253
  end
254

  
255
  # Returns the issues
256
  # Valid options are :order, :offset, :limit, :include, :conditions
257
  def issues(options={})
258
    order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
259

  
260
    issues = Issue.visible.where(options[:conditions]).all(
261
      :include => ([:status, :project] + (options[:include] || [])).uniq,
262
      :conditions => statement,
263
      :order => order_option,
264
      :joins => joins_for_order_statement(order_option.join(',')),
265
      :limit  => options[:limit],
266
      :offset => options[:offset]
267
    )
268

  
269
    if has_column?(:spent_hours)
270
      Issue.load_visible_spent_hours(issues)
271
    end
272
    if has_column?(:relations)
273
      Issue.load_visible_relations(issues)
274
    end
275
    issues
276
  rescue ::ActiveRecord::StatementInvalid => e
277
    raise StatementInvalid.new(e.message)
278
  end
279

  
280
  # Returns the issues ids
281
  def issue_ids(options={})
282
    order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
283

  
284
    Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
285
                     :conditions => statement,
286
                     :order => order_option,
287
                     :joins => joins_for_order_statement(order_option.join(',')),
288
                     :limit  => options[:limit],
289
                     :offset => options[:offset]).find_ids
290
  rescue ::ActiveRecord::StatementInvalid => e
291
    raise StatementInvalid.new(e.message)
292
  end
293

  
294
  # Returns the journals
295
  # Valid options are :order, :offset, :limit
296
  def journals(options={})
297
    Journal.visible.all(
298
      :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
299
      :conditions => statement,
300
      :order => options[:order],
301
      :limit => options[:limit],
302
      :offset => options[:offset]
303
    )
304
  rescue ::ActiveRecord::StatementInvalid => e
305
    raise StatementInvalid.new(e.message)
306
  end
307

  
308
  # Returns the versions
309
  # Valid options are :conditions
310
  def versions(options={})
311
    Version.visible.where(options[:conditions]).all(
312
      :include => :project,
313
      :conditions => project_statement
314
    )
315
  rescue ::ActiveRecord::StatementInvalid => e
316
    raise StatementInvalid.new(e.message)
317
  end
318

  
319
  def sql_for_watcher_id_field(field, operator, value)
320
    db_table = Watcher.table_name
321
    "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
322
      sql_for_field(field, '=', value, db_table, 'user_id') + ')'
323
  end
324

  
325
  def sql_for_member_of_group_field(field, operator, value)
326
    if operator == '*' # Any group
327
      groups = Group.all
328
      operator = '=' # Override the operator since we want to find by assigned_to
329
    elsif operator == "!*"
330
      groups = Group.all
331
      operator = '!' # Override the operator since we want to find by assigned_to
332
    else
333
      groups = Group.find_all_by_id(value)
334
    end
335
    groups ||= []
336

  
337
    members_of_groups = groups.inject([]) {|user_ids, group|
338
      user_ids + group.user_ids + [group.id]
339
    }.uniq.compact.sort.collect(&:to_s)
340

  
341
    '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
342
  end
343

  
344
  def sql_for_assigned_to_role_field(field, operator, value)
345
    case operator
346
    when "*", "!*" # Member / Not member
347
      sw = operator == "!*" ? 'NOT' : ''
348
      nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
349
      "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
350
        " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
351
    when "=", "!"
352
      role_cond = value.any? ?
353
        "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
354
        "1=0"
355

  
356
      sw = operator == "!" ? 'NOT' : ''
357
      nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
358
      "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
359
        " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
360
    end
361
  end
362

  
363
  def sql_for_is_private_field(field, operator, value)
364
    op = (operator == "=" ? 'IN' : 'NOT IN')
365
    va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
366

  
367
    "#{Issue.table_name}.is_private #{op} (#{va})"
368
  end
369

  
370
  def sql_for_relations(field, operator, value, options={})
371
    relation_options = IssueRelation::TYPES[field]
372
    return relation_options unless relation_options
373

  
374
    relation_type = field
375
    join_column, target_join_column = "issue_from_id", "issue_to_id"
376
    if relation_options[:reverse] || options[:reverse]
377
      relation_type = relation_options[:reverse] || relation_type
378
      join_column, target_join_column = target_join_column, join_column
379
    end
380

  
381
    sql = case operator
382
      when "*", "!*"
383
        op = (operator == "*" ? 'IN' : 'NOT IN')
384
        "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
385
      when "=", "!"
386
        op = (operator == "=" ? 'IN' : 'NOT IN')
387
        "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
388
      when "=p", "=!p", "!p"
389
        op = (operator == "!p" ? 'NOT IN' : 'IN')
390
        comp = (operator == "=!p" ? '<>' : '=')
391
        "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
392
      end
393

  
394
    if relation_options[:sym] == field && !options[:reverse]
395
      sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
396
      sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
397
    else
398
      sql
399
    end
400
  end
401

  
402
  IssueRelation::TYPES.keys.each do |relation_type|
403
    alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
404
  end
405
end
app/models/issue_relation.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18
# Class used to represent the relations of an issue
19
class IssueRelations < Array
20
  include Redmine::I18n
18
class IssueRelation < ActiveRecord::Base
19
  # Class used to represent the relations of an issue
20
  class Relations < Array
21
    include Redmine::I18n
21 22

  
22
  def initialize(issue, *args)
23
    @issue = issue
24
    super(*args)
23
    def initialize(issue, *args)
24
      @issue = issue
25
      super(*args)
26
    end
27

  
28
    def to_s(*args)
29
      map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
30
    end
25 31
  end
26 32

  
27
  def to_s(*args)
28
    map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
29
  end
30
end
31

  
32
class IssueRelation < ActiveRecord::Base
33 33
  belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
34 34
  belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
35 35

  
app/models/issue_status.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
28 28
  validates_length_of :name, :maximum => 30
29 29
  validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
30 30

  
31
  scope :sorted, order("#{table_name}.position ASC")
31
  scope :sorted, lambda { order("#{table_name}.position ASC") }
32 32
  scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
33 33

  
34 34
  def update_default
app/models/journal.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
28 28
  acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
29 29
                :description => :notes,
30 30
                :author => :user,
31
                :group => :issue,
31 32
                :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
32 33
                :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
33 34

  
app/models/journal_detail.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
2
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
......
27 27
  end
28 28

  
29 29
  def normalize(v)
30
    if v == true
30
    case v
31
    when true
31 32
      "1"
32
    elsif v == false
33
    when false
33 34
      "0"
35
    when Date
36
      v.strftime("%Y-%m-%d")
34 37
    else
35 38
      v
36 39
    end
app/models/journal_observer.rb
1 1
# Redmine - project management software
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff