Revision 1298:4f746d8966dd app/models
| app/models/attachment.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 17 | 17 |
|
| 18 | 18 |
require "digest/md5" |
| 19 |
require "fileutils" |
|
| 19 | 20 |
|
| 20 | 21 |
class Attachment < ActiveRecord::Base |
| 21 | 22 |
belongs_to :container, :polymorphic => true |
| ... | ... | |
| 92 | 93 |
|
| 93 | 94 |
def filename=(arg) |
| 94 | 95 |
write_attribute :filename, sanitize_filename(arg.to_s) |
| 95 |
if new_record? && disk_filename.blank? |
|
| 96 |
self.disk_filename = Attachment.disk_filename(filename) |
|
| 97 |
end |
|
| 98 | 96 |
filename |
| 99 | 97 |
end |
| 100 | 98 |
|
| ... | ... | |
| 102 | 100 |
# and computes its MD5 hash |
| 103 | 101 |
def files_to_final_location |
| 104 | 102 |
if @temp_file && (@temp_file.size > 0) |
| 103 |
self.disk_directory = target_directory |
|
| 104 |
self.disk_filename = Attachment.disk_filename(filename, disk_directory) |
|
| 105 | 105 |
logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
|
| 106 |
path = File.dirname(diskfile) |
|
| 107 |
unless File.directory?(path) |
|
| 108 |
FileUtils.mkdir_p(path) |
|
| 109 |
end |
|
| 106 | 110 |
md5 = Digest::MD5.new |
| 107 | 111 |
File.open(diskfile, "wb") do |f| |
| 108 | 112 |
if @temp_file.respond_to?(:read) |
| ... | ... | |
| 134 | 138 |
|
| 135 | 139 |
# Returns file's location on disk |
| 136 | 140 |
def diskfile |
| 137 |
File.join(self.class.storage_path, disk_filename.to_s) |
|
| 141 |
File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
|
|
| 138 | 142 |
end |
| 139 | 143 |
|
| 140 | 144 |
def title |
| ... | ... | |
| 154 | 158 |
end |
| 155 | 159 |
|
| 156 | 160 |
def visible?(user=User.current) |
| 157 |
container && container.attachments_visible?(user) |
|
| 161 |
if container_id |
|
| 162 |
container && container.attachments_visible?(user) |
|
| 163 |
else |
|
| 164 |
author == user |
|
| 165 |
end |
|
| 158 | 166 |
end |
| 159 | 167 |
|
| 160 | 168 |
def deletable?(user=User.current) |
| 161 |
container && container.attachments_deletable?(user) |
|
| 169 |
if container_id |
|
| 170 |
container && container.attachments_deletable?(user) |
|
| 171 |
else |
|
| 172 |
author == user |
|
| 173 |
end |
|
| 162 | 174 |
end |
| 163 | 175 |
|
| 164 | 176 |
def image? |
| ... | ... | |
| 251 | 263 |
Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
|
| 252 | 264 |
end |
| 253 | 265 |
|
| 266 |
# Moves an existing attachment to its target directory |
|
| 267 |
def move_to_target_directory! |
|
| 268 |
if !new_record? & readable? |
|
| 269 |
src = diskfile |
|
| 270 |
self.disk_directory = target_directory |
|
| 271 |
dest = diskfile |
|
| 272 |
if src != dest && FileUtils.mkdir_p(File.dirname(dest)) && FileUtils.mv(src, dest) |
|
| 273 |
update_column :disk_directory, disk_directory |
|
| 274 |
end |
|
| 275 |
end |
|
| 276 |
end |
|
| 277 |
|
|
| 278 |
# Moves existing attachments that are stored at the root of the files |
|
| 279 |
# directory (ie. created before Redmine 2.3) to their target subdirectories |
|
| 280 |
def self.move_from_root_to_target_directory |
|
| 281 |
Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
|
|
| 282 |
attachment.move_to_target_directory! |
|
| 283 |
end |
|
| 284 |
end |
|
| 285 |
|
|
| 254 | 286 |
private |
| 255 | 287 |
|
| 256 | 288 |
# Physically deletes the file from the file system |
| ... | ... | |
| 268 | 300 |
@filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_') |
| 269 | 301 |
end |
| 270 | 302 |
|
| 271 |
# Returns an ASCII or hashed filename |
|
| 272 |
def self.disk_filename(filename) |
|
| 303 |
# Returns the subdirectory in which the attachment will be saved |
|
| 304 |
def target_directory |
|
| 305 |
time = created_on || DateTime.now |
|
| 306 |
time.strftime("%Y/%m")
|
|
| 307 |
end |
|
| 308 |
|
|
| 309 |
# Returns an ASCII or hashed filename that do not |
|
| 310 |
# exists yet in the given subdirectory |
|
| 311 |
def self.disk_filename(filename, directory=nil) |
|
| 273 | 312 |
timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
|
| 274 | 313 |
ascii = '' |
| 275 | 314 |
if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
|
| ... | ... | |
| 279 | 318 |
# keep the extension if any |
| 280 | 319 |
ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
|
| 281 | 320 |
end |
| 282 |
while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
|
|
| 321 |
while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
|
|
| 283 | 322 |
timestamp.succ! |
| 284 | 323 |
end |
| 285 | 324 |
"#{timestamp}_#{ascii}"
|
| app/models/auth_source.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 48 | 48 |
write_ciphered_attribute(:account_password, arg) |
| 49 | 49 |
end |
| 50 | 50 |
|
| 51 |
def searchable? |
|
| 52 |
false |
|
| 53 |
end |
|
| 54 |
|
|
| 55 |
def self.search(q) |
|
| 56 |
results = [] |
|
| 57 |
AuthSource.all.each do |source| |
|
| 58 |
begin |
|
| 59 |
if source.searchable? |
|
| 60 |
results += source.search(q) |
|
| 61 |
end |
|
| 62 |
rescue AuthSourceException => e |
|
| 63 |
logger.error "Error while searching users in #{source.name}: #{e.message}"
|
|
| 64 |
end |
|
| 65 |
end |
|
| 66 |
results |
|
| 67 |
end |
|
| 68 |
|
|
| 51 | 69 |
def allow_password_changes? |
| 52 | 70 |
self.class.allow_password_changes? |
| 53 | 71 |
end |
| app/models/auth_source_ldap.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 17 | 17 |
|
| 18 |
require 'iconv' |
|
| 19 | 18 |
require 'net/ldap' |
| 20 | 19 |
require 'net/ldap/dn' |
| 21 | 20 |
require 'timeout' |
| ... | ... | |
| 64 | 63 |
"LDAP" |
| 65 | 64 |
end |
| 66 | 65 |
|
| 66 |
# Returns true if this source can be searched for users |
|
| 67 |
def searchable? |
|
| 68 |
!account.to_s.include?("$login") && %w(login firstname lastname mail).all? {|a| send("attr_#{a}?")}
|
|
| 69 |
end |
|
| 70 |
|
|
| 71 |
# Searches the source for users and returns an array of results |
|
| 72 |
def search(q) |
|
| 73 |
q = q.to_s.strip |
|
| 74 |
return [] unless searchable? && q.present? |
|
| 75 |
|
|
| 76 |
results = [] |
|
| 77 |
search_filter = base_filter & Net::LDAP::Filter.begins(self.attr_login, q) |
|
| 78 |
ldap_con = initialize_ldap_con(self.account, self.account_password) |
|
| 79 |
ldap_con.search(:base => self.base_dn, |
|
| 80 |
:filter => search_filter, |
|
| 81 |
:attributes => ['dn', self.attr_login, self.attr_firstname, self.attr_lastname, self.attr_mail], |
|
| 82 |
:size => 10) do |entry| |
|
| 83 |
attrs = get_user_attributes_from_ldap_entry(entry) |
|
| 84 |
attrs[:login] = AuthSourceLdap.get_attr(entry, self.attr_login) |
|
| 85 |
results << attrs |
|
| 86 |
end |
|
| 87 |
results |
|
| 88 |
rescue Net::LDAP::LdapError => e |
|
| 89 |
raise AuthSourceException.new(e.message) |
|
| 90 |
end |
|
| 91 |
|
|
| 67 | 92 |
private |
| 68 | 93 |
|
| 69 | 94 |
def with_timeout(&block) |
| ... | ... | |
| 84 | 109 |
nil |
| 85 | 110 |
end |
| 86 | 111 |
|
| 112 |
def base_filter |
|
| 113 |
filter = Net::LDAP::Filter.eq("objectClass", "*")
|
|
| 114 |
if f = ldap_filter |
|
| 115 |
filter = filter & f |
|
| 116 |
end |
|
| 117 |
filter |
|
| 118 |
end |
|
| 119 |
|
|
| 87 | 120 |
def validate_filter |
| 88 | 121 |
if filter.present? && ldap_filter.nil? |
| 89 | 122 |
errors.add(:filter, :invalid) |
| ... | ... | |
| 140 | 173 |
else |
| 141 | 174 |
ldap_con = initialize_ldap_con(self.account, self.account_password) |
| 142 | 175 |
end |
| 143 |
login_filter = Net::LDAP::Filter.eq( self.attr_login, login ) |
|
| 144 |
object_filter = Net::LDAP::Filter.eq( "objectClass", "*" ) |
|
| 145 | 176 |
attrs = {}
|
| 146 |
|
|
| 147 |
search_filter = object_filter & login_filter |
|
| 148 |
if f = ldap_filter |
|
| 149 |
search_filter = search_filter & f |
|
| 150 |
end |
|
| 177 |
search_filter = base_filter & Net::LDAP::Filter.eq(self.attr_login, login) |
|
| 151 | 178 |
|
| 152 | 179 |
ldap_con.search( :base => self.base_dn, |
| 153 | 180 |
:filter => search_filter, |
| app/models/board.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 30 | 30 |
validates_length_of :description, :maximum => 255 |
| 31 | 31 |
validate :validate_board |
| 32 | 32 |
|
| 33 |
scope :visible, lambda {|*args| { :include => :project,
|
|
| 34 |
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } } |
|
| 33 |
scope :visible, lambda {|*args|
|
|
| 34 |
includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args)) |
|
| 35 |
} |
|
| 35 | 36 |
|
| 36 | 37 |
safe_attributes 'name', 'description', 'parent_id', 'move_to' |
| 37 | 38 |
|
| app/models/change.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/changeset.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 17 | 17 |
|
| 18 |
require 'iconv' |
|
| 19 |
|
|
| 20 | 18 |
class Changeset < ActiveRecord::Base |
| 21 | 19 |
belongs_to :repository |
| 22 | 20 |
belongs_to :user |
| ... | ... | |
| 49 | 47 |
validates_uniqueness_of :revision, :scope => :repository_id |
| 50 | 48 |
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true |
| 51 | 49 |
|
| 52 |
scope :visible, |
|
| 53 |
lambda {|*args| { :include => {:repository => :project},
|
|
| 54 |
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
|
|
| 50 |
scope :visible, lambda {|*args|
|
|
| 51 |
includes(:repository => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
|
|
| 52 |
} |
|
| 55 | 53 |
|
| 56 | 54 |
after_create :scan_for_issues |
| 57 | 55 |
before_create :before_create_cs |
| app/models/comment.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/comment_observer.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/custom_field.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 29 | 29 |
|
| 30 | 30 |
validate :validate_custom_field |
| 31 | 31 |
before_validation :set_searchable |
| 32 |
after_save :handle_multiplicity_change |
|
| 33 |
|
|
| 34 |
scope :sorted, lambda { order("#{table_name}.position ASC") }
|
|
| 32 | 35 |
|
| 33 | 36 |
CUSTOM_FIELDS_TABS = [ |
| 34 | 37 |
{:name => 'IssueCustomField', :partial => 'custom_fields/index',
|
| ... | ... | |
| 169 | 172 |
keyword |
| 170 | 173 |
end |
| 171 | 174 |
end |
| 172 |
|
|
| 175 |
|
|
| 173 | 176 |
# Returns a ORDER BY clause that can used to sort customized |
| 174 | 177 |
# objects by their value of the custom field. |
| 175 | 178 |
# Returns nil if the custom field can not be used for sorting. |
| ... | ... | |
| 178 | 181 |
case field_format |
| 179 | 182 |
when 'string', 'text', 'list', 'date', 'bool' |
| 180 | 183 |
# COALESCE is here to make sure that blank and NULL values are sorted equally |
| 181 |
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
|
|
| 182 |
" WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
|
|
| 183 |
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
|
| 184 |
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
|
|
| 184 |
"COALESCE(#{join_alias}.value, '')"
|
|
| 185 | 185 |
when 'int', 'float' |
| 186 | 186 |
# Make the database cast values into numeric |
| 187 | 187 |
# Postgresql will raise an error if a value can not be casted! |
| 188 | 188 |
# CustomValue validations should ensure that it doesn't occur |
| 189 |
"(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
|
|
| 190 |
" WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
|
|
| 191 |
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
|
| 192 |
" AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
|
|
| 189 |
"CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,3))"
|
|
| 193 | 190 |
when 'user', 'version' |
| 194 | 191 |
value_class.fields_for_order_statement(value_join_alias) |
| 195 | 192 |
else |
| ... | ... | |
| 199 | 196 |
|
| 200 | 197 |
# Returns a GROUP BY clause that can used to group by custom value |
| 201 | 198 |
# Returns nil if the custom field can not be used for grouping. |
| 202 |
def group_statement
|
|
| 199 |
def group_statement |
|
| 203 | 200 |
return nil if multiple? |
| 204 | 201 |
case field_format |
| 205 | 202 |
when 'list', 'date', 'bool', 'int' |
| 206 | 203 |
order_statement |
| 207 | 204 |
when 'user', 'version' |
| 208 |
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
|
|
| 209 |
" WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
|
|
| 210 |
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
|
| 211 |
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
|
|
| 205 |
"COALESCE(#{join_alias}.value, '')"
|
|
| 212 | 206 |
else |
| 213 | 207 |
nil |
| 214 | 208 |
end |
| ... | ... | |
| 227 | 221 |
" AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
|
| 228 | 222 |
" AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
|
| 229 | 223 |
" LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
|
| 230 |
" ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
|
|
| 224 |
" ON CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,0)) = #{value_join_alias}.id"
|
|
| 225 |
when 'int', 'float' |
|
| 226 |
"LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
|
|
| 227 |
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
|
| 228 |
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
|
|
| 229 |
" AND #{join_alias}.custom_field_id = #{id}" +
|
|
| 230 |
" AND #{join_alias}.value <> ''" +
|
|
| 231 |
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
|
| 232 |
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
|
| 233 |
" AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
|
|
| 234 |
" AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)"
|
|
| 235 |
when 'string', 'text', 'list', 'date', 'bool' |
|
| 236 |
"LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
|
|
| 237 |
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
|
| 238 |
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
|
|
| 239 |
" AND #{join_alias}.custom_field_id = #{id}" +
|
|
| 240 |
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
|
| 241 |
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
|
| 242 |
" AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
|
|
| 243 |
" AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)"
|
|
| 231 | 244 |
else |
| 232 | 245 |
nil |
| 233 | 246 |
end |
| ... | ... | |
| 262 | 275 |
|
| 263 | 276 |
# to move in project_custom_field |
| 264 | 277 |
def self.for_all |
| 265 |
find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
|
|
| 278 |
where(:is_for_all => true).order('position').all
|
|
| 266 | 279 |
end |
| 267 | 280 |
|
| 268 | 281 |
def type_name |
| ... | ... | |
| 323 | 336 |
end |
| 324 | 337 |
errs |
| 325 | 338 |
end |
| 339 |
|
|
| 340 |
# Removes multiple values for the custom field after setting the multiple attribute to false |
|
| 341 |
# We kepp the value with the highest id for each customized object |
|
| 342 |
def handle_multiplicity_change |
|
| 343 |
if !new_record? && multiple_was && !multiple |
|
| 344 |
ids = custom_values. |
|
| 345 |
where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
|
|
| 346 |
" AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
|
|
| 347 |
" AND cve.id > #{CustomValue.table_name}.id)").
|
|
| 348 |
pluck(:id) |
|
| 349 |
|
|
| 350 |
if ids.any? |
|
| 351 |
custom_values.where(:id => ids).delete_all |
|
| 352 |
end |
|
| 353 |
end |
|
| 354 |
end |
|
| 326 | 355 |
end |
| app/models/custom_field_value.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/custom_value.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/document.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 19 | 19 |
include Redmine::SafeAttributes |
| 20 | 20 |
belongs_to :project |
| 21 | 21 |
belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id" |
| 22 |
acts_as_attachable :delete_permission => :manage_documents
|
|
| 22 |
acts_as_attachable :delete_permission => :delete_documents
|
|
| 23 | 23 |
|
| 24 | 24 |
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
|
| 25 | 25 |
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
|
| 26 |
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
|
|
| 26 |
:author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) },
|
|
| 27 | 27 |
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
|
| 28 | 28 |
acts_as_activity_provider :find_options => {:include => :project}
|
| 29 | 29 |
|
| 30 | 30 |
validates_presence_of :project, :title, :category |
| 31 | 31 |
validates_length_of :title, :maximum => 60 |
| 32 | 32 |
|
| 33 |
scope :visible, lambda {|*args| { :include => :project,
|
|
| 34 |
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_documents, *args) } } |
|
| 33 |
scope :visible, lambda {|*args|
|
|
| 34 |
includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args)) |
|
| 35 |
} |
|
| 35 | 36 |
|
| 36 | 37 |
safe_attributes 'category_id', 'title', 'description' |
| 37 | 38 |
|
| app/models/document_category.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/document_category_custom_field.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/document_observer.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/enabled_module.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/enumeration.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 24 | 24 |
|
| 25 | 25 |
acts_as_list :scope => 'type = \'#{type}\''
|
| 26 | 26 |
acts_as_customizable |
| 27 |
acts_as_tree :order => 'position ASC'
|
|
| 27 |
acts_as_tree :order => "#{Enumeration.table_name}.position ASC"
|
|
| 28 | 28 |
|
| 29 | 29 |
before_destroy :check_integrity |
| 30 | 30 |
before_save :check_default |
| ... | ... | |
| 35 | 35 |
validates_uniqueness_of :name, :scope => [:type, :project_id] |
| 36 | 36 |
validates_length_of :name, :maximum => 30 |
| 37 | 37 |
|
| 38 |
scope :shared, where(:project_id => nil) |
|
| 39 |
scope :sorted, order("#{table_name}.position ASC")
|
|
| 40 |
scope :active, where(:active => true) |
|
| 38 |
scope :shared, lambda { where(:project_id => nil) }
|
|
| 39 |
scope :sorted, lambda { order("#{table_name}.position ASC") }
|
|
| 40 |
scope :active, lambda { where(:active => true) }
|
|
| 41 |
scope :system, lambda { where(:project_id => nil) }
|
|
| 41 | 42 |
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
| 42 | 43 |
|
| 43 | 44 |
def self.default |
| app/models/group.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 25 | 25 |
|
| 26 | 26 |
validates_presence_of :lastname |
| 27 | 27 |
validates_uniqueness_of :lastname, :case_sensitive => false |
| 28 |
validates_length_of :lastname, :maximum => 30
|
|
| 28 |
validates_length_of :lastname, :maximum => 255
|
|
| 29 | 29 |
|
| 30 | 30 |
before_destroy :remove_references_before_destroy |
| 31 | 31 |
|
| 32 |
scope :sorted, order("#{table_name}.lastname ASC")
|
|
| 32 |
scope :sorted, lambda { order("#{table_name}.lastname ASC") }
|
|
| 33 |
scope :named, lambda {|arg| where("LOWER(#{table_name}.lastname) = LOWER(?)", arg.to_s.strip)}
|
|
| 33 | 34 |
|
| 34 | 35 |
safe_attributes 'name', |
| 35 | 36 |
'user_ids', |
| ... | ... | |
| 62 | 63 |
|
| 63 | 64 |
def user_removed(user) |
| 64 | 65 |
members.each do |member| |
| 65 |
MemberRole.find(:all, :include => :member, |
|
| 66 |
:conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
|
|
| 66 |
MemberRole. |
|
| 67 |
includes(:member). |
|
| 68 |
where("#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids).
|
|
| 69 |
all. |
|
| 70 |
each(&:destroy) |
|
| 67 | 71 |
end |
| 68 | 72 |
end |
| 69 | 73 |
|
| app/models/group_custom_field.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/issue.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 67 | 67 |
|
| 68 | 68 |
validates_length_of :subject, :maximum => 255 |
| 69 | 69 |
validates_inclusion_of :done_ratio, :in => 0..100 |
| 70 |
validates_numericality_of :estimated_hours, :allow_nil => true |
|
| 70 |
validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
|
|
| 71 |
validates :start_date, :date => true |
|
| 72 |
validates :due_date, :date => true |
|
| 71 | 73 |
validate :validate_issue, :validate_required_fields |
| 72 | 74 |
|
| 73 |
scope :visible, |
|
| 74 |
lambda {|*args| { :include => :project,
|
|
| 75 |
:conditions => Issue.visible_condition(args.shift || User.current, *args) } }
|
|
| 75 |
scope :visible, lambda {|*args|
|
|
| 76 |
includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
|
|
| 77 |
} |
|
| 76 | 78 |
|
| 77 | 79 |
scope :open, lambda {|*args|
|
| 78 | 80 |
is_closed = args.size > 0 ? !args.first : false |
| 79 |
{:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
|
|
| 81 |
includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
|
|
| 80 | 82 |
} |
| 81 | 83 |
|
| 82 |
scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
|
|
| 83 |
scope :on_active_project, :include => [:status, :project, :tracker], |
|
| 84 |
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
|
|
| 84 |
scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
|
|
| 85 |
scope :on_active_project, lambda {
|
|
| 86 |
includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
|
|
| 87 |
} |
|
| 88 |
scope :fixed_version, lambda {|versions|
|
|
| 89 |
ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
|
|
| 90 |
ids.any? ? where(:fixed_version_id => ids) : where('1=0')
|
|
| 91 |
} |
|
| 85 | 92 |
|
| 86 | 93 |
before_create :default_assign |
| 87 |
before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change |
|
| 94 |
before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
|
|
| 88 | 95 |
after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
|
| 89 | 96 |
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal |
| 90 | 97 |
# Should be after_create but would be called before previous after_save callbacks |
| ... | ... | |
| 133 | 140 |
end |
| 134 | 141 |
end |
| 135 | 142 |
|
| 143 |
# Returns true if user or current user is allowed to edit or add a note to the issue |
|
| 144 |
def editable?(user=User.current) |
|
| 145 |
user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project) |
|
| 146 |
end |
|
| 147 |
|
|
| 136 | 148 |
def initialize(attributes=nil, *args) |
| 137 | 149 |
super |
| 138 | 150 |
if new_record? |
| ... | ... | |
| 143 | 155 |
end |
| 144 | 156 |
end |
| 145 | 157 |
|
| 158 |
def create_or_update |
|
| 159 |
super |
|
| 160 |
ensure |
|
| 161 |
@status_was = nil |
|
| 162 |
end |
|
| 163 |
private :create_or_update |
|
| 164 |
|
|
| 146 | 165 |
# AR#Persistence#destroy would raise and RecordNotFound exception |
| 147 | 166 |
# if the issue was already deleted or updated (non matching lock_version). |
| 148 | 167 |
# This is a problem when bulk deleting issues or deleting a project |
| ... | ... | |
| 165 | 184 |
super |
| 166 | 185 |
end |
| 167 | 186 |
|
| 187 |
alias :base_reload :reload |
|
| 168 | 188 |
def reload(*args) |
| 169 | 189 |
@workflow_rule_by_attribute = nil |
| 170 | 190 |
@assignable_versions = nil |
| 171 |
super |
|
| 191 |
@relations = nil |
|
| 192 |
base_reload(*args) |
|
| 172 | 193 |
end |
| 173 | 194 |
|
| 174 | 195 |
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields |
| ... | ... | |
| 526 | 547 |
end |
| 527 | 548 |
|
| 528 | 549 |
def validate_issue |
| 529 |
if due_date.nil? && @attributes['due_date'].present? |
|
| 530 |
errors.add :due_date, :not_a_date |
|
| 531 |
end |
|
| 532 |
|
|
| 533 |
if start_date.nil? && @attributes['start_date'].present? |
|
| 534 |
errors.add :start_date, :not_a_date |
|
| 535 |
end |
|
| 536 |
|
|
| 537 | 550 |
if due_date && start_date && due_date < start_date |
| 538 | 551 |
errors.add :due_date, :greater_than_start_date |
| 539 | 552 |
end |
| ... | ... | |
| 563 | 576 |
elsif @parent_issue |
| 564 | 577 |
if !valid_parent_project?(@parent_issue) |
| 565 | 578 |
errors.add :parent_issue_id, :invalid |
| 579 |
elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self)) |
|
| 580 |
errors.add :parent_issue_id, :invalid |
|
| 566 | 581 |
elsif !new_record? |
| 567 | 582 |
# moving an existing issue |
| 568 | 583 |
if @parent_issue.root_id != root_id |
| ... | ... | |
| 633 | 648 |
scope |
| 634 | 649 |
end |
| 635 | 650 |
|
| 651 |
# Returns the initial status of the issue |
|
| 652 |
# Returns nil for a new issue |
|
| 653 |
def status_was |
|
| 654 |
if status_id_was && status_id_was.to_i > 0 |
|
| 655 |
@status_was ||= IssueStatus.find_by_id(status_id_was) |
|
| 656 |
end |
|
| 657 |
end |
|
| 658 |
|
|
| 636 | 659 |
# Return true if the issue is closed, otherwise false |
| 637 | 660 |
def closed? |
| 638 | 661 |
self.status.is_closed? |
| ... | ... | |
| 653 | 676 |
# Return true if the issue is being closed |
| 654 | 677 |
def closing? |
| 655 | 678 |
if !new_record? && status_id_changed? |
| 656 |
status_was = IssueStatus.find_by_id(status_id_was) |
|
| 657 |
status_new = IssueStatus.find_by_id(status_id) |
|
| 658 |
if status_was && status_new && !status_was.is_closed? && status_new.is_closed? |
|
| 679 |
if status_was && status && !status_was.is_closed? && status.is_closed? |
|
| 659 | 680 |
return true |
| 660 | 681 |
end |
| 661 | 682 |
end |
| ... | ... | |
| 785 | 806 |
end |
| 786 | 807 |
|
| 787 | 808 |
def relations |
| 788 |
@relations ||= IssueRelations.new(self, (relations_from + relations_to).sort) |
|
| 809 |
@relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
|
|
| 789 | 810 |
end |
| 790 | 811 |
|
| 791 | 812 |
# Preloads relations for a collection of issues |
| ... | ... | |
| 822 | 843 |
relations_from.select {|relation| relation.issue_from_id == issue.id} +
|
| 823 | 844 |
relations_to.select {|relation| relation.issue_to_id == issue.id}
|
| 824 | 845 |
|
| 825 |
issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort) |
|
| 846 |
issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
|
|
| 826 | 847 |
end |
| 827 | 848 |
end |
| 828 | 849 |
end |
| ... | ... | |
| 832 | 853 |
IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id]) |
| 833 | 854 |
end |
| 834 | 855 |
|
| 856 |
# Returns all the other issues that depend on the issue |
|
| 835 | 857 |
def all_dependent_issues(except=[]) |
| 836 | 858 |
except << self |
| 837 | 859 |
dependencies = [] |
| 838 |
relations_from.each do |relation| |
|
| 839 |
if relation.issue_to && !except.include?(relation.issue_to) |
|
| 840 |
dependencies << relation.issue_to |
|
| 841 |
dependencies += relation.issue_to.all_dependent_issues(except) |
|
| 842 |
end |
|
| 860 |
dependencies += relations_from.map(&:issue_to) |
|
| 861 |
dependencies += children unless leaf? |
|
| 862 |
dependencies.compact! |
|
| 863 |
dependencies -= except |
|
| 864 |
dependencies += dependencies.map {|issue| issue.all_dependent_issues(except)}.flatten
|
|
| 865 |
if parent |
|
| 866 |
dependencies << parent |
|
| 867 |
dependencies += parent.all_dependent_issues(except + parent.descendants) |
|
| 843 | 868 |
end |
| 844 | 869 |
dependencies |
| 845 | 870 |
end |
| ... | ... | |
| 873 | 898 |
@soonest_start = nil if reload |
| 874 | 899 |
@soonest_start ||= ( |
| 875 | 900 |
relations_to(reload).collect{|relation| relation.successor_soonest_start} +
|
| 876 |
ancestors.collect(&:soonest_start)
|
|
| 901 |
[(@parent_issue || parent).try(:soonest_start)]
|
|
| 877 | 902 |
).compact.max |
| 878 | 903 |
end |
| 879 | 904 |
|
| ... | ... | |
| 936 | 961 |
|
| 937 | 962 |
# Returns a string of css classes that apply to the issue |
| 938 | 963 |
def css_classes |
| 939 |
s = "issue status-#{status_id} #{priority.try(:css_classes)}"
|
|
| 964 |
s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
|
|
| 940 | 965 |
s << ' closed' if closed? |
| 941 | 966 |
s << ' overdue' if overdue? |
| 942 | 967 |
s << ' child' if child? |
| ... | ... | |
| 1005 | 1030 |
end |
| 1006 | 1031 |
end |
| 1007 | 1032 |
|
| 1008 |
# Returns true if issue's project is a valid
|
|
| 1009 |
# parent issue project
|
|
| 1033 |
# Returns true if issue's project is a valid
|
|
| 1034 |
# parent issue project
|
|
| 1010 | 1035 |
def valid_parent_project?(issue=parent) |
| 1011 | 1036 |
return true if issue.nil? || issue.project_id == project_id |
| 1012 | 1037 |
|
| ... | ... | |
| 1128 | 1153 |
end |
| 1129 | 1154 |
|
| 1130 | 1155 |
unless @copied_from.leaf? || @copy_options[:subtasks] == false |
| 1131 |
@copied_from.children.each do |child| |
|
| 1156 |
copy_options = (@copy_options || {}).merge(:subtasks => false)
|
|
| 1157 |
copied_issue_ids = {@copied_from.id => self.id}
|
|
| 1158 |
@copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
|
|
| 1159 |
# Do not copy self when copying an issue as a descendant of the copied issue |
|
| 1160 |
next if child == self |
|
| 1161 |
# Do not copy subtasks of issues that were not copied |
|
| 1162 |
next unless copied_issue_ids[child.parent_id] |
|
| 1163 |
# Do not copy subtasks that are not visible to avoid potential disclosure of private data |
|
| 1132 | 1164 |
unless child.visible? |
| 1133 |
# Do not copy subtasks that are not visible to avoid potential disclosure of private data |
|
| 1134 | 1165 |
logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
|
| 1135 | 1166 |
next |
| 1136 | 1167 |
end |
| 1137 |
copy = Issue.new.copy_from(child, @copy_options)
|
|
| 1168 |
copy = Issue.new.copy_from(child, copy_options) |
|
| 1138 | 1169 |
copy.author = author |
| 1139 | 1170 |
copy.project = project |
| 1140 |
copy.parent_issue_id = id |
|
| 1141 |
# Children subtasks are copied recursively |
|
| 1171 |
copy.parent_issue_id = copied_issue_ids[child.parent_id] |
|
| 1142 | 1172 |
unless copy.save |
| 1143 | 1173 |
logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
|
| 1174 |
next |
|
| 1144 | 1175 |
end |
| 1176 |
copied_issue_ids[child.id] = copy.id |
|
| 1145 | 1177 |
end |
| 1146 | 1178 |
end |
| 1147 | 1179 |
@after_create_from_copy_handled = true |
| ... | ... | |
| 1303 | 1335 |
end |
| 1304 | 1336 |
end |
| 1305 | 1337 |
|
| 1306 |
# Make sure updated_on is updated when adding a note |
|
| 1338 |
# Make sure updated_on is updated when adding a note and set updated_on now |
|
| 1339 |
# so we can set closed_on with the same value on closing |
|
| 1307 | 1340 |
def force_updated_on_change |
| 1308 |
if @current_journal |
|
| 1341 |
if @current_journal || changed?
|
|
| 1309 | 1342 |
self.updated_on = current_time_from_proper_timezone |
| 1343 |
if new_record? |
|
| 1344 |
self.created_on = updated_on |
|
| 1345 |
end |
|
| 1346 |
end |
|
| 1347 |
end |
|
| 1348 |
|
|
| 1349 |
# Callback for setting closed_on when the issue is closed. |
|
| 1350 |
# The closed_on attribute stores the time of the last closing |
|
| 1351 |
# and is preserved when the issue is reopened. |
|
| 1352 |
def update_closed_on |
|
| 1353 |
if closing? || (new_record? && closed?) |
|
| 1354 |
self.closed_on = updated_on |
|
| 1310 | 1355 |
end |
| 1311 | 1356 |
end |
| 1312 | 1357 |
|
| ... | ... | |
| 1316 | 1361 |
if @current_journal |
| 1317 | 1362 |
# attributes changes |
| 1318 | 1363 |
if @attributes_before_change |
| 1319 |
(Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
|
|
| 1364 |
(Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
|
|
| 1320 | 1365 |
before = @attributes_before_change[c] |
| 1321 | 1366 |
after = send(c) |
| 1322 | 1367 |
next if before == after || (before.blank? && after.blank?) |
| app/models/issue_category.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 24 | 24 |
validates_presence_of :name |
| 25 | 25 |
validates_uniqueness_of :name, :scope => [:project_id] |
| 26 | 26 |
validates_length_of :name, :maximum => 30 |
| 27 |
|
|
| 27 |
|
|
| 28 | 28 |
safe_attributes 'name', 'assigned_to_id' |
| 29 | 29 |
|
| 30 | 30 |
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
| ... | ... | |
| 35 | 35 |
# If a category is specified, issues are reassigned to this category |
| 36 | 36 |
def destroy(reassign_to = nil) |
| 37 | 37 |
if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project |
| 38 |
Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")
|
|
| 38 |
Issue.update_all({:category_id => reassign_to.id}, {:category_id => id})
|
|
| 39 | 39 |
end |
| 40 | 40 |
destroy_without_reassign |
| 41 | 41 |
end |
| app/models/issue_custom_field.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/issue_observer.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/issue_priority.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/issue_priority_custom_field.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| app/models/issue_query.rb | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
class IssueQuery < Query |
|
| 19 |
|
|
| 20 |
self.queried_class = Issue |
|
| 21 |
|
|
| 22 |
self.available_columns = [ |
|
| 23 |
QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
|
|
| 24 |
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
|
|
| 25 |
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
|
|
| 26 |
QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
|
|
| 27 |
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
|
|
| 28 |
QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
|
|
| 29 |
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
|
|
| 30 |
QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
|
|
| 31 |
QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
|
|
| 32 |
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
|
|
| 33 |
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
|
|
| 34 |
QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
|
|
| 35 |
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
|
|
| 36 |
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
|
|
| 37 |
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
|
|
| 38 |
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
|
|
| 39 |
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
|
|
| 40 |
QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
|
|
| 41 |
QueryColumn.new(:relations, :caption => :label_related_issues), |
|
| 42 |
QueryColumn.new(:description, :inline => false) |
|
| 43 |
] |
|
| 44 |
|
|
| 45 |
scope :visible, lambda {|*args|
|
|
| 46 |
user = args.shift || User.current |
|
| 47 |
base = Project.allowed_to_condition(user, :view_issues, *args) |
|
| 48 |
user_id = user.logged? ? user.id : 0 |
|
| 49 |
|
|
| 50 |
includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
|
|
| 51 |
} |
|
| 52 |
|
|
| 53 |
def initialize(attributes=nil, *args) |
|
| 54 |
super attributes |
|
| 55 |
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
|
|
| 56 |
end |
|
| 57 |
|
|
| 58 |
# Returns true if the query is visible to +user+ or the current user. |
|
| 59 |
def visible?(user=User.current) |
|
| 60 |
(project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id) |
|
| 61 |
end |
|
| 62 |
|
|
| 63 |
def initialize_available_filters |
|
| 64 |
principals = [] |
|
| 65 |
subprojects = [] |
|
| 66 |
versions = [] |
|
| 67 |
categories = [] |
|
| 68 |
issue_custom_fields = [] |
|
| 69 |
|
|
| 70 |
if project |
|
| 71 |
principals += project.principals.sort |
|
| 72 |
unless project.leaf? |
|
| 73 |
subprojects = project.descendants.visible.all |
|
| 74 |
principals += Principal.member_of(subprojects) |
|
| 75 |
end |
|
| 76 |
versions = project.shared_versions.all |
|
| 77 |
categories = project.issue_categories.all |
|
| 78 |
issue_custom_fields = project.all_issue_custom_fields |
|
| 79 |
else |
|
| 80 |
if all_projects.any? |
|
| 81 |
principals += Principal.member_of(all_projects) |
|
| 82 |
end |
|
| 83 |
versions = Version.visible.find_all_by_sharing('system')
|
|
| 84 |
issue_custom_fields = IssueCustomField.where(:is_filter => true, :is_for_all => true).all |
|
| 85 |
end |
|
| 86 |
principals.uniq! |
|
| 87 |
principals.sort! |
|
| 88 |
users = principals.select {|p| p.is_a?(User)}
|
|
| 89 |
|
|
| 90 |
|
|
| 91 |
add_available_filter "status_id", |
|
| 92 |
:type => :list_status, :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
|
|
| 93 |
|
|
| 94 |
if project.nil? |
|
| 95 |
project_values = [] |
|
| 96 |
if User.current.logged? && User.current.memberships.any? |
|
| 97 |
project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
|
|
| 98 |
end |
|
| 99 |
project_values += all_projects_values |
|
| 100 |
add_available_filter("project_id",
|
|
| 101 |
:type => :list, :values => project_values |
|
| 102 |
) unless project_values.empty? |
|
| 103 |
end |
|
| 104 |
|
|
| 105 |
add_available_filter "tracker_id", |
|
| 106 |
:type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
|
|
| 107 |
add_available_filter "priority_id", |
|
| 108 |
:type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
|
|
| 109 |
|
|
| 110 |
author_values = [] |
|
| 111 |
author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
|
| 112 |
author_values += users.collect{|s| [s.name, s.id.to_s] }
|
|
| 113 |
add_available_filter("author_id",
|
|
| 114 |
:type => :list, :values => author_values |
|
| 115 |
) unless author_values.empty? |
|
| 116 |
|
|
| 117 |
assigned_to_values = [] |
|
| 118 |
assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
|
| 119 |
assigned_to_values += (Setting.issue_group_assignment? ? |
|
| 120 |
principals : users).collect{|s| [s.name, s.id.to_s] }
|
|
| 121 |
add_available_filter("assigned_to_id",
|
|
| 122 |
:type => :list_optional, :values => assigned_to_values |
|
| 123 |
) unless assigned_to_values.empty? |
|
| 124 |
|
|
| 125 |
group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
|
|
| 126 |
add_available_filter("member_of_group",
|
|
| 127 |
:type => :list_optional, :values => group_values |
|
| 128 |
) unless group_values.empty? |
|
| 129 |
|
|
| 130 |
role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
|
|
| 131 |
add_available_filter("assigned_to_role",
|
|
| 132 |
:type => :list_optional, :values => role_values |
|
| 133 |
) unless role_values.empty? |
|
| 134 |
|
|
| 135 |
if versions.any? |
|
| 136 |
add_available_filter "fixed_version_id", |
|
| 137 |
:type => :list_optional, |
|
| 138 |
:values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
|
|
| 139 |
end |
|
| 140 |
|
|
| 141 |
if categories.any? |
|
| 142 |
add_available_filter "category_id", |
|
| 143 |
:type => :list_optional, |
|
| 144 |
:values => categories.collect{|s| [s.name, s.id.to_s] }
|
|
| 145 |
end |
|
| 146 |
|
|
| 147 |
add_available_filter "subject", :type => :text |
|
| 148 |
add_available_filter "created_on", :type => :date_past |
|
| 149 |
add_available_filter "updated_on", :type => :date_past |
|
| 150 |
add_available_filter "closed_on", :type => :date_past |
|
| 151 |
add_available_filter "start_date", :type => :date |
|
| 152 |
add_available_filter "due_date", :type => :date |
|
| 153 |
add_available_filter "estimated_hours", :type => :float |
|
| 154 |
add_available_filter "done_ratio", :type => :integer |
|
| 155 |
|
|
| 156 |
if User.current.allowed_to?(:set_issues_private, nil, :global => true) || |
|
| 157 |
User.current.allowed_to?(:set_own_issues_private, nil, :global => true) |
|
| 158 |
add_available_filter "is_private", |
|
| 159 |
:type => :list, |
|
| 160 |
:values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] |
|
| 161 |
end |
|
| 162 |
|
|
| 163 |
if User.current.logged? |
|
| 164 |
add_available_filter "watcher_id", |
|
| 165 |
:type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
|
|
| 166 |
end |
|
| 167 |
|
|
| 168 |
if subprojects.any? |
|
| 169 |
add_available_filter "subproject_id", |
|
| 170 |
:type => :list_subprojects, |
|
| 171 |
:values => subprojects.collect{|s| [s.name, s.id.to_s] }
|
|
| 172 |
end |
|
| 173 |
|
|
| 174 |
add_custom_fields_filters(issue_custom_fields) |
|
| 175 |
|
|
| 176 |
add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version |
|
| 177 |
|
|
| 178 |
IssueRelation::TYPES.each do |relation_type, options| |
|
| 179 |
add_available_filter relation_type, :type => :relation, :label => options[:name] |
|
| 180 |
end |
|
| 181 |
|
|
| 182 |
Tracker.disabled_core_fields(trackers).each {|field|
|
|
| 183 |
delete_available_filter field |
|
| 184 |
} |
|
| 185 |
end |
|
| 186 |
|
|
| 187 |
def available_columns |
|
| 188 |
return @available_columns if @available_columns |
|
| 189 |
@available_columns = self.class.available_columns.dup |
|
| 190 |
@available_columns += (project ? |
|
| 191 |
project.all_issue_custom_fields : |
|
| 192 |
IssueCustomField.all |
|
| 193 |
).collect {|cf| QueryCustomFieldColumn.new(cf) }
|
|
| 194 |
|
|
| 195 |
if User.current.allowed_to?(:view_time_entries, project, :global => true) |
|
| 196 |
index = nil |
|
| 197 |
@available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
|
|
| 198 |
index = (index ? index + 1 : -1) |
|
| 199 |
# insert the column after estimated_hours or at the end |
|
| 200 |
@available_columns.insert index, QueryColumn.new(:spent_hours, |
|
| 201 |
:sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
|
|
| 202 |
:default_order => 'desc', |
|
| 203 |
:caption => :label_spent_time |
|
| 204 |
) |
|
| 205 |
end |
|
| 206 |
|
|
| 207 |
if User.current.allowed_to?(:set_issues_private, nil, :global => true) || |
|
| 208 |
User.current.allowed_to?(:set_own_issues_private, nil, :global => true) |
|
| 209 |
@available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
|
|
| 210 |
end |
|
| 211 |
|
|
| 212 |
disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
|
|
| 213 |
@available_columns.reject! {|column|
|
|
| 214 |
disabled_fields.include?(column.name.to_s) |
|
| 215 |
} |
|
| 216 |
|
|
| 217 |
@available_columns |
|
| 218 |
end |
|
| 219 |
|
|
| 220 |
def default_columns_names |
|
| 221 |
@default_columns_names ||= begin |
|
| 222 |
default_columns = Setting.issue_list_default_columns.map(&:to_sym) |
|
| 223 |
|
|
| 224 |
project.present? ? default_columns : [:project] | default_columns |
|
| 225 |
end |
|
| 226 |
end |
|
| 227 |
|
|
| 228 |
# Returns the issue count |
|
| 229 |
def issue_count |
|
| 230 |
Issue.visible.count(:include => [:status, :project], :conditions => statement) |
|
| 231 |
rescue ::ActiveRecord::StatementInvalid => e |
|
| 232 |
raise StatementInvalid.new(e.message) |
|
| 233 |
end |
|
| 234 |
|
|
| 235 |
# Returns the issue count by group or nil if query is not grouped |
|
| 236 |
def issue_count_by_group |
|
| 237 |
r = nil |
|
| 238 |
if grouped? |
|
| 239 |
begin |
|
| 240 |
# Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value |
|
| 241 |
r = Issue.visible.count(:joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement) |
|
| 242 |
rescue ActiveRecord::RecordNotFound |
|
| 243 |
r = {nil => issue_count}
|
|
| 244 |
end |
|
| 245 |
c = group_by_column |
|
| 246 |
if c.is_a?(QueryCustomFieldColumn) |
|
| 247 |
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
|
|
| 248 |
end |
|
| 249 |
end |
|
| 250 |
r |
|
| 251 |
rescue ::ActiveRecord::StatementInvalid => e |
|
| 252 |
raise StatementInvalid.new(e.message) |
|
| 253 |
end |
|
| 254 |
|
|
| 255 |
# Returns the issues |
|
| 256 |
# Valid options are :order, :offset, :limit, :include, :conditions |
|
| 257 |
def issues(options={})
|
|
| 258 |
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?) |
|
| 259 |
|
|
| 260 |
issues = Issue.visible.where(options[:conditions]).all( |
|
| 261 |
:include => ([:status, :project] + (options[:include] || [])).uniq, |
|
| 262 |
:conditions => statement, |
|
| 263 |
:order => order_option, |
|
| 264 |
:joins => joins_for_order_statement(order_option.join(',')),
|
|
| 265 |
:limit => options[:limit], |
|
| 266 |
:offset => options[:offset] |
|
| 267 |
) |
|
| 268 |
|
|
| 269 |
if has_column?(:spent_hours) |
|
| 270 |
Issue.load_visible_spent_hours(issues) |
|
| 271 |
end |
|
| 272 |
if has_column?(:relations) |
|
| 273 |
Issue.load_visible_relations(issues) |
|
| 274 |
end |
|
| 275 |
issues |
|
| 276 |
rescue ::ActiveRecord::StatementInvalid => e |
|
| 277 |
raise StatementInvalid.new(e.message) |
|
| 278 |
end |
|
| 279 |
|
|
| 280 |
# Returns the issues ids |
|
| 281 |
def issue_ids(options={})
|
|
| 282 |
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?) |
|
| 283 |
|
|
| 284 |
Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq, |
|
| 285 |
:conditions => statement, |
|
| 286 |
:order => order_option, |
|
| 287 |
:joins => joins_for_order_statement(order_option.join(',')),
|
|
| 288 |
:limit => options[:limit], |
|
| 289 |
:offset => options[:offset]).find_ids |
|
| 290 |
rescue ::ActiveRecord::StatementInvalid => e |
|
| 291 |
raise StatementInvalid.new(e.message) |
|
| 292 |
end |
|
| 293 |
|
|
| 294 |
# Returns the journals |
|
| 295 |
# Valid options are :order, :offset, :limit |
|
| 296 |
def journals(options={})
|
|
| 297 |
Journal.visible.all( |
|
| 298 |
:include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
|
|
| 299 |
:conditions => statement, |
|
| 300 |
:order => options[:order], |
|
| 301 |
:limit => options[:limit], |
|
| 302 |
:offset => options[:offset] |
|
| 303 |
) |
|
| 304 |
rescue ::ActiveRecord::StatementInvalid => e |
|
| 305 |
raise StatementInvalid.new(e.message) |
|
| 306 |
end |
|
| 307 |
|
|
| 308 |
# Returns the versions |
|
| 309 |
# Valid options are :conditions |
|
| 310 |
def versions(options={})
|
|
| 311 |
Version.visible.where(options[:conditions]).all( |
|
| 312 |
:include => :project, |
|
| 313 |
:conditions => project_statement |
|
| 314 |
) |
|
| 315 |
rescue ::ActiveRecord::StatementInvalid => e |
|
| 316 |
raise StatementInvalid.new(e.message) |
|
| 317 |
end |
|
| 318 |
|
|
| 319 |
def sql_for_watcher_id_field(field, operator, value) |
|
| 320 |
db_table = Watcher.table_name |
|
| 321 |
"#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
|
|
| 322 |
sql_for_field(field, '=', value, db_table, 'user_id') + ')' |
|
| 323 |
end |
|
| 324 |
|
|
| 325 |
def sql_for_member_of_group_field(field, operator, value) |
|
| 326 |
if operator == '*' # Any group |
|
| 327 |
groups = Group.all |
|
| 328 |
operator = '=' # Override the operator since we want to find by assigned_to |
|
| 329 |
elsif operator == "!*" |
|
| 330 |
groups = Group.all |
|
| 331 |
operator = '!' # Override the operator since we want to find by assigned_to |
|
| 332 |
else |
|
| 333 |
groups = Group.find_all_by_id(value) |
|
| 334 |
end |
|
| 335 |
groups ||= [] |
|
| 336 |
|
|
| 337 |
members_of_groups = groups.inject([]) {|user_ids, group|
|
|
| 338 |
user_ids + group.user_ids + [group.id] |
|
| 339 |
}.uniq.compact.sort.collect(&:to_s) |
|
| 340 |
|
|
| 341 |
'(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
|
|
| 342 |
end |
|
| 343 |
|
|
| 344 |
def sql_for_assigned_to_role_field(field, operator, value) |
|
| 345 |
case operator |
|
| 346 |
when "*", "!*" # Member / Not member |
|
| 347 |
sw = operator == "!*" ? 'NOT' : '' |
|
| 348 |
nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
|
|
| 349 |
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
|
|
| 350 |
" WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
|
|
| 351 |
when "=", "!" |
|
| 352 |
role_cond = value.any? ? |
|
| 353 |
"#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
|
|
| 354 |
"1=0" |
|
| 355 |
|
|
| 356 |
sw = operator == "!" ? 'NOT' : '' |
|
| 357 |
nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
|
|
| 358 |
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
|
|
| 359 |
" WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
|
|
| 360 |
end |
|
| 361 |
end |
|
| 362 |
|
|
| 363 |
def sql_for_is_private_field(field, operator, value) |
|
| 364 |
op = (operator == "=" ? 'IN' : 'NOT IN') |
|
| 365 |
va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
|
|
| 366 |
|
|
| 367 |
"#{Issue.table_name}.is_private #{op} (#{va})"
|
|
| 368 |
end |
|
| 369 |
|
|
| 370 |
def sql_for_relations(field, operator, value, options={})
|
|
| 371 |
relation_options = IssueRelation::TYPES[field] |
|
| 372 |
return relation_options unless relation_options |
|
| 373 |
|
|
| 374 |
relation_type = field |
|
| 375 |
join_column, target_join_column = "issue_from_id", "issue_to_id" |
|
| 376 |
if relation_options[:reverse] || options[:reverse] |
|
| 377 |
relation_type = relation_options[:reverse] || relation_type |
|
| 378 |
join_column, target_join_column = target_join_column, join_column |
|
| 379 |
end |
|
| 380 |
|
|
| 381 |
sql = case operator |
|
| 382 |
when "*", "!*" |
|
| 383 |
op = (operator == "*" ? 'IN' : 'NOT IN') |
|
| 384 |
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
|
|
| 385 |
when "=", "!" |
|
| 386 |
op = (operator == "=" ? 'IN' : 'NOT IN') |
|
| 387 |
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
|
|
| 388 |
when "=p", "=!p", "!p" |
|
| 389 |
op = (operator == "!p" ? 'NOT IN' : 'IN') |
|
| 390 |
comp = (operator == "=!p" ? '<>' : '=') |
|
| 391 |
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
|
|
| 392 |
end |
|
| 393 |
|
|
| 394 |
if relation_options[:sym] == field && !options[:reverse] |
|
| 395 |
sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)] |
|
| 396 |
sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ") |
|
| 397 |
else |
|
| 398 |
sql |
|
| 399 |
end |
|
| 400 |
end |
|
| 401 |
|
|
| 402 |
IssueRelation::TYPES.keys.each do |relation_type| |
|
| 403 |
alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
|
|
| 404 |
end |
|
| 405 |
end |
|
| app/models/issue_relation.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 15 | 15 |
# along with this program; if not, write to the Free Software |
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 17 | 17 |
|
| 18 |
# Class used to represent the relations of an issue |
|
| 19 |
class IssueRelations < Array |
|
| 20 |
include Redmine::I18n |
|
| 18 |
class IssueRelation < ActiveRecord::Base |
|
| 19 |
# Class used to represent the relations of an issue |
|
| 20 |
class Relations < Array |
|
| 21 |
include Redmine::I18n |
|
| 21 | 22 |
|
| 22 |
def initialize(issue, *args) |
|
| 23 |
@issue = issue |
|
| 24 |
super(*args) |
|
| 23 |
def initialize(issue, *args) |
|
| 24 |
@issue = issue |
|
| 25 |
super(*args) |
|
| 26 |
end |
|
| 27 |
|
|
| 28 |
def to_s(*args) |
|
| 29 |
map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
|
|
| 30 |
end |
|
| 25 | 31 |
end |
| 26 | 32 |
|
| 27 |
def to_s(*args) |
|
| 28 |
map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
|
|
| 29 |
end |
|
| 30 |
end |
|
| 31 |
|
|
| 32 |
class IssueRelation < ActiveRecord::Base |
|
| 33 | 33 |
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' |
| 34 | 34 |
belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' |
| 35 | 35 |
|
| app/models/issue_status.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 28 | 28 |
validates_length_of :name, :maximum => 30 |
| 29 | 29 |
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true |
| 30 | 30 |
|
| 31 |
scope :sorted, order("#{table_name}.position ASC")
|
|
| 31 |
scope :sorted, lambda { order("#{table_name}.position ASC") }
|
|
| 32 | 32 |
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
| 33 | 33 |
|
| 34 | 34 |
def update_default |
| app/models/journal.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 28 | 28 |
acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
|
| 29 | 29 |
:description => :notes, |
| 30 | 30 |
:author => :user, |
| 31 |
:group => :issue, |
|
| 31 | 32 |
:type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
|
| 32 | 33 |
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
|
| 33 | 34 |
|
| app/models/journal_detail.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
| 2 |
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
| 3 | 3 |
# |
| 4 | 4 |
# This program is free software; you can redistribute it and/or |
| 5 | 5 |
# modify it under the terms of the GNU General Public License |
| ... | ... | |
| 27 | 27 |
end |
| 28 | 28 |
|
| 29 | 29 |
def normalize(v) |
| 30 |
if v == true |
|
| 30 |
case v |
|
| 31 |
when true |
|
| 31 | 32 |
"1" |
| 32 |
elsif v == false
|
|
| 33 |
when false
|
|
| 33 | 34 |
"0" |
| 35 |
when Date |
|
| 36 |
v.strftime("%Y-%m-%d")
|
|
| 34 | 37 |
else |
| 35 | 38 |
v |
| 36 | 39 |
end |
| app/models/journal_observer.rb | ||
|---|---|---|
| 1 | 1 |
# Redmine - project management software |
Also available in: Unified diff