Revision 912:5e80956cc792 app/models

View differences:

app/models/attachment.rb
24 24
  validates_presence_of :container, :filename, :author
25 25
  validates_length_of :filename, :maximum => 255
26 26
  validates_length_of :disk_filename, :maximum => 255
27
  validate :validate_max_file_size
27 28

  
28 29
  acts_as_event :title => :filename,
29 30
                :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
......
43 44
                                                        "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44 45

  
45 46
  cattr_accessor :storage_path
46
  @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{RAILS_ROOT}/files"
47
  @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
47 48

  
48
  def validate
49
  before_save :files_to_final_location
50
  after_destroy :delete_from_disk
51

  
52
  def validate_max_file_size
49 53
    if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 54
      errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
51 55
    end
......
72 76

  
73 77
  # Copies the temporary file to its final location
74 78
  # and computes its MD5 hash
75
  def before_save
79
  def files_to_final_location
76 80
    if @temp_file && (@temp_file.size > 0)
77
      logger.debug("saving '#{self.diskfile}'")
81
      logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
78 82
      md5 = Digest::MD5.new
79 83
      File.open(diskfile, "wb") do |f|
80 84
        buffer = ""
......
85 89
      end
86 90
      self.digest = md5.hexdigest
87 91
    end
92
    @temp_file = nil
88 93
    # Don't save the content type if it's longer than the authorized length
89 94
    if self.content_type && self.content_type.length > 255
90 95
      self.content_type = nil
......
92 97
  end
93 98

  
94 99
  # Deletes file on the disk
95
  def after_destroy
100
  def delete_from_disk
96 101
    File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
97 102
  end
98 103

  
......
118 123
  end
119 124

  
120 125
  def image?
121
    self.filename =~ /\.(jpe?g|gif|png)$/i
126
    self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
122 127
  end
123 128

  
124 129
  def is_text?
......
149 154
                              :file => file,
150 155
                              :description => attachment['description'].to_s.strip,
151 156
                              :author => User.current)
157
        obj.attachments << a
152 158

  
153 159
        if a.new_record?
154 160
          obj.unsaved_attachments ||= []
......
161 167
    {:files => attached, :unsaved => obj.unsaved_attachments}
162 168
  end
163 169

  
170
  def self.latest_attach(attachments, filename)
171
    attachments.sort_by(&:created_on).reverse.detect { 
172
      |att| att.filename.downcase == filename.downcase
173
     }
174
  end
175

  
164 176
private
165 177
  def sanitize_filename(value)
166 178
    # get only the filename, not the whole path
167 179
    just_filename = value.gsub(/^.*(\\|\/)/, '')
168
    # NOTE: File.basename doesn't work right with Windows paths on Unix
169
    # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
170 180

  
171
    # Finally, replace all non alphanumeric, hyphens or periods with underscore
172
    @filename = just_filename.gsub(/[^\w\.\-]/,'_')
181
    # Finally, replace invalid characters with underscore
182
    @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
173 183
  end
174 184

  
175 185
  # Returns an ASCII or hashed filename
app/models/auth_source.rb
1
# redMine - project management software
2
# Copyright (C) 2006  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
class AuthSource < ActiveRecord::Base
19 19
  include Redmine::Ciphering
20
  
20

  
21 21
  has_many :users
22
  
22

  
23 23
  validates_presence_of :name
24 24
  validates_uniqueness_of :name
25 25
  validates_length_of :name, :maximum => 60
26 26

  
27 27
  def authenticate(login, password)
28 28
  end
29
  
29

  
30 30
  def test_connection
31 31
  end
32
  
32

  
33 33
  def auth_method_name
34 34
    "Abstract"
35 35
  end
36
  
36

  
37 37
  def account_password
38 38
    read_ciphered_attribute(:account_password)
39 39
  end
40
  
40

  
41 41
  def account_password=(arg)
42 42
    write_ciphered_attribute(:account_password, arg)
43 43
  end
app/models/auth_source_ldap.rb
1
# redMine - project management software
2
# Copyright (C) 2006  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
......
18 18
require 'net/ldap'
19 19
require 'iconv'
20 20

  
21
class AuthSourceLdap < AuthSource 
21
class AuthSourceLdap < AuthSource
22 22
  validates_presence_of :host, :port, :attr_login
23 23
  validates_length_of :name, :host, :maximum => 60, :allow_nil => true
24 24
  validates_length_of :account, :account_password, :base_dn, :maximum => 255, :allow_nil => true
25 25
  validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
26 26
  validates_numericality_of :port, :only_integer => true
27
  
27

  
28 28
  before_validation :strip_ldap_attributes
29
  
29

  
30 30
  def after_initialize
31 31
    self.port = 389 if self.port == 0
32 32
  end
33
  
33

  
34 34
  def authenticate(login, password)
35 35
    return nil if login.blank? || password.blank?
36 36
    attrs = get_user_dn(login)
37
    
37

  
38 38
    if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
39 39
      logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
40 40
      return attrs.except(:dn)
......
50 50
  rescue  Net::LDAP::LdapError => text
51 51
    raise "LdapError: " + text
52 52
  end
53
 
53

  
54 54
  def auth_method_name
55 55
    "LDAP"
56 56
  end
57
  
57

  
58 58
  private
59
  
59

  
60 60
  def strip_ldap_attributes
61 61
    [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
62 62
      write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
63 63
    end
64 64
  end
65
  
65

  
66 66
  def initialize_ldap_con(ldap_user, ldap_password)
67 67
    options = { :host => self.host,
68 68
                :port => self.port,
......
102 102
  # Get the user's dn and any attributes for them, given their login
103 103
  def get_user_dn(login)
104 104
    ldap_con = initialize_ldap_con(self.account, self.account_password)
105
    login_filter = Net::LDAP::Filter.eq( self.attr_login, login ) 
106
    object_filter = Net::LDAP::Filter.eq( "objectClass", "*" ) 
105
    login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
106
    object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
107 107
    attrs = {}
108
    
109
    ldap_con.search( :base => self.base_dn, 
110
                     :filter => object_filter & login_filter, 
108

  
109
    ldap_con.search( :base => self.base_dn,
110
                     :filter => object_filter & login_filter,
111 111
                     :attributes=> search_attributes) do |entry|
112 112

  
113 113
      if onthefly_register?
......
121 121

  
122 122
    attrs
123 123
  end
124
  
124

  
125 125
  def self.get_attr(entry, attr_name)
126 126
    if !attr_name.blank?
127 127
      entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
app/models/board.rb
1
# redMine - project management software
2
# Copyright (C) 2006-2007  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
......
22 22
  belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
23 23
  acts_as_list :scope => :project_id
24 24
  acts_as_watchable
25
  
25

  
26 26
  validates_presence_of :name, :description
27 27
  validates_length_of :name, :maximum => 30
28 28
  validates_length_of :description, :maximum => 255
29
  
29

  
30
  named_scope :visible, lambda {|*args| { :include => :project,
31
                                          :conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
32

  
30 33
  def visible?(user=User.current)
31 34
    !user.nil? && user.allowed_to?(:view_messages, project)
32 35
  end
33
  
36

  
34 37
  def to_s
35 38
    name
36 39
  end
37
  
40

  
38 41
  def reset_counters!
39 42
    self.class.reset_counters!(id)
40 43
  end
41
  
44

  
42 45
  # Updates topics_count, messages_count and last_message_id attributes for +board_id+
43 46
  def self.reset_counters!(board_id)
44 47
    board_id = board_id.to_i
app/models/change.rb
20 20

  
21 21
  validates_presence_of :changeset_id, :action, :path
22 22
  before_save :init_path
23
  before_validation :replace_invalid_utf8_of_path
23 24

  
24 25
  def relative_path
25 26
    changeset.repository.relative_path(path)
26 27
  end
27 28

  
28
  def before_validation
29
  def replace_invalid_utf8_of_path
29 30
    self.path      = Redmine::CodesetUtil.replace_invalid_utf8(self.path)
30 31
    self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path)
31 32
  end
app/models/changeset.rb
22 22
  belongs_to :user
23 23
  has_many :changes, :dependent => :delete_all
24 24
  has_and_belongs_to_many :issues
25
  has_and_belongs_to_many :parents,
26
                          :class_name => "Changeset",
27
                          :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
28
                          :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
29
  has_and_belongs_to_many :children,
30
                          :class_name => "Changeset",
31
                          :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
32
                          :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
25 33

  
26 34
  acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 35
                :description => :long_comments,
......
44 52
  named_scope :visible, lambda {|*args| { :include => {:repository => :project},
45 53
                                          :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
46 54

  
55
  after_create :scan_for_issues
56
  before_create :before_create_cs
57

  
47 58
  def revision=(r)
48 59
    write_attribute :revision, (r.nil? ? nil : r.to_s)
49 60
  end
......
79 90
    user || committer.to_s.split('<').first
80 91
  end
81 92

  
82
  def before_create
93
  def before_create_cs
83 94
    self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
84 95
    self.comments  = self.class.normalize_comments(
85 96
                       self.comments, repository.repo_log_encoding)
86 97
    self.user = repository.find_committer_user(self.committer)
87 98
  end
88 99

  
89
  def after_create
100
  def scan_for_issues
90 101
    scan_comment_for_issue_ids
91 102
  end
92 103

  
......
193 204
  def fix_issue(issue)
194 205
    status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
195 206
    if status.nil?
196
      logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
207
      logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
197 208
      return issue
198 209
    end
199 210

  
......
253 264
  end
254 265

  
255 266
  def self.to_utf8(str, encoding)
256
    return str if str.nil?
257
    str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
258
    if str.empty?
259
      str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
260
      return str
261
    end
262
    enc = encoding.blank? ? "UTF-8" : encoding
263
    if str.respond_to?(:force_encoding)
264
      if enc.upcase != "UTF-8"
265
        str.force_encoding(enc)
266
        str = str.encode("UTF-8", :invalid => :replace,
267
              :undef => :replace, :replace => '?')
268
      else
269
        str.force_encoding("UTF-8")
270
        if ! str.valid_encoding?
271
          str = str.encode("US-ASCII", :invalid => :replace,
272
                :undef => :replace, :replace => '?').encode("UTF-8")
273
        end
274
      end
275
    else
276
      ic = Iconv.new('UTF-8', enc)
277
      txtar = ""
278
      begin
279
        txtar += ic.iconv(str)
280
      rescue Iconv::IllegalSequence
281
        txtar += $!.success
282
        str = '?' + $!.failed[1,$!.failed.length]
283
        retry
284
      rescue
285
        txtar += $!.success
286
      end
287
      str = txtar
288
    end
289
    str
267
    Redmine::CodesetUtil.to_utf8(str, encoding)
290 268
  end
291 269
end
app/models/changeset.rb.rej
1
--- app/models/changeset.rb
2
+++ app/models/changeset.rb
3
@@ -152,6 +152,15 @@
4
   def self.normalize_comments(str)
5
     to_utf8(str.to_s.strip)
6
   end
7
+
8
+  # Creates a new Change from it's common parameters
9
+  def create_change(change)
10
+    Change.create(:changeset => self,
11
+                  :action => change[:action],
12
+                  :path => change[:path],
13
+                  :from_path => change[:from_path],
14
+                  :from_revision => change[:from_revision])
15
+  end
16
   
17
   private
18
 
app/models/comment.rb
1
# redMine - project management software
2
# Copyright (C) 2006  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
app/models/comment_observer.rb
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
app/models/custom_field.rb
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
......
19 19
  has_many :custom_values, :dependent => :delete_all
20 20
  acts_as_list :scope => 'type = \'#{self.class}\''
21 21
  serialize :possible_values
22
  
22

  
23 23
  validates_presence_of :name, :field_format
24 24
  validates_uniqueness_of :name, :scope => :type
25 25
  validates_length_of :name, :maximum => 30
26 26
  validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
27 27

  
28
  validate :validate_values
29

  
28 30
  def initialize(attributes = nil)
29 31
    super
30 32
    self.possible_values ||= []
31 33
  end
32
  
34

  
33 35
  def before_validation
34 36
    # make sure these fields are not searchable
35 37
    self.searchable = false if %w(int float date bool).include?(field_format)
36 38
    true
37 39
  end
38
  
39
  def validate
40

  
41
  def validate_values
40 42
    if self.field_format == "list"
41 43
      errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
42 44
      errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
43 45
    end
44
    
46

  
47
    if regexp.present?
48
      begin
49
        Regexp.new(regexp)
50
      rescue
51
        errors.add(:regexp, :invalid)
52
      end
53
    end
54

  
45 55
    # validate default value
46 56
    v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
47 57
    v.custom_field.is_required = false
48 58
    errors.add(:default_value, :invalid) unless v.valid?
49 59
  end
50
  
60

  
51 61
  def possible_values_options(obj=nil)
52 62
    case field_format
53 63
    when 'user', 'version'
......
56 66
        when 'user'
57 67
          obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
58 68
        when 'version'
59
          obj.project.versions.sort.collect {|u| [u.to_s, u.id.to_s]}
69
          obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
60 70
        end
61 71
      elsif obj.is_a?(Array)
62 72
        obj.collect {|o| possible_values_options(o)}.inject {|memo, v| memo & v}
......
67 77
      read_attribute :possible_values
68 78
    end
69 79
  end
70
  
80

  
71 81
  def possible_values(obj=nil)
72 82
    case field_format
73 83
    when 'user', 'version'
......
76 86
      read_attribute :possible_values
77 87
    end
78 88
  end
79
  
89

  
80 90
  # Makes possible_values accept a multiline string
81 91
  def possible_values=(arg)
82 92
    if arg.is_a?(Array)
......
85 95
      self.possible_values = arg.to_s.split(/[\n\r]+/)
86 96
    end
87 97
  end
88
  
98

  
89 99
  def cast_value(value)
90 100
    casted = nil
91 101
    unless value.blank?
......
106 116
    end
107 117
    casted
108 118
  end
109
  
119

  
110 120
  # Returns a ORDER BY clause that can used to sort customized
111 121
  # objects by their value of the custom field.
112 122
  # Returns false, if the custom field can not be used for sorting.
......
114 124
    case field_format
115 125
      when 'string', 'text', 'list', 'date', 'bool'
116 126
        # COALESCE is here to make sure that blank and NULL values are sorted equally
117
        "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + 
127
        "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
118 128
          " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
119 129
          " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
120 130
          " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
......
122 132
        # Make the database cast values into numeric
123 133
        # Postgresql will raise an error if a value can not be casted!
124 134
        # CustomValue validations should ensure that it doesn't occur
125
        "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" + 
135
        "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
126 136
          " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
127 137
          " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
128 138
          " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
......
134 144
  def <=>(field)
135 145
    position <=> field.position
136 146
  end
137
  
147

  
138 148
  def self.customized_class
139 149
    self.name =~ /^(.+)CustomField$/
140 150
    begin; $1.constantize; rescue nil; end
141 151
  end
142
  
152

  
143 153
  # to move in project_custom_field
144 154
  def self.for_all
145 155
    find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
146 156
  end
147
  
157

  
148 158
  def type_name
149 159
    nil
150 160
  end
app/models/custom_value.rb
1
# redMine - project management software
2
# Copyright (C) 2006  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
......
19 19
  belongs_to :custom_field
20 20
  belongs_to :customized, :polymorphic => true
21 21

  
22
  validate :validate_custom_value
23

  
22 24
  def after_initialize
23 25
    if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?))
24 26
      self.value ||= custom_field.default_value
25 27
    end
26 28
  end
27
  
29

  
28 30
  # Returns true if the boolean custom value is true
29 31
  def true?
30 32
    self.value == '1'
31 33
  end
32
  
34

  
33 35
  def editable?
34 36
    custom_field.editable?
35 37
  end
36
  
38

  
37 39
  def visible?
38 40
    custom_field.visible?
39 41
  end
40
  
42

  
41 43
  def required?
42 44
    custom_field.is_required?
43 45
  end
44
  
46

  
45 47
  def to_s
46 48
    value.to_s
47 49
  end
48
  
50

  
49 51
protected
50
  def validate
52
  def validate_custom_value
51 53
    if value.blank?
52
      errors.add(:value, :blank) if custom_field.is_required? and value.blank?    
54
      errors.add(:value, :blank) if custom_field.is_required? and value.blank?
53 55
    else
54 56
      errors.add(:value, :invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
55 57
      errors.add(:value, :too_short, :count => custom_field.min_length) if custom_field.min_length > 0 and value.length < custom_field.min_length
56 58
      errors.add(:value, :too_long, :count => custom_field.max_length) if custom_field.max_length > 0 and value.length > custom_field.max_length
57
    
59

  
58 60
      # Format specific validations
59 61
      case custom_field.field_format
60 62
      when 'int'
......
62 64
      when 'float'
63 65
        begin; Kernel.Float(value); rescue; errors.add(:value, :invalid) end
64 66
      when 'date'
65
        errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
67
        errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
66 68
      when 'list'
67 69
        errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value)
68 70
      end
app/models/enumeration.rb
1
# redMine - project management software
2
# Copyright (C) 2006  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
class Enumeration < ActiveRecord::Base
19 19
  default_scope :order => "#{Enumeration.table_name}.position ASC"
20
  
20

  
21 21
  belongs_to :project
22
  
22

  
23 23
  acts_as_list :scope => 'type = \'#{type}\''
24 24
  acts_as_customizable
25 25
  acts_as_tree :order => 'position ASC'
26 26

  
27 27
  before_destroy :check_integrity
28
  
28
  before_save    :check_default
29

  
29 30
  validates_presence_of :name
30 31
  validates_uniqueness_of :name, :scope => [:type, :project_id]
31 32
  validates_length_of :name, :maximum => 30
......
45 46
      find(:first, :conditions => { :is_default => true })
46 47
    end
47 48
  end
48
  
49

  
49 50
  # Overloaded on concrete classes
50 51
  def option_name
51 52
    nil
52 53
  end
53 54

  
54
  def before_save
55
  def check_default
55 56
    if is_default? && is_default_changed?
56 57
      Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type})
57 58
    end
58 59
  end
59
  
60

  
60 61
  # Overloaded on concrete classes
61 62
  def objects_count
62 63
    0
63 64
  end
64
  
65

  
65 66
  def in_use?
66 67
    self.objects_count != 0
67 68
  end
......
70 71
  def is_override?
71 72
    !self.parent.nil?
72 73
  end
73
  
74

  
74 75
  alias :destroy_without_reassign :destroy
75
  
76

  
76 77
  # Destroy the enumeration
77 78
  # If a enumeration is specified, objects are reassigned
78 79
  def destroy(reassign_to = nil)
......
81 82
    end
82 83
    destroy_without_reassign
83 84
  end
84
  
85

  
85 86
  def <=>(enumeration)
86 87
    position <=> enumeration.position
87 88
  end
88
  
89

  
89 90
  def to_s; name end
90 91

  
91 92
  # Returns the Subclasses of Enumeration.  Each Subclass needs to be
......
115 116

  
116 117
    return true
117 118
  end
118
  
119

  
119 120
  # Are the new and previous fields equal?
120 121
  def self.same_active_state?(new, previous)
121 122
    new = (new == "1" ? true : false)
122 123
    return new == previous
123 124
  end
124
  
125

  
125 126
private
126 127
  def check_integrity
127 128
    raise "Can't delete enumeration" if self.in_use?
app/models/group.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2009  Jean-Philippe Lang
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
......
18 18
class Group < Principal
19 19
  has_and_belongs_to_many :users, :after_add => :user_added,
20 20
                                  :after_remove => :user_removed
21
  
21

  
22 22
  acts_as_customizable
23
  
23

  
24 24
  validates_presence_of :lastname
25 25
  validates_uniqueness_of :lastname, :case_sensitive => false
26 26
  validates_length_of :lastname, :maximum => 30
27
    
27

  
28
  before_destroy :remove_references_before_destroy
29

  
28 30
  def to_s
29 31
    lastname.to_s
30 32
  end
31
  
33

  
34
  alias :name :to_s
35

  
32 36
  def user_added(user)
33 37
    members.each do |member|
34 38
      next if member.project.nil?
......
39 43
      user_member.save!
40 44
    end
41 45
  end
42
  
46

  
43 47
  def user_removed(user)
44 48
    members.each do |member|
45 49
      MemberRole.find(:all, :include => :member,
46 50
                            :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
47 51
    end
48 52
  end
53

  
54
  def self.human_attribute_name(attribute_key_name)
55
    attr_name = attribute_key_name
56
    if attr_name == 'lastname'
57
      attr_name = "name"
58
    end
59
    super(attr_name)
60
  end
61

  
62
  private
63

  
64
  # Removes references that are not handled by associations
65
  def remove_references_before_destroy
66
    return if self.id.nil?
67

  
68
    Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
69
  end
49 70
end
app/models/group_custom_field.rb
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2009  Jean-Philippe Lang
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
app/models/issue.rb
22 22
  belongs_to :tracker
23 23
  belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24
  belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25
  belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
25
  belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 26
  belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27
  belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28
  belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
......
35 35
  has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36

  
37 37
  acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38
  acts_as_attachable :after_remove => :attachment_removed
38
  acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 39
  acts_as_customizable
40 40
  acts_as_watchable
41 41
  acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
......
58 58
  validates_length_of :subject, :maximum => 255
59 59
  validates_inclusion_of :done_ratio, :in => 0..100
60 60
  validates_numericality_of :estimated_hours, :allow_nil => true
61
  validate :validate_issue
61 62

  
62 63
  named_scope :visible, lambda {|*args| { :include => :project,
63 64
                                          :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
......
93 94
      when 'all'
94 95
        nil
95 96
      when 'default'
96
        "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
97
        user_ids = [user.id] + user.groups.map(&:id)
98
        "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
97 99
      when 'own'
98
        "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
100
        user_ids = [user.id] + user.groups.map(&:id)
101
        "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
99 102
      else
100 103
        '1=0'
101 104
      end
......
109 112
      when 'all'
110 113
        true
111 114
      when 'default'
112
        !self.is_private? || self.author == user || self.assigned_to == user
115
        !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
113 116
      when 'own'
114
        self.author == user || self.assigned_to == user
117
        self.author == user || user.is_or_belongs_to?(assigned_to)
115 118
      else
116 119
        false
117 120
      end
......
227 230
    @custom_field_values = nil
228 231
    result
229 232
  end
230
  
233

  
231 234
  def description=(arg)
232 235
    if arg.is_a?(String)
233 236
      arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
......
335 338
    Setting.issue_done_ratio == 'issue_field'
336 339
  end
337 340

  
338
  def validate
341
  def validate_issue
339 342
    if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
340 343
      errors.add :due_date, :not_a_date
341 344
    end
......
352 355
      if !assignable_versions.include?(fixed_version)
353 356
        errors.add :fixed_version_id, :inclusion
354 357
      elsif reopened? && fixed_version.closed?
355
        errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
358
        errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
356 359
      end
357 360
    end
358 361

  
......
449 452
  def assignable_users
450 453
    users = project.assignable_users
451 454
    users << author if author
455
    users << assigned_to if assigned_to
452 456
    users.uniq.sort
453 457
  end
454 458

  
......
482 486
    # Author and assignee are always notified unless they have been
483 487
    # locked or don't want to be notified
484 488
    notified << author if author && author.active? && author.notify_about?(self)
485
    notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
489
    if assigned_to
490
      if assigned_to.is_a?(Group)
491
        notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
492
      else
493
        notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
494
      end
495
    end
486 496
    notified.uniq!
487 497
    # Remove users that can not view the issue
488 498
    notified.reject! {|user| !visible?(user)}
......
499 509
  end
500 510

  
501 511
  def relations
502
    (relations_from + relations_to).sort
512
    @relations ||= (relations_from + relations_to).sort
513
  end
514

  
515
  # Preloads relations for a collection of issues
516
  def self.load_relations(issues)
517
    if issues.any?
518
      relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
519
      issues.each do |issue|
520
        issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
521
      end
522
    end
523
  end
524

  
525
  # Finds an issue relation given its id.
526
  def find_relation(relation_id)
527
    IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
503 528
  end
504 529

  
505 530
  def all_dependent_issues(except=[])
......
592 617
        @time_entry.project = project
593 618
        @time_entry.issue = self
594 619
        @time_entry.user = User.current
595
        @time_entry.spent_on = Date.today
620
        @time_entry.spent_on = User.current.today
596 621
        @time_entry.attributes = params[:time_entry]
597 622
        self.time_entries << @time_entry
598 623
      end
599 624

  
600 625
      if valid?
601 626
        attachments = Attachment.attach_files(self, params[:attachments])
602

  
603
        attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
604 627
        # TODO: Rename hook
605 628
        Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
606 629
        begin
......
612 635
          end
613 636
        rescue ActiveRecord::StaleObjectError
614 637
          attachments[:files].each(&:destroy)
615
          errors.add_to_base l(:notice_locking_conflict)
638
          errors.add :base, l(:notice_locking_conflict)
616 639
          raise ActiveRecord::Rollback
617 640
        end
618 641
      end
......
831 854
  end
832 855

  
833 856
  # Callback on attachment deletion
857
  def attachment_added(obj)
858
    if @current_journal && !obj.new_record?
859
      @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
860
    end
861
  end
862

  
863
  # Callback on attachment deletion
834 864
  def attachment_removed(obj)
835 865
    journal = init_journal(User.current)
836 866
    journal.details << JournalDetail.new(:property => 'attachment',
app/models/issue_category.rb
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
class IssueCategory < ActiveRecord::Base
19 19
  belongs_to :project
20
  belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
20
  belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
21 21
  has_many :issues, :foreign_key => 'category_id', :dependent => :nullify
22
  
22

  
23 23
  validates_presence_of :name
24 24
  validates_uniqueness_of :name, :scope => [:project_id]
25 25
  validates_length_of :name, :maximum => 30
26 26
  
27
  attr_protected :project_id
28

  
27 29
  named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
28
  
30

  
29 31
  alias :destroy_without_reassign :destroy
30
  
32

  
31 33
  # Destroy the category
32 34
  # If a category is specified, issues are reassigned to this category
33 35
  def destroy(reassign_to = nil)
......
36 38
    end
37 39
    destroy_without_reassign
38 40
  end
39
  
41

  
40 42
  def <=>(category)
41 43
    name <=> category.name
42 44
  end
43
  
45

  
44 46
  def to_s; name end
45 47
end
app/models/issue_custom_field.rb
1
# redMine - project management software
2
# Copyright (C) 2006  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
......
19 19
  has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id"
20 20
  has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id"
21 21
  has_many :issues, :through => :issue_custom_values
22
    
22

  
23 23
  def type_name
24 24
    :label_issue_plural
25 25
  end
app/models/issue_observer.rb
1
# redMine - project management software
2
# Copyright (C) 2006-2007  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
app/models/issue_priority.rb
1
# redMine - project management software
2
# Copyright (C) 2006  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
app/models/issue_priority_custom_field.rb
1
# redMine - project management software
2
# Copyright (C) 2006  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
app/models/issue_relation.rb
1
# redMine - project management software
2
# Copyright (C) 2006-2007  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
......
18 18
class IssueRelation < ActiveRecord::Base
19 19
  belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
20 20
  belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
21
  
21

  
22 22
  TYPE_RELATES      = "relates"
23 23
  TYPE_DUPLICATES   = "duplicates"
24 24
  TYPE_DUPLICATED   = "duplicated"
......
26 26
  TYPE_BLOCKED      = "blocked"
27 27
  TYPE_PRECEDES     = "precedes"
28 28
  TYPE_FOLLOWS      = "follows"
29
  
29

  
30 30
  TYPES = { TYPE_RELATES =>     { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES },
31 31
            TYPE_DUPLICATES =>  { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED },
32 32
            TYPE_DUPLICATED =>  { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
......
35 35
            TYPE_PRECEDES =>    { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS },
36 36
            TYPE_FOLLOWS =>     { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }
37 37
          }.freeze
38
  
38

  
39 39
  validates_presence_of :issue_from, :issue_to, :relation_type
40 40
  validates_inclusion_of :relation_type, :in => TYPES.keys
41 41
  validates_numericality_of :delay, :allow_nil => true
42 42
  validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
43
  
43

  
44
  validate :validate_issue_relation
45

  
44 46
  attr_protected :issue_from_id, :issue_to_id
45
  
46
  def validate
47

  
48
  before_save :handle_issue_order
49

  
50
  def visible?(user=User.current)
51
    (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
52
  end
53

  
54
  def deletable?(user=User.current)
55
    visible?(user) &&
56
      ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
57
        (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
58
  end
59

  
60
  def after_initialize
61
    if new_record?
62
      if relation_type.blank?
63
        self.relation_type = IssueRelation::TYPE_RELATES
64
      end
65
    end
66
  end
67

  
68
  def validate_issue_relation
47 69
    if issue_from && issue_to
48 70
      errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
49 71
      errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
50 72
      #detect circular dependencies depending wether the relation should be reversed
51 73
      if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
52
        errors.add_to_base :circular_dependency if issue_from.all_dependent_issues.include? issue_to
74
        errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
53 75
      else
54
        errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from
76
        errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
55 77
      end
56
      errors.add_to_base :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
78
      errors.add :base, :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
57 79
    end
58 80
  end
59
  
81

  
60 82
  def other_issue(issue)
61 83
    (self.issue_from_id == issue.id) ? issue_to : issue_from
62 84
  end
63
  
85

  
64 86
  # Returns the relation type for +issue+
65 87
  def relation_type_for(issue)
66 88
    if TYPES[relation_type]
......
71 93
      end
72 94
    end
73 95
  end
74
  
96

  
75 97
  def label_for(issue)
76 98
    TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
77 99
  end
78
  
79
  def before_save
100

  
101
  def handle_issue_order
80 102
    reverse_if_needed
81
    
103

  
82 104
    if TYPE_PRECEDES == relation_type
83 105
      self.delay ||= 0
84 106
    else
......
86 108
    end
87 109
    set_issue_to_dates
88 110
  end
89
  
111

  
90 112
  def set_issue_to_dates
91 113
    soonest_start = self.successor_soonest_start
92 114
    if soonest_start && issue_to
93 115
      issue_to.reschedule_after(soonest_start)
94 116
    end
95 117
  end
96
  
118

  
97 119
  def successor_soonest_start
98 120
    if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date)
99 121
      (issue_from.due_date || issue_from.start_date) + 1 + delay
100 122
    end
101 123
  end
102
  
124

  
103 125
  def <=>(relation)
104 126
    TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
105 127
  end
106
  
128

  
107 129
  private
108
  
130

  
109 131
  # Reverses the relation if needed so that it gets stored in the proper way
132
  # Should not be reversed before validation so that it can be displayed back
133
  # as entered on new relation form
110 134
  def reverse_if_needed
111 135
    if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
112 136
      issue_tmp = issue_to
app/models/issue_status.rb
1
# redMine - project management software
2
# Copyright (C) 2006  Jean-Philippe Lang
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3 3
#
4 4
# This program is free software; you can redistribute it and/or
5 5
# modify it under the terms of the GNU General Public License
6 6
# as published by the Free Software Foundation; either version 2
7 7
# of the License, or (at your option) any later version.
8
# 
8
#
9 9
# This program is distributed in the hope that it will be useful,
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU General Public License for more details.
13
# 
13
#
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, write to the Free Software
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
class IssueStatus < ActiveRecord::Base
19
  before_destroy :check_integrity  
19
  before_destroy :check_integrity
20 20
  has_many :workflows, :foreign_key => "old_status_id"
21 21
  acts_as_list
22
  
22

  
23 23
  before_destroy :delete_workflows
24
  after_save     :update_default
24 25

  
25 26
  validates_presence_of :name
26 27
  validates_uniqueness_of :name
27 28
  validates_length_of :name, :maximum => 30
28 29
  validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
29
  
30

  
30 31
  named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
31 32

  
32
  def after_save
33
  def update_default
33 34
    IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
34
  end  
35
  
35
  end
36

  
36 37
  # Returns the default status for new issues
37 38
  def self.default
38 39
    find(:first, :conditions =>["is_default=?", true])
39 40
  end
40
  
41

  
41 42
  # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
42 43
  def self.update_issue_done_ratios
43 44
    if Issue.use_status_for_done_ratio?
......
57 58
      role_ids = roles.collect(&:id)
58 59
      transitions = workflows.select do |w|
59 60
        role_ids.include?(w.role_id) &&
60
        w.tracker_id == tracker.id && 
61
        (author || !w.author) &&
62
        (assignee || !w.assignee)
61
        w.tracker_id == tracker.id &&
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff