##// END OF EJS Templates
Adds custom fields for versions (#4219)....
Jean-Philippe Lang -
r2950:ba7cf9c3ce6c
parent child
Show More
@@ -0,0 +1,22
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class VersionCustomField < CustomField
19 def type_name
20 :label_version_plural
21 end
22 end
@@ -1,69 +1,71
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 VersionsController < ApplicationController
19 19 menu_item :roadmap
20 20 before_filter :find_version, :except => :close_completed
21 21 before_filter :find_project, :only => :close_completed
22 22 before_filter :authorize
23 23
24 helper :custom_fields
25
24 26 def show
25 27 end
26 28
27 29 def edit
28 30 if request.post? and @version.update_attributes(params[:version])
29 31 flash[:notice] = l(:notice_successful_update)
30 32 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
31 33 end
32 34 end
33 35
34 36 def close_completed
35 37 if request.post?
36 38 @project.close_completed_versions
37 39 end
38 40 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
39 41 end
40 42
41 43 def destroy
42 44 @version.destroy
43 45 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
44 46 rescue
45 47 flash[:error] = l(:notice_unable_delete_version)
46 48 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
47 49 end
48 50
49 51 def status_by
50 52 respond_to do |format|
51 53 format.html { render :action => 'show' }
52 54 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
53 55 end
54 56 end
55 57
56 58 private
57 59 def find_version
58 60 @version = Version.find(params[:id])
59 61 @project = @version.project
60 62 rescue ActiveRecord::RecordNotFound
61 63 render_404
62 64 end
63 65
64 66 def find_project
65 67 @project = Project.find(params[:project_id])
66 68 rescue ActiveRecord::RecordNotFound
67 69 render_404
68 70 end
69 71 end
@@ -1,92 +1,93
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 module CustomFieldsHelper
19 19
20 20 def custom_fields_tabs
21 21 tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
22 22 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
23 23 {:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
24 {:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
24 25 {:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
25 26 {:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
26 27 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
27 28 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
28 29 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
29 30 ]
30 31 end
31 32
32 33 # Return custom field html tag corresponding to its format
33 34 def custom_field_tag(name, custom_value)
34 35 custom_field = custom_value.custom_field
35 36 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
36 37 field_id = "#{name}_custom_field_values_#{custom_field.id}"
37 38
38 39 case custom_field.field_format
39 40 when "date"
40 41 text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
41 42 calendar_for(field_id)
42 43 when "text"
43 44 text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
44 45 when "bool"
45 46 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
46 47 when "list"
47 48 blank_option = custom_field.is_required? ?
48 49 (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
49 50 '<option></option>'
50 51 select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
51 52 else
52 53 text_field_tag(field_name, custom_value.value, :id => field_id)
53 54 end
54 55 end
55 56
56 57 # Return custom field label tag
57 58 def custom_field_label_tag(name, custom_value)
58 59 content_tag "label", custom_value.custom_field.name +
59 60 (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
60 61 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
61 62 :class => (custom_value.errors.empty? ? nil : "error" )
62 63 end
63 64
64 65 # Return custom field tag with its label tag
65 66 def custom_field_tag_with_label(name, custom_value)
66 67 custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
67 68 end
68 69
69 70 # Return a string used to display a custom value
70 71 def show_value(custom_value)
71 72 return "" unless custom_value
72 73 format_value(custom_value.value, custom_value.custom_field.field_format)
73 74 end
74 75
75 76 # Return a string used to display a custom value
76 77 def format_value(value, field_format)
77 78 return "" unless value && !value.empty?
78 79 case field_format
79 80 when "date"
80 81 begin; format_date(value.to_date); rescue; value end
81 82 when "bool"
82 83 l(value == "1" ? :general_text_Yes : :general_text_No)
83 84 else
84 85 value
85 86 end
86 87 end
87 88
88 89 # Return an array of custom field formats which can be used in select_tag
89 90 def custom_field_formats_for_select
90 91 CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] }
91 92 end
92 93 end
@@ -1,162 +1,163
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 acts_as_customizable
22 23 acts_as_attachable :view_permission => :view_files,
23 24 :delete_permission => :manage_files
24 25
25 26 VERSION_STATUSES = %w(open locked closed)
26 27
27 28 validates_presence_of :name
28 29 validates_uniqueness_of :name, :scope => [:project_id]
29 30 validates_length_of :name, :maximum => 60
30 31 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
31 32 validates_inclusion_of :status, :in => VERSION_STATUSES
32 33
33 34 named_scope :open, :conditions => {:status => 'open'}
34 35
35 36 def start_date
36 37 effective_date
37 38 end
38 39
39 40 def due_date
40 41 effective_date
41 42 end
42 43
43 44 # Returns the total estimated time for this version
44 45 def estimated_hours
45 46 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
46 47 end
47 48
48 49 # Returns the total reported time for this version
49 50 def spent_hours
50 51 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
51 52 end
52 53
53 54 def closed?
54 55 status == 'closed'
55 56 end
56 57
57 58 # Returns true if the version is completed: due date reached and no open issues
58 59 def completed?
59 60 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
60 61 end
61 62
62 63 # Returns the completion percentage of this version based on the amount of open/closed issues
63 64 # and the time spent on the open issues.
64 65 def completed_pourcent
65 66 if issues_count == 0
66 67 0
67 68 elsif open_issues_count == 0
68 69 100
69 70 else
70 71 issues_progress(false) + issues_progress(true)
71 72 end
72 73 end
73 74
74 75 # Returns the percentage of issues that have been marked as 'closed'.
75 76 def closed_pourcent
76 77 if issues_count == 0
77 78 0
78 79 else
79 80 issues_progress(false)
80 81 end
81 82 end
82 83
83 84 # Returns true if the version is overdue: due date reached and some open issues
84 85 def overdue?
85 86 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
86 87 end
87 88
88 89 # Returns assigned issues count
89 90 def issues_count
90 91 @issue_count ||= fixed_issues.count
91 92 end
92 93
93 94 # Returns the total amount of open issues for this version.
94 95 def open_issues_count
95 96 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
96 97 end
97 98
98 99 # Returns the total amount of closed issues for this version.
99 100 def closed_issues_count
100 101 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
101 102 end
102 103
103 104 def wiki_page
104 105 if project.wiki && !wiki_page_title.blank?
105 106 @wiki_page ||= project.wiki.find_page(wiki_page_title)
106 107 end
107 108 @wiki_page
108 109 end
109 110
110 111 def to_s; name end
111 112
112 113 # Versions are sorted by effective_date and name
113 114 # Those with no effective_date are at the end, sorted by name
114 115 def <=>(version)
115 116 if self.effective_date
116 117 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
117 118 else
118 119 version.effective_date ? 1 : (self.name <=> version.name)
119 120 end
120 121 end
121 122
122 123 private
123 124 def check_integrity
124 125 raise "Can't delete version" if self.fixed_issues.find(:first)
125 126 end
126 127
127 128 # Returns the average estimated time of assigned issues
128 129 # or 1 if no issue has an estimated time
129 130 # Used to weigth unestimated issues in progress calculation
130 131 def estimated_average
131 132 if @estimated_average.nil?
132 133 average = fixed_issues.average(:estimated_hours).to_f
133 134 if average == 0
134 135 average = 1
135 136 end
136 137 @estimated_average = average
137 138 end
138 139 @estimated_average
139 140 end
140 141
141 142 # Returns the total progress of open or closed issues. The returned percentage takes into account
142 143 # the amount of estimated time set for this version.
143 144 #
144 145 # Examples:
145 146 # issues_progress(true) => returns the progress percentage for open issues.
146 147 # issues_progress(false) => returns the progress percentage for closed issues.
147 148 def issues_progress(open)
148 149 @issues_progress ||= {}
149 150 @issues_progress[open] ||= begin
150 151 progress = 0
151 152 if issues_count > 0
152 153 ratio = open ? 'done_ratio' : 100
153 154
154 155 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
155 156 :include => :status,
156 157 :conditions => ["is_closed = ?", !open]).to_f
157 158 progress = done / (estimated_average * issues_count)
158 159 end
159 160 progress
160 161 end
161 162 end
162 163 end
@@ -1,9 +1,13
1 1 <%= error_messages_for 'version' %>
2 2
3 3 <div class="box">
4 4 <p><%= f.text_field :name, :size => 60, :required => true %></p>
5 5 <p><%= f.text_field :description, :size => 60 %></p>
6 6 <p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
7 7 <p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
8 8 <p><%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
9
10 <% @version.custom_field_values.each do |value| %>
11 <p><%= custom_field_tag_with_label :version, value %></p>
12 <% end %>
9 13 </div>
@@ -1,20 +1,27
1 1 <% if version.completed? %>
2 2 <p><%= format_date(version.effective_date) %></p>
3 3 <% elsif version.effective_date %>
4 4 <p><strong><%= due_date_distance_in_words(version.effective_date) %></strong> (<%= format_date(version.effective_date) %>)</p>
5 5 <% end %>
6 6
7 7 <p><%=h version.description %></p>
8 <ul>
9 <% version.custom_values.each do |custom_value| %>
10 <% if !custom_value.value.blank? %>
11 <li><%=h custom_value.custom_field.name %>: <%=h show_value(custom_value) %></li>
12 <% end %>
13 <% end %>
14 </ul>
8 15
9 16 <% if version.fixed_issues.count > 0 %>
10 17 <%= progress_bar([version.closed_pourcent, version.completed_pourcent], :width => '40em', :legend => ('%0.0f%' % version.completed_pourcent)) %>
11 18 <p class="progress-info">
12 19 <%= link_to_if(version.closed_issues_count > 0, l(:label_x_closed_issues_abbr, :count => version.closed_issues_count), :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'c', :fixed_version_id => version, :set_filter => 1) %>
13 20 (<%= '%0.0f' % (version.closed_issues_count.to_f / version.fixed_issues.count * 100) %>%)
14 21 &#160;
15 22 <%= link_to_if(version.open_issues_count > 0, l(:label_x_open_issues_abbr, :count => version.open_issues_count), :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'o', :fixed_version_id => version, :set_filter => 1) %>
16 23 (<%= '%0.0f' % (version.open_issues_count.to_f / version.fixed_issues.count * 100) %>%)
17 24 </p>
18 25 <% else %>
19 26 <p><em><%= l(:label_roadmap_no_issues) %></em></p>
20 27 <% end %>
General Comments 0
You need to be logged in to leave comments. Login now