@@ -1,96 +1,98 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006-2007 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require "digest/md5" |
|
19 | 19 | |
|
20 | 20 | class Attachment < ActiveRecord::Base |
|
21 | 21 | belongs_to :container, :polymorphic => true |
|
22 | 22 | belongs_to :author, :class_name => "User", :foreign_key => "author_id" |
|
23 | 23 | |
|
24 | 24 | validates_presence_of :container, :filename |
|
25 | ||
|
25 | validates_length_of :filename, :maximum => 255 | |
|
26 | validates_length_of :disk_filename, :maximum => 255 | |
|
27 | ||
|
26 | 28 | cattr_accessor :storage_path |
|
27 | 29 | @@storage_path = "#{RAILS_ROOT}/files" |
|
28 | 30 | |
|
29 | 31 | def validate |
|
30 | 32 | errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes |
|
31 | 33 | end |
|
32 | 34 | |
|
33 | 35 | def file=(incomming_file) |
|
34 | 36 | unless incomming_file.nil? |
|
35 | 37 | @temp_file = incomming_file |
|
36 | 38 | if @temp_file.size > 0 |
|
37 | 39 | self.filename = sanitize_filename(@temp_file.original_filename) |
|
38 | 40 | self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename |
|
39 | 41 | self.content_type = @temp_file.content_type |
|
40 | 42 | self.filesize = @temp_file.size |
|
41 | 43 | end |
|
42 | 44 | end |
|
43 | 45 | end |
|
44 | 46 | |
|
45 | 47 | def file |
|
46 | 48 | nil |
|
47 | 49 | end |
|
48 | 50 | |
|
49 | 51 | # Copy temp file to its final location |
|
50 | 52 | def before_save |
|
51 | 53 | if @temp_file && (@temp_file.size > 0) |
|
52 | 54 | logger.debug("saving '#{self.diskfile}'") |
|
53 | 55 | File.open(diskfile, "wb") do |f| |
|
54 | 56 | f.write(@temp_file.read) |
|
55 | 57 | end |
|
56 | 58 | self.digest = Digest::MD5.hexdigest(File.read(diskfile)) |
|
57 | 59 | end |
|
58 | 60 | end |
|
59 | 61 | |
|
60 | 62 | # Deletes file on the disk |
|
61 | 63 | def after_destroy |
|
62 | 64 | if self.filename? |
|
63 | 65 | File.delete(diskfile) if File.exist?(diskfile) |
|
64 | 66 | end |
|
65 | 67 | end |
|
66 | 68 | |
|
67 | 69 | # Returns file's location on disk |
|
68 | 70 | def diskfile |
|
69 | 71 | "#{@@storage_path}/#{self.disk_filename}" |
|
70 | 72 | end |
|
71 | 73 | |
|
72 | 74 | def increment_download |
|
73 | 75 | increment!(:downloads) |
|
74 | 76 | end |
|
75 | 77 | |
|
76 | 78 | # returns last created projects |
|
77 | 79 | def self.most_downloaded |
|
78 | 80 | find(:all, :limit => 5, :order => "downloads DESC") |
|
79 | 81 | end |
|
80 | 82 | |
|
81 | 83 | def project |
|
82 | 84 | container.is_a?(Project) ? container : container.project |
|
83 | 85 | end |
|
84 | 86 | |
|
85 | 87 | private |
|
86 | 88 | def sanitize_filename(value) |
|
87 | 89 | # get only the filename, not the whole path |
|
88 | 90 | just_filename = value.gsub(/^.*(\\|\/)/, '') |
|
89 | 91 | # NOTE: File.basename doesn't work right with Windows paths on Unix |
|
90 | 92 | # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/')) |
|
91 | 93 | |
|
92 | 94 | # Finally, replace all non alphanumeric, underscore or periods with underscore |
|
93 | 95 | @filename = just_filename.gsub(/[^\w\.\-]/,'_') |
|
94 | 96 | end |
|
95 | 97 | |
|
96 | 98 | end |
@@ -1,60 +1,61 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class CustomField < ActiveRecord::Base |
|
19 | 19 | has_many :custom_values, :dependent => :delete_all |
|
20 | 20 | serialize :possible_values |
|
21 | 21 | |
|
22 | 22 | FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 }, |
|
23 | 23 | "text" => { :name => :label_text, :order => 2 }, |
|
24 | 24 | "int" => { :name => :label_integer, :order => 3 }, |
|
25 | 25 | "list" => { :name => :label_list, :order => 4 }, |
|
26 | 26 | "date" => { :name => :label_date, :order => 5 }, |
|
27 | 27 | "bool" => { :name => :label_boolean, :order => 6 } |
|
28 | 28 | }.freeze |
|
29 | 29 | |
|
30 | 30 | validates_presence_of :name, :field_format |
|
31 | 31 | validates_uniqueness_of :name |
|
32 | validates_length_of :name, :maximum => 30 | |
|
32 | 33 | validates_format_of :name, :with => /^[\w\s\'\-]*$/i |
|
33 | 34 | validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys |
|
34 | 35 | |
|
35 | 36 | def initialize(attributes = nil) |
|
36 | 37 | super |
|
37 | 38 | self.possible_values ||= [] |
|
38 | 39 | end |
|
39 | 40 | |
|
40 | 41 | def before_validation |
|
41 | 42 | # remove empty values |
|
42 | 43 | self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact |
|
43 | 44 | end |
|
44 | 45 | |
|
45 | 46 | def validate |
|
46 | 47 | if self.field_format == "list" |
|
47 | 48 | errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty? |
|
48 | 49 | errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array |
|
49 | 50 | end |
|
50 | 51 | end |
|
51 | 52 | |
|
52 | 53 | # to move in project_custom_field |
|
53 | 54 | def self.for_all |
|
54 | 55 | find(:all, :conditions => ["is_for_all=?", true]) |
|
55 | 56 | end |
|
56 | 57 | |
|
57 | 58 | def type_name |
|
58 | 59 | nil |
|
59 | 60 | end |
|
60 | 61 | end |
@@ -1,24 +1,25 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class Document < ActiveRecord::Base |
|
19 | 19 | belongs_to :project |
|
20 | 20 | belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id" |
|
21 | 21 | has_many :attachments, :as => :container, :dependent => :destroy |
|
22 | 22 | |
|
23 | 23 | validates_presence_of :project, :title, :category |
|
24 | validates_length_of :title, :maximum => 60 | |
|
24 | 25 | end |
@@ -1,50 +1,51 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class Enumeration < ActiveRecord::Base |
|
19 | 19 | before_destroy :check_integrity |
|
20 | 20 | |
|
21 | 21 | validates_presence_of :opt, :name |
|
22 | 22 | validates_uniqueness_of :name, :scope => [:opt] |
|
23 | validates_length_of :name, :maximum => 30 | |
|
23 | 24 | validates_format_of :name, :with => /^[\w\s\'\-]*$/i |
|
24 | 25 | |
|
25 | 26 | OPTIONS = { |
|
26 | 27 | "IPRI" => :enumeration_issue_priorities, |
|
27 | 28 | "DCAT" => :enumeration_doc_categories, |
|
28 | 29 | "ACTI" => :enumeration_activities |
|
29 | 30 | }.freeze |
|
30 | 31 | |
|
31 | 32 | def self.get_values(option) |
|
32 | 33 | find(:all, :conditions => ['opt=?', option]) |
|
33 | 34 | end |
|
34 | 35 | |
|
35 | 36 | def option_name |
|
36 | 37 | OPTIONS[self.opt] |
|
37 | 38 | end |
|
38 | 39 | |
|
39 | 40 | private |
|
40 | 41 | def check_integrity |
|
41 | 42 | case self.opt |
|
42 | 43 | when "IPRI" |
|
43 | 44 | raise "Can't delete enumeration" if Issue.find(:first, :conditions => ["priority_id=?", self.id]) |
|
44 | 45 | when "DCAT" |
|
45 | 46 | raise "Can't delete enumeration" if Document.find(:first, :conditions => ["category_id=?", self.id]) |
|
46 | 47 | when "ACTI" |
|
47 | 48 | raise "Can't delete enumeration" if TimeEntry.find(:first, :conditions => ["activity_id=?", self.id]) |
|
48 | 49 | end |
|
49 | 50 | end |
|
50 | 51 | end |
@@ -1,131 +1,132 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006-2007 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class Issue < ActiveRecord::Base |
|
19 | 19 | belongs_to :project |
|
20 | 20 | belongs_to :tracker |
|
21 | 21 | belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' |
|
22 | 22 | belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' |
|
23 | 23 | belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' |
|
24 | 24 | belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' |
|
25 | 25 | belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id' |
|
26 | 26 | belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' |
|
27 | 27 | |
|
28 | 28 | has_many :journals, :as => :journalized, :dependent => :destroy |
|
29 | 29 | has_many :attachments, :as => :container, :dependent => :destroy |
|
30 | 30 | has_many :time_entries, :dependent => :nullify |
|
31 | 31 | has_many :custom_values, :dependent => :delete_all, :as => :customized |
|
32 | 32 | has_many :custom_fields, :through => :custom_values |
|
33 | 33 | has_and_belongs_to_many :changesets, :order => "revision ASC" |
|
34 | 34 | |
|
35 | 35 | has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all |
|
36 | 36 | has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all |
|
37 | 37 | |
|
38 | 38 | acts_as_watchable |
|
39 | 39 | |
|
40 | 40 | validates_presence_of :subject, :description, :priority, :tracker, :author, :status |
|
41 | validates_length_of :subject, :maximum => 255 | |
|
41 | 42 | validates_inclusion_of :done_ratio, :in => 0..100 |
|
42 | 43 | validates_associated :custom_values, :on => :update |
|
43 | 44 | |
|
44 | 45 | # set default status for new issues |
|
45 | 46 | def before_validation |
|
46 | 47 | self.status = IssueStatus.default if status.nil? |
|
47 | 48 | end |
|
48 | 49 | |
|
49 | 50 | def validate |
|
50 | 51 | if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? |
|
51 | 52 | errors.add :due_date, :activerecord_error_not_a_date |
|
52 | 53 | end |
|
53 | 54 | |
|
54 | 55 | if self.due_date and self.start_date and self.due_date < self.start_date |
|
55 | 56 | errors.add :due_date, :activerecord_error_greater_than_start_date |
|
56 | 57 | end |
|
57 | 58 | |
|
58 | 59 | if start_date && soonest_start && start_date < soonest_start |
|
59 | 60 | errors.add :start_date, :activerecord_error_invalid |
|
60 | 61 | end |
|
61 | 62 | end |
|
62 | 63 | |
|
63 | 64 | def before_create |
|
64 | 65 | # default assignment based on category |
|
65 | 66 | if assigned_to.nil? && category && category.assigned_to |
|
66 | 67 | self.assigned_to = category.assigned_to |
|
67 | 68 | end |
|
68 | 69 | end |
|
69 | 70 | |
|
70 | 71 | def before_save |
|
71 | 72 | if @current_journal |
|
72 | 73 | # attributes changes |
|
73 | 74 | (Issue.column_names - %w(id description)).each {|c| |
|
74 | 75 | @current_journal.details << JournalDetail.new(:property => 'attr', |
|
75 | 76 | :prop_key => c, |
|
76 | 77 | :old_value => @issue_before_change.send(c), |
|
77 | 78 | :value => send(c)) unless send(c)==@issue_before_change.send(c) |
|
78 | 79 | } |
|
79 | 80 | # custom fields changes |
|
80 | 81 | custom_values.each {|c| |
|
81 | 82 | @current_journal.details << JournalDetail.new(:property => 'cf', |
|
82 | 83 | :prop_key => c.custom_field_id, |
|
83 | 84 | :old_value => @custom_values_before_change[c.custom_field_id], |
|
84 | 85 | :value => c.value) unless @custom_values_before_change[c.custom_field_id]==c.value |
|
85 | 86 | } |
|
86 | 87 | @current_journal.save unless @current_journal.details.empty? and @current_journal.notes.empty? |
|
87 | 88 | end |
|
88 | 89 | end |
|
89 | 90 | |
|
90 | 91 | def after_save |
|
91 | 92 | relations_from.each(&:set_issue_to_dates) |
|
92 | 93 | end |
|
93 | 94 | |
|
94 | 95 | def custom_value_for(custom_field) |
|
95 | 96 | self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id } |
|
96 | 97 | return nil |
|
97 | 98 | end |
|
98 | 99 | |
|
99 | 100 | def init_journal(user, notes = "") |
|
100 | 101 | @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) |
|
101 | 102 | @issue_before_change = self.clone |
|
102 | 103 | @custom_values_before_change = {} |
|
103 | 104 | self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } |
|
104 | 105 | @current_journal |
|
105 | 106 | end |
|
106 | 107 | |
|
107 | 108 | def spent_hours |
|
108 | 109 | @spent_hours ||= time_entries.sum(:hours) || 0 |
|
109 | 110 | end |
|
110 | 111 | |
|
111 | 112 | def relations |
|
112 | 113 | (relations_from + relations_to).sort |
|
113 | 114 | end |
|
114 | 115 | |
|
115 | 116 | def all_dependent_issues |
|
116 | 117 | dependencies = [] |
|
117 | 118 | relations_from.each do |relation| |
|
118 | 119 | dependencies << relation.issue_to |
|
119 | 120 | dependencies += relation.issue_to.all_dependent_issues |
|
120 | 121 | end |
|
121 | 122 | dependencies |
|
122 | 123 | end |
|
123 | 124 | |
|
124 | 125 | def duration |
|
125 | 126 | (start_date && due_date) ? due_date - start_date : 0 |
|
126 | 127 | end |
|
127 | 128 | |
|
128 | 129 | def soonest_start |
|
129 | 130 | @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min |
|
130 | 131 | end |
|
131 | 132 | end |
@@ -1,58 +1,59 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class IssueStatus < ActiveRecord::Base |
|
19 | 19 | before_destroy :check_integrity |
|
20 | 20 | has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all |
|
21 | 21 | acts_as_list |
|
22 | 22 | |
|
23 | 23 | validates_presence_of :name |
|
24 | 24 | validates_uniqueness_of :name |
|
25 | validates_length_of :name, :maximum => 30 | |
|
25 | 26 | validates_format_of :name, :with => /^[\w\s\'\-]*$/i |
|
26 | 27 | validates_length_of :html_color, :is => 6 |
|
27 | 28 | validates_format_of :html_color, :with => /^[a-f0-9]*$/i |
|
28 | 29 | |
|
29 | 30 | def before_save |
|
30 | 31 | IssueStatus.update_all "is_default=#{connection.quoted_false}" if self.is_default? |
|
31 | 32 | end |
|
32 | 33 | |
|
33 | 34 | # Returns the default status for new issues |
|
34 | 35 | def self.default |
|
35 | 36 | find(:first, :conditions =>["is_default=?", true]) |
|
36 | 37 | end |
|
37 | 38 | |
|
38 | 39 | # Returns an array of all statuses the given role can switch to |
|
39 | 40 | # Uses association cache when called more than one time |
|
40 | 41 | def new_statuses_allowed_to(role, tracker) |
|
41 | 42 | new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker |
|
42 | 43 | new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : [] |
|
43 | 44 | end |
|
44 | 45 | |
|
45 | 46 | # Same thing as above but uses a database query |
|
46 | 47 | # More efficient than the previous method if called just once |
|
47 | 48 | def find_new_statuses_allowed_to(role, tracker) |
|
48 | 49 | new_statuses = workflows.find(:all, |
|
49 | 50 | :include => :new_status, |
|
50 | 51 | :conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker |
|
51 | 52 | new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : [] |
|
52 | 53 | end |
|
53 | 54 | |
|
54 | 55 | private |
|
55 | 56 | def check_integrity |
|
56 | 57 | raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id]) |
|
57 | 58 | end |
|
58 | 59 | end |
@@ -1,29 +1,31 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class News < ActiveRecord::Base |
|
19 | 19 | belongs_to :project |
|
20 | 20 | belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' |
|
21 | 21 | has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on" |
|
22 | 22 | |
|
23 | 23 | validates_presence_of :title, :description |
|
24 | validates_length_of :title, :maximum => 60 | |
|
25 | validates_length_of :summary, :maximum => 255 | |
|
24 | 26 | |
|
25 | 27 | # returns latest news for projects visible by user |
|
26 | 28 | def self.latest(user=nil, count=5) |
|
27 | 29 | find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") |
|
28 | 30 | end |
|
29 | 31 | end |
@@ -1,239 +1,240 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006-2007 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class Query < ActiveRecord::Base |
|
19 | 19 | belongs_to :project |
|
20 | 20 | belongs_to :user |
|
21 | 21 | serialize :filters |
|
22 | 22 | |
|
23 | 23 | attr_protected :project, :user |
|
24 | 24 | attr_accessor :executed_by |
|
25 | 25 | |
|
26 | 26 | validates_presence_of :name, :on => :save |
|
27 | validates_length_of :name, :maximum => 255 | |
|
27 | 28 | |
|
28 | 29 | @@operators = { "=" => :label_equals, |
|
29 | 30 | "!" => :label_not_equals, |
|
30 | 31 | "o" => :label_open_issues, |
|
31 | 32 | "c" => :label_closed_issues, |
|
32 | 33 | "!*" => :label_none, |
|
33 | 34 | "*" => :label_all, |
|
34 | 35 | "<t+" => :label_in_less_than, |
|
35 | 36 | ">t+" => :label_in_more_than, |
|
36 | 37 | "t+" => :label_in, |
|
37 | 38 | "t" => :label_today, |
|
38 | 39 | ">t-" => :label_less_than_ago, |
|
39 | 40 | "<t-" => :label_more_than_ago, |
|
40 | 41 | "t-" => :label_ago, |
|
41 | 42 | "~" => :label_contains, |
|
42 | 43 | "!~" => :label_not_contains } |
|
43 | 44 | |
|
44 | 45 | cattr_reader :operators |
|
45 | 46 | |
|
46 | 47 | @@operators_by_filter_type = { :list => [ "=", "!" ], |
|
47 | 48 | :list_status => [ "o", "=", "!", "c", "*" ], |
|
48 | 49 | :list_optional => [ "=", "!", "!*", "*" ], |
|
49 | 50 | :list_one_or_more => [ "*", "=" ], |
|
50 | 51 | :date => [ "<t+", ">t+", "t+", "t", ">t-", "<t-", "t-" ], |
|
51 | 52 | :date_past => [ ">t-", "<t-", "t-", "t" ], |
|
52 | 53 | :string => [ "=", "~", "!", "!~" ], |
|
53 | 54 | :text => [ "~", "!~" ] } |
|
54 | 55 | |
|
55 | 56 | cattr_reader :operators_by_filter_type |
|
56 | 57 | |
|
57 | 58 | def initialize(attributes = nil) |
|
58 | 59 | super attributes |
|
59 | 60 | self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } |
|
60 | 61 | end |
|
61 | 62 | |
|
62 | 63 | def executed_by=(user) |
|
63 | 64 | @executed_by = user |
|
64 | 65 | set_language_if_valid(user.language) if user |
|
65 | 66 | end |
|
66 | 67 | |
|
67 | 68 | def validate |
|
68 | 69 | filters.each_key do |field| |
|
69 | 70 | errors.add label_for(field), :activerecord_error_blank unless |
|
70 | 71 | # filter requires one or more values |
|
71 | 72 | (values_for(field) and !values_for(field).first.empty?) or |
|
72 | 73 | # filter doesn't require any value |
|
73 | 74 | ["o", "c", "!*", "*", "t"].include? operator_for(field) |
|
74 | 75 | end if filters |
|
75 | 76 | end |
|
76 | 77 | |
|
77 | 78 | def editable_by?(user) |
|
78 | 79 | return false unless user |
|
79 | 80 | return true if !is_public && self.user_id == user.id |
|
80 | 81 | is_public && user.authorized_to(project, "projects/add_query") |
|
81 | 82 | end |
|
82 | 83 | |
|
83 | 84 | def available_filters |
|
84 | 85 | return @available_filters if @available_filters |
|
85 | 86 | @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, |
|
86 | 87 | "tracker_id" => { :type => :list, :order => 2, :values => Tracker.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, |
|
87 | 88 | "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI']).collect{|s| [s.name, s.id.to_s] } }, |
|
88 | 89 | "subject" => { :type => :text, :order => 8 }, |
|
89 | 90 | "created_on" => { :type => :date_past, :order => 9 }, |
|
90 | 91 | "updated_on" => { :type => :date_past, :order => 10 }, |
|
91 | 92 | "start_date" => { :type => :date, :order => 11 }, |
|
92 | 93 | "due_date" => { :type => :date, :order => 12 } } |
|
93 | 94 | unless project.nil? |
|
94 | 95 | # project specific filters |
|
95 | 96 | user_values = [] |
|
96 | 97 | user_values << ["<< #{l(:label_me)} >>", "me"] if executed_by |
|
97 | 98 | user_values += @project.users.collect{|s| [s.name, s.id.to_s] } |
|
98 | 99 | |
|
99 | 100 | @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } |
|
100 | 101 | @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } |
|
101 | 102 | @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } |
|
102 | 103 | @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } |
|
103 | 104 | unless @project.active_children.empty? |
|
104 | 105 | @available_filters["subproject_id"] = { :type => :list_one_or_more, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } |
|
105 | 106 | end |
|
106 | 107 | @project.all_custom_fields.select(&:is_filter?).each do |field| |
|
107 | 108 | case field.field_format |
|
108 | 109 | when "string", "int" |
|
109 | 110 | options = { :type => :string, :order => 20 } |
|
110 | 111 | when "text" |
|
111 | 112 | options = { :type => :text, :order => 20 } |
|
112 | 113 | when "list" |
|
113 | 114 | options = { :type => :list_optional, :values => field.possible_values, :order => 20} |
|
114 | 115 | when "date" |
|
115 | 116 | options = { :type => :date, :order => 20 } |
|
116 | 117 | when "bool" |
|
117 | 118 | options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } |
|
118 | 119 | end |
|
119 | 120 | @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) |
|
120 | 121 | end |
|
121 | 122 | # remove category filter if no category defined |
|
122 | 123 | @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty? |
|
123 | 124 | end |
|
124 | 125 | @available_filters |
|
125 | 126 | end |
|
126 | 127 | |
|
127 | 128 | def add_filter(field, operator, values) |
|
128 | 129 | # values must be an array |
|
129 | 130 | return unless values and values.is_a? Array # and !values.first.empty? |
|
130 | 131 | # check if field is defined as an available filter |
|
131 | 132 | if available_filters.has_key? field |
|
132 | 133 | filter_options = available_filters[field] |
|
133 | 134 | # check if operator is allowed for that filter |
|
134 | 135 | #if @@operators_by_filter_type[filter_options[:type]].include? operator |
|
135 | 136 | # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]}) |
|
136 | 137 | # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator |
|
137 | 138 | #end |
|
138 | 139 | filters[field] = {:operator => operator, :values => values } |
|
139 | 140 | end |
|
140 | 141 | end |
|
141 | 142 | |
|
142 | 143 | def add_short_filter(field, expression) |
|
143 | 144 | return unless expression |
|
144 | 145 | parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first |
|
145 | 146 | add_filter field, (parms[0] || "="), [parms[1] || ""] |
|
146 | 147 | end |
|
147 | 148 | |
|
148 | 149 | def has_filter?(field) |
|
149 | 150 | filters and filters[field] |
|
150 | 151 | end |
|
151 | 152 | |
|
152 | 153 | def operator_for(field) |
|
153 | 154 | has_filter?(field) ? filters[field][:operator] : nil |
|
154 | 155 | end |
|
155 | 156 | |
|
156 | 157 | def values_for(field) |
|
157 | 158 | has_filter?(field) ? filters[field][:values] : nil |
|
158 | 159 | end |
|
159 | 160 | |
|
160 | 161 | def label_for(field) |
|
161 | 162 | label = @available_filters[field][:name] if @available_filters.has_key?(field) |
|
162 | 163 | label ||= field.gsub(/\_id$/, "") |
|
163 | 164 | end |
|
164 | 165 | |
|
165 | 166 | def statement |
|
166 | 167 | sql = "1=1" |
|
167 | 168 | if has_filter?("subproject_id") |
|
168 | 169 | subproject_ids = [] |
|
169 | 170 | if operator_for("subproject_id") == "=" |
|
170 | 171 | subproject_ids = values_for("subproject_id").each(&:to_i) |
|
171 | 172 | else |
|
172 | 173 | subproject_ids = project.active_children.collect{|p| p.id} |
|
173 | 174 | end |
|
174 | 175 | sql << " AND #{Issue.table_name}.project_id IN (%d,%s)" % [project.id, subproject_ids.join(",")] if project |
|
175 | 176 | else |
|
176 | 177 | sql << " AND #{Issue.table_name}.project_id=%d" % project.id if project |
|
177 | 178 | end |
|
178 | 179 | filters.each_key do |field| |
|
179 | 180 | next if field == "subproject_id" |
|
180 | 181 | v = values_for(field).clone |
|
181 | 182 | next unless v and !v.empty? |
|
182 | 183 | |
|
183 | 184 | sql = sql + " AND " unless sql.empty? |
|
184 | 185 | sql << "(" |
|
185 | 186 | |
|
186 | 187 | if field =~ /^cf_(\d+)$/ |
|
187 | 188 | # custom field |
|
188 | 189 | db_table = CustomValue.table_name |
|
189 | 190 | db_field = "value" |
|
190 | 191 | sql << "#{db_table}.custom_field_id = #{$1} AND " |
|
191 | 192 | else |
|
192 | 193 | # regular field |
|
193 | 194 | db_table = Issue.table_name |
|
194 | 195 | db_field = field |
|
195 | 196 | end |
|
196 | 197 | |
|
197 | 198 | # "me" value subsitution |
|
198 | 199 | if %w(assigned_to_id author_id).include?(field) |
|
199 | 200 | v.push(executed_by ? executed_by.id.to_s : "0") if v.delete("me") |
|
200 | 201 | end |
|
201 | 202 | |
|
202 | 203 | case operator_for field |
|
203 | 204 | when "=" |
|
204 | 205 | sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" |
|
205 | 206 | when "!" |
|
206 | 207 | sql = sql + "#{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" |
|
207 | 208 | when "!*" |
|
208 | 209 | sql = sql + "#{db_table}.#{db_field} IS NULL" |
|
209 | 210 | when "*" |
|
210 | 211 | sql = sql + "#{db_table}.#{db_field} IS NOT NULL" |
|
211 | 212 | when "o" |
|
212 | 213 | sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" |
|
213 | 214 | when "c" |
|
214 | 215 | sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" |
|
215 | 216 | when ">t-" |
|
216 | 217 | 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)] |
|
217 | 218 | when "<t-" |
|
218 | 219 | sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time) |
|
219 | 220 | when "t-" |
|
220 | 221 | 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)] |
|
221 | 222 | when ">t+" |
|
222 | 223 | sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time) |
|
223 | 224 | when "<t+" |
|
224 | 225 | 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)] |
|
225 | 226 | when "t+" |
|
226 | 227 | 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)] |
|
227 | 228 | when "t" |
|
228 | 229 | 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)] |
|
229 | 230 | when "~" |
|
230 | 231 | sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'" |
|
231 | 232 | when "!~" |
|
232 | 233 | sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'" |
|
233 | 234 | end |
|
234 | 235 | sql << ")" |
|
235 | 236 | |
|
236 | 237 | end if filters and valid? |
|
237 | 238 | sql |
|
238 | 239 | end |
|
239 | 240 | end |
@@ -1,37 +1,38 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class Role < ActiveRecord::Base |
|
19 | 19 | before_destroy :check_integrity |
|
20 | 20 | has_and_belongs_to_many :permissions |
|
21 | 21 | has_many :workflows, :dependent => :delete_all |
|
22 | 22 | has_many :members |
|
23 | 23 | acts_as_list |
|
24 | 24 | |
|
25 | 25 | validates_presence_of :name |
|
26 | 26 | validates_uniqueness_of :name |
|
27 | validates_length_of :name, :maximum => 30 | |
|
27 | 28 | validates_format_of :name, :with => /^[\w\s\'\-]*$/i |
|
28 | 29 | |
|
29 | 30 | def <=>(role) |
|
30 | 31 | position <=> role.position |
|
31 | 32 | end |
|
32 | 33 | |
|
33 | 34 | private |
|
34 | 35 | def check_integrity |
|
35 | 36 | raise "Can't delete role" if Member.find(:first, :conditions =>["role_id=?", self.id]) |
|
36 | 37 | end |
|
37 | 38 | end |
@@ -1,33 +1,34 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class Tracker < ActiveRecord::Base |
|
19 | 19 | before_destroy :check_integrity |
|
20 | 20 | has_many :issues |
|
21 | 21 | has_many :workflows, :dependent => :delete_all |
|
22 | 22 | 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' |
|
23 | 23 | acts_as_list |
|
24 | 24 | |
|
25 | 25 | validates_presence_of :name |
|
26 | 26 | validates_uniqueness_of :name |
|
27 | validates_length_of :name, :maximum => 30 | |
|
27 | 28 | validates_format_of :name, :with => /^[\w\s\'\-]*$/i |
|
28 | 29 | |
|
29 | 30 | private |
|
30 | 31 | def check_integrity |
|
31 | 32 | raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id]) |
|
32 | 33 | end |
|
33 | 34 | end |
@@ -1,61 +1,62 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class Version < ActiveRecord::Base |
|
19 | 19 | before_destroy :check_integrity |
|
20 | 20 | belongs_to :project |
|
21 | 21 | has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id' |
|
22 | 22 | has_many :attachments, :as => :container, :dependent => :destroy |
|
23 | 23 | |
|
24 | 24 | validates_presence_of :name |
|
25 | 25 | validates_uniqueness_of :name, :scope => [:project_id] |
|
26 | validates_length_of :name, :maximum => 30 | |
|
26 | 27 | validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :activerecord_error_not_a_date, :allow_nil => true |
|
27 | 28 | |
|
28 | 29 | def start_date |
|
29 | 30 | effective_date |
|
30 | 31 | end |
|
31 | 32 | |
|
32 | 33 | def due_date |
|
33 | 34 | effective_date |
|
34 | 35 | end |
|
35 | 36 | |
|
36 | 37 | def completed? |
|
37 | 38 | effective_date && effective_date <= Date.today |
|
38 | 39 | end |
|
39 | 40 | |
|
40 | 41 | def wiki_page |
|
41 | 42 | if project.wiki && !wiki_page_title.blank? |
|
42 | 43 | @wiki_page ||= project.wiki.find_page(wiki_page_title) |
|
43 | 44 | end |
|
44 | 45 | @wiki_page |
|
45 | 46 | end |
|
46 | 47 | |
|
47 | 48 | # Versions are sorted by effective_date |
|
48 | 49 | # Those with no effective_date are at the end, sorted by name |
|
49 | 50 | def <=>(version) |
|
50 | 51 | if self.effective_date |
|
51 | 52 | version.effective_date ? (self.effective_date <=> version.effective_date) : -1 |
|
52 | 53 | else |
|
53 | 54 | version.effective_date ? 1 : (self.name <=> version.name) |
|
54 | 55 | end |
|
55 | 56 | end |
|
56 | 57 | |
|
57 | 58 | private |
|
58 | 59 | def check_integrity |
|
59 | 60 | raise "Can't delete version" if self.fixed_issues.find(:first) |
|
60 | 61 | end |
|
61 | 62 | end |
General Comments 0
You need to be logged in to leave comments.
Login now