Added branch for sys controller refactoring.
git-svn-id: http://redmine.rubyforge.org/svn/branches/work@1006 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require "digest/md5"
|
||||
|
||||
class Attachment < ActiveRecord::Base
|
||||
belongs_to :container, :polymorphic => true
|
||||
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
|
||||
|
||||
validates_presence_of :container, :filename, :author
|
||||
validates_length_of :filename, :maximum => 255
|
||||
validates_length_of :disk_filename, :maximum => 255
|
||||
|
||||
acts_as_event :title => :filename,
|
||||
:description => :filename,
|
||||
:url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id}}
|
||||
|
||||
cattr_accessor :storage_path
|
||||
@@storage_path = "#{RAILS_ROOT}/files"
|
||||
|
||||
def validate
|
||||
errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
|
||||
end
|
||||
|
||||
def file=(incomming_file)
|
||||
unless incomming_file.nil?
|
||||
@temp_file = incomming_file
|
||||
if @temp_file.size > 0
|
||||
self.filename = sanitize_filename(@temp_file.original_filename)
|
||||
self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename
|
||||
self.content_type = @temp_file.content_type.chomp
|
||||
self.filesize = @temp_file.size
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def file
|
||||
nil
|
||||
end
|
||||
|
||||
# Copy temp file to its final location
|
||||
def before_save
|
||||
if @temp_file && (@temp_file.size > 0)
|
||||
logger.debug("saving '#{self.diskfile}'")
|
||||
File.open(diskfile, "wb") do |f|
|
||||
f.write(@temp_file.read)
|
||||
end
|
||||
self.digest = Digest::MD5.hexdigest(File.read(diskfile))
|
||||
end
|
||||
# Don't save the content type if it's longer than the authorized length
|
||||
if self.content_type && self.content_type.length > 255
|
||||
self.content_type = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes file on the disk
|
||||
def after_destroy
|
||||
if self.filename?
|
||||
File.delete(diskfile) if File.exist?(diskfile)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns file's location on disk
|
||||
def diskfile
|
||||
"#{@@storage_path}/#{self.disk_filename}"
|
||||
end
|
||||
|
||||
def increment_download
|
||||
increment!(:downloads)
|
||||
end
|
||||
|
||||
def project
|
||||
container.project
|
||||
end
|
||||
|
||||
def image?
|
||||
self.filename =~ /\.(jpeg|jpg|gif|png)$/i
|
||||
end
|
||||
|
||||
private
|
||||
def sanitize_filename(value)
|
||||
# get only the filename, not the whole path
|
||||
just_filename = value.gsub(/^.*(\\|\/)/, '')
|
||||
# NOTE: File.basename doesn't work right with Windows paths on Unix
|
||||
# INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
|
||||
|
||||
# Finally, replace all non alphanumeric, underscore or periods with underscore
|
||||
@filename = just_filename.gsub(/[^\w\.\-]/,'_')
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class AuthSource < ActiveRecord::Base
|
||||
has_many :users
|
||||
|
||||
validates_presence_of :name
|
||||
validates_uniqueness_of :name
|
||||
validates_length_of :name, :host, :maximum => 60
|
||||
validates_length_of :account_password, :maximum => 60, :allow_nil => true
|
||||
validates_length_of :account, :base_dn, :maximum => 255
|
||||
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30
|
||||
|
||||
def authenticate(login, password)
|
||||
end
|
||||
|
||||
def test_connection
|
||||
end
|
||||
|
||||
def auth_method_name
|
||||
"Abstract"
|
||||
end
|
||||
|
||||
# Try to authenticate a user not yet registered against available sources
|
||||
def self.authenticate(login, password)
|
||||
AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|
|
||||
begin
|
||||
logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
|
||||
attrs = source.authenticate(login, password)
|
||||
rescue
|
||||
attrs = nil
|
||||
end
|
||||
return attrs if attrs
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'net/ldap'
|
||||
require 'iconv'
|
||||
|
||||
class AuthSourceLdap < AuthSource
|
||||
validates_presence_of :host, :port, :attr_login
|
||||
validates_presence_of :attr_firstname, :attr_lastname, :attr_mail, :if => Proc.new { |a| a.onthefly_register? }
|
||||
|
||||
def after_initialize
|
||||
self.port = 389 if self.port == 0
|
||||
end
|
||||
|
||||
def authenticate(login, password)
|
||||
attrs = []
|
||||
# get user's DN
|
||||
ldap_con = initialize_ldap_con(self.account, self.account_password)
|
||||
login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
|
||||
object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
|
||||
dn = String.new
|
||||
ldap_con.search( :base => self.base_dn,
|
||||
:filter => object_filter & login_filter,
|
||||
# only ask for the DN if on-the-fly registration is disabled
|
||||
:attributes=> (onthefly_register? ? ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] : ['dn'])) do |entry|
|
||||
dn = entry.dn
|
||||
attrs = [:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
|
||||
:lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
|
||||
:mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
|
||||
:auth_source_id => self.id ] if onthefly_register?
|
||||
end
|
||||
return nil if dn.empty?
|
||||
logger.debug "DN found for #{login}: #{dn}" if logger && logger.debug?
|
||||
# authenticate user
|
||||
ldap_con = initialize_ldap_con(dn, password)
|
||||
return nil unless ldap_con.bind
|
||||
# return user's attributes
|
||||
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
|
||||
attrs
|
||||
rescue Net::LDAP::LdapError => text
|
||||
raise "LdapError: " + text
|
||||
end
|
||||
|
||||
# test the connection to the LDAP
|
||||
def test_connection
|
||||
ldap_con = initialize_ldap_con(self.account, self.account_password)
|
||||
ldap_con.open { }
|
||||
rescue Net::LDAP::LdapError => text
|
||||
raise "LdapError: " + text
|
||||
end
|
||||
|
||||
def auth_method_name
|
||||
"LDAP"
|
||||
end
|
||||
|
||||
private
|
||||
def initialize_ldap_con(ldap_user, ldap_password)
|
||||
Net::LDAP.new( {:host => self.host,
|
||||
:port => self.port,
|
||||
:auth => { :method => :simple, :username => ldap_user, :password => ldap_password },
|
||||
:encryption => (self.tls ? :simple_tls : nil)}
|
||||
)
|
||||
end
|
||||
|
||||
def self.get_attr(entry, attr_name)
|
||||
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Board < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
|
||||
has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC"
|
||||
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
|
||||
acts_as_list :scope => :project_id
|
||||
acts_as_watchable
|
||||
|
||||
validates_presence_of :name, :description
|
||||
validates_length_of :name, :maximum => 30
|
||||
validates_length_of :description, :maximum => 255
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Change < ActiveRecord::Base
|
||||
belongs_to :changeset
|
||||
|
||||
validates_presence_of :changeset_id, :action, :path
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Changeset < ActiveRecord::Base
|
||||
belongs_to :repository
|
||||
has_many :changes, :dependent => :delete_all
|
||||
has_and_belongs_to_many :issues
|
||||
|
||||
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
|
||||
:description => :comments,
|
||||
:datetime => :committed_on,
|
||||
:author => :committer,
|
||||
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
|
||||
|
||||
acts_as_searchable :columns => 'comments',
|
||||
:include => :repository,
|
||||
:project_key => "#{Repository.table_name}.project_id",
|
||||
:date_column => 'committed_on'
|
||||
|
||||
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
|
||||
validates_numericality_of :revision, :only_integer => true
|
||||
validates_uniqueness_of :revision, :scope => :repository_id
|
||||
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
|
||||
|
||||
def comments=(comment)
|
||||
write_attribute(:comments, comment.strip)
|
||||
end
|
||||
|
||||
def committed_on=(date)
|
||||
self.commit_date = date
|
||||
super
|
||||
end
|
||||
|
||||
def after_create
|
||||
scan_comment_for_issue_ids
|
||||
end
|
||||
|
||||
def scan_comment_for_issue_ids
|
||||
return if comments.blank?
|
||||
# keywords used to reference issues
|
||||
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
|
||||
# keywords used to fix issues
|
||||
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
|
||||
# status and optional done ratio applied
|
||||
fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
|
||||
done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
|
||||
|
||||
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
|
||||
return if kw_regexp.blank?
|
||||
|
||||
referenced_issues = []
|
||||
|
||||
if ref_keywords.delete('*')
|
||||
# find any issue ID in the comments
|
||||
target_issue_ids = []
|
||||
comments.scan(%r{([\s\(,-^])#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
|
||||
referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
|
||||
end
|
||||
|
||||
comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
|
||||
action = match[0]
|
||||
target_issue_ids = match[1].scan(/\d+/)
|
||||
target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
|
||||
if fix_status && fix_keywords.include?(action.downcase)
|
||||
# update status of issues
|
||||
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
|
||||
target_issues.each do |issue|
|
||||
# don't change the status is the issue is already closed
|
||||
next if issue.status.is_closed?
|
||||
issue.status = fix_status
|
||||
issue.done_ratio = done_ratio if done_ratio
|
||||
issue.save
|
||||
end
|
||||
end
|
||||
referenced_issues += target_issues
|
||||
end
|
||||
|
||||
self.issues = referenced_issues.uniq
|
||||
end
|
||||
|
||||
# Returns the previous changeset
|
||||
def previous
|
||||
@previous ||= Changeset.find(:first, :conditions => ['revision < ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision DESC')
|
||||
end
|
||||
|
||||
# Returns the next changeset
|
||||
def next
|
||||
@next ||= Changeset.find(:first, :conditions => ['revision > ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision ASC')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Comment < ActiveRecord::Base
|
||||
belongs_to :commented, :polymorphic => true, :counter_cache => true
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
|
||||
validates_presence_of :commented, :author, :comments
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class CustomField < ActiveRecord::Base
|
||||
has_many :custom_values, :dependent => :delete_all
|
||||
acts_as_list :scope => 'type = \'#{self.class}\''
|
||||
serialize :possible_values
|
||||
|
||||
FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
|
||||
"text" => { :name => :label_text, :order => 2 },
|
||||
"int" => { :name => :label_integer, :order => 3 },
|
||||
"float" => { :name => :label_float, :order => 4 },
|
||||
"list" => { :name => :label_list, :order => 5 },
|
||||
"date" => { :name => :label_date, :order => 6 },
|
||||
"bool" => { :name => :label_boolean, :order => 7 }
|
||||
}.freeze
|
||||
|
||||
validates_presence_of :name, :field_format
|
||||
validates_uniqueness_of :name
|
||||
validates_length_of :name, :maximum => 30
|
||||
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
|
||||
validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
|
||||
|
||||
def initialize(attributes = nil)
|
||||
super
|
||||
self.possible_values ||= []
|
||||
end
|
||||
|
||||
def before_validation
|
||||
# remove empty values
|
||||
self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact
|
||||
# make sure these fields are not searchable
|
||||
self.searchable = false if %w(int float date bool).include?(field_format)
|
||||
true
|
||||
end
|
||||
|
||||
def validate
|
||||
if self.field_format == "list"
|
||||
errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
|
||||
errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
|
||||
end
|
||||
end
|
||||
|
||||
def <=>(field)
|
||||
position <=> field.position
|
||||
end
|
||||
|
||||
# to move in project_custom_field
|
||||
def self.for_all
|
||||
find(:all, :conditions => ["is_for_all=?", true])
|
||||
end
|
||||
|
||||
def type_name
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class CustomValue < ActiveRecord::Base
|
||||
belongs_to :custom_field
|
||||
belongs_to :customized, :polymorphic => true
|
||||
|
||||
protected
|
||||
def validate
|
||||
errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.blank?
|
||||
errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
|
||||
errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length and value.length > 0
|
||||
errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
|
||||
case custom_field.field_format
|
||||
when 'int'
|
||||
errors.add(:value, :activerecord_error_not_a_number) unless value.blank? || value =~ /^[+-]?\d+$/
|
||||
when 'float'
|
||||
begin; !value.blank? && Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
|
||||
when 'date'
|
||||
errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.blank?
|
||||
when 'list'
|
||||
errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) or value.blank?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Document < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
|
||||
has_many :attachments, :as => :container, :dependent => :destroy
|
||||
|
||||
acts_as_searchable :columns => ['title', 'description']
|
||||
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
|
||||
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
|
||||
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
|
||||
|
||||
validates_presence_of :project, :title, :category
|
||||
validates_length_of :title, :maximum => 60
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class EnabledModule < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
|
||||
validates_presence_of :name
|
||||
validates_uniqueness_of :name, :scope => :project_id
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Enumeration < ActiveRecord::Base
|
||||
acts_as_list :scope => 'opt = \'#{opt}\''
|
||||
|
||||
before_destroy :check_integrity
|
||||
|
||||
validates_presence_of :opt, :name
|
||||
validates_uniqueness_of :name, :scope => [:opt]
|
||||
validates_length_of :name, :maximum => 30
|
||||
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
|
||||
|
||||
OPTIONS = {
|
||||
"IPRI" => :enumeration_issue_priorities,
|
||||
"DCAT" => :enumeration_doc_categories,
|
||||
"ACTI" => :enumeration_activities
|
||||
}.freeze
|
||||
|
||||
def self.get_values(option)
|
||||
find(:all, :conditions => {:opt => option}, :order => 'position')
|
||||
end
|
||||
|
||||
def self.default(option)
|
||||
find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position')
|
||||
end
|
||||
|
||||
def option_name
|
||||
OPTIONS[self.opt]
|
||||
end
|
||||
|
||||
def before_save
|
||||
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default?
|
||||
end
|
||||
|
||||
def <=>(enumeration)
|
||||
position <=> enumeration.position
|
||||
end
|
||||
|
||||
def to_s; name end
|
||||
|
||||
private
|
||||
def check_integrity
|
||||
case self.opt
|
||||
when "IPRI"
|
||||
raise "Can't delete enumeration" if Issue.find(:first, :conditions => ["priority_id=?", self.id])
|
||||
when "DCAT"
|
||||
raise "Can't delete enumeration" if Document.find(:first, :conditions => ["category_id=?", self.id])
|
||||
when "ACTI"
|
||||
raise "Can't delete enumeration" if TimeEntry.find(:first, :conditions => ["activity_id=?", self.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,221 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Issue < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
belongs_to :tracker
|
||||
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
|
||||
belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
|
||||
belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
|
||||
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
|
||||
|
||||
has_many :journals, :as => :journalized, :dependent => :destroy
|
||||
has_many :attachments, :as => :container, :dependent => :destroy
|
||||
has_many :time_entries, :dependent => :nullify
|
||||
has_many :custom_values, :dependent => :delete_all, :as => :customized
|
||||
has_many :custom_fields, :through => :custom_values
|
||||
has_and_belongs_to_many :changesets, :order => "revision ASC"
|
||||
|
||||
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
|
||||
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
|
||||
|
||||
acts_as_watchable
|
||||
acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
|
||||
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
|
||||
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
|
||||
|
||||
validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
|
||||
validates_length_of :subject, :maximum => 255
|
||||
validates_inclusion_of :done_ratio, :in => 0..100
|
||||
validates_numericality_of :estimated_hours, :allow_nil => true
|
||||
validates_associated :custom_values, :on => :update
|
||||
|
||||
def after_initialize
|
||||
if new_record?
|
||||
# set default values for new records only
|
||||
self.status ||= IssueStatus.default
|
||||
self.priority ||= Enumeration.default('IPRI')
|
||||
end
|
||||
end
|
||||
|
||||
def copy_from(arg)
|
||||
issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
|
||||
self.attributes = issue.attributes.dup
|
||||
self.custom_values = issue.custom_values.collect {|v| v.clone}
|
||||
self
|
||||
end
|
||||
|
||||
# Move an issue to a new project and tracker
|
||||
def move_to(new_project, new_tracker = nil)
|
||||
transaction do
|
||||
if new_project && project_id != new_project.id
|
||||
# delete issue relations
|
||||
self.relations_from.clear
|
||||
self.relations_to.clear
|
||||
# issue is moved to another project
|
||||
self.category = nil
|
||||
self.fixed_version = nil
|
||||
self.project = new_project
|
||||
end
|
||||
if new_tracker
|
||||
self.tracker = new_tracker
|
||||
end
|
||||
if save
|
||||
# Manually update project_id on related time entries
|
||||
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
|
||||
else
|
||||
rollback_db_transaction
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
def priority_id=(pid)
|
||||
self.priority = nil
|
||||
write_attribute(:priority_id, pid)
|
||||
end
|
||||
|
||||
def validate
|
||||
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
|
||||
errors.add :due_date, :activerecord_error_not_a_date
|
||||
end
|
||||
|
||||
if self.due_date and self.start_date and self.due_date < self.start_date
|
||||
errors.add :due_date, :activerecord_error_greater_than_start_date
|
||||
end
|
||||
|
||||
if start_date && soonest_start && start_date < soonest_start
|
||||
errors.add :start_date, :activerecord_error_invalid
|
||||
end
|
||||
end
|
||||
|
||||
def validate_on_create
|
||||
errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
|
||||
end
|
||||
|
||||
def before_create
|
||||
# default assignment based on category
|
||||
if assigned_to.nil? && category && category.assigned_to
|
||||
self.assigned_to = category.assigned_to
|
||||
end
|
||||
end
|
||||
|
||||
def before_save
|
||||
if @current_journal
|
||||
# attributes changes
|
||||
(Issue.column_names - %w(id description)).each {|c|
|
||||
@current_journal.details << JournalDetail.new(:property => 'attr',
|
||||
:prop_key => c,
|
||||
:old_value => @issue_before_change.send(c),
|
||||
:value => send(c)) unless send(c)==@issue_before_change.send(c)
|
||||
}
|
||||
# custom fields changes
|
||||
custom_values.each {|c|
|
||||
next if (@custom_values_before_change[c.custom_field_id]==c.value ||
|
||||
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
|
||||
@current_journal.details << JournalDetail.new(:property => 'cf',
|
||||
:prop_key => c.custom_field_id,
|
||||
:old_value => @custom_values_before_change[c.custom_field_id],
|
||||
:value => c.value)
|
||||
}
|
||||
@current_journal.save
|
||||
end
|
||||
# Save the issue even if the journal is not saved (because empty)
|
||||
true
|
||||
end
|
||||
|
||||
def after_save
|
||||
# Update start/due dates of following issues
|
||||
relations_from.each(&:set_issue_to_dates)
|
||||
|
||||
# Close duplicates if the issue was closed
|
||||
if @issue_before_change && !@issue_before_change.closed? && self.closed?
|
||||
duplicates.each do |duplicate|
|
||||
# Don't re-close it if it's already closed
|
||||
next if duplicate.closed?
|
||||
# Same user and notes
|
||||
duplicate.init_journal(@current_journal.user, @current_journal.notes)
|
||||
duplicate.update_attribute :status, self.status
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def custom_value_for(custom_field)
|
||||
self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
|
||||
return nil
|
||||
end
|
||||
|
||||
def init_journal(user, notes = "")
|
||||
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
|
||||
@issue_before_change = self.clone
|
||||
@custom_values_before_change = {}
|
||||
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
|
||||
@current_journal
|
||||
end
|
||||
|
||||
# Return true if the issue is closed, otherwise false
|
||||
def closed?
|
||||
self.status.is_closed?
|
||||
end
|
||||
|
||||
# Users the issue can be assigned to
|
||||
def assignable_users
|
||||
project.assignable_users
|
||||
end
|
||||
|
||||
# Returns the mail adresses of users that should be notified for the issue
|
||||
def recipients
|
||||
recipients = project.recipients
|
||||
# Author and assignee are always notified
|
||||
recipients << author.mail if author
|
||||
recipients << assigned_to.mail if assigned_to
|
||||
recipients.compact.uniq
|
||||
end
|
||||
|
||||
def spent_hours
|
||||
@spent_hours ||= time_entries.sum(:hours) || 0
|
||||
end
|
||||
|
||||
def relations
|
||||
(relations_from + relations_to).sort
|
||||
end
|
||||
|
||||
def all_dependent_issues
|
||||
dependencies = []
|
||||
relations_from.each do |relation|
|
||||
dependencies << relation.issue_to
|
||||
dependencies += relation.issue_to.all_dependent_issues
|
||||
end
|
||||
dependencies
|
||||
end
|
||||
|
||||
# Returns an array of the duplicate issues
|
||||
def duplicates
|
||||
relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
|
||||
end
|
||||
|
||||
def duration
|
||||
(start_date && due_date) ? due_date - start_date : 0
|
||||
end
|
||||
|
||||
def soonest_start
|
||||
@soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class IssueCategory < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
|
||||
has_many :issues, :foreign_key => 'category_id', :dependent => :nullify
|
||||
|
||||
validates_presence_of :name
|
||||
validates_uniqueness_of :name, :scope => [:project_id]
|
||||
validates_length_of :name, :maximum => 30
|
||||
|
||||
alias :destroy_without_reassign :destroy
|
||||
|
||||
# Destroy the category
|
||||
# If a category is specified, issues are reassigned to this category
|
||||
def destroy(reassign_to = nil)
|
||||
if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project
|
||||
Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")
|
||||
end
|
||||
destroy_without_reassign
|
||||
end
|
||||
|
||||
def <=>(category)
|
||||
name <=> category.name
|
||||
end
|
||||
|
||||
def to_s; name end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class IssueCustomField < CustomField
|
||||
has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id"
|
||||
has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id"
|
||||
has_many :issues, :through => :issue_custom_values
|
||||
|
||||
def type_name
|
||||
:label_issue_plural
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class IssueRelation < ActiveRecord::Base
|
||||
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
|
||||
belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
|
||||
|
||||
TYPE_RELATES = "relates"
|
||||
TYPE_DUPLICATES = "duplicates"
|
||||
TYPE_BLOCKS = "blocks"
|
||||
TYPE_PRECEDES = "precedes"
|
||||
|
||||
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
|
||||
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicates, :order => 2 },
|
||||
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
|
||||
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
|
||||
}.freeze
|
||||
|
||||
validates_presence_of :issue_from, :issue_to, :relation_type
|
||||
validates_inclusion_of :relation_type, :in => TYPES.keys
|
||||
validates_numericality_of :delay, :allow_nil => true
|
||||
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
|
||||
|
||||
def validate
|
||||
if issue_from && issue_to
|
||||
errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id
|
||||
errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
|
||||
errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from
|
||||
end
|
||||
end
|
||||
|
||||
def other_issue(issue)
|
||||
(self.issue_from_id == issue.id) ? issue_to : issue_from
|
||||
end
|
||||
|
||||
def label_for(issue)
|
||||
TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
|
||||
end
|
||||
|
||||
def before_save
|
||||
if TYPE_PRECEDES == relation_type
|
||||
self.delay ||= 0
|
||||
else
|
||||
self.delay = nil
|
||||
end
|
||||
set_issue_to_dates
|
||||
end
|
||||
|
||||
def set_issue_to_dates
|
||||
soonest_start = self.successor_soonest_start
|
||||
if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
|
||||
issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration
|
||||
issue_to.save
|
||||
end
|
||||
end
|
||||
|
||||
def successor_soonest_start
|
||||
return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
|
||||
(issue_from.due_date || issue_from.start_date) + 1 + delay
|
||||
end
|
||||
|
||||
def <=>(relation)
|
||||
TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class IssueStatus < ActiveRecord::Base
|
||||
before_destroy :check_integrity
|
||||
has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all
|
||||
acts_as_list
|
||||
|
||||
validates_presence_of :name
|
||||
validates_uniqueness_of :name
|
||||
validates_length_of :name, :maximum => 30
|
||||
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
|
||||
|
||||
def before_save
|
||||
IssueStatus.update_all "is_default=#{connection.quoted_false}" if self.is_default?
|
||||
end
|
||||
|
||||
# Returns the default status for new issues
|
||||
def self.default
|
||||
find(:first, :conditions =>["is_default=?", true])
|
||||
end
|
||||
|
||||
# Returns an array of all statuses the given role can switch to
|
||||
# Uses association cache when called more than one time
|
||||
def new_statuses_allowed_to(role, tracker)
|
||||
new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker
|
||||
new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : []
|
||||
end
|
||||
|
||||
# Same thing as above but uses a database query
|
||||
# More efficient than the previous method if called just once
|
||||
def find_new_statuses_allowed_to(role, tracker)
|
||||
new_statuses = workflows.find(:all,
|
||||
:include => :new_status,
|
||||
:conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker
|
||||
new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : []
|
||||
end
|
||||
|
||||
def new_status_allowed_to?(status, role, tracker)
|
||||
status && role && tracker ?
|
||||
!workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? :
|
||||
false
|
||||
end
|
||||
|
||||
def to_s; name end
|
||||
|
||||
private
|
||||
def check_integrity
|
||||
raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Journal < ActiveRecord::Base
|
||||
belongs_to :journalized, :polymorphic => true
|
||||
# added as a quick fix to allow eager loading of the polymorphic association
|
||||
# since always associated to an issue, for now
|
||||
belongs_to :issue, :foreign_key => :journalized_id
|
||||
|
||||
belongs_to :user
|
||||
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
|
||||
|
||||
acts_as_searchable :columns => 'notes',
|
||||
:include => :issue,
|
||||
:project_key => "#{Issue.table_name}.project_id",
|
||||
:date_column => "#{Issue.table_name}.created_on"
|
||||
|
||||
acts_as_event :title => Proc.new {|o| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') },
|
||||
:description => :notes,
|
||||
:author => :user,
|
||||
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id}}
|
||||
|
||||
def save
|
||||
# Do not save an empty journal
|
||||
(details.empty? && notes.blank?) ? false : super
|
||||
end
|
||||
|
||||
# Returns the new status if the journal contains a status change, otherwise nil
|
||||
def new_status
|
||||
c = details.detect {|detail| detail.prop_key == 'status_id'}
|
||||
(c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class JournalDetail < ActiveRecord::Base
|
||||
belongs_to :journal
|
||||
|
||||
def before_save
|
||||
self.value = value[0..254] if value && value.is_a?(String)
|
||||
self.old_value = old_value[0..254] if old_value && old_value.is_a?(String)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class MailHandler < ActionMailer::Base
|
||||
|
||||
# Processes incoming emails
|
||||
# Currently, it only supports adding a note to an existing issue
|
||||
# by replying to the initial notification message
|
||||
def receive(email)
|
||||
# find related issue by parsing the subject
|
||||
m = email.subject.match %r{\[.*#(\d+)\]}
|
||||
return unless m
|
||||
issue = Issue.find_by_id(m[1])
|
||||
return unless issue
|
||||
|
||||
# find user
|
||||
user = User.find_active(:first, :conditions => {:mail => email.from.first})
|
||||
return unless user
|
||||
# check permission
|
||||
return unless user.allowed_to?(:add_issue_notes, issue.project)
|
||||
|
||||
# add the note
|
||||
issue.init_journal(user, email.body.chomp)
|
||||
issue.save
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,160 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Mailer < ActionMailer::Base
|
||||
helper ApplicationHelper
|
||||
helper IssuesHelper
|
||||
helper CustomFieldsHelper
|
||||
|
||||
include ActionController::UrlWriter
|
||||
|
||||
def issue_add(issue)
|
||||
recipients issue.recipients
|
||||
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}"
|
||||
body :issue => issue,
|
||||
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
|
||||
end
|
||||
|
||||
def issue_edit(journal)
|
||||
issue = journal.journalized
|
||||
recipients issue.recipients
|
||||
# Watchers in cc
|
||||
cc(issue.watcher_recipients - @recipients)
|
||||
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}"
|
||||
body :issue => issue,
|
||||
:journal => journal,
|
||||
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
|
||||
end
|
||||
|
||||
def document_added(document)
|
||||
recipients document.project.recipients
|
||||
subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
|
||||
body :document => document,
|
||||
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
|
||||
end
|
||||
|
||||
def attachments_added(attachments)
|
||||
container = attachments.first.container
|
||||
added_to = ''
|
||||
added_to_url = ''
|
||||
case container.class.name
|
||||
when 'Version'
|
||||
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
|
||||
added_to = "#{l(:label_version)}: #{container.name}"
|
||||
when 'Document'
|
||||
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
|
||||
added_to = "#{l(:label_document)}: #{container.title}"
|
||||
end
|
||||
recipients container.project.recipients
|
||||
subject "[#{container.project.name}] #{l(:label_attachment_new)}"
|
||||
body :attachments => attachments,
|
||||
:added_to => added_to,
|
||||
:added_to_url => added_to_url
|
||||
end
|
||||
|
||||
def news_added(news)
|
||||
recipients news.project.recipients
|
||||
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
|
||||
body :news => news,
|
||||
:news_url => url_for(:controller => 'news', :action => 'show', :id => news)
|
||||
end
|
||||
|
||||
def message_posted(message, recipients)
|
||||
recipients(recipients)
|
||||
subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
|
||||
body :message => message,
|
||||
:message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
|
||||
end
|
||||
|
||||
def account_information(user, password)
|
||||
set_language_if_valid user.language
|
||||
recipients user.mail
|
||||
subject l(:mail_subject_register)
|
||||
body :user => user,
|
||||
:password => password,
|
||||
:login_url => url_for(:controller => 'account', :action => 'login')
|
||||
end
|
||||
|
||||
def account_activation_request(user)
|
||||
# Send the email to all active administrators
|
||||
recipients User.find_active(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
|
||||
subject l(:mail_subject_account_activation_request)
|
||||
body :user => user,
|
||||
:url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
|
||||
end
|
||||
|
||||
def lost_password(token)
|
||||
set_language_if_valid(token.user.language)
|
||||
recipients token.user.mail
|
||||
subject l(:mail_subject_lost_password)
|
||||
body :token => token,
|
||||
:url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
|
||||
end
|
||||
|
||||
def register(token)
|
||||
set_language_if_valid(token.user.language)
|
||||
recipients token.user.mail
|
||||
subject l(:mail_subject_register)
|
||||
body :token => token,
|
||||
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
|
||||
end
|
||||
|
||||
def test(user)
|
||||
set_language_if_valid(user.language)
|
||||
recipients user.mail
|
||||
subject 'Redmine test'
|
||||
body :url => url_for(:controller => 'welcome')
|
||||
end
|
||||
|
||||
private
|
||||
def initialize_defaults(method_name)
|
||||
super
|
||||
set_language_if_valid Setting.default_language
|
||||
from Setting.mail_from
|
||||
default_url_options[:host] = Setting.host_name
|
||||
default_url_options[:protocol] = Setting.protocol
|
||||
end
|
||||
|
||||
# Overrides the create_mail method
|
||||
def create_mail
|
||||
# Removes the current user from the recipients and cc
|
||||
# if he doesn't want to receive notifications about what he does
|
||||
if User.current.pref[:no_self_notified]
|
||||
recipients.delete(User.current.mail) if recipients
|
||||
cc.delete(User.current.mail) if cc
|
||||
end
|
||||
# Blind carbon copy recipients
|
||||
if Setting.bcc_recipients?
|
||||
bcc([recipients, cc].flatten.compact.uniq)
|
||||
recipients []
|
||||
cc []
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# Renders a message with the corresponding layout
|
||||
def render_message(method_name, body)
|
||||
layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
|
||||
body[:content_for_layout] = render(:file => method_name, :body => body)
|
||||
ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}")
|
||||
end
|
||||
|
||||
# Makes partial rendering work with Rails 1.2 (retro-compatibility)
|
||||
def self.controller_path
|
||||
''
|
||||
end unless respond_to?('controller_path')
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Member < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :role
|
||||
belongs_to :project
|
||||
|
||||
validates_presence_of :role, :user, :project
|
||||
validates_uniqueness_of :user_id, :scope => :project_id
|
||||
|
||||
def validate
|
||||
errors.add :role_id, :activerecord_error_invalid if role && !role.member?
|
||||
end
|
||||
|
||||
def name
|
||||
self.user.name
|
||||
end
|
||||
|
||||
def before_destroy
|
||||
# remove category based auto assignments for this member
|
||||
project.issue_categories.update_all "assigned_to_id = NULL", ["assigned_to_id = ?", self.user.id]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Message < ActiveRecord::Base
|
||||
belongs_to :board
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
|
||||
has_many :attachments, :as => :container, :dependent => :destroy
|
||||
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
|
||||
|
||||
acts_as_searchable :columns => ['subject', 'content'],
|
||||
:include => :board,
|
||||
:project_key => 'project_id',
|
||||
:date_column => 'created_on'
|
||||
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
|
||||
:description => :content,
|
||||
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}}
|
||||
|
||||
attr_protected :locked, :sticky
|
||||
validates_presence_of :subject, :content
|
||||
validates_length_of :subject, :maximum => 255
|
||||
|
||||
def validate_on_create
|
||||
# Can not reply to a locked topic
|
||||
errors.add_to_base 'Topic is locked' if root.locked?
|
||||
end
|
||||
|
||||
def after_create
|
||||
board.update_attribute(:last_message_id, self.id)
|
||||
board.increment! :messages_count
|
||||
if parent
|
||||
parent.reload.update_attribute(:last_reply_id, self.id)
|
||||
else
|
||||
board.increment! :topics_count
|
||||
end
|
||||
end
|
||||
|
||||
def after_destroy
|
||||
# The following line is required so that the previous counter
|
||||
# updates (due to children removal) are not overwritten
|
||||
board.reload
|
||||
board.decrement! :messages_count
|
||||
board.decrement! :topics_count unless parent
|
||||
end
|
||||
|
||||
def sticky?
|
||||
sticky == 1
|
||||
end
|
||||
|
||||
def project
|
||||
board.project
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class MessageObserver < ActiveRecord::Observer
|
||||
def after_create(message)
|
||||
# send notification to the authors of the thread
|
||||
recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
|
||||
# send notification to the board watchers
|
||||
recipients += message.board.watcher_recipients
|
||||
recipients = recipients.compact.uniq
|
||||
Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class News < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
|
||||
|
||||
validates_presence_of :title, :description
|
||||
validates_length_of :title, :maximum => 60
|
||||
validates_length_of :summary, :maximum => 255
|
||||
|
||||
acts_as_searchable :columns => ['title', 'description']
|
||||
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
|
||||
|
||||
# returns latest news for projects visible by user
|
||||
def self.latest(user=nil, count=5)
|
||||
find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,202 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Project < ActiveRecord::Base
|
||||
# Project statuses
|
||||
STATUS_ACTIVE = 1
|
||||
STATUS_ARCHIVED = 9
|
||||
|
||||
has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
|
||||
has_many :users, :through => :members
|
||||
has_many :custom_values, :dependent => :delete_all, :as => :customized
|
||||
has_many :enabled_modules, :dependent => :delete_all
|
||||
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
|
||||
has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
|
||||
has_many :issue_changes, :through => :issues, :source => :journals
|
||||
has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
|
||||
has_many :time_entries, :dependent => :delete_all
|
||||
has_many :queries, :dependent => :delete_all
|
||||
has_many :documents, :dependent => :destroy
|
||||
has_many :news, :dependent => :delete_all, :include => :author
|
||||
has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
|
||||
has_many :boards, :order => "position ASC"
|
||||
has_one :repository, :dependent => :destroy
|
||||
has_many :changesets, :through => :repository
|
||||
has_one :wiki, :dependent => :destroy
|
||||
# Custom field for the project issues
|
||||
has_and_belongs_to_many :custom_fields,
|
||||
:class_name => 'IssueCustomField',
|
||||
:order => "#{CustomField.table_name}.position",
|
||||
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
|
||||
:association_foreign_key => 'custom_field_id'
|
||||
|
||||
acts_as_tree :order => "name", :counter_cache => true
|
||||
|
||||
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
|
||||
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
|
||||
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
|
||||
|
||||
attr_protected :status, :enabled_module_names
|
||||
|
||||
validates_presence_of :name, :description, :identifier
|
||||
validates_uniqueness_of :name, :identifier
|
||||
validates_associated :custom_values, :on => :update
|
||||
validates_associated :repository, :wiki
|
||||
validates_length_of :name, :maximum => 30
|
||||
validates_length_of :description, :maximum => 255
|
||||
validates_length_of :homepage, :maximum => 60
|
||||
validates_length_of :identifier, :in => 3..20
|
||||
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
|
||||
|
||||
before_destroy :delete_all_members
|
||||
|
||||
def identifier=(identifier)
|
||||
super unless identifier_frozen?
|
||||
end
|
||||
|
||||
def identifier_frozen?
|
||||
errors[:identifier].nil? && !(new_record? || identifier.blank?)
|
||||
end
|
||||
|
||||
def issues_with_subprojects(include_subprojects=false)
|
||||
conditions = nil
|
||||
if include_subprojects && !active_children.empty?
|
||||
ids = [id] + active_children.collect {|c| c.id}
|
||||
conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"]
|
||||
end
|
||||
conditions ||= ["#{Issue.table_name}.project_id = ?", id]
|
||||
# Quick and dirty fix for Rails 2 compatibility
|
||||
Issue.send(:with_scope, :find => { :conditions => conditions }) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Return all issues status changes for the project between the 2 given dates
|
||||
def issues_status_changes(from, to)
|
||||
Journal.find(:all, :include => [:issue, :details, :user],
|
||||
:conditions => ["#{Journal.table_name}.journalized_type = 'Issue'" +
|
||||
" AND #{Issue.table_name}.project_id = ?" +
|
||||
" AND #{JournalDetail.table_name}.prop_key = 'status_id'" +
|
||||
" AND #{Journal.table_name}.created_on BETWEEN ? AND ?",
|
||||
id, from, to+1])
|
||||
end
|
||||
|
||||
# returns latest created projects
|
||||
# non public projects will be returned only if user is a member of those
|
||||
def self.latest(user=nil, count=5)
|
||||
find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
|
||||
end
|
||||
|
||||
def self.visible_by(user=nil)
|
||||
if user && user.admin?
|
||||
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
|
||||
elsif user && user.memberships.any?
|
||||
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
|
||||
else
|
||||
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
|
||||
end
|
||||
end
|
||||
|
||||
def active?
|
||||
self.status == STATUS_ACTIVE
|
||||
end
|
||||
|
||||
def archive
|
||||
# Archive subprojects if any
|
||||
children.each do |subproject|
|
||||
subproject.archive
|
||||
end
|
||||
update_attribute :status, STATUS_ARCHIVED
|
||||
end
|
||||
|
||||
def unarchive
|
||||
return false if parent && !parent.active?
|
||||
update_attribute :status, STATUS_ACTIVE
|
||||
end
|
||||
|
||||
def active_children
|
||||
children.select {|child| child.active?}
|
||||
end
|
||||
|
||||
# Deletes all project's members
|
||||
def delete_all_members
|
||||
Member.delete_all(['project_id = ?', id])
|
||||
end
|
||||
|
||||
# Users issues can be assigned to
|
||||
def assignable_users
|
||||
members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
|
||||
end
|
||||
|
||||
# Returns the mail adresses of users that should be always notified on project events
|
||||
def recipients
|
||||
members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
|
||||
end
|
||||
|
||||
# Returns an array of all custom fields enabled for project issues
|
||||
# (explictly associated custom fields and custom fields enabled for all projects)
|
||||
def custom_fields_for_issues(tracker)
|
||||
all_custom_fields.select {|c| tracker.custom_fields.include? c }
|
||||
end
|
||||
|
||||
def all_custom_fields
|
||||
@all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
|
||||
end
|
||||
|
||||
def <=>(project)
|
||||
name.downcase <=> project.name.downcase
|
||||
end
|
||||
|
||||
def allows_to?(action)
|
||||
if action.is_a? Hash
|
||||
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
|
||||
else
|
||||
allowed_permissions.include? action
|
||||
end
|
||||
end
|
||||
|
||||
def module_enabled?(module_name)
|
||||
module_name = module_name.to_s
|
||||
enabled_modules.detect {|m| m.name == module_name}
|
||||
end
|
||||
|
||||
def enabled_module_names=(module_names)
|
||||
enabled_modules.clear
|
||||
module_names = [] unless module_names && module_names.is_a?(Array)
|
||||
module_names.each do |name|
|
||||
enabled_modules << EnabledModule.new(:name => name.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def validate
|
||||
errors.add(parent_id, " must be a root project") if parent and parent.parent
|
||||
errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
|
||||
end
|
||||
|
||||
private
|
||||
def allowed_permissions
|
||||
@allowed_permissions ||= begin
|
||||
module_names = enabled_modules.collect {|m| m.name}
|
||||
Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_actions
|
||||
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class ProjectCustomField < CustomField
|
||||
def type_name
|
||||
:label_project_plural
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,342 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class QueryColumn
|
||||
attr_accessor :name, :sortable
|
||||
include GLoc
|
||||
|
||||
def initialize(name, options={})
|
||||
self.name = name
|
||||
self.sortable = options[:sortable]
|
||||
end
|
||||
|
||||
def caption
|
||||
set_language_if_valid(User.current.language)
|
||||
l("field_#{name}")
|
||||
end
|
||||
end
|
||||
|
||||
class QueryCustomFieldColumn < QueryColumn
|
||||
|
||||
def initialize(custom_field)
|
||||
self.name = "cf_#{custom_field.id}".to_sym
|
||||
self.sortable = false
|
||||
@cf = custom_field
|
||||
end
|
||||
|
||||
def caption
|
||||
@cf.name
|
||||
end
|
||||
|
||||
def custom_field
|
||||
@cf
|
||||
end
|
||||
end
|
||||
|
||||
class Query < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
belongs_to :user
|
||||
serialize :filters
|
||||
serialize :column_names
|
||||
|
||||
attr_protected :project, :user
|
||||
|
||||
validates_presence_of :name, :on => :save
|
||||
validates_length_of :name, :maximum => 255
|
||||
|
||||
@@operators = { "=" => :label_equals,
|
||||
"!" => :label_not_equals,
|
||||
"o" => :label_open_issues,
|
||||
"c" => :label_closed_issues,
|
||||
"!*" => :label_none,
|
||||
"*" => :label_all,
|
||||
">=" => '>=',
|
||||
"<=" => '<=',
|
||||
"<t+" => :label_in_less_than,
|
||||
">t+" => :label_in_more_than,
|
||||
"t+" => :label_in,
|
||||
"t" => :label_today,
|
||||
"w" => :label_this_week,
|
||||
">t-" => :label_less_than_ago,
|
||||
"<t-" => :label_more_than_ago,
|
||||
"t-" => :label_ago,
|
||||
"~" => :label_contains,
|
||||
"!~" => :label_not_contains }
|
||||
|
||||
cattr_reader :operators
|
||||
|
||||
@@operators_by_filter_type = { :list => [ "=", "!" ],
|
||||
:list_status => [ "o", "=", "!", "c", "*" ],
|
||||
:list_optional => [ "=", "!", "!*", "*" ],
|
||||
:list_one_or_more => [ "*", "=" ],
|
||||
:date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
|
||||
:date_past => [ ">t-", "<t-", "t-", "t", "w" ],
|
||||
:string => [ "=", "~", "!", "!~" ],
|
||||
:text => [ "~", "!~" ],
|
||||
:integer => [ "=", ">=", "<=" ] }
|
||||
|
||||
cattr_reader :operators_by_filter_type
|
||||
|
||||
@@available_columns = [
|
||||
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
|
||||
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
|
||||
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position"),
|
||||
QueryColumn.new(:subject),
|
||||
QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
|
||||
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on"),
|
||||
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
|
||||
QueryColumn.new(:fixed_version),
|
||||
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
|
||||
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
|
||||
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
|
||||
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
|
||||
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on"),
|
||||
]
|
||||
cattr_reader :available_columns
|
||||
|
||||
def initialize(attributes = nil)
|
||||
super attributes
|
||||
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
|
||||
set_language_if_valid(User.current.language)
|
||||
end
|
||||
|
||||
def validate
|
||||
filters.each_key do |field|
|
||||
errors.add label_for(field), :activerecord_error_blank unless
|
||||
# filter requires one or more values
|
||||
(values_for(field) and !values_for(field).first.empty?) or
|
||||
# filter doesn't require any value
|
||||
["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
|
||||
end if filters
|
||||
end
|
||||
|
||||
def editable_by?(user)
|
||||
return false unless user
|
||||
return true if !is_public && self.user_id == user.id
|
||||
is_public && user.allowed_to?(:manage_public_queries, project)
|
||||
end
|
||||
|
||||
def available_filters
|
||||
return @available_filters if @available_filters
|
||||
@available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
|
||||
"tracker_id" => { :type => :list, :order => 2, :values => Tracker.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
|
||||
"priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI']).collect{|s| [s.name, s.id.to_s] } },
|
||||
"subject" => { :type => :text, :order => 8 },
|
||||
"created_on" => { :type => :date_past, :order => 9 },
|
||||
"updated_on" => { :type => :date_past, :order => 10 },
|
||||
"start_date" => { :type => :date, :order => 11 },
|
||||
"due_date" => { :type => :date, :order => 12 },
|
||||
"done_ratio" => { :type => :integer, :order => 13 }}
|
||||
|
||||
user_values = []
|
||||
user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
||||
if project
|
||||
user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
|
||||
else
|
||||
# members of the user's projects
|
||||
user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
|
||||
end
|
||||
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
|
||||
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
|
||||
|
||||
if project
|
||||
# project specific filters
|
||||
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
|
||||
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
|
||||
unless @project.active_children.empty?
|
||||
@available_filters["subproject_id"] = { :type => :list_one_or_more, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
|
||||
end
|
||||
@project.all_custom_fields.select(&:is_filter?).each do |field|
|
||||
case field.field_format
|
||||
when "text"
|
||||
options = { :type => :text, :order => 20 }
|
||||
when "list"
|
||||
options = { :type => :list_optional, :values => field.possible_values, :order => 20}
|
||||
when "date"
|
||||
options = { :type => :date, :order => 20 }
|
||||
when "bool"
|
||||
options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
|
||||
else
|
||||
options = { :type => :string, :order => 20 }
|
||||
end
|
||||
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
|
||||
end
|
||||
# remove category filter if no category defined
|
||||
@available_filters.delete "category_id" if @available_filters["category_id"][:values].empty?
|
||||
end
|
||||
@available_filters
|
||||
end
|
||||
|
||||
def add_filter(field, operator, values)
|
||||
# values must be an array
|
||||
return unless values and values.is_a? Array # and !values.first.empty?
|
||||
# check if field is defined as an available filter
|
||||
if available_filters.has_key? field
|
||||
filter_options = available_filters[field]
|
||||
# check if operator is allowed for that filter
|
||||
#if @@operators_by_filter_type[filter_options[:type]].include? operator
|
||||
# allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
|
||||
# filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
|
||||
#end
|
||||
filters[field] = {:operator => operator, :values => values }
|
||||
end
|
||||
end
|
||||
|
||||
def add_short_filter(field, expression)
|
||||
return unless expression
|
||||
parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
|
||||
add_filter field, (parms[0] || "="), [parms[1] || ""]
|
||||
end
|
||||
|
||||
def has_filter?(field)
|
||||
filters and filters[field]
|
||||
end
|
||||
|
||||
def operator_for(field)
|
||||
has_filter?(field) ? filters[field][:operator] : nil
|
||||
end
|
||||
|
||||
def values_for(field)
|
||||
has_filter?(field) ? filters[field][:values] : nil
|
||||
end
|
||||
|
||||
def label_for(field)
|
||||
label = @available_filters[field][:name] if @available_filters.has_key?(field)
|
||||
label ||= field.gsub(/\_id$/, "")
|
||||
end
|
||||
|
||||
def available_columns
|
||||
return @available_columns if @available_columns
|
||||
@available_columns = Query.available_columns
|
||||
@available_columns += (project ?
|
||||
project.custom_fields :
|
||||
IssueCustomField.find(:all, :conditions => {:is_for_all => true})
|
||||
).collect {|cf| QueryCustomFieldColumn.new(cf) }
|
||||
end
|
||||
|
||||
def columns
|
||||
if has_default_columns?
|
||||
available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
|
||||
else
|
||||
# preserve the column_names order
|
||||
column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
|
||||
end
|
||||
end
|
||||
|
||||
def column_names=(names)
|
||||
names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
|
||||
names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
|
||||
write_attribute(:column_names, names)
|
||||
end
|
||||
|
||||
def has_column?(column)
|
||||
column_names && column_names.include?(column.name)
|
||||
end
|
||||
|
||||
def has_default_columns?
|
||||
column_names.nil? || column_names.empty?
|
||||
end
|
||||
|
||||
def statement
|
||||
# project/subprojects clause
|
||||
clause = ''
|
||||
if project && has_filter?("subproject_id")
|
||||
subproject_ids = []
|
||||
if operator_for("subproject_id") == "="
|
||||
subproject_ids = values_for("subproject_id").each(&:to_i)
|
||||
else
|
||||
subproject_ids = project.active_children.collect{|p| p.id}
|
||||
end
|
||||
clause << "#{Issue.table_name}.project_id IN (%d,%s)" % [project.id, subproject_ids.join(",")] if project
|
||||
elsif project
|
||||
clause << "#{Issue.table_name}.project_id=%d" % project.id
|
||||
else
|
||||
clause << Project.visible_by(User.current)
|
||||
end
|
||||
|
||||
# filters clauses
|
||||
filters_clauses = []
|
||||
filters.each_key do |field|
|
||||
next if field == "subproject_id"
|
||||
v = values_for(field).clone
|
||||
next unless v and !v.empty?
|
||||
|
||||
sql = ''
|
||||
if field =~ /^cf_(\d+)$/
|
||||
# custom field
|
||||
db_table = CustomValue.table_name
|
||||
db_field = 'value'
|
||||
sql << "#{Issue.table_name}.id IN (SELECT #{db_table}.customized_id FROM #{db_table} where #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} AND "
|
||||
else
|
||||
# regular field
|
||||
db_table = Issue.table_name
|
||||
db_field = field
|
||||
sql << '('
|
||||
end
|
||||
|
||||
# "me" value subsitution
|
||||
if %w(assigned_to_id author_id).include?(field)
|
||||
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
|
||||
end
|
||||
|
||||
case operator_for field
|
||||
when "="
|
||||
sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
|
||||
when "!"
|
||||
sql = sql + "#{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
|
||||
when "!*"
|
||||
sql = sql + "#{db_table}.#{db_field} IS NULL"
|
||||
when "*"
|
||||
sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
|
||||
when ">="
|
||||
sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
|
||||
when "<="
|
||||
sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
|
||||
when "o"
|
||||
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
|
||||
when "c"
|
||||
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
|
||||
when ">t-"
|
||||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
|
||||
when "<t-"
|
||||
sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
|
||||
when "t-"
|
||||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
|
||||
when ">t+"
|
||||
sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
|
||||
when "<t+"
|
||||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
|
||||
when "t+"
|
||||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
|
||||
when "t"
|
||||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
|
||||
when "w"
|
||||
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Time.now.at_beginning_of_week), connection.quoted_date(Time.now.next_week.yesterday)]
|
||||
when "~"
|
||||
sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
|
||||
when "!~"
|
||||
sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
|
||||
end
|
||||
sql << ')'
|
||||
filters_clauses << sql
|
||||
end if filters and valid?
|
||||
|
||||
clause << ' AND ' unless clause.empty?
|
||||
clause << filters_clauses.join(' AND ') unless filters_clauses.empty?
|
||||
clause
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,91 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Repository < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC"
|
||||
has_many :changes, :through => :changesets
|
||||
|
||||
def scm
|
||||
@scm ||= self.scm_adapter.new url, root_url, login, password
|
||||
update_attribute(:root_url, @scm.root_url) if root_url.blank?
|
||||
@scm
|
||||
end
|
||||
|
||||
def scm_name
|
||||
self.class.scm_name
|
||||
end
|
||||
|
||||
def supports_cat?
|
||||
scm.supports_cat?
|
||||
end
|
||||
|
||||
def supports_annotate?
|
||||
scm.supports_annotate?
|
||||
end
|
||||
|
||||
def entries(path=nil, identifier=nil)
|
||||
scm.entries(path, identifier)
|
||||
end
|
||||
|
||||
def diff(path, rev, rev_to, type)
|
||||
scm.diff(path, rev, rev_to, type)
|
||||
end
|
||||
|
||||
# Default behaviour: we search in cached changesets
|
||||
def changesets_for_path(path)
|
||||
path = "/#{path}" unless path.starts_with?('/')
|
||||
Change.find(:all, :include => :changeset,
|
||||
:conditions => ["repository_id = ? AND path = ?", id, path],
|
||||
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
|
||||
end
|
||||
|
||||
def latest_changeset
|
||||
@latest_changeset ||= changesets.find(:first)
|
||||
end
|
||||
|
||||
def scan_changesets_for_issue_ids
|
||||
self.changesets.each(&:scan_comment_for_issue_ids)
|
||||
end
|
||||
|
||||
# fetch new changesets for all repositories
|
||||
# can be called periodically by an external script
|
||||
# eg. ruby script/runner "Repository.fetch_changesets"
|
||||
def self.fetch_changesets
|
||||
find(:all).each(&:fetch_changesets)
|
||||
end
|
||||
|
||||
# scan changeset comments to find related and fixed issues for all repositories
|
||||
def self.scan_changesets_for_issue_ids
|
||||
find(:all).each(&:scan_changesets_for_issue_ids)
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'Abstract'
|
||||
end
|
||||
|
||||
def self.available_scm
|
||||
subclasses.collect {|klass| [klass.scm_name, klass.name]}
|
||||
end
|
||||
|
||||
def self.factory(klass_name, *args)
|
||||
klass = "Repository::#{klass_name}".constantize
|
||||
klass.new(*args)
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,86 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/bazaar_adapter'
|
||||
|
||||
class Repository::Bazaar < Repository
|
||||
attr_protected :root_url
|
||||
validates_presence_of :url
|
||||
|
||||
def scm_adapter
|
||||
Redmine::Scm::Adapters::BazaarAdapter
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'Bazaar'
|
||||
end
|
||||
|
||||
def entries(path=nil, identifier=nil)
|
||||
entries = scm.entries(path, identifier)
|
||||
if entries
|
||||
entries.each do |e|
|
||||
next if e.lastrev.revision.blank?
|
||||
c = Change.find(:first,
|
||||
:include => :changeset,
|
||||
:conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id],
|
||||
:order => "#{Changeset.table_name}.revision DESC")
|
||||
if c
|
||||
e.lastrev.identifier = c.changeset.revision
|
||||
e.lastrev.name = c.changeset.revision
|
||||
e.lastrev.author = c.changeset.committer
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_changesets
|
||||
scm_info = scm.info
|
||||
if scm_info
|
||||
# latest revision found in database
|
||||
db_revision = latest_changeset ? latest_changeset.revision : 0
|
||||
# latest revision in the repository
|
||||
scm_revision = scm_info.lastrev.identifier.to_i
|
||||
if db_revision < scm_revision
|
||||
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
|
||||
identifier_from = db_revision + 1
|
||||
while (identifier_from <= scm_revision)
|
||||
# loads changesets by batches of 200
|
||||
identifier_to = [identifier_from + 199, scm_revision].min
|
||||
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
|
||||
transaction do
|
||||
revisions.reverse_each do |revision|
|
||||
changeset = Changeset.create(:repository => self,
|
||||
:revision => revision.identifier,
|
||||
:committer => revision.author,
|
||||
:committed_on => revision.time,
|
||||
:scmid => revision.scmid,
|
||||
:comments => revision.message)
|
||||
|
||||
revision.paths.each do |change|
|
||||
Change.create(:changeset => changeset,
|
||||
:action => change[:action],
|
||||
:path => change[:path],
|
||||
:revision => change[:revision])
|
||||
end
|
||||
end
|
||||
end unless revisions.nil?
|
||||
identifier_from = identifier_to + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,150 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/cvs_adapter'
|
||||
require 'digest/sha1'
|
||||
|
||||
class Repository::Cvs < Repository
|
||||
validates_presence_of :url, :root_url
|
||||
|
||||
def scm_adapter
|
||||
Redmine::Scm::Adapters::CvsAdapter
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'CVS'
|
||||
end
|
||||
|
||||
def entry(path, identifier)
|
||||
e = entries(path, identifier)
|
||||
e ? e.first : nil
|
||||
end
|
||||
|
||||
def entries(path=nil, identifier=nil)
|
||||
entries=scm.entries(path, identifier)
|
||||
if entries
|
||||
entries.each() do |entry|
|
||||
unless entry.lastrev.nil? || entry.lastrev.identifier
|
||||
change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
|
||||
if change
|
||||
entry.lastrev.identifier=change.changeset.revision
|
||||
entry.lastrev.author=change.changeset.committer
|
||||
entry.lastrev.revision=change.revision
|
||||
entry.lastrev.branch=change.branch
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
entries
|
||||
end
|
||||
|
||||
def diff(path, rev, rev_to, type)
|
||||
#convert rev to revision. CVS can't handle changesets here
|
||||
diff=[]
|
||||
changeset_from=changesets.find_by_revision(rev)
|
||||
if rev_to.to_i > 0
|
||||
changeset_to=changesets.find_by_revision(rev_to)
|
||||
end
|
||||
changeset_from.changes.each() do |change_from|
|
||||
|
||||
revision_from=nil
|
||||
revision_to=nil
|
||||
|
||||
revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
|
||||
|
||||
if revision_from
|
||||
if changeset_to
|
||||
changeset_to.changes.each() do |change_to|
|
||||
revision_to=change_to.revision if change_to.path==change_from.path
|
||||
end
|
||||
end
|
||||
unless revision_to
|
||||
revision_to=scm.get_previous_revision(revision_from)
|
||||
end
|
||||
diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
|
||||
end
|
||||
end
|
||||
return diff
|
||||
end
|
||||
|
||||
def fetch_changesets
|
||||
#not the preferred way with CVS. maybe we should introduce always a cron-job for this
|
||||
last_commit = changesets.maximum(:committed_on)
|
||||
|
||||
# some nifty bits to introduce a commit-id with cvs
|
||||
# natively cvs doesn't provide any kind of changesets, there is only a revision per file.
|
||||
# we now take a guess using the author, the commitlog and the commit-date.
|
||||
|
||||
# last one is the next step to take. the commit-date is not equal for all
|
||||
# commits in one changeset. cvs update the commit-date when the *,v file was touched. so
|
||||
# we use a small delta here, to merge all changes belonging to _one_ changeset
|
||||
time_delta=10.seconds
|
||||
|
||||
transaction do
|
||||
scm.revisions('', last_commit, nil, :with_paths => true) do |revision|
|
||||
# only add the change to the database, if it doen't exists. the cvs log
|
||||
# is not exclusive at all.
|
||||
unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
|
||||
revision
|
||||
cs = changesets.find(:first, :conditions=>{
|
||||
:committed_on=>revision.time-time_delta..revision.time+time_delta,
|
||||
:committer=>revision.author,
|
||||
:comments=>revision.message
|
||||
})
|
||||
|
||||
# create a new changeset....
|
||||
unless cs
|
||||
# we use a negative changeset-number here (just for inserting)
|
||||
# later on, we calculate a continous positive number
|
||||
next_rev = changesets.minimum(:revision)
|
||||
next_rev = 0 if next_rev.nil? or next_rev > 0
|
||||
next_rev = next_rev - 1
|
||||
|
||||
cs=Changeset.create(:repository => self,
|
||||
:revision => next_rev,
|
||||
:committer => revision.author,
|
||||
:committed_on => revision.time,
|
||||
:comments => revision.message)
|
||||
end
|
||||
|
||||
#convert CVS-File-States to internal Action-abbrevations
|
||||
#default action is (M)odified
|
||||
action="M"
|
||||
if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
|
||||
action="A" #add-action always at first revision (= 1.1)
|
||||
elsif revision.paths[0][:action]=="dead"
|
||||
action="D" #dead-state is similar to Delete
|
||||
end
|
||||
|
||||
Change.create(:changeset => cs,
|
||||
:action => action,
|
||||
:path => scm.with_leading_slash(revision.paths[0][:path]),
|
||||
:revision => revision.paths[0][:revision],
|
||||
:branch => revision.paths[0][:branch]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
next_rev = [changesets.maximum(:revision) || 0, 0].max
|
||||
changesets.find(:all, :conditions=>["revision < 0"], :order=>"committed_on ASC").each() do |changeset|
|
||||
next_rev = next_rev + 1
|
||||
changeset.revision = next_rev
|
||||
changeset.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/darcs_adapter'
|
||||
|
||||
class Repository::Darcs < Repository
|
||||
validates_presence_of :url
|
||||
|
||||
def scm_adapter
|
||||
Redmine::Scm::Adapters::DarcsAdapter
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'Darcs'
|
||||
end
|
||||
|
||||
def entries(path=nil, identifier=nil)
|
||||
entries=scm.entries(path, identifier)
|
||||
if entries
|
||||
entries.each do |entry|
|
||||
# Search the DB for the entry's last change
|
||||
changeset = changesets.find_by_scmid(entry.lastrev.scmid) if entry.lastrev && !entry.lastrev.scmid.blank?
|
||||
if changeset
|
||||
entry.lastrev.identifier = changeset.revision
|
||||
entry.lastrev.name = changeset.revision
|
||||
entry.lastrev.time = changeset.committed_on
|
||||
entry.lastrev.author = changeset.committer
|
||||
end
|
||||
end
|
||||
end
|
||||
entries
|
||||
end
|
||||
|
||||
def diff(path, rev, rev_to, type)
|
||||
patch_from = changesets.find_by_revision(rev)
|
||||
patch_to = changesets.find_by_revision(rev_to) if rev_to
|
||||
if path.blank?
|
||||
path = patch_from.changes.collect{|change| change.path}.join(' ')
|
||||
end
|
||||
scm.diff(path, patch_from.scmid, patch_to.scmid, type)
|
||||
end
|
||||
|
||||
def fetch_changesets
|
||||
scm_info = scm.info
|
||||
if scm_info
|
||||
db_last_id = latest_changeset ? latest_changeset.scmid : nil
|
||||
next_rev = latest_changeset ? latest_changeset.revision + 1 : 1
|
||||
# latest revision in the repository
|
||||
scm_revision = scm_info.lastrev.scmid
|
||||
unless changesets.find_by_scmid(scm_revision)
|
||||
revisions = scm.revisions('', db_last_id, nil, :with_path => true)
|
||||
transaction do
|
||||
revisions.reverse_each do |revision|
|
||||
changeset = Changeset.create(:repository => self,
|
||||
:revision => next_rev,
|
||||
:scmid => revision.scmid,
|
||||
:committer => revision.author,
|
||||
:committed_on => revision.time,
|
||||
:comments => revision.message)
|
||||
|
||||
next if changeset.new_record?
|
||||
|
||||
revision.paths.each do |change|
|
||||
Change.create(:changeset => changeset,
|
||||
:action => change[:action],
|
||||
:path => change[:path],
|
||||
:from_path => change[:from_path],
|
||||
:from_revision => change[:from_revision])
|
||||
end
|
||||
next_rev += 1
|
||||
end if revisions
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/mercurial_adapter'
|
||||
|
||||
class Repository::Mercurial < Repository
|
||||
attr_protected :root_url
|
||||
validates_presence_of :url
|
||||
|
||||
def scm_adapter
|
||||
Redmine::Scm::Adapters::MercurialAdapter
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'Mercurial'
|
||||
end
|
||||
|
||||
def entries(path=nil, identifier=nil)
|
||||
entries=scm.entries(path, identifier)
|
||||
if entries
|
||||
entries.each do |entry|
|
||||
next unless entry.is_file?
|
||||
# Search the DB for the entry's last change
|
||||
change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
|
||||
if change
|
||||
entry.lastrev.identifier = change.changeset.revision
|
||||
entry.lastrev.name = change.changeset.revision
|
||||
entry.lastrev.author = change.changeset.committer
|
||||
entry.lastrev.revision = change.revision
|
||||
end
|
||||
end
|
||||
end
|
||||
entries
|
||||
end
|
||||
|
||||
def fetch_changesets
|
||||
scm_info = scm.info
|
||||
if scm_info
|
||||
# latest revision found in database
|
||||
db_revision = latest_changeset ? latest_changeset.revision : nil
|
||||
# latest revision in the repository
|
||||
scm_revision = scm_info.lastrev.identifier.to_i
|
||||
|
||||
unless changesets.find_by_revision(scm_revision)
|
||||
revisions = scm.revisions('', db_revision, nil)
|
||||
transaction do
|
||||
revisions.reverse_each do |revision|
|
||||
changeset = Changeset.create(:repository => self,
|
||||
:revision => revision.identifier,
|
||||
:scmid => revision.scmid,
|
||||
:committer => revision.author,
|
||||
:committed_on => revision.time,
|
||||
:comments => revision.message)
|
||||
|
||||
revision.paths.each do |change|
|
||||
Change.create(:changeset => changeset,
|
||||
:action => change[:action],
|
||||
:path => change[:path],
|
||||
:from_path => change[:from_path],
|
||||
:from_revision => change[:from_revision])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/subversion_adapter'
|
||||
|
||||
class Repository::Subversion < Repository
|
||||
attr_protected :root_url
|
||||
validates_presence_of :url
|
||||
validates_format_of :url, :with => /^(http|https|svn|svn\+ssh|file):\/\/.+/i
|
||||
|
||||
def scm_adapter
|
||||
Redmine::Scm::Adapters::SubversionAdapter
|
||||
end
|
||||
|
||||
def self.scm_name
|
||||
'Subversion'
|
||||
end
|
||||
|
||||
def changesets_for_path(path)
|
||||
revisions = scm.revisions(path)
|
||||
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
|
||||
end
|
||||
|
||||
def fetch_changesets
|
||||
scm_info = scm.info
|
||||
if scm_info
|
||||
# latest revision found in database
|
||||
db_revision = latest_changeset ? latest_changeset.revision : 0
|
||||
# latest revision in the repository
|
||||
scm_revision = scm_info.lastrev.identifier.to_i
|
||||
if db_revision < scm_revision
|
||||
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
|
||||
identifier_from = db_revision + 1
|
||||
while (identifier_from <= scm_revision)
|
||||
# loads changesets by batches of 200
|
||||
identifier_to = [identifier_from + 199, scm_revision].min
|
||||
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
|
||||
transaction do
|
||||
revisions.reverse_each do |revision|
|
||||
changeset = Changeset.create(:repository => self,
|
||||
:revision => revision.identifier,
|
||||
:committer => revision.author,
|
||||
:committed_on => revision.time,
|
||||
:comments => revision.message)
|
||||
|
||||
revision.paths.each do |change|
|
||||
Change.create(:changeset => changeset,
|
||||
:action => change[:action],
|
||||
:path => change[:path],
|
||||
:from_path => change[:from_path],
|
||||
:from_revision => change[:from_revision])
|
||||
end
|
||||
end
|
||||
end unless revisions.nil?
|
||||
identifier_from = identifier_to + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,108 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Role < ActiveRecord::Base
|
||||
# Built-in roles
|
||||
BUILTIN_NON_MEMBER = 1
|
||||
BUILTIN_ANONYMOUS = 2
|
||||
|
||||
before_destroy :check_deletable
|
||||
has_many :workflows, :dependent => :delete_all
|
||||
has_many :members
|
||||
acts_as_list
|
||||
|
||||
serialize :permissions
|
||||
attr_protected :builtin
|
||||
|
||||
validates_presence_of :name
|
||||
validates_uniqueness_of :name
|
||||
validates_length_of :name, :maximum => 30
|
||||
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
|
||||
|
||||
def permissions
|
||||
read_attribute(:permissions) || []
|
||||
end
|
||||
|
||||
def permissions=(perms)
|
||||
perms = perms.collect {|p| p.to_sym unless p.blank? }.compact if perms
|
||||
write_attribute(:permissions, perms)
|
||||
end
|
||||
|
||||
def <=>(role)
|
||||
position <=> role.position
|
||||
end
|
||||
|
||||
# Return true if the role is a builtin role
|
||||
def builtin?
|
||||
self.builtin != 0
|
||||
end
|
||||
|
||||
# Return true if the role is a project member role
|
||||
def member?
|
||||
!self.builtin?
|
||||
end
|
||||
|
||||
# Return true if role is allowed to do the specified action
|
||||
# action can be:
|
||||
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
|
||||
# * a permission Symbol (eg. :edit_project)
|
||||
def allowed_to?(action)
|
||||
if action.is_a? Hash
|
||||
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
|
||||
else
|
||||
allowed_permissions.include? action
|
||||
end
|
||||
end
|
||||
|
||||
# Return all the permissions that can be given to the role
|
||||
def setable_permissions
|
||||
setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
|
||||
setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
|
||||
setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
|
||||
setable_permissions
|
||||
end
|
||||
|
||||
# Find all the roles that can be given to a project member
|
||||
def self.find_all_givable
|
||||
find(:all, :conditions => {:builtin => 0}, :order => 'position')
|
||||
end
|
||||
|
||||
# Return the builtin 'non member' role
|
||||
def self.non_member
|
||||
find(:first, :conditions => {:builtin => BUILTIN_NON_MEMBER}) || raise('Missing non-member builtin role.')
|
||||
end
|
||||
|
||||
# Return the builtin 'anonymous' role
|
||||
def self.anonymous
|
||||
find(:first, :conditions => {:builtin => BUILTIN_ANONYMOUS}) || raise('Missing anonymous builtin role.')
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def allowed_permissions
|
||||
@allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
|
||||
end
|
||||
|
||||
def allowed_actions
|
||||
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
|
||||
end
|
||||
|
||||
def check_deletable
|
||||
raise "Can't delete role" if members.any?
|
||||
raise "Can't delete builtin role" if builtin?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,119 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Setting < ActiveRecord::Base
|
||||
|
||||
DATE_FORMATS = [
|
||||
'%Y-%m-%d',
|
||||
'%d/%m/%Y',
|
||||
'%d.%m.%Y',
|
||||
'%d-%m-%Y',
|
||||
'%m/%d/%Y',
|
||||
'%d %b %Y',
|
||||
'%d %B %Y',
|
||||
'%b %d, %Y',
|
||||
'%B %d, %Y'
|
||||
]
|
||||
|
||||
TIME_FORMATS = [
|
||||
'%H:%M',
|
||||
'%I:%M %p'
|
||||
]
|
||||
|
||||
cattr_accessor :available_settings
|
||||
@@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml"))
|
||||
Redmine::Plugin.registered_plugins.each do |id, plugin|
|
||||
next unless plugin.settings
|
||||
@@available_settings["plugin_#{id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
|
||||
end
|
||||
|
||||
validates_uniqueness_of :name
|
||||
validates_inclusion_of :name, :in => @@available_settings.keys
|
||||
validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' }
|
||||
|
||||
# Hash used to cache setting values
|
||||
@cached_settings = {}
|
||||
@cached_cleared_on = Time.now
|
||||
|
||||
def value
|
||||
v = read_attribute(:value)
|
||||
# Unserialize serialized settings
|
||||
v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String)
|
||||
v
|
||||
end
|
||||
|
||||
def value=(v)
|
||||
v = v.to_yaml if v && @@available_settings[name]['serialized']
|
||||
write_attribute(:value, v)
|
||||
end
|
||||
|
||||
# Returns the value of the setting named name
|
||||
def self.[](name)
|
||||
v = @cached_settings[name]
|
||||
v ? v : (@cached_settings[name] = find_or_default(name).value)
|
||||
end
|
||||
|
||||
def self.[]=(name, v)
|
||||
setting = find_or_default(name)
|
||||
setting.value = (v ? v : "")
|
||||
@cached_settings[name] = nil
|
||||
setting.save
|
||||
setting.value
|
||||
end
|
||||
|
||||
# Defines getter and setter for each setting
|
||||
# Then setting values can be read using: Setting.some_setting_name
|
||||
# or set using Setting.some_setting_name = "some value"
|
||||
@@available_settings.each do |name, params|
|
||||
src = <<-END_SRC
|
||||
def self.#{name}
|
||||
self[:#{name}]
|
||||
end
|
||||
|
||||
def self.#{name}?
|
||||
self[:#{name}].to_i > 0
|
||||
end
|
||||
|
||||
def self.#{name}=(value)
|
||||
self[:#{name}] = value
|
||||
end
|
||||
END_SRC
|
||||
class_eval src, __FILE__, __LINE__
|
||||
end
|
||||
|
||||
# Checks if settings have changed since the values were read
|
||||
# and clears the cache hash if it's the case
|
||||
# Called once per request
|
||||
def self.check_cache
|
||||
settings_updated_on = Setting.maximum(:updated_on)
|
||||
if settings_updated_on && @cached_cleared_on <= settings_updated_on
|
||||
@cached_settings.clear
|
||||
@cached_cleared_on = Time.now
|
||||
logger.info "Settings cache cleared." if logger
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Returns the Setting instance for the setting named name
|
||||
# (record found in database or new record with default value)
|
||||
def self.find_or_default(name)
|
||||
name = name.to_s
|
||||
raise "There's no setting named #{name}" unless @@available_settings.has_key?(name)
|
||||
setting = find_by_name(name)
|
||||
setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class TimeEntry < ActiveRecord::Base
|
||||
# could have used polymorphic association
|
||||
# project association here allows easy loading of time entries at project level with one database trip
|
||||
belongs_to :project
|
||||
belongs_to :issue
|
||||
belongs_to :user
|
||||
belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
|
||||
|
||||
attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
|
||||
|
||||
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
|
||||
validates_numericality_of :hours, :allow_nil => true
|
||||
validates_length_of :comments, :maximum => 255
|
||||
|
||||
def before_validation
|
||||
self.project = issue.project if issue && project.nil?
|
||||
end
|
||||
|
||||
def validate
|
||||
errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000)
|
||||
errors.add :project_id, :activerecord_error_invalid if project.nil?
|
||||
errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
|
||||
end
|
||||
|
||||
# tyear, tmonth, tweek assigned where setting spent_on attributes
|
||||
# these attributes make time aggregations easier
|
||||
def spent_on=(date)
|
||||
super
|
||||
self.tyear = spent_on ? spent_on.year : nil
|
||||
self.tmonth = spent_on ? spent_on.month : nil
|
||||
self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Token < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
|
||||
@@validity_time = 1.day
|
||||
|
||||
def before_create
|
||||
self.value = Token.generate_token_value
|
||||
end
|
||||
|
||||
# Return true if token has expired
|
||||
def expired?
|
||||
return Time.now > self.created_on + @@validity_time
|
||||
end
|
||||
|
||||
# Delete all expired tokens
|
||||
def self.destroy_expired
|
||||
Token.delete_all ["action <> 'feeds' AND created_on < ?", Time.now - @@validity_time]
|
||||
end
|
||||
|
||||
private
|
||||
def self.generate_token_value
|
||||
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
||||
token_value = ''
|
||||
40.times { |i| token_value << chars[rand(chars.size-1)] }
|
||||
token_value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Tracker < ActiveRecord::Base
|
||||
before_destroy :check_integrity
|
||||
has_many :issues
|
||||
has_many :workflows, :dependent => :delete_all
|
||||
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
|
||||
acts_as_list
|
||||
|
||||
validates_presence_of :name
|
||||
validates_uniqueness_of :name
|
||||
validates_length_of :name, :maximum => 30
|
||||
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
|
||||
|
||||
def to_s; name end
|
||||
|
||||
def <=>(tracker)
|
||||
name <=> tracker.name
|
||||
end
|
||||
|
||||
def self.all
|
||||
find(:all, :order => 'position')
|
||||
end
|
||||
|
||||
private
|
||||
def check_integrity
|
||||
raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,255 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require "digest/sha1"
|
||||
|
||||
class User < ActiveRecord::Base
|
||||
# Account statuses
|
||||
STATUS_ANONYMOUS = 0
|
||||
STATUS_ACTIVE = 1
|
||||
STATUS_REGISTERED = 2
|
||||
STATUS_LOCKED = 3
|
||||
|
||||
has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all
|
||||
has_many :projects, :through => :memberships
|
||||
has_many :custom_values, :dependent => :delete_all, :as => :customized
|
||||
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
|
||||
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
|
||||
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
|
||||
belongs_to :auth_source
|
||||
|
||||
attr_accessor :password, :password_confirmation
|
||||
attr_accessor :last_before_login_on
|
||||
# Prevents unauthorized assignments
|
||||
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
|
||||
|
||||
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
|
||||
validates_uniqueness_of :login, :mail
|
||||
# Login must contain lettres, numbers, underscores only
|
||||
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
|
||||
validates_length_of :login, :maximum => 30
|
||||
validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-]*$/i
|
||||
validates_length_of :firstname, :lastname, :maximum => 30
|
||||
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
|
||||
validates_length_of :mail, :maximum => 60, :allow_nil => true
|
||||
validates_length_of :password, :minimum => 4, :allow_nil => true
|
||||
validates_confirmation_of :password, :allow_nil => true
|
||||
validates_associated :custom_values, :on => :update
|
||||
|
||||
def before_create
|
||||
self.mail_notification = false
|
||||
true
|
||||
end
|
||||
|
||||
def before_save
|
||||
# update hashed_password if password was set
|
||||
self.hashed_password = User.hash_password(self.password) if self.password
|
||||
end
|
||||
|
||||
def self.active
|
||||
with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def self.find_active(*args)
|
||||
active do
|
||||
find(*args)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the user that matches provided login and password, or nil
|
||||
def self.try_to_login(login, password)
|
||||
user = find(:first, :conditions => ["login=?", login])
|
||||
if user
|
||||
# user is already in local database
|
||||
return nil if !user.active?
|
||||
if user.auth_source
|
||||
# user has an external authentication method
|
||||
return nil unless user.auth_source.authenticate(login, password)
|
||||
else
|
||||
# authentication with local password
|
||||
return nil unless User.hash_password(password) == user.hashed_password
|
||||
end
|
||||
else
|
||||
# user is not yet registered, try to authenticate with available sources
|
||||
attrs = AuthSource.authenticate(login, password)
|
||||
if attrs
|
||||
onthefly = new(*attrs)
|
||||
onthefly.login = login
|
||||
onthefly.language = Setting.default_language
|
||||
if onthefly.save
|
||||
user = find(:first, :conditions => ["login=?", login])
|
||||
logger.info("User '#{user.login}' created on the fly.") if logger
|
||||
end
|
||||
end
|
||||
end
|
||||
user.update_attribute(:last_login_on, Time.now) if user
|
||||
user
|
||||
|
||||
rescue => text
|
||||
raise text
|
||||
end
|
||||
|
||||
# Return user's full name for display
|
||||
def name
|
||||
"#{firstname} #{lastname}"
|
||||
end
|
||||
|
||||
def active?
|
||||
self.status == STATUS_ACTIVE
|
||||
end
|
||||
|
||||
def registered?
|
||||
self.status == STATUS_REGISTERED
|
||||
end
|
||||
|
||||
def locked?
|
||||
self.status == STATUS_LOCKED
|
||||
end
|
||||
|
||||
def check_password?(clear_password)
|
||||
User.hash_password(clear_password) == self.hashed_password
|
||||
end
|
||||
|
||||
def pref
|
||||
self.preference ||= UserPreference.new(:user => self)
|
||||
end
|
||||
|
||||
def time_zone
|
||||
self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
|
||||
end
|
||||
|
||||
# Return user's RSS key (a 40 chars long string), used to access feeds
|
||||
def rss_key
|
||||
token = self.rss_token || Token.create(:user => self, :action => 'feeds')
|
||||
token.value
|
||||
end
|
||||
|
||||
# Return an array of project ids for which the user has explicitly turned mail notifications on
|
||||
def notified_projects_ids
|
||||
@notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
|
||||
end
|
||||
|
||||
def notified_project_ids=(ids)
|
||||
Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
|
||||
Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
|
||||
@notified_projects_ids = nil
|
||||
notified_projects_ids
|
||||
end
|
||||
|
||||
def self.find_by_rss_key(key)
|
||||
token = Token.find_by_value(key)
|
||||
token && token.user.active? ? token.user : nil
|
||||
end
|
||||
|
||||
def self.find_by_autologin_key(key)
|
||||
token = Token.find_by_action_and_value('autologin', key)
|
||||
token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
|
||||
end
|
||||
|
||||
def <=>(user)
|
||||
user.nil? ? -1 : (lastname == user.lastname ? firstname <=> user.firstname : lastname <=> user.lastname)
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def logged?
|
||||
true
|
||||
end
|
||||
|
||||
# Return user's role for project
|
||||
def role_for_project(project)
|
||||
# No role on archived projects
|
||||
return nil unless project && project.active?
|
||||
if logged?
|
||||
# Find project membership
|
||||
membership = memberships.detect {|m| m.project_id == project.id}
|
||||
if membership
|
||||
membership.role
|
||||
else
|
||||
@role_non_member ||= Role.non_member
|
||||
end
|
||||
else
|
||||
@role_anonymous ||= Role.anonymous
|
||||
end
|
||||
end
|
||||
|
||||
# Return true if the user is a member of project
|
||||
def member_of?(project)
|
||||
role_for_project(project).member?
|
||||
end
|
||||
|
||||
# Return true if the user is allowed to do the specified action on project
|
||||
# action can be:
|
||||
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
|
||||
# * a permission Symbol (eg. :edit_project)
|
||||
def allowed_to?(action, project)
|
||||
# No action allowed on archived projects
|
||||
return false unless project.active?
|
||||
# No action allowed on disabled modules
|
||||
return false unless project.allows_to?(action)
|
||||
# Admin users are authorized for anything else
|
||||
return true if admin?
|
||||
|
||||
role = role_for_project(project)
|
||||
return false unless role
|
||||
role.allowed_to?(action) && (project.is_public? || role.member?)
|
||||
end
|
||||
|
||||
def self.current=(user)
|
||||
@current_user = user
|
||||
end
|
||||
|
||||
def self.current
|
||||
@current_user ||= User.anonymous
|
||||
end
|
||||
|
||||
def self.anonymous
|
||||
return @anonymous_user if @anonymous_user
|
||||
anonymous_user = AnonymousUser.find(:first)
|
||||
if anonymous_user.nil?
|
||||
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
|
||||
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
|
||||
end
|
||||
@anonymous_user = anonymous_user
|
||||
end
|
||||
|
||||
private
|
||||
# Return password digest
|
||||
def self.hash_password(clear_password)
|
||||
Digest::SHA1.hexdigest(clear_password || "")
|
||||
end
|
||||
end
|
||||
|
||||
class AnonymousUser < User
|
||||
|
||||
def validate_on_create
|
||||
# There should be only one AnonymousUser in the database
|
||||
errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
|
||||
end
|
||||
|
||||
# Overrides a few properties
|
||||
def logged?; false end
|
||||
def admin; false end
|
||||
def name; 'Anonymous' end
|
||||
def mail; nil end
|
||||
def time_zone; nil end
|
||||
def rss_key; nil end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class UserCustomField < CustomField
|
||||
def type_name
|
||||
:label_user_plural
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class UserPreference < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
serialize :others
|
||||
|
||||
attr_protected :others
|
||||
|
||||
def initialize(attributes = nil)
|
||||
super
|
||||
self.others ||= {}
|
||||
end
|
||||
|
||||
def before_save
|
||||
self.others ||= {}
|
||||
end
|
||||
|
||||
def [](attr_name)
|
||||
if attribute_present? attr_name
|
||||
super
|
||||
else
|
||||
others ? others[attr_name] : nil
|
||||
end
|
||||
end
|
||||
|
||||
def []=(attr_name, value)
|
||||
if attribute_present? attr_name
|
||||
super
|
||||
else
|
||||
self.others ||= {}
|
||||
self.others.store attr_name, value
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,96 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Version < ActiveRecord::Base
|
||||
before_destroy :check_integrity
|
||||
belongs_to :project
|
||||
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
|
||||
has_many :attachments, :as => :container, :dependent => :destroy
|
||||
|
||||
validates_presence_of :name
|
||||
validates_uniqueness_of :name, :scope => [:project_id]
|
||||
validates_length_of :name, :maximum => 30
|
||||
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :activerecord_error_not_a_date, :allow_nil => true
|
||||
|
||||
def start_date
|
||||
effective_date
|
||||
end
|
||||
|
||||
def due_date
|
||||
effective_date
|
||||
end
|
||||
|
||||
# Returns true if the version is completed: due date reached and no open issues
|
||||
def completed?
|
||||
effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
|
||||
end
|
||||
|
||||
def completed_pourcent
|
||||
if fixed_issues.count == 0
|
||||
0
|
||||
elsif open_issues_count == 0
|
||||
100
|
||||
else
|
||||
(closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count
|
||||
end
|
||||
end
|
||||
|
||||
def closed_pourcent
|
||||
if fixed_issues.count == 0
|
||||
0
|
||||
else
|
||||
closed_issues_count * 100.0 / fixed_issues.count
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the version is overdue: due date reached and some open issues
|
||||
def overdue?
|
||||
effective_date && (effective_date < Date.today) && (open_issues_count > 0)
|
||||
end
|
||||
|
||||
def open_issues_count
|
||||
@open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
|
||||
end
|
||||
|
||||
def closed_issues_count
|
||||
@closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
|
||||
end
|
||||
|
||||
def wiki_page
|
||||
if project.wiki && !wiki_page_title.blank?
|
||||
@wiki_page ||= project.wiki.find_page(wiki_page_title)
|
||||
end
|
||||
@wiki_page
|
||||
end
|
||||
|
||||
def to_s; name end
|
||||
|
||||
# Versions are sorted by effective_date
|
||||
# Those with no effective_date are at the end, sorted by name
|
||||
def <=>(version)
|
||||
if self.effective_date
|
||||
version.effective_date ? (self.effective_date <=> version.effective_date) : -1
|
||||
else
|
||||
version.effective_date ? 1 : (self.name <=> version.name)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def check_integrity
|
||||
raise "Can't delete version" if self.fixed_issues.find(:first)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Watcher < ActiveRecord::Base
|
||||
belongs_to :watchable, :polymorphic => true
|
||||
belongs_to :user
|
||||
|
||||
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Wiki < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
has_many :pages, :class_name => 'WikiPage', :dependent => :destroy
|
||||
has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
|
||||
|
||||
validates_presence_of :start_page
|
||||
validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
|
||||
|
||||
# find the page with the given title
|
||||
# if page doesn't exist, return a new page
|
||||
def find_or_new_page(title)
|
||||
title = start_page if title.blank?
|
||||
find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
|
||||
end
|
||||
|
||||
# find the page with the given title
|
||||
def find_page(title, options = {})
|
||||
title = start_page if title.blank?
|
||||
title = Wiki.titleize(title)
|
||||
page = pages.find_by_title(title)
|
||||
if !page && !(options[:with_redirect] == false)
|
||||
# search for a redirect
|
||||
redirect = redirects.find_by_title(title)
|
||||
page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
|
||||
end
|
||||
page
|
||||
end
|
||||
|
||||
# turn a string into a valid page title
|
||||
def self.titleize(title)
|
||||
# replace spaces with _ and remove unwanted caracters
|
||||
title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
|
||||
# upcase the first letter
|
||||
title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
|
||||
title
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'zlib'
|
||||
|
||||
class WikiContent < ActiveRecord::Base
|
||||
set_locking_column :version
|
||||
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
|
||||
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
|
||||
validates_presence_of :text
|
||||
|
||||
acts_as_versioned
|
||||
class Version
|
||||
belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
|
||||
belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
|
||||
attr_protected :data
|
||||
|
||||
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
|
||||
:description => :comments,
|
||||
:datetime => :updated_on,
|
||||
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
|
||||
|
||||
def text=(plain)
|
||||
case Setting.wiki_compression
|
||||
when 'gzip'
|
||||
begin
|
||||
self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
|
||||
self.compression = 'gzip'
|
||||
rescue
|
||||
self.data = plain
|
||||
self.compression = ''
|
||||
end
|
||||
else
|
||||
self.data = plain
|
||||
self.compression = ''
|
||||
end
|
||||
plain
|
||||
end
|
||||
|
||||
def text
|
||||
@text ||= case compression
|
||||
when 'gzip'
|
||||
Zlib::Inflate.inflate(data)
|
||||
else
|
||||
# uncompressed data
|
||||
data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,115 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'diff'
|
||||
|
||||
class WikiPage < ActiveRecord::Base
|
||||
belongs_to :wiki
|
||||
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
|
||||
has_many :attachments, :as => :container, :dependent => :destroy
|
||||
|
||||
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
|
||||
:description => :text,
|
||||
:datetime => :created_on,
|
||||
:url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
|
||||
|
||||
acts_as_searchable :columns => ['title', 'text'],
|
||||
:include => [:wiki, :content],
|
||||
:project_key => "#{Wiki.table_name}.project_id"
|
||||
|
||||
attr_accessor :redirect_existing_links
|
||||
|
||||
validates_presence_of :title
|
||||
validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
|
||||
validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
|
||||
validates_associated :content
|
||||
|
||||
def title=(value)
|
||||
value = Wiki.titleize(value)
|
||||
@previous_title = read_attribute(:title) if @previous_title.blank?
|
||||
write_attribute(:title, value)
|
||||
end
|
||||
|
||||
def before_save
|
||||
self.title = Wiki.titleize(title)
|
||||
# Manage redirects if the title has changed
|
||||
if !@previous_title.blank? && (@previous_title != title) && !new_record?
|
||||
# Update redirects that point to the old title
|
||||
wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
|
||||
r.redirects_to = title
|
||||
r.title == r.redirects_to ? r.destroy : r.save
|
||||
end
|
||||
# Remove redirects for the new title
|
||||
wiki.redirects.find_all_by_title(title).each(&:destroy)
|
||||
# Create a redirect to the new title
|
||||
wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
|
||||
@previous_title = nil
|
||||
end
|
||||
end
|
||||
|
||||
def before_destroy
|
||||
# Remove redirects to this page
|
||||
wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
|
||||
end
|
||||
|
||||
def pretty_title
|
||||
WikiPage.pretty_title(title)
|
||||
end
|
||||
|
||||
def content_for_version(version=nil)
|
||||
result = content.versions.find_by_version(version.to_i) if version
|
||||
result ||= content
|
||||
result
|
||||
end
|
||||
|
||||
def diff(version_to=nil, version_from=nil)
|
||||
version_to = version_to ? version_to.to_i : self.content.version
|
||||
version_from = version_from ? version_from.to_i : version_to - 1
|
||||
version_to, version_from = version_from, version_to unless version_from < version_to
|
||||
|
||||
content_to = content.versions.find_by_version(version_to)
|
||||
content_from = content.versions.find_by_version(version_from)
|
||||
|
||||
(content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
|
||||
end
|
||||
|
||||
def self.pretty_title(str)
|
||||
(str && str.is_a?(String)) ? str.tr('_', ' ') : str
|
||||
end
|
||||
|
||||
def project
|
||||
wiki.project
|
||||
end
|
||||
|
||||
def text
|
||||
content.text if content
|
||||
end
|
||||
end
|
||||
|
||||
class WikiDiff
|
||||
attr_reader :diff, :words, :content_to, :content_from
|
||||
|
||||
def initialize(content_to, content_from)
|
||||
@content_to = content_to
|
||||
@content_from = content_from
|
||||
@words = content_to.text.split(/(\s+)/)
|
||||
@words = @words.select {|word| word != ' '}
|
||||
words_from = content_from.text.split(/(\s+)/)
|
||||
words_from = words_from.select {|word| word != ' '}
|
||||
@diff = words_from.diff @words
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class WikiRedirect < ActiveRecord::Base
|
||||
belongs_to :wiki
|
||||
|
||||
validates_presence_of :title, :redirects_to
|
||||
validates_length_of :title, :redirects_to, :maximum => 255
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
class Workflow < ActiveRecord::Base
|
||||
belongs_to :role
|
||||
belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id'
|
||||
belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'
|
||||
|
||||
validates_presence_of :role, :old_status, :new_status
|
||||
end
|
||||
Reference in New Issue
Block a user