Revision 441:cbce1fd3b1b7 app/models/.svn/text-base

View differences:

app/models/.svn/text-base/attachment.rb.svn-base
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.
......
20 20
class Attachment < ActiveRecord::Base
21 21
  belongs_to :container, :polymorphic => true
22 22
  belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
  
23

  
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
......
31 31
  acts_as_activity_provider :type => 'files',
32 32
                            :permission => :view_files,
33 33
                            :author_key => :author_id,
34
                            :find_options => {:select => "#{Attachment.table_name}.*", 
34
                            :find_options => {:select => "#{Attachment.table_name}.*",
35 35
                                              :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 36
                                                        "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
37
  
37

  
38 38
  acts_as_activity_provider :type => 'documents',
39 39
                            :permission => :view_documents,
40 40
                            :author_key => :author_id,
41
                            :find_options => {:select => "#{Attachment.table_name}.*", 
41
                            :find_options => {:select => "#{Attachment.table_name}.*",
42 42
                                              :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 43
                                                        "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44 44

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

  
48 48
  def validate
49 49
    if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 50
      errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
......
76 76
    if @temp_file && (@temp_file.size > 0)
77 77
      logger.debug("saving '#{self.diskfile}'")
78 78
      md5 = Digest::MD5.new
79
      File.open(diskfile, "wb") do |f| 
79
      File.open(diskfile, "wb") do |f|
80 80
        buffer = ""
81 81
        while (buffer = @temp_file.read(8192))
82 82
          f.write(buffer)
......
100 100
  def diskfile
101 101
    "#{@@storage_path}/#{self.disk_filename}"
102 102
  end
103
  
103

  
104 104
  def increment_download
105 105
    increment!(:downloads)
106 106
  end
......
108 108
  def project
109 109
    container.project
110 110
  end
111
  
111

  
112 112
  def visible?(user=User.current)
113 113
    container.attachments_visible?(user)
114 114
  end
115
  
115

  
116 116
  def deletable?(user=User.current)
117 117
    container.attachments_deletable?(user)
118 118
  end
119
  
119

  
120 120
  def image?
121 121
    self.filename =~ /\.(jpe?g|gif|png)$/i
122 122
  end
123
  
123

  
124 124
  def is_text?
125 125
    Redmine::MimeType.is_type?('text', filename)
126 126
  end
127
  
127

  
128 128
  def is_diff?
129 129
    self.filename =~ /\.(patch|diff)$/i
130 130
  end
131
  
131

  
132 132
  # Returns true if the file is readable
133 133
  def readable?
134 134
    File.readable?(diskfile)
......
145 145
      attachments.each_value do |attachment|
146 146
        file = attachment['file']
147 147
        next unless file && file.size > 0
148
        a = Attachment.create(:container => obj, 
148
        a = Attachment.create(:container => obj,
149 149
                              :file => file,
150 150
                              :description => attachment['description'].to_s.strip,
151 151
                              :author => User.current)
......
160 160
    end
161 161
    {:files => attached, :unsaved => obj.unsaved_attachments}
162 162
  end
163
  
163

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

  
171 171
    # Finally, replace all non alphanumeric, hyphens or periods with underscore
172
    @filename = just_filename.gsub(/[^\w\.\-]/,'_') 
172
    @filename = just_filename.gsub(/[^\w\.\-]/,'_')
173 173
  end
174
  
174

  
175 175
  # Returns an ASCII or hashed filename
176 176
  def self.disk_filename(filename)
177 177
    timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
app/models/.svn/text-base/change.rb.svn-base
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.
17 17

  
18 18
class Change < ActiveRecord::Base
19 19
  belongs_to :changeset
20
  
20

  
21 21
  validates_presence_of :changeset_id, :action, :path
22 22
  before_save :init_path
23
  
23

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

  
28
  def before_validation
29
    self.path      = Redmine::CodesetUtil.replace_invalid_utf8(self.path)
30
    self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path)
31
  end
32

  
28 33
  def init_path
29 34
    self.path ||= ""
30 35
  end
app/models/.svn/text-base/changeset.rb.svn-base
1 1
# Redmine - project management software
2
# Copyright (C) 2006-2010  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.
......
27 27
                :description => :long_comments,
28 28
                :datetime => :committed_on,
29 29
                :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
30
                
30

  
31 31
  acts_as_searchable :columns => 'comments',
32 32
                     :include => {:repository => :project},
33 33
                     :project_key => "#{Repository.table_name}.project_id",
34 34
                     :date_column => 'committed_on'
35
                     
35

  
36 36
  acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 37
                            :author_key => :user_id,
38 38
                            :find_options => {:include => [:user, {:repository => :project}]}
39
  
39

  
40 40
  validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 41
  validates_uniqueness_of :revision, :scope => :repository_id
42 42
  validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43
  
43

  
44 44
  named_scope :visible, lambda {|*args| { :include => {:repository => :project},
45
                                          :conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
46
                                          
45
                                          :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
46

  
47 47
  def revision=(r)
48 48
    write_attribute :revision, (r.nil? ? nil : r.to_s)
49 49
  end
......
70 70
      identifier
71 71
    end
72 72
  end
73
  
73

  
74 74
  def project
75 75
    repository.project
76 76
  end
77
  
77

  
78 78
  def author
79 79
    user || committer.to_s.split('<').first
80 80
  end
81
  
81

  
82 82
  def before_create
83 83
    self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
84
    self.comments  = self.class.normalize_comments(self.comments, repository.repo_log_encoding)
84
    self.comments  = self.class.normalize_comments(
85
                       self.comments, repository.repo_log_encoding)
85 86
    self.user = repository.find_committer_user(self.committer)
86 87
  end
87 88

  
88 89
  def after_create
89 90
    scan_comment_for_issue_ids
90 91
  end
91
  
92

  
92 93
  TIMELOG_RE = /
93 94
    (
94 95
    ((\d+)(h|hours?))((\d+)(m|min)?)?
......
100 101
    (\d+([\.,]\d+)?)h?
101 102
    )
102 103
    /x
103
  
104

  
104 105
  def scan_comment_for_issue_ids
105 106
    return if comments.blank?
106 107
    # keywords used to reference issues
......
108 109
    ref_keywords_any = ref_keywords.delete('*')
109 110
    # keywords used to fix issues
110 111
    fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
111
    
112

  
112 113
    kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
113
    
114

  
114 115
    referenced_issues = []
115
    
116

  
116 117
    comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
117 118
      action, refs = match[2], match[3]
118 119
      next unless action.present? || ref_keywords_any
119
      
120

  
120 121
      refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
121 122
        issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
122 123
        if issue
......
126 127
        end
127 128
      end
128 129
    end
129
    
130

  
130 131
    referenced_issues.uniq!
131 132
    self.issues = referenced_issues unless referenced_issues.empty?
132 133
  end
133
  
134

  
134 135
  def short_comments
135 136
    @short_comments || split_comments.first
136 137
  end
137
  
138

  
138 139
  def long_comments
139 140
    @long_comments || split_comments.last
140 141
  end
......
146 147
      "r#{revision}"
147 148
    end
148 149
  end
149
  
150

  
150 151
  # Returns the previous changeset
151 152
  def previous
152
    @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
153
    @previous ||= Changeset.find(:first,
154
                    :conditions => ['id < ? AND repository_id = ?',
155
                                    self.id, self.repository_id],
156
                    :order => 'id DESC')
153 157
  end
154 158

  
155 159
  # Returns the next changeset
156 160
  def next
157
    @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
161
    @next ||= Changeset.find(:first,
162
                    :conditions => ['id > ? AND repository_id = ?',
163
                                    self.id, self.repository_id],
164
                    :order => 'id ASC')
158 165
  end
159
  
166

  
160 167
  # Creates a new Change from it's common parameters
161 168
  def create_change(change)
162
    Change.create(:changeset => self,
163
                  :action => change[:action],
164
                  :path => change[:path],
165
                  :from_path => change[:from_path],
169
    Change.create(:changeset     => self,
170
                  :action        => change[:action],
171
                  :path          => change[:path],
172
                  :from_path     => change[:from_path],
166 173
                  :from_revision => change[:from_revision])
167 174
  end
168 175

  
......
174 181
    return nil if id.blank?
175 182
    issue = Issue.find_by_id(id.to_i, :include => :project)
176 183
    if issue
177
      unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
184
      unless issue.project &&
185
                (project == issue.project || project.is_ancestor_of?(issue.project) ||
186
                 project.is_descendant_of?(issue.project))
178 187
        issue = nil
179 188
      end
180 189
    end
181 190
    issue
182 191
  end
183
  
192

  
184 193
  def fix_issue(issue)
185 194
    status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
186 195
    if status.nil?
187 196
      logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
188 197
      return issue
189 198
    end
190
    
199

  
191 200
    # the issue may have been updated by the closure of another one (eg. duplicate)
192 201
    issue.reload
193 202
    # don't change the status is the issue is closed
194 203
    return if issue.status && issue.status.is_closed?
195
    
204

  
196 205
    journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
197 206
    issue.status = status
198 207
    unless Setting.commit_fix_done_ratio.blank?
......
205 214
    end
206 215
    issue
207 216
  end
208
  
217

  
209 218
  def log_time(issue, hours)
210 219
    time_entry = TimeEntry.new(
211 220
      :user => user,
212 221
      :hours => hours,
213 222
      :issue => issue,
214 223
      :spent_on => commit_date,
215
      :comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language)
224
      :comments => l(:text_time_logged_by_changeset, :value => text_tag,
225
                     :locale => Setting.default_language)
216 226
      )
217 227
    time_entry.activity = log_time_activity unless log_time_activity.nil?
218
    
228

  
219 229
    unless time_entry.save
220 230
      logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
221 231
    end
222 232
    time_entry
223 233
  end
224
  
234

  
225 235
  def log_time_activity
226 236
    if Setting.commit_logtime_activity_id.to_i > 0
227 237
      TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
228 238
    end
229 239
  end
230
  
240

  
231 241
  def split_comments
232 242
    comments =~ /\A(.+?)\r?\n(.*)$/m
233 243
    @short_comments = $1 || comments
......
242 252
    Changeset.to_utf8(str.to_s.strip, encoding)
243 253
  end
244 254

  
245
  private
246

  
247 255
  def self.to_utf8(str, encoding)
248
    return str if str.blank?
249
    unless encoding.blank? || encoding == 'UTF-8'
250
      begin
251
        str = Iconv.conv('UTF-8', encoding, str)
252
      rescue Iconv::Failure
253
        # do nothing here
254
      end
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
255 261
    end
262
    enc = encoding.blank? ? "UTF-8" : encoding
256 263
    if str.respond_to?(:force_encoding)
257
      str.force_encoding('UTF-8')
258
      if ! str.valid_encoding?
259
        str = str.encode("US-ASCII", :invalid => :replace,
260
              :undef => :replace, :replace => '?').encode("UTF-8")
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
261 274
      end
262 275
    else
263
      # removes invalid UTF8 sequences
276
      ic = Iconv.new('UTF-8', enc)
277
      txtar = ""
264 278
      begin
265
        str = Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + '  ')[0..-3]
266
      rescue Iconv::InvalidEncoding
267
        # "UTF-8//IGNORE" is not supported on some OS
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
268 286
      end
287
      str = txtar
269 288
    end
270 289
    str
271 290
  end
app/models/.svn/text-base/custom_field.rb.svn-base
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
......
48 48
    errors.add(:default_value, :invalid) unless v.valid?
49 49
  end
50 50
  
51
  def possible_values_options(obj=nil)
52
    case field_format
53
    when 'user', 'version'
54
      if obj.respond_to?(:project) && obj.project
55
        case field_format
56
        when 'user'
57
          obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
58
        when 'version'
59
          obj.project.versions.sort.collect {|u| [u.to_s, u.id.to_s]}
60
        end
61
      elsif obj.is_a?(Array)
62
        obj.collect {|o| possible_values_options(o)}.inject {|memo, v| memo & v}
63
      else
64
        []
65
      end
66
    else
67
      read_attribute :possible_values
68
    end
69
  end
70
  
71
  def possible_values(obj=nil)
72
    case field_format
73
    when 'user', 'version'
74
      possible_values_options(obj).collect(&:last)
75
    else
76
      read_attribute :possible_values
77
    end
78
  end
79
  
51 80
  # Makes possible_values accept a multiline string
52 81
  def possible_values=(arg)
53 82
    if arg.is_a?(Array)
......
71 100
        casted = value.to_i
72 101
      when 'float'
73 102
        casted = value.to_f
103
      when 'user', 'version'
104
        casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
74 105
      end
75 106
    end
76 107
    casted
app/models/.svn/text-base/document.rb.svn-base
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.
......
25 25
                :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
26 26
                :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
27 27
  acts_as_activity_provider :find_options => {:include => :project}
28
  
28

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

  
32 32
  named_scope :visible, lambda {|*args| { :include => :project,
33
                                          :conditions => Project.allowed_to_condition(args.first || User.current, :view_documents) } }
34
  
33
                                          :conditions => Project.allowed_to_condition(args.shift || User.current, :view_documents, *args) } }
34

  
35 35
  def visible?(user=User.current)
36 36
    !user.nil? && user.allowed_to?(:view_documents, project)
37 37
  end
38
  
38

  
39 39
  def after_initialize
40 40
    if new_record?
41 41
      self.category ||= DocumentCategory.default
42 42
    end
43 43
  end
44
  
44

  
45 45
  def updated_on
46 46
    unless @updated_on
47 47
      a = attachments.find(:first, :order => 'created_on DESC')
app/models/.svn/text-base/document_category.rb.svn-base
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/.svn/text-base/document_category_custom_field.rb.svn-base
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.
......
20 20
    :enumeration_doc_categories
21 21
  end
22 22
end
23

  
app/models/.svn/text-base/document_observer.rb.svn-base
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/.svn/text-base/enabled_module.rb.svn-base
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.
17 17

  
18 18
class EnabledModule < ActiveRecord::Base
19 19
  belongs_to :project
20
  
20

  
21 21
  validates_presence_of :name
22 22
  validates_uniqueness_of :name, :scope => :project_id
23
  
23

  
24 24
  after_create :module_enabled
25
  
25

  
26 26
  private
27
  
27

  
28 28
  # after_create callback used to do things when a module is enabled
29 29
  def module_enabled
30 30
    case name
app/models/.svn/text-base/issue.rb.svn-base
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 Issue < ActiveRecord::Base
19 19
  include Redmine::SafeAttributes
20
  
20

  
21 21
  belongs_to :project
22 22
  belongs_to :tracker
23 23
  belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
......
30 30
  has_many :journals, :as => :journalized, :dependent => :destroy
31 31
  has_many :time_entries, :dependent => :delete_all
32 32
  has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
  
33

  
34 34
  has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
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 38
  acts_as_attachable :after_remove => :attachment_removed
39 39
  acts_as_customizable
......
45 45
  acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46
                :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47
                :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
  
48

  
49 49
  acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50
                            :author_key => :author_id
51 51

  
......
60 60
  validates_numericality_of :estimated_hours, :allow_nil => true
61 61

  
62 62
  named_scope :visible, lambda {|*args| { :include => :project,
63
                                          :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
64
  
63
                                          :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64

  
65 65
  named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
66 66

  
67 67
  named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
68 68
  named_scope :with_limit, lambda { |limit| { :limit => limit} }
69 69
  named_scope :on_active_project, :include => [:status, :project, :tracker],
70 70
                                  :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
71
  named_scope :for_gantt, lambda {
72
    {
73
      :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version]
74
    }
75
  }
76 71

  
77 72
  named_scope :without_version, lambda {
78 73
    {
......
90 85
  before_save :close_duplicates, :update_done_ratio_from_issue_status
91 86
  after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
92 87
  after_destroy :update_parent_attributes
93
  
88

  
89
  # Returns a SQL conditions string used to find all issues visible by the specified user
90
  def self.visible_condition(user, options={})
91
    Project.allowed_to_condition(user, :view_issues, options) do |role, user|
92
      case role.issues_visibility
93
      when 'all'
94
        nil
95
      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
      when 'own'
98
        "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
99
      else
100
        '1=0'
101
      end
102
    end
103
  end
104

  
94 105
  # Returns true if usr or current user is allowed to view the issue
95 106
  def visible?(usr=nil)
96
    (usr || User.current).allowed_to?(:view_issues, self.project)
107
    (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
108
      case role.issues_visibility
109
      when 'all'
110
        true
111
      when 'default'
112
        !self.is_private? || self.author == user || self.assigned_to == user
113
      when 'own'
114
        self.author == user || self.assigned_to == user
115
      else
116
        false
117
      end
118
    end
97 119
  end
98
  
120

  
99 121
  def after_initialize
100 122
    if new_record?
101 123
      # set default values for new records only
......
103 125
      self.priority ||= IssuePriority.default
104 126
    end
105 127
  end
106
  
128

  
107 129
  # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
108 130
  def available_custom_fields
109
    (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
131
    (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
110 132
  end
111
  
133

  
112 134
  def copy_from(arg)
113 135
    issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
114 136
    self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
......
116 138
    self.status = issue.status
117 139
    self
118 140
  end
119
  
141

  
120 142
  # Moves/copies an issue to a new project and tracker
121 143
  # Returns the moved/copied issue on success, false on failure
122 144
  def move_to_project(*args)
......
124 146
      move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
125 147
    end || false
126 148
  end
127
  
149

  
128 150
  def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
129 151
    options ||= {}
130 152
    issue = options[:copy] ? self.class.new.copy_from(self) : self
131
    
153

  
132 154
    if new_project && issue.project_id != new_project.id
133 155
      # delete issue relations
134 156
      unless Setting.cross_project_issue_relations?
......
153 175
      issue.reset_custom_values!
154 176
    end
155 177
    if options[:copy]
178
      issue.author = User.current
156 179
      issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
157 180
      issue.status = if options[:attributes] && options[:attributes][:status_id]
158 181
                       IssueStatus.find_by_id(options[:attributes][:status_id])
......
165 188
      issue.attributes = options[:attributes]
166 189
    end
167 190
    if issue.save
168
      unless options[:copy]
191
      if options[:copy]
192
        if current_journal && current_journal.notes.present?
193
          issue.init_journal(current_journal.user, current_journal.notes)
194
          issue.current_journal.notify = false
195
          issue.save
196
        end
197
      else
169 198
        # Manually update project_id on related time entries
170 199
        TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
171
        
200

  
172 201
        issue.children.each do |child|
173 202
          unless child.move_to_project_without_transaction(new_project)
174 203
            # Move failed and transaction was rollback'd
......
186 215
    self.status = nil
187 216
    write_attribute(:status_id, sid)
188 217
  end
189
  
218

  
190 219
  def priority_id=(pid)
191 220
    self.priority = nil
192 221
    write_attribute(:priority_id, pid)
......
198 227
    @custom_field_values = nil
199 228
    result
200 229
  end
201
  
230

  
202 231
  # Overrides attributes= so that tracker_id gets assigned first
203 232
  def attributes_with_tracker_first=(new_attributes, *args)
204 233
    return if new_attributes.nil?
......
210 239
  end
211 240
  # Do not redefine alias chain on reload (see #4838)
212 241
  alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
213
  
242

  
214 243
  def estimated_hours=(h)
215 244
    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
216 245
  end
217
  
246

  
218 247
  safe_attributes 'tracker_id',
219 248
    'status_id',
220 249
    'parent_issue_id',
......
232 261
    'custom_fields',
233 262
    'lock_version',
234 263
    :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
235
  
264

  
236 265
  safe_attributes 'status_id',
237 266
    'assigned_to_id',
238 267
    'fixed_version_id',
239 268
    'done_ratio',
240 269
    :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
241 270

  
271
  safe_attributes 'is_private',
272
    :if => lambda {|issue, user|
273
      user.allowed_to?(:set_issues_private, issue.project) ||
274
        (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
275
    }
276

  
242 277
  # Safely sets attributes
243 278
  # Should be called from controllers instead of #attributes=
244 279
  # attr_accessible is too rough because we still want things like
......
246 281
  # TODO: move workflow/permission checks from controllers to here
247 282
  def safe_attributes=(attrs, user=User.current)
248 283
    return unless attrs.is_a?(Hash)
249
    
284

  
250 285
    # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
251 286
    attrs = delete_unsafe_attributes(attrs, user)
252
    return if attrs.empty? 
253
    
287
    return if attrs.empty?
288

  
254 289
    # Tracker must be set before since new_statuses_allowed_to depends on it.
255 290
    if t = attrs.delete('tracker_id')
256 291
      self.tracker_id = t
257 292
    end
258
    
293

  
259 294
    if attrs['status_id']
260 295
      unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
261 296
        attrs.delete('status_id')
262 297
      end
263 298
    end
264
    
299

  
265 300
    unless leaf?
266 301
      attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
267 302
    end
268
    
303

  
269 304
    if attrs.has_key?('parent_issue_id')
270 305
      if !user.allowed_to?(:manage_subtasks, project)
271 306
        attrs.delete('parent_issue_id')
......
273 308
        attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
274 309
      end
275 310
    end
276
    
311

  
277 312
    self.attributes = attrs
278 313
  end
279
  
314

  
280 315
  def done_ratio
281 316
    if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
282 317
      status.default_done_ratio
......
292 327
  def self.use_field_for_done_ratio?
293 328
    Setting.issue_done_ratio == 'issue_field'
294 329
  end
295
  
330

  
296 331
  def validate
297 332
    if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
298 333
      errors.add :due_date, :not_a_date
299 334
    end
300
    
335

  
301 336
    if self.due_date and self.start_date and self.due_date < self.start_date
302 337
      errors.add :due_date, :greater_than_start_date
303 338
    end
304
    
339

  
305 340
    if start_date && soonest_start && start_date < soonest_start
306 341
      errors.add :start_date, :invalid
307 342
    end
308
    
343

  
309 344
    if fixed_version
310 345
      if !assignable_versions.include?(fixed_version)
311 346
        errors.add :fixed_version_id, :inclusion
......
313 348
        errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
314 349
      end
315 350
    end
316
    
351

  
317 352
    # Checks that the issue can not be added/moved to a disabled tracker
318 353
    if project && (tracker_id_changed? || project_id_changed?)
319 354
      unless project.trackers.include?(tracker)
320 355
        errors.add :tracker_id, :inclusion
321 356
      end
322 357
    end
323
    
358

  
324 359
    # Checks parent issue assignment
325 360
    if @parent_issue
326 361
      if @parent_issue.project_id != project_id
......
337 372
      end
338 373
    end
339 374
  end
340
  
375

  
341 376
  # Set the done_ratio using the status if that setting is set.  This will keep the done_ratios
342 377
  # even if the user turns off the setting later
343 378
  def update_done_ratio_from_issue_status
......
345 380
      self.done_ratio = status.default_done_ratio
346 381
    end
347 382
  end
348
  
383

  
349 384
  def init_journal(user, notes = "")
350 385
    @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
351 386
    @issue_before_change = self.clone
......
356 391
    updated_on_will_change!
357 392
    @current_journal
358 393
  end
359
  
394

  
360 395
  # Return true if the issue is closed, otherwise false
361 396
  def closed?
362 397
    self.status.is_closed?
363 398
  end
364
  
399

  
365 400
  # Return true if the issue is being reopened
366 401
  def reopened?
367 402
    if !new_record? && status_id_changed?
......
385 420
    end
386 421
    false
387 422
  end
388
  
423

  
389 424
  # Returns true if the issue is overdue
390 425
  def overdue?
391 426
    !due_date.nil? && (due_date < Date.today) && !status.is_closed?
......
402 437
  def children?
403 438
    !leaf?
404 439
  end
405
  
440

  
406 441
  # Users the issue can be assigned to
407 442
  def assignable_users
408 443
    users = project.assignable_users
409 444
    users << author if author
410 445
    users.uniq.sort
411 446
  end
412
  
447

  
413 448
  # Versions that the issue can be assigned to
414 449
  def assignable_versions
415 450
    @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
416 451
  end
417
  
452

  
418 453
  # Returns true if this issue is blocked by another issue that is still open
419 454
  def blocked?
420 455
    !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
421 456
  end
422
  
457

  
423 458
  # Returns an array of status that user is able to apply
424 459
  def new_statuses_allowed_to(user, include_default=false)
425 460
    statuses = status.find_new_statuses_allowed_to(
......
433 468
    statuses = statuses.uniq.sort
434 469
    blocked? ? statuses.reject {|s| s.is_closed?} : statuses
435 470
  end
436
  
471

  
437 472
  # Returns the mail adresses of users that should be notified
438 473
  def recipients
439 474
    notified = project.notified_users
......
446 481
    notified.reject! {|user| !visible?(user)}
447 482
    notified.collect(&:mail)
448 483
  end
449
  
484

  
450 485
  # Returns the total number of hours spent on this issue and its descendants
451 486
  #
452 487
  # Example:
......
455 490
  def spent_hours
456 491
    @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
457 492
  end
458
  
493

  
459 494
  def relations
460 495
    (relations_from + relations_to).sort
461 496
  end
462
  
463
  def all_dependent_issues(except=nil)
464
    except ||= self
497

  
498
  def all_dependent_issues(except=[])
499
    except << self
465 500
    dependencies = []
466 501
    relations_from.each do |relation|
467
      if relation.issue_to && relation.issue_to != except
502
      if relation.issue_to && !except.include?(relation.issue_to)
468 503
        dependencies << relation.issue_to
469 504
        dependencies += relation.issue_to.all_dependent_issues(except)
470 505
      end
471 506
    end
472 507
    dependencies
473 508
  end
474
  
509

  
475 510
  # Returns an array of issues that duplicate this one
476 511
  def duplicates
477 512
    relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
478 513
  end
479
  
514

  
480 515
  # Returns the due date or the target due date if any
481 516
  # Used on gantt chart
482 517
  def due_before
483 518
    due_date || (fixed_version ? fixed_version.effective_date : nil)
484 519
  end
485
  
520

  
486 521
  # Returns the time scheduled for this issue.
487
  # 
522
  #
488 523
  # Example:
489 524
  #   Start Date: 2/26/09, End Date: 3/04/09
490 525
  #   duration => 6
491 526
  def duration
492 527
    (start_date && due_date) ? due_date - start_date : 0
493 528
  end
494
  
529

  
495 530
  def soonest_start
496 531
    @soonest_start ||= (
497 532
        relations_to.collect{|relation| relation.successor_soonest_start} +
498 533
        ancestors.collect(&:soonest_start)
499 534
      ).compact.max
500 535
  end
501
  
536

  
502 537
  def reschedule_after(date)
503 538
    return if date.nil?
504 539
    if leaf?
......
512 547
      end
513 548
    end
514 549
  end
515
  
550

  
516 551
  def <=>(issue)
517 552
    if issue.nil?
518 553
      -1
......
522 557
      (lft || 0) <=> (issue.lft || 0)
523 558
    end
524 559
  end
525
  
560

  
526 561
  def to_s
527 562
    "#{tracker} ##{id}: #{subject}"
528 563
  end
529
  
564

  
530 565
  # Returns a string of css classes that apply to the issue
531 566
  def css_classes
532 567
    s = "issue status-#{status.position} priority-#{priority.position}"
533 568
    s << ' closed' if closed?
534 569
    s << ' overdue' if overdue?
570
    s << ' child' if child?
571
    s << ' parent' unless leaf?
572
    s << ' private' if is_private?
535 573
    s << ' created-by-me' if User.current.logged? && author_id == User.current.id
536 574
    s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
537 575
    s
......
541 579
  # Returns false if save fails
542 580
  def save_issue_with_child_records(params, existing_time_entry=nil)
543 581
    Issue.transaction do
544
      if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
582
      if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
545 583
        @time_entry = existing_time_entry || TimeEntry.new
546 584
        @time_entry.project = project
547 585
        @time_entry.issue = self
......
550 588
        @time_entry.attributes = params[:time_entry]
551 589
        self.time_entries << @time_entry
552 590
      end
553
  
591

  
554 592
      if valid?
555 593
        attachments = Attachment.attach_files(self, params[:attachments])
556
  
594

  
557 595
        attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
558 596
        # TODO: Rename hook
559 597
        Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
......
578 616
    # Update issues assigned to the version
579 617
    update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
580 618
  end
581
  
619

  
582 620
  # Unassigns issues from versions that are no longer shared
583 621
  # after +project+ was moved
584 622
  def self.update_versions_from_hierarchy_change(project)
......
596 634
      nil
597 635
    end
598 636
  end
599
  
637

  
600 638
  def parent_issue_id
601 639
    if instance_variable_defined? :@parent_issue
602 640
      @parent_issue.nil? ? nil : @parent_issue.id
......
645 683
  def self.by_subproject(project)
646 684
    ActiveRecord::Base.connection.select_all("select    s.id as status_id, 
647 685
                                                s.is_closed as closed, 
648
                                                i.project_id as project_id,
649
                                                count(i.id) as total 
686
                                                #{Issue.table_name}.project_id as project_id,
687
                                                count(#{Issue.table_name}.id) as total 
650 688
                                              from 
651
                                                #{Issue.table_name} i, #{IssueStatus.table_name} s
689
                                                #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
652 690
                                              where 
653
                                                i.status_id=s.id 
654
                                                and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
655
                                              group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
691
                                                #{Issue.table_name}.status_id=s.id
692
                                                and #{Issue.table_name}.project_id = #{Project.table_name}.id
693
                                                and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
694
                                                and #{Issue.table_name}.project_id <> #{project.id}
695
                                              group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
656 696
  end
657 697
  # End ReportsController extraction
658
  
698

  
659 699
  # Returns an array of projects that current user can move issues to
660 700
  def self.allowed_target_projects_on_move
661 701
    projects = []
......
671 711
    end
672 712
    projects
673 713
  end
674
   
714

  
675 715
  private
676
  
716

  
677 717
  def update_nested_set_attributes
678 718
    if root_id.nil?
679 719
      # issue was just created
......
720 760
    end
721 761
    remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
722 762
  end
723
  
763

  
724 764
  def update_parent_attributes
725 765
    recalculate_attributes_for(parent_id) if parent_id
726 766
  end
......
731 771
      if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
732 772
        p.priority = IssuePriority.find_by_position(priority_position)
733 773
      end
734
      
774

  
735 775
      # start/due dates = lowest/highest dates of children
736 776
      p.start_date = p.children.minimum(:start_date)
737 777
      p.due_date = p.children.maximum(:due_date)
738 778
      if p.start_date && p.due_date && p.due_date < p.start_date
739 779
        p.start_date, p.due_date = p.due_date, p.start_date
740 780
      end
741
      
781

  
742 782
      # done ratio = weighted average ratio of leaves
743 783
      unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
744 784
        leaves_count = p.leaves.count
......
752 792
          p.done_ratio = progress.round
753 793
        end
754 794
      end
755
      
795

  
756 796
      # estimate = sum of leaves estimates
757 797
      p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
758 798
      p.estimated_hours = nil if p.estimated_hours == 0.0
759
      
799

  
760 800
      # ancestors will be recursively updated
761 801
      p.save(false)
762 802
    end
763 803
  end
764
  
804

  
765 805
  # Update issues so their versions are not pointing to a
766 806
  # fixed_version that is not shared with the issue's project
767 807
  def self.update_versions(conditions=nil)
......
781 821
      end
782 822
    end
783 823
  end
784
  
824

  
785 825
  # Callback on attachment deletion
786 826
  def attachment_removed(obj)
787 827
    journal = init_journal(User.current)
......
790 830
                                         :old_value => obj.filename)
791 831
    journal.save
792 832
  end
793
  
833

  
794 834
  # Default assignment based on category
795 835
  def default_assign
796 836
    if assigned_to.nil? && category && category.assigned_to
......
823 863
      end
824 864
    end
825 865
  end
826
  
866

  
827 867
  # Saves the changes in a Journal
828 868
  # Called after_save
829 869
  def create_journal
......
839 879
      custom_values.each {|c|
840 880
        next if (@custom_values_before_change[c.custom_field_id]==c.value ||
841 881
                  (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
842
        @current_journal.details << JournalDetail.new(:property => 'cf', 
882
        @current_journal.details << JournalDetail.new(:property => 'cf',
843 883
                                                      :prop_key => c.custom_field_id,
844 884
                                                      :old_value => @custom_values_before_change[c.custom_field_id],
845 885
                                                      :value => c.value)
846
      }      
886
      }
847 887
      @current_journal.save
848 888
      # reset current journal
849 889
      init_journal @current_journal.user, @current_journal.notes
......
862 902
    select_field = options.delete(:field)
863 903
    joins = options.delete(:joins)
864 904

  
865
    where = "i.#{select_field}=j.id"
866
    
905
    where = "#{Issue.table_name}.#{select_field}=j.id"
906

  
867 907
    ActiveRecord::Base.connection.select_all("select    s.id as status_id, 
868 908
                                                s.is_closed as closed, 
869 909
                                                j.id as #{select_field},
870
                                                count(i.id) as total 
910
                                                count(#{Issue.table_name}.id) as total 
871 911
                                              from 
872
                                                  #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
912
                                                  #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
873 913
                                              where 
874
                                                i.status_id=s.id 
914
                                                #{Issue.table_name}.status_id=s.id 
875 915
                                                and #{where}
876
                                                and i.project_id=#{project.id}
916
                                                and #{Issue.table_name}.project_id=#{Project.table_name}.id
917
                                                and #{visible_condition(User.current, :project => project)}
877 918
                                              group by s.id, s.is_closed, j.id")
878 919
  end
879
  
880

  
881 920
end
app/models/.svn/text-base/journal.rb.svn-base
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
......
32 32
                :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
33 33

  
34 34
  acts_as_activity_provider :type => 'issues',
35
                            :permission => :view_issues,
36 35
                            :author_key => :user_id,
37 36
                            :find_options => {:include => [{:issue => :project}, :details, :user],
38 37
                                              :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
39 38
                                                             " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
40 39
  
40
  named_scope :visible, lambda {|*args| {
41
    :include => {:issue => :project},
42
    :conditions => Issue.visible_condition(args.shift || User.current, *args)
43
  }}
44
  
41 45
  def save(*args)
42 46
    # Do not save an empty journal
43 47
    (details.empty? && notes.blank?) ? false : super
......
73 77
    s << ' has-details' unless details.blank?
74 78
    s
75 79
  end
80
  
81
  def notify?
82
    @notify != false
83
  end
84
  
85
  def notify=(arg)
86
    @notify = arg
87
  end
76 88
end
app/models/.svn/text-base/journal_detail.rb.svn-base
17 17

  
18 18
class JournalDetail < ActiveRecord::Base
19 19
  belongs_to :journal
20
  before_save :normalize_values
21
  
22
  private
23
  
24
  def normalize_values
25
    self.value = normalize(value)
26
    self.old_value = normalize(old_value)
27
  end
28
  
29
  def normalize(v)
30
    if v == true
31
      "1"
32
    elsif v == false
33
      "0"
34
    else
35
      v
36
    end
37
  end
20 38
end
app/models/.svn/text-base/journal_observer.rb.svn-base
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
......
17 17

  
18 18
class JournalObserver < ActiveRecord::Observer
19 19
  def after_create(journal)
20
    if Setting.notified_events.include?('issue_updated') ||
21
        (Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
22
        (Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
23
        (Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
20
    if journal.notify? &&
21
        (Setting.notified_events.include?('issue_updated') ||
22
          (Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
23
          (Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
24
          (Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
25
        )
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff