Revision 912:5e80956cc792 app/models
| app/models/attachment.rb | ||
|---|---|---|
| 24 | 24 |
validates_presence_of :container, :filename, :author |
| 25 | 25 |
validates_length_of :filename, :maximum => 255 |
| 26 | 26 |
validates_length_of :disk_filename, :maximum => 255 |
| 27 |
validate :validate_max_file_size |
|
| 27 | 28 |
|
| 28 | 29 |
acts_as_event :title => :filename, |
| 29 | 30 |
:url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
|
| ... | ... | |
| 43 | 44 |
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
|
| 44 | 45 |
|
| 45 | 46 |
cattr_accessor :storage_path |
| 46 |
@@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{RAILS_ROOT}/files"
|
|
| 47 |
@@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
|
|
| 47 | 48 |
|
| 48 |
def validate |
|
| 49 |
before_save :files_to_final_location |
|
| 50 |
after_destroy :delete_from_disk |
|
| 51 |
|
|
| 52 |
def validate_max_file_size |
|
| 49 | 53 |
if self.filesize > Setting.attachment_max_size.to_i.kilobytes |
| 50 | 54 |
errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes) |
| 51 | 55 |
end |
| ... | ... | |
| 72 | 76 |
|
| 73 | 77 |
# Copies the temporary file to its final location |
| 74 | 78 |
# and computes its MD5 hash |
| 75 |
def before_save
|
|
| 79 |
def files_to_final_location
|
|
| 76 | 80 |
if @temp_file && (@temp_file.size > 0) |
| 77 |
logger.debug("saving '#{self.diskfile}'")
|
|
| 81 |
logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
|
|
| 78 | 82 |
md5 = Digest::MD5.new |
| 79 | 83 |
File.open(diskfile, "wb") do |f| |
| 80 | 84 |
buffer = "" |
| ... | ... | |
| 85 | 89 |
end |
| 86 | 90 |
self.digest = md5.hexdigest |
| 87 | 91 |
end |
| 92 |
@temp_file = nil |
|
| 88 | 93 |
# Don't save the content type if it's longer than the authorized length |
| 89 | 94 |
if self.content_type && self.content_type.length > 255 |
| 90 | 95 |
self.content_type = nil |
| ... | ... | |
| 92 | 97 |
end |
| 93 | 98 |
|
| 94 | 99 |
# Deletes file on the disk |
| 95 |
def after_destroy
|
|
| 100 |
def delete_from_disk
|
|
| 96 | 101 |
File.delete(diskfile) if !filename.blank? && File.exist?(diskfile) |
| 97 | 102 |
end |
| 98 | 103 |
|
| ... | ... | |
| 118 | 123 |
end |
| 119 | 124 |
|
| 120 | 125 |
def image? |
| 121 |
self.filename =~ /\.(jpe?g|gif|png)$/i
|
|
| 126 |
self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
|
|
| 122 | 127 |
end |
| 123 | 128 |
|
| 124 | 129 |
def is_text? |
| ... | ... | |
| 149 | 154 |
:file => file, |
| 150 | 155 |
:description => attachment['description'].to_s.strip, |
| 151 | 156 |
:author => User.current) |
| 157 |
obj.attachments << a |
|
| 152 | 158 |
|
| 153 | 159 |
if a.new_record? |
| 154 | 160 |
obj.unsaved_attachments ||= [] |
| ... | ... | |
| 161 | 167 |
{:files => attached, :unsaved => obj.unsaved_attachments}
|
| 162 | 168 |
end |
| 163 | 169 |
|
| 170 |
def self.latest_attach(attachments, filename) |
|
| 171 |
attachments.sort_by(&:created_on).reverse.detect {
|
|
| 172 |
|att| att.filename.downcase == filename.downcase |
|
| 173 |
} |
|
| 174 |
end |
|
| 175 |
|
|
| 164 | 176 |
private |
| 165 | 177 |
def sanitize_filename(value) |
| 166 | 178 |
# get only the filename, not the whole path |
| 167 | 179 |
just_filename = value.gsub(/^.*(\\|\/)/, '') |
| 168 |
# NOTE: File.basename doesn't work right with Windows paths on Unix |
|
| 169 |
# INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
|
|
| 170 | 180 |
|
| 171 |
# Finally, replace all non alphanumeric, hyphens or periods with underscore
|
|
| 172 |
@filename = just_filename.gsub(/[^\w\.\-]/,'_')
|
|
| 181 |
# Finally, replace invalid characters with underscore
|
|
| 182 |
@filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
|
|
| 173 | 183 |
end |
| 174 | 184 |
|
| 175 | 185 |
# Returns an ASCII or hashed filename |
| app/models/auth_source.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006 Jean-Philippe Lang |
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 17 | 17 |
|
| 18 | 18 |
class AuthSource < ActiveRecord::Base |
| 19 | 19 |
include Redmine::Ciphering |
| 20 |
|
|
| 20 |
|
|
| 21 | 21 |
has_many :users |
| 22 |
|
|
| 22 |
|
|
| 23 | 23 |
validates_presence_of :name |
| 24 | 24 |
validates_uniqueness_of :name |
| 25 | 25 |
validates_length_of :name, :maximum => 60 |
| 26 | 26 |
|
| 27 | 27 |
def authenticate(login, password) |
| 28 | 28 |
end |
| 29 |
|
|
| 29 |
|
|
| 30 | 30 |
def test_connection |
| 31 | 31 |
end |
| 32 |
|
|
| 32 |
|
|
| 33 | 33 |
def auth_method_name |
| 34 | 34 |
"Abstract" |
| 35 | 35 |
end |
| 36 |
|
|
| 36 |
|
|
| 37 | 37 |
def account_password |
| 38 | 38 |
read_ciphered_attribute(:account_password) |
| 39 | 39 |
end |
| 40 |
|
|
| 40 |
|
|
| 41 | 41 |
def account_password=(arg) |
| 42 | 42 |
write_ciphered_attribute(:account_password, arg) |
| 43 | 43 |
end |
| app/models/auth_source_ldap.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006 Jean-Philippe Lang |
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| ... | ... | |
| 18 | 18 |
require 'net/ldap' |
| 19 | 19 |
require 'iconv' |
| 20 | 20 |
|
| 21 |
class AuthSourceLdap < AuthSource
|
|
| 21 |
class AuthSourceLdap < AuthSource |
|
| 22 | 22 |
validates_presence_of :host, :port, :attr_login |
| 23 | 23 |
validates_length_of :name, :host, :maximum => 60, :allow_nil => true |
| 24 | 24 |
validates_length_of :account, :account_password, :base_dn, :maximum => 255, :allow_nil => true |
| 25 | 25 |
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true |
| 26 | 26 |
validates_numericality_of :port, :only_integer => true |
| 27 |
|
|
| 27 |
|
|
| 28 | 28 |
before_validation :strip_ldap_attributes |
| 29 |
|
|
| 29 |
|
|
| 30 | 30 |
def after_initialize |
| 31 | 31 |
self.port = 389 if self.port == 0 |
| 32 | 32 |
end |
| 33 |
|
|
| 33 |
|
|
| 34 | 34 |
def authenticate(login, password) |
| 35 | 35 |
return nil if login.blank? || password.blank? |
| 36 | 36 |
attrs = get_user_dn(login) |
| 37 |
|
|
| 37 |
|
|
| 38 | 38 |
if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password) |
| 39 | 39 |
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
|
| 40 | 40 |
return attrs.except(:dn) |
| ... | ... | |
| 50 | 50 |
rescue Net::LDAP::LdapError => text |
| 51 | 51 |
raise "LdapError: " + text |
| 52 | 52 |
end |
| 53 |
|
|
| 53 |
|
|
| 54 | 54 |
def auth_method_name |
| 55 | 55 |
"LDAP" |
| 56 | 56 |
end |
| 57 |
|
|
| 57 |
|
|
| 58 | 58 |
private |
| 59 |
|
|
| 59 |
|
|
| 60 | 60 |
def strip_ldap_attributes |
| 61 | 61 |
[:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr| |
| 62 | 62 |
write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil? |
| 63 | 63 |
end |
| 64 | 64 |
end |
| 65 |
|
|
| 65 |
|
|
| 66 | 66 |
def initialize_ldap_con(ldap_user, ldap_password) |
| 67 | 67 |
options = { :host => self.host,
|
| 68 | 68 |
:port => self.port, |
| ... | ... | |
| 102 | 102 |
# Get the user's dn and any attributes for them, given their login |
| 103 | 103 |
def get_user_dn(login) |
| 104 | 104 |
ldap_con = initialize_ldap_con(self.account, self.account_password) |
| 105 |
login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
|
|
| 106 |
object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
|
|
| 105 |
login_filter = Net::LDAP::Filter.eq( self.attr_login, login ) |
|
| 106 |
object_filter = Net::LDAP::Filter.eq( "objectClass", "*" ) |
|
| 107 | 107 |
attrs = {}
|
| 108 |
|
|
| 109 |
ldap_con.search( :base => self.base_dn,
|
|
| 110 |
:filter => object_filter & login_filter,
|
|
| 108 |
|
|
| 109 |
ldap_con.search( :base => self.base_dn, |
|
| 110 |
:filter => object_filter & login_filter, |
|
| 111 | 111 |
:attributes=> search_attributes) do |entry| |
| 112 | 112 |
|
| 113 | 113 |
if onthefly_register? |
| ... | ... | |
| 121 | 121 |
|
| 122 | 122 |
attrs |
| 123 | 123 |
end |
| 124 |
|
|
| 124 |
|
|
| 125 | 125 |
def self.get_attr(entry, attr_name) |
| 126 | 126 |
if !attr_name.blank? |
| 127 | 127 |
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] |
| app/models/board.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| ... | ... | |
| 22 | 22 |
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id |
| 23 | 23 |
acts_as_list :scope => :project_id |
| 24 | 24 |
acts_as_watchable |
| 25 |
|
|
| 25 |
|
|
| 26 | 26 |
validates_presence_of :name, :description |
| 27 | 27 |
validates_length_of :name, :maximum => 30 |
| 28 | 28 |
validates_length_of :description, :maximum => 255 |
| 29 |
|
|
| 29 |
|
|
| 30 |
named_scope :visible, lambda {|*args| { :include => :project,
|
|
| 31 |
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } } |
|
| 32 |
|
|
| 30 | 33 |
def visible?(user=User.current) |
| 31 | 34 |
!user.nil? && user.allowed_to?(:view_messages, project) |
| 32 | 35 |
end |
| 33 |
|
|
| 36 |
|
|
| 34 | 37 |
def to_s |
| 35 | 38 |
name |
| 36 | 39 |
end |
| 37 |
|
|
| 40 |
|
|
| 38 | 41 |
def reset_counters! |
| 39 | 42 |
self.class.reset_counters!(id) |
| 40 | 43 |
end |
| 41 |
|
|
| 44 |
|
|
| 42 | 45 |
# Updates topics_count, messages_count and last_message_id attributes for +board_id+ |
| 43 | 46 |
def self.reset_counters!(board_id) |
| 44 | 47 |
board_id = board_id.to_i |
| app/models/change.rb | ||
|---|---|---|
| 20 | 20 |
|
| 21 | 21 |
validates_presence_of :changeset_id, :action, :path |
| 22 | 22 |
before_save :init_path |
| 23 |
before_validation :replace_invalid_utf8_of_path |
|
| 23 | 24 |
|
| 24 | 25 |
def relative_path |
| 25 | 26 |
changeset.repository.relative_path(path) |
| 26 | 27 |
end |
| 27 | 28 |
|
| 28 |
def before_validation
|
|
| 29 |
def replace_invalid_utf8_of_path
|
|
| 29 | 30 |
self.path = Redmine::CodesetUtil.replace_invalid_utf8(self.path) |
| 30 | 31 |
self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path) |
| 31 | 32 |
end |
| app/models/changeset.rb | ||
|---|---|---|
| 22 | 22 |
belongs_to :user |
| 23 | 23 |
has_many :changes, :dependent => :delete_all |
| 24 | 24 |
has_and_belongs_to_many :issues |
| 25 |
has_and_belongs_to_many :parents, |
|
| 26 |
:class_name => "Changeset", |
|
| 27 |
:join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
|
|
| 28 |
:association_foreign_key => 'parent_id', :foreign_key => 'changeset_id' |
|
| 29 |
has_and_belongs_to_many :children, |
|
| 30 |
:class_name => "Changeset", |
|
| 31 |
:join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
|
|
| 32 |
:association_foreign_key => 'changeset_id', :foreign_key => 'parent_id' |
|
| 25 | 33 |
|
| 26 | 34 |
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
|
| 27 | 35 |
:description => :long_comments, |
| ... | ... | |
| 44 | 52 |
named_scope :visible, lambda {|*args| { :include => {:repository => :project},
|
| 45 | 53 |
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } } |
| 46 | 54 |
|
| 55 |
after_create :scan_for_issues |
|
| 56 |
before_create :before_create_cs |
|
| 57 |
|
|
| 47 | 58 |
def revision=(r) |
| 48 | 59 |
write_attribute :revision, (r.nil? ? nil : r.to_s) |
| 49 | 60 |
end |
| ... | ... | |
| 79 | 90 |
user || committer.to_s.split('<').first
|
| 80 | 91 |
end |
| 81 | 92 |
|
| 82 |
def before_create |
|
| 93 |
def before_create_cs
|
|
| 83 | 94 |
self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding) |
| 84 | 95 |
self.comments = self.class.normalize_comments( |
| 85 | 96 |
self.comments, repository.repo_log_encoding) |
| 86 | 97 |
self.user = repository.find_committer_user(self.committer) |
| 87 | 98 |
end |
| 88 | 99 |
|
| 89 |
def after_create
|
|
| 100 |
def scan_for_issues
|
|
| 90 | 101 |
scan_comment_for_issue_ids |
| 91 | 102 |
end |
| 92 | 103 |
|
| ... | ... | |
| 193 | 204 |
def fix_issue(issue) |
| 194 | 205 |
status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i) |
| 195 | 206 |
if status.nil? |
| 196 |
logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
|
|
| 207 |
logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
|
|
| 197 | 208 |
return issue |
| 198 | 209 |
end |
| 199 | 210 |
|
| ... | ... | |
| 253 | 264 |
end |
| 254 | 265 |
|
| 255 | 266 |
def self.to_utf8(str, encoding) |
| 256 |
return str if str.nil? |
|
| 257 |
str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
|
|
| 258 |
if str.empty? |
|
| 259 |
str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
|
|
| 260 |
return str |
|
| 261 |
end |
|
| 262 |
enc = encoding.blank? ? "UTF-8" : encoding |
|
| 263 |
if str.respond_to?(:force_encoding) |
|
| 264 |
if enc.upcase != "UTF-8" |
|
| 265 |
str.force_encoding(enc) |
|
| 266 |
str = str.encode("UTF-8", :invalid => :replace,
|
|
| 267 |
:undef => :replace, :replace => '?') |
|
| 268 |
else |
|
| 269 |
str.force_encoding("UTF-8")
|
|
| 270 |
if ! str.valid_encoding? |
|
| 271 |
str = str.encode("US-ASCII", :invalid => :replace,
|
|
| 272 |
:undef => :replace, :replace => '?').encode("UTF-8")
|
|
| 273 |
end |
|
| 274 |
end |
|
| 275 |
else |
|
| 276 |
ic = Iconv.new('UTF-8', enc)
|
|
| 277 |
txtar = "" |
|
| 278 |
begin |
|
| 279 |
txtar += ic.iconv(str) |
|
| 280 |
rescue Iconv::IllegalSequence |
|
| 281 |
txtar += $!.success |
|
| 282 |
str = '?' + $!.failed[1,$!.failed.length] |
|
| 283 |
retry |
|
| 284 |
rescue |
|
| 285 |
txtar += $!.success |
|
| 286 |
end |
|
| 287 |
str = txtar |
|
| 288 |
end |
|
| 289 |
str |
|
| 267 |
Redmine::CodesetUtil.to_utf8(str, encoding) |
|
| 290 | 268 |
end |
| 291 | 269 |
end |
| app/models/changeset.rb.rej | ||
|---|---|---|
| 1 |
--- app/models/changeset.rb |
|
| 2 |
+++ app/models/changeset.rb |
|
| 3 |
@@ -152,6 +152,15 @@ |
|
| 4 |
def self.normalize_comments(str) |
|
| 5 |
to_utf8(str.to_s.strip) |
|
| 6 |
end |
|
| 7 |
+ |
|
| 8 |
+ # Creates a new Change from it's common parameters |
|
| 9 |
+ def create_change(change) |
|
| 10 |
+ Change.create(:changeset => self, |
|
| 11 |
+ :action => change[:action], |
|
| 12 |
+ :path => change[:path], |
|
| 13 |
+ :from_path => change[:from_path], |
|
| 14 |
+ :from_revision => change[:from_revision]) |
|
| 15 |
+ end |
|
| 16 |
|
|
| 17 |
private |
|
| 18 |
|
|
| app/models/comment.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006 Jean-Philippe Lang |
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| app/models/comment_observer.rb | ||
|---|---|---|
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| app/models/custom_field.rb | ||
|---|---|---|
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| ... | ... | |
| 19 | 19 |
has_many :custom_values, :dependent => :delete_all |
| 20 | 20 |
acts_as_list :scope => 'type = \'#{self.class}\''
|
| 21 | 21 |
serialize :possible_values |
| 22 |
|
|
| 22 |
|
|
| 23 | 23 |
validates_presence_of :name, :field_format |
| 24 | 24 |
validates_uniqueness_of :name, :scope => :type |
| 25 | 25 |
validates_length_of :name, :maximum => 30 |
| 26 | 26 |
validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats |
| 27 | 27 |
|
| 28 |
validate :validate_values |
|
| 29 |
|
|
| 28 | 30 |
def initialize(attributes = nil) |
| 29 | 31 |
super |
| 30 | 32 |
self.possible_values ||= [] |
| 31 | 33 |
end |
| 32 |
|
|
| 34 |
|
|
| 33 | 35 |
def before_validation |
| 34 | 36 |
# make sure these fields are not searchable |
| 35 | 37 |
self.searchable = false if %w(int float date bool).include?(field_format) |
| 36 | 38 |
true |
| 37 | 39 |
end |
| 38 |
|
|
| 39 |
def validate |
|
| 40 |
|
|
| 41 |
def validate_values
|
|
| 40 | 42 |
if self.field_format == "list" |
| 41 | 43 |
errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty? |
| 42 | 44 |
errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array |
| 43 | 45 |
end |
| 44 |
|
|
| 46 |
|
|
| 47 |
if regexp.present? |
|
| 48 |
begin |
|
| 49 |
Regexp.new(regexp) |
|
| 50 |
rescue |
|
| 51 |
errors.add(:regexp, :invalid) |
|
| 52 |
end |
|
| 53 |
end |
|
| 54 |
|
|
| 45 | 55 |
# validate default value |
| 46 | 56 |
v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil) |
| 47 | 57 |
v.custom_field.is_required = false |
| 48 | 58 |
errors.add(:default_value, :invalid) unless v.valid? |
| 49 | 59 |
end |
| 50 |
|
|
| 60 |
|
|
| 51 | 61 |
def possible_values_options(obj=nil) |
| 52 | 62 |
case field_format |
| 53 | 63 |
when 'user', 'version' |
| ... | ... | |
| 56 | 66 |
when 'user' |
| 57 | 67 |
obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
|
| 58 | 68 |
when 'version' |
| 59 |
obj.project.versions.sort.collect {|u| [u.to_s, u.id.to_s]}
|
|
| 69 |
obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
|
|
| 60 | 70 |
end |
| 61 | 71 |
elsif obj.is_a?(Array) |
| 62 | 72 |
obj.collect {|o| possible_values_options(o)}.inject {|memo, v| memo & v}
|
| ... | ... | |
| 67 | 77 |
read_attribute :possible_values |
| 68 | 78 |
end |
| 69 | 79 |
end |
| 70 |
|
|
| 80 |
|
|
| 71 | 81 |
def possible_values(obj=nil) |
| 72 | 82 |
case field_format |
| 73 | 83 |
when 'user', 'version' |
| ... | ... | |
| 76 | 86 |
read_attribute :possible_values |
| 77 | 87 |
end |
| 78 | 88 |
end |
| 79 |
|
|
| 89 |
|
|
| 80 | 90 |
# Makes possible_values accept a multiline string |
| 81 | 91 |
def possible_values=(arg) |
| 82 | 92 |
if arg.is_a?(Array) |
| ... | ... | |
| 85 | 95 |
self.possible_values = arg.to_s.split(/[\n\r]+/) |
| 86 | 96 |
end |
| 87 | 97 |
end |
| 88 |
|
|
| 98 |
|
|
| 89 | 99 |
def cast_value(value) |
| 90 | 100 |
casted = nil |
| 91 | 101 |
unless value.blank? |
| ... | ... | |
| 106 | 116 |
end |
| 107 | 117 |
casted |
| 108 | 118 |
end |
| 109 |
|
|
| 119 |
|
|
| 110 | 120 |
# Returns a ORDER BY clause that can used to sort customized |
| 111 | 121 |
# objects by their value of the custom field. |
| 112 | 122 |
# Returns false, if the custom field can not be used for sorting. |
| ... | ... | |
| 114 | 124 |
case field_format |
| 115 | 125 |
when 'string', 'text', 'list', 'date', 'bool' |
| 116 | 126 |
# COALESCE is here to make sure that blank and NULL values are sorted equally |
| 117 |
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
|
|
| 127 |
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
|
|
| 118 | 128 |
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
|
| 119 | 129 |
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
| 120 | 130 |
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
|
| ... | ... | |
| 122 | 132 |
# Make the database cast values into numeric |
| 123 | 133 |
# Postgresql will raise an error if a value can not be casted! |
| 124 | 134 |
# CustomValue validations should ensure that it doesn't occur |
| 125 |
"(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
|
|
| 135 |
"(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
|
|
| 126 | 136 |
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
|
| 127 | 137 |
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
| 128 | 138 |
" AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
|
| ... | ... | |
| 134 | 144 |
def <=>(field) |
| 135 | 145 |
position <=> field.position |
| 136 | 146 |
end |
| 137 |
|
|
| 147 |
|
|
| 138 | 148 |
def self.customized_class |
| 139 | 149 |
self.name =~ /^(.+)CustomField$/ |
| 140 | 150 |
begin; $1.constantize; rescue nil; end |
| 141 | 151 |
end |
| 142 |
|
|
| 152 |
|
|
| 143 | 153 |
# to move in project_custom_field |
| 144 | 154 |
def self.for_all |
| 145 | 155 |
find(:all, :conditions => ["is_for_all=?", true], :order => 'position') |
| 146 | 156 |
end |
| 147 |
|
|
| 157 |
|
|
| 148 | 158 |
def type_name |
| 149 | 159 |
nil |
| 150 | 160 |
end |
| app/models/custom_value.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006 Jean-Philippe Lang |
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| ... | ... | |
| 19 | 19 |
belongs_to :custom_field |
| 20 | 20 |
belongs_to :customized, :polymorphic => true |
| 21 | 21 |
|
| 22 |
validate :validate_custom_value |
|
| 23 |
|
|
| 22 | 24 |
def after_initialize |
| 23 | 25 |
if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?)) |
| 24 | 26 |
self.value ||= custom_field.default_value |
| 25 | 27 |
end |
| 26 | 28 |
end |
| 27 |
|
|
| 29 |
|
|
| 28 | 30 |
# Returns true if the boolean custom value is true |
| 29 | 31 |
def true? |
| 30 | 32 |
self.value == '1' |
| 31 | 33 |
end |
| 32 |
|
|
| 34 |
|
|
| 33 | 35 |
def editable? |
| 34 | 36 |
custom_field.editable? |
| 35 | 37 |
end |
| 36 |
|
|
| 38 |
|
|
| 37 | 39 |
def visible? |
| 38 | 40 |
custom_field.visible? |
| 39 | 41 |
end |
| 40 |
|
|
| 42 |
|
|
| 41 | 43 |
def required? |
| 42 | 44 |
custom_field.is_required? |
| 43 | 45 |
end |
| 44 |
|
|
| 46 |
|
|
| 45 | 47 |
def to_s |
| 46 | 48 |
value.to_s |
| 47 | 49 |
end |
| 48 |
|
|
| 50 |
|
|
| 49 | 51 |
protected |
| 50 |
def validate |
|
| 52 |
def validate_custom_value
|
|
| 51 | 53 |
if value.blank? |
| 52 |
errors.add(:value, :blank) if custom_field.is_required? and value.blank?
|
|
| 54 |
errors.add(:value, :blank) if custom_field.is_required? and value.blank? |
|
| 53 | 55 |
else |
| 54 | 56 |
errors.add(:value, :invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp) |
| 55 | 57 |
errors.add(:value, :too_short, :count => custom_field.min_length) if custom_field.min_length > 0 and value.length < custom_field.min_length |
| 56 | 58 |
errors.add(:value, :too_long, :count => custom_field.max_length) if custom_field.max_length > 0 and value.length > custom_field.max_length |
| 57 |
|
|
| 59 |
|
|
| 58 | 60 |
# Format specific validations |
| 59 | 61 |
case custom_field.field_format |
| 60 | 62 |
when 'int' |
| ... | ... | |
| 62 | 64 |
when 'float' |
| 63 | 65 |
begin; Kernel.Float(value); rescue; errors.add(:value, :invalid) end |
| 64 | 66 |
when 'date' |
| 65 |
errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
|
|
| 67 |
errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
|
|
| 66 | 68 |
when 'list' |
| 67 | 69 |
errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value) |
| 68 | 70 |
end |
| app/models/enumeration.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006 Jean-Philippe Lang |
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 17 | 17 |
|
| 18 | 18 |
class Enumeration < ActiveRecord::Base |
| 19 | 19 |
default_scope :order => "#{Enumeration.table_name}.position ASC"
|
| 20 |
|
|
| 20 |
|
|
| 21 | 21 |
belongs_to :project |
| 22 |
|
|
| 22 |
|
|
| 23 | 23 |
acts_as_list :scope => 'type = \'#{type}\''
|
| 24 | 24 |
acts_as_customizable |
| 25 | 25 |
acts_as_tree :order => 'position ASC' |
| 26 | 26 |
|
| 27 | 27 |
before_destroy :check_integrity |
| 28 |
|
|
| 28 |
before_save :check_default |
|
| 29 |
|
|
| 29 | 30 |
validates_presence_of :name |
| 30 | 31 |
validates_uniqueness_of :name, :scope => [:type, :project_id] |
| 31 | 32 |
validates_length_of :name, :maximum => 30 |
| ... | ... | |
| 45 | 46 |
find(:first, :conditions => { :is_default => true })
|
| 46 | 47 |
end |
| 47 | 48 |
end |
| 48 |
|
|
| 49 |
|
|
| 49 | 50 |
# Overloaded on concrete classes |
| 50 | 51 |
def option_name |
| 51 | 52 |
nil |
| 52 | 53 |
end |
| 53 | 54 |
|
| 54 |
def before_save
|
|
| 55 |
def check_default
|
|
| 55 | 56 |
if is_default? && is_default_changed? |
| 56 | 57 |
Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type})
|
| 57 | 58 |
end |
| 58 | 59 |
end |
| 59 |
|
|
| 60 |
|
|
| 60 | 61 |
# Overloaded on concrete classes |
| 61 | 62 |
def objects_count |
| 62 | 63 |
0 |
| 63 | 64 |
end |
| 64 |
|
|
| 65 |
|
|
| 65 | 66 |
def in_use? |
| 66 | 67 |
self.objects_count != 0 |
| 67 | 68 |
end |
| ... | ... | |
| 70 | 71 |
def is_override? |
| 71 | 72 |
!self.parent.nil? |
| 72 | 73 |
end |
| 73 |
|
|
| 74 |
|
|
| 74 | 75 |
alias :destroy_without_reassign :destroy |
| 75 |
|
|
| 76 |
|
|
| 76 | 77 |
# Destroy the enumeration |
| 77 | 78 |
# If a enumeration is specified, objects are reassigned |
| 78 | 79 |
def destroy(reassign_to = nil) |
| ... | ... | |
| 81 | 82 |
end |
| 82 | 83 |
destroy_without_reassign |
| 83 | 84 |
end |
| 84 |
|
|
| 85 |
|
|
| 85 | 86 |
def <=>(enumeration) |
| 86 | 87 |
position <=> enumeration.position |
| 87 | 88 |
end |
| 88 |
|
|
| 89 |
|
|
| 89 | 90 |
def to_s; name end |
| 90 | 91 |
|
| 91 | 92 |
# Returns the Subclasses of Enumeration. Each Subclass needs to be |
| ... | ... | |
| 115 | 116 |
|
| 116 | 117 |
return true |
| 117 | 118 |
end |
| 118 |
|
|
| 119 |
|
|
| 119 | 120 |
# Are the new and previous fields equal? |
| 120 | 121 |
def self.same_active_state?(new, previous) |
| 121 | 122 |
new = (new == "1" ? true : false) |
| 122 | 123 |
return new == previous |
| 123 | 124 |
end |
| 124 |
|
|
| 125 |
|
|
| 125 | 126 |
private |
| 126 | 127 |
def check_integrity |
| 127 | 128 |
raise "Can't delete enumeration" if self.in_use? |
| app/models/group.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2009 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| ... | ... | |
| 18 | 18 |
class Group < Principal |
| 19 | 19 |
has_and_belongs_to_many :users, :after_add => :user_added, |
| 20 | 20 |
:after_remove => :user_removed |
| 21 |
|
|
| 21 |
|
|
| 22 | 22 |
acts_as_customizable |
| 23 |
|
|
| 23 |
|
|
| 24 | 24 |
validates_presence_of :lastname |
| 25 | 25 |
validates_uniqueness_of :lastname, :case_sensitive => false |
| 26 | 26 |
validates_length_of :lastname, :maximum => 30 |
| 27 |
|
|
| 27 |
|
|
| 28 |
before_destroy :remove_references_before_destroy |
|
| 29 |
|
|
| 28 | 30 |
def to_s |
| 29 | 31 |
lastname.to_s |
| 30 | 32 |
end |
| 31 |
|
|
| 33 |
|
|
| 34 |
alias :name :to_s |
|
| 35 |
|
|
| 32 | 36 |
def user_added(user) |
| 33 | 37 |
members.each do |member| |
| 34 | 38 |
next if member.project.nil? |
| ... | ... | |
| 39 | 43 |
user_member.save! |
| 40 | 44 |
end |
| 41 | 45 |
end |
| 42 |
|
|
| 46 |
|
|
| 43 | 47 |
def user_removed(user) |
| 44 | 48 |
members.each do |member| |
| 45 | 49 |
MemberRole.find(:all, :include => :member, |
| 46 | 50 |
:conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
|
| 47 | 51 |
end |
| 48 | 52 |
end |
| 53 |
|
|
| 54 |
def self.human_attribute_name(attribute_key_name) |
|
| 55 |
attr_name = attribute_key_name |
|
| 56 |
if attr_name == 'lastname' |
|
| 57 |
attr_name = "name" |
|
| 58 |
end |
|
| 59 |
super(attr_name) |
|
| 60 |
end |
|
| 61 |
|
|
| 62 |
private |
|
| 63 |
|
|
| 64 |
# Removes references that are not handled by associations |
|
| 65 |
def remove_references_before_destroy |
|
| 66 |
return if self.id.nil? |
|
| 67 |
|
|
| 68 |
Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] |
|
| 69 |
end |
|
| 49 | 70 |
end |
| app/models/group_custom_field.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2009 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| app/models/issue.rb | ||
|---|---|---|
| 22 | 22 |
belongs_to :tracker |
| 23 | 23 |
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' |
| 24 | 24 |
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' |
| 25 |
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
|
|
| 25 |
belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
|
|
| 26 | 26 |
belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' |
| 27 | 27 |
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id' |
| 28 | 28 |
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' |
| ... | ... | |
| 35 | 35 |
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all |
| 36 | 36 |
|
| 37 | 37 |
acts_as_nested_set :scope => 'root_id', :dependent => :destroy |
| 38 |
acts_as_attachable :after_remove => :attachment_removed |
|
| 38 |
acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
|
|
| 39 | 39 |
acts_as_customizable |
| 40 | 40 |
acts_as_watchable |
| 41 | 41 |
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
|
| ... | ... | |
| 58 | 58 |
validates_length_of :subject, :maximum => 255 |
| 59 | 59 |
validates_inclusion_of :done_ratio, :in => 0..100 |
| 60 | 60 |
validates_numericality_of :estimated_hours, :allow_nil => true |
| 61 |
validate :validate_issue |
|
| 61 | 62 |
|
| 62 | 63 |
named_scope :visible, lambda {|*args| { :include => :project,
|
| 63 | 64 |
:conditions => Issue.visible_condition(args.shift || User.current, *args) } } |
| ... | ... | |
| 93 | 94 |
when 'all' |
| 94 | 95 |
nil |
| 95 | 96 |
when 'default' |
| 96 |
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
|
|
| 97 |
user_ids = [user.id] + user.groups.map(&:id) |
|
| 98 |
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
|
| 97 | 99 |
when 'own' |
| 98 |
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
|
|
| 100 |
user_ids = [user.id] + user.groups.map(&:id) |
|
| 101 |
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
|
| 99 | 102 |
else |
| 100 | 103 |
'1=0' |
| 101 | 104 |
end |
| ... | ... | |
| 109 | 112 |
when 'all' |
| 110 | 113 |
true |
| 111 | 114 |
when 'default' |
| 112 |
!self.is_private? || self.author == user || self.assigned_to == user
|
|
| 115 |
!self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
|
|
| 113 | 116 |
when 'own' |
| 114 |
self.author == user || self.assigned_to == user
|
|
| 117 |
self.author == user || user.is_or_belongs_to?(assigned_to)
|
|
| 115 | 118 |
else |
| 116 | 119 |
false |
| 117 | 120 |
end |
| ... | ... | |
| 227 | 230 |
@custom_field_values = nil |
| 228 | 231 |
result |
| 229 | 232 |
end |
| 230 |
|
|
| 233 |
|
|
| 231 | 234 |
def description=(arg) |
| 232 | 235 |
if arg.is_a?(String) |
| 233 | 236 |
arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n") |
| ... | ... | |
| 335 | 338 |
Setting.issue_done_ratio == 'issue_field' |
| 336 | 339 |
end |
| 337 | 340 |
|
| 338 |
def validate |
|
| 341 |
def validate_issue
|
|
| 339 | 342 |
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? |
| 340 | 343 |
errors.add :due_date, :not_a_date |
| 341 | 344 |
end |
| ... | ... | |
| 352 | 355 |
if !assignable_versions.include?(fixed_version) |
| 353 | 356 |
errors.add :fixed_version_id, :inclusion |
| 354 | 357 |
elsif reopened? && fixed_version.closed? |
| 355 |
errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
|
|
| 358 |
errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
|
|
| 356 | 359 |
end |
| 357 | 360 |
end |
| 358 | 361 |
|
| ... | ... | |
| 449 | 452 |
def assignable_users |
| 450 | 453 |
users = project.assignable_users |
| 451 | 454 |
users << author if author |
| 455 |
users << assigned_to if assigned_to |
|
| 452 | 456 |
users.uniq.sort |
| 453 | 457 |
end |
| 454 | 458 |
|
| ... | ... | |
| 482 | 486 |
# Author and assignee are always notified unless they have been |
| 483 | 487 |
# locked or don't want to be notified |
| 484 | 488 |
notified << author if author && author.active? && author.notify_about?(self) |
| 485 |
notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self) |
|
| 489 |
if assigned_to |
|
| 490 |
if assigned_to.is_a?(Group) |
|
| 491 |
notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
|
|
| 492 |
else |
|
| 493 |
notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self) |
|
| 494 |
end |
|
| 495 |
end |
|
| 486 | 496 |
notified.uniq! |
| 487 | 497 |
# Remove users that can not view the issue |
| 488 | 498 |
notified.reject! {|user| !visible?(user)}
|
| ... | ... | |
| 499 | 509 |
end |
| 500 | 510 |
|
| 501 | 511 |
def relations |
| 502 |
(relations_from + relations_to).sort |
|
| 512 |
@relations ||= (relations_from + relations_to).sort |
|
| 513 |
end |
|
| 514 |
|
|
| 515 |
# Preloads relations for a collection of issues |
|
| 516 |
def self.load_relations(issues) |
|
| 517 |
if issues.any? |
|
| 518 |
relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
|
|
| 519 |
issues.each do |issue| |
|
| 520 |
issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
|
|
| 521 |
end |
|
| 522 |
end |
|
| 523 |
end |
|
| 524 |
|
|
| 525 |
# Finds an issue relation given its id. |
|
| 526 |
def find_relation(relation_id) |
|
| 527 |
IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id]) |
|
| 503 | 528 |
end |
| 504 | 529 |
|
| 505 | 530 |
def all_dependent_issues(except=[]) |
| ... | ... | |
| 592 | 617 |
@time_entry.project = project |
| 593 | 618 |
@time_entry.issue = self |
| 594 | 619 |
@time_entry.user = User.current |
| 595 |
@time_entry.spent_on = Date.today
|
|
| 620 |
@time_entry.spent_on = User.current.today
|
|
| 596 | 621 |
@time_entry.attributes = params[:time_entry] |
| 597 | 622 |
self.time_entries << @time_entry |
| 598 | 623 |
end |
| 599 | 624 |
|
| 600 | 625 |
if valid? |
| 601 | 626 |
attachments = Attachment.attach_files(self, params[:attachments]) |
| 602 |
|
|
| 603 |
attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
|
|
| 604 | 627 |
# TODO: Rename hook |
| 605 | 628 |
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
|
| 606 | 629 |
begin |
| ... | ... | |
| 612 | 635 |
end |
| 613 | 636 |
rescue ActiveRecord::StaleObjectError |
| 614 | 637 |
attachments[:files].each(&:destroy) |
| 615 |
errors.add_to_base l(:notice_locking_conflict)
|
|
| 638 |
errors.add :base, l(:notice_locking_conflict)
|
|
| 616 | 639 |
raise ActiveRecord::Rollback |
| 617 | 640 |
end |
| 618 | 641 |
end |
| ... | ... | |
| 831 | 854 |
end |
| 832 | 855 |
|
| 833 | 856 |
# Callback on attachment deletion |
| 857 |
def attachment_added(obj) |
|
| 858 |
if @current_journal && !obj.new_record? |
|
| 859 |
@current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename) |
|
| 860 |
end |
|
| 861 |
end |
|
| 862 |
|
|
| 863 |
# Callback on attachment deletion |
|
| 834 | 864 |
def attachment_removed(obj) |
| 835 | 865 |
journal = init_journal(User.current) |
| 836 | 866 |
journal.details << JournalDetail.new(:property => 'attachment', |
| app/models/issue_category.rb | ||
|---|---|---|
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 17 | 17 |
|
| 18 | 18 |
class IssueCategory < ActiveRecord::Base |
| 19 | 19 |
belongs_to :project |
| 20 |
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
|
|
| 20 |
belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
|
|
| 21 | 21 |
has_many :issues, :foreign_key => 'category_id', :dependent => :nullify |
| 22 |
|
|
| 22 |
|
|
| 23 | 23 |
validates_presence_of :name |
| 24 | 24 |
validates_uniqueness_of :name, :scope => [:project_id] |
| 25 | 25 |
validates_length_of :name, :maximum => 30 |
| 26 | 26 |
|
| 27 |
attr_protected :project_id |
|
| 28 |
|
|
| 27 | 29 |
named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
|
| 28 |
|
|
| 30 |
|
|
| 29 | 31 |
alias :destroy_without_reassign :destroy |
| 30 |
|
|
| 32 |
|
|
| 31 | 33 |
# Destroy the category |
| 32 | 34 |
# If a category is specified, issues are reassigned to this category |
| 33 | 35 |
def destroy(reassign_to = nil) |
| ... | ... | |
| 36 | 38 |
end |
| 37 | 39 |
destroy_without_reassign |
| 38 | 40 |
end |
| 39 |
|
|
| 41 |
|
|
| 40 | 42 |
def <=>(category) |
| 41 | 43 |
name <=> category.name |
| 42 | 44 |
end |
| 43 |
|
|
| 45 |
|
|
| 44 | 46 |
def to_s; name end |
| 45 | 47 |
end |
| app/models/issue_custom_field.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006 Jean-Philippe Lang |
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| ... | ... | |
| 19 | 19 |
has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id"
|
| 20 | 20 |
has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id"
|
| 21 | 21 |
has_many :issues, :through => :issue_custom_values |
| 22 |
|
|
| 22 |
|
|
| 23 | 23 |
def type_name |
| 24 | 24 |
:label_issue_plural |
| 25 | 25 |
end |
| app/models/issue_observer.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| app/models/issue_priority.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006 Jean-Philippe Lang |
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| app/models/issue_priority_custom_field.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006 Jean-Philippe Lang |
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| app/models/issue_relation.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| ... | ... | |
| 18 | 18 |
class IssueRelation < ActiveRecord::Base |
| 19 | 19 |
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' |
| 20 | 20 |
belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' |
| 21 |
|
|
| 21 |
|
|
| 22 | 22 |
TYPE_RELATES = "relates" |
| 23 | 23 |
TYPE_DUPLICATES = "duplicates" |
| 24 | 24 |
TYPE_DUPLICATED = "duplicated" |
| ... | ... | |
| 26 | 26 |
TYPE_BLOCKED = "blocked" |
| 27 | 27 |
TYPE_PRECEDES = "precedes" |
| 28 | 28 |
TYPE_FOLLOWS = "follows" |
| 29 |
|
|
| 29 |
|
|
| 30 | 30 |
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES },
|
| 31 | 31 |
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED },
|
| 32 | 32 |
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
|
| ... | ... | |
| 35 | 35 |
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS },
|
| 36 | 36 |
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }
|
| 37 | 37 |
}.freeze |
| 38 |
|
|
| 38 |
|
|
| 39 | 39 |
validates_presence_of :issue_from, :issue_to, :relation_type |
| 40 | 40 |
validates_inclusion_of :relation_type, :in => TYPES.keys |
| 41 | 41 |
validates_numericality_of :delay, :allow_nil => true |
| 42 | 42 |
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id |
| 43 |
|
|
| 43 |
|
|
| 44 |
validate :validate_issue_relation |
|
| 45 |
|
|
| 44 | 46 |
attr_protected :issue_from_id, :issue_to_id |
| 45 |
|
|
| 46 |
def validate |
|
| 47 |
|
|
| 48 |
before_save :handle_issue_order |
|
| 49 |
|
|
| 50 |
def visible?(user=User.current) |
|
| 51 |
(issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user)) |
|
| 52 |
end |
|
| 53 |
|
|
| 54 |
def deletable?(user=User.current) |
|
| 55 |
visible?(user) && |
|
| 56 |
((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) || |
|
| 57 |
(issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project))) |
|
| 58 |
end |
|
| 59 |
|
|
| 60 |
def after_initialize |
|
| 61 |
if new_record? |
|
| 62 |
if relation_type.blank? |
|
| 63 |
self.relation_type = IssueRelation::TYPE_RELATES |
|
| 64 |
end |
|
| 65 |
end |
|
| 66 |
end |
|
| 67 |
|
|
| 68 |
def validate_issue_relation |
|
| 47 | 69 |
if issue_from && issue_to |
| 48 | 70 |
errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id |
| 49 | 71 |
errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? |
| 50 | 72 |
#detect circular dependencies depending wether the relation should be reversed |
| 51 | 73 |
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] |
| 52 |
errors.add_to_base :circular_dependency if issue_from.all_dependent_issues.include? issue_to
|
|
| 74 |
errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
|
|
| 53 | 75 |
else |
| 54 |
errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from
|
|
| 76 |
errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
|
|
| 55 | 77 |
end |
| 56 |
errors.add_to_base :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
|
|
| 78 |
errors.add :base, :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
|
|
| 57 | 79 |
end |
| 58 | 80 |
end |
| 59 |
|
|
| 81 |
|
|
| 60 | 82 |
def other_issue(issue) |
| 61 | 83 |
(self.issue_from_id == issue.id) ? issue_to : issue_from |
| 62 | 84 |
end |
| 63 |
|
|
| 85 |
|
|
| 64 | 86 |
# Returns the relation type for +issue+ |
| 65 | 87 |
def relation_type_for(issue) |
| 66 | 88 |
if TYPES[relation_type] |
| ... | ... | |
| 71 | 93 |
end |
| 72 | 94 |
end |
| 73 | 95 |
end |
| 74 |
|
|
| 96 |
|
|
| 75 | 97 |
def label_for(issue) |
| 76 | 98 |
TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow |
| 77 | 99 |
end |
| 78 |
|
|
| 79 |
def before_save
|
|
| 100 |
|
|
| 101 |
def handle_issue_order
|
|
| 80 | 102 |
reverse_if_needed |
| 81 |
|
|
| 103 |
|
|
| 82 | 104 |
if TYPE_PRECEDES == relation_type |
| 83 | 105 |
self.delay ||= 0 |
| 84 | 106 |
else |
| ... | ... | |
| 86 | 108 |
end |
| 87 | 109 |
set_issue_to_dates |
| 88 | 110 |
end |
| 89 |
|
|
| 111 |
|
|
| 90 | 112 |
def set_issue_to_dates |
| 91 | 113 |
soonest_start = self.successor_soonest_start |
| 92 | 114 |
if soonest_start && issue_to |
| 93 | 115 |
issue_to.reschedule_after(soonest_start) |
| 94 | 116 |
end |
| 95 | 117 |
end |
| 96 |
|
|
| 118 |
|
|
| 97 | 119 |
def successor_soonest_start |
| 98 | 120 |
if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date) |
| 99 | 121 |
(issue_from.due_date || issue_from.start_date) + 1 + delay |
| 100 | 122 |
end |
| 101 | 123 |
end |
| 102 |
|
|
| 124 |
|
|
| 103 | 125 |
def <=>(relation) |
| 104 | 126 |
TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] |
| 105 | 127 |
end |
| 106 |
|
|
| 128 |
|
|
| 107 | 129 |
private |
| 108 |
|
|
| 130 |
|
|
| 109 | 131 |
# Reverses the relation if needed so that it gets stored in the proper way |
| 132 |
# Should not be reversed before validation so that it can be displayed back |
|
| 133 |
# as entered on new relation form |
|
| 110 | 134 |
def reverse_if_needed |
| 111 | 135 |
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] |
| 112 | 136 |
issue_tmp = issue_to |
| app/models/issue_status.rb | ||
|---|---|---|
| 1 |
# redMine - project management software
|
|
| 2 |
# Copyright (C) 2006 Jean-Philippe Lang |
|
| 1 |
# Redmine - project management software
|
|
| 2 |
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| 6 | 6 |
# as published by the Free Software Foundation; either version 2 |
| 7 | 7 |
# of the License, or (at your option) any later version. |
| 8 |
#
|
|
| 8 |
# |
|
| 9 | 9 |
# This program is distributed in the hope that it will be useful, |
| 10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | 12 |
# GNU General Public License for more details. |
| 13 |
#
|
|
| 13 |
# |
|
| 14 | 14 |
# You should have received a copy of the GNU General Public License |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 17 | 17 |
|
| 18 | 18 |
class IssueStatus < ActiveRecord::Base |
| 19 |
before_destroy :check_integrity
|
|
| 19 |
before_destroy :check_integrity |
|
| 20 | 20 |
has_many :workflows, :foreign_key => "old_status_id" |
| 21 | 21 |
acts_as_list |
| 22 |
|
|
| 22 |
|
|
| 23 | 23 |
before_destroy :delete_workflows |
| 24 |
after_save :update_default |
|
| 24 | 25 |
|
| 25 | 26 |
validates_presence_of :name |
| 26 | 27 |
validates_uniqueness_of :name |
| 27 | 28 |
validates_length_of :name, :maximum => 30 |
| 28 | 29 |
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true |
| 29 |
|
|
| 30 |
|
|
| 30 | 31 |
named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
|
| 31 | 32 |
|
| 32 |
def after_save
|
|
| 33 |
def update_default
|
|
| 33 | 34 |
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
|
| 34 |
end
|
|
| 35 |
|
|
| 35 |
end |
|
| 36 |
|
|
| 36 | 37 |
# Returns the default status for new issues |
| 37 | 38 |
def self.default |
| 38 | 39 |
find(:first, :conditions =>["is_default=?", true]) |
| 39 | 40 |
end |
| 40 |
|
|
| 41 |
|
|
| 41 | 42 |
# Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+ |
| 42 | 43 |
def self.update_issue_done_ratios |
| 43 | 44 |
if Issue.use_status_for_done_ratio? |
| ... | ... | |
| 57 | 58 |
role_ids = roles.collect(&:id) |
| 58 | 59 |
transitions = workflows.select do |w| |
| 59 | 60 |
role_ids.include?(w.role_id) && |
| 60 |
w.tracker_id == tracker.id && |
|
| 61 |
(author || !w.author) && |
|
| 62 |
(assignee || !w.assignee) |
|
| 61 |
w.tracker_id == tracker.id && |
|
Also available in: Unified diff