Revision 441:cbce1fd3b1b7 app/models/.svn/text-base
| 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 |
) |
|
Also available in: Unified diff