Index: app/controllers/gantts_controller.rb =================================================================== --- app/controllers/gantts_controller.rb (revision 3735) +++ app/controllers/gantts_controller.rb (working copy) @@ -23,7 +23,7 @@ :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] ) # Issues that don't have a due date but that are assigned to a version with a date - events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version], + events += @query.issues(:include => [:tracker, :assigned_to, :priority, :versions], :order => "start_date, effective_date", :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] ) Index: app/controllers/issue_versions_controller.rb =================================================================== --- app/controllers/issue_versions_controller.rb (revision 0) +++ app/controllers/issue_versions_controller.rb (revision 0) @@ -0,0 +1,53 @@ +# 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 IssueVersionsController < ApplicationController + before_filter :find_issue, :find_project_from_association, :authorize + helper :projects + + def new + @version = Version.find(params[:id]) + @issue.versions << @version if request.post? + respond_to do |format| + format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } + format.js do + render :update do |page| + page.replace_html "versions", :partial => 'issues/versions' + end + end + end + end + + def destroy + @version = Version.find(params[:id]) + if request.post? && @issue.versions.include?(@version) + @issue.versions.delete(@version) + @issue.reload + end + respond_to do |format| + format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } + format.js { render(:update) {|page| page.replace_html "versions", :partial => 'issues/versions'} } + end + end + +private + def find_issue + @issue = @object = Issue.find(params[:issue_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end Index: app/controllers/issues_controller.rb =================================================================== --- app/controllers/issues_controller.rb (revision 3735) +++ app/controllers/issues_controller.rb (working copy) @@ -73,7 +73,7 @@ @issue_count = @query.issue_count @issue_pages = Paginator.new self, @issue_count, limit, params['page'] - @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version], + @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :versions], :order => sort_clause, :offset => @issue_pages.current.offset, :limit => limit) @@ -158,7 +158,7 @@ # Attributes that can be updated on workflow transition (without :edit permission) # TODO: make it configurable (at least per role) - UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION) + UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION) def edit update_issue_from_params @@ -229,6 +229,13 @@ @issues.each do |issue| issue.reload journal = issue.init_journal(User.current, params[:notes]) + if attributes.include?(:version_id) + if attributes[:version_id].blank? + issue.versions.clear + else + issue.versions << Version.find(attributes[:version_id]) + end + end issue.safe_attributes = attributes call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) unless issue.save Index: app/controllers/projects_controller.rb =================================================================== --- app/controllers/projects_controller.rb (revision 3735) +++ app/controllers/projects_controller.rb (working copy) @@ -302,9 +302,9 @@ unless @selected_tracker_ids.empty? @versions.each do |version| issues = version.fixed_issues.visible.find(:all, - :include => [:project, :status, :tracker, :priority], - :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids}, - :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id") + :include => [:project, :status, :tracker, :priority], + :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids}, + :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id") @issues_by_version[version] = issues end end Index: app/controllers/reports_controller.rb =================================================================== --- app/controllers/reports_controller.rb (revision 3735) +++ app/controllers/reports_controller.rb (working copy) @@ -47,7 +47,7 @@ @data = Issue.by_tracker(@project) @report_title = l(:field_tracker) when "version" - @field = "fixed_version_id" + @field = "version_id" @rows = @project.shared_versions.sort @data = Issue.by_version(@project) @report_title = l(:field_version) Index: app/controllers/timelog_controller.rb =================================================================== --- app/controllers/timelog_controller.rb (revision 3735) +++ app/controllers/timelog_controller.rb (working copy) @@ -33,7 +33,7 @@ @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id", :klass => Project, :label => :label_project}, - 'version' => {:sql => "#{Issue.table_name}.fixed_version_id", + 'version' => {:sql => "#{Version.table_name}.id", :klass => Version, :label => :label_version}, 'category' => {:sql => "#{Issue.table_name}.category_id", @@ -101,6 +101,15 @@ sql << " FROM #{TimeEntry.table_name}" sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id" sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id" + # Note - If an issue is in multiple versions and if the user asks to group times + # by version, then that issue's time will be included for each version. So if the + # issue is in 3 versions, then its time will be counted 3 times over, once per + # version. That is intentional for the breakdown by versions, but means the + # total time will be too high. + if @criterias.include?('version') + sql << " LEFT JOIN issues_versions ON #{Issue.table_name}.id = issues_versions.issue_id" + sql << " LEFT JOIN #{Version.table_name} ON issues_versions.version_id = #{Version.table_name}.id" + end sql << " WHERE" sql << " (%s) AND" % sql_condition sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)] Index: app/controllers/versions_controller.rb =================================================================== --- app/controllers/versions_controller.rb (revision 3735) +++ app/controllers/versions_controller.rb (working copy) @@ -43,12 +43,6 @@ flash[:notice] = l(:notice_successful_create) redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project end - format.js do - # IE doesn't support the replace_html rjs method for select box options - render(:update) {|page| page.replace "issue_fixed_version_id", - content_tag('select', '' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]') - } - end end else respond_to do |format| Index: app/helpers/issues_helper.rb =================================================================== --- app/helpers/issues_helper.rb (revision 3735) +++ app/helpers/issues_helper.rb (working copy) @@ -110,7 +110,7 @@ value = format_date(detail.value.to_date) if detail.value old_value = format_date(detail.old_value.to_date) if detail.old_value - when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key) + when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'version_id'].include?(detail.prop_key) value = find_name_by_reflection(field, detail.value) old_value = find_name_by_reflection(field, detail.old_value) @@ -189,7 +189,7 @@ l(:field_subject), l(:field_assigned_to), l(:field_category), - l(:field_fixed_version), + l(:field_version), l(:field_author), l(:field_start_date), l(:field_due_date), @@ -216,7 +216,7 @@ issue.subject, issue.assigned_to, issue.category, - issue.fixed_version, + issue.versions.map {|v| v.id}.join("|"), issue.author.name, format_date(issue.start_date), format_date(issue.due_date), Index: app/helpers/versions_helper.rb =================================================================== --- app/helpers/versions_helper.rb (revision 3735) +++ app/helpers/versions_helper.rb (working copy) @@ -27,7 +27,8 @@ begin # Total issue count Issue.count(:group => criteria, - :conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s} + :include => :versions, + :conditions => ["#{Version.table_name}.id = ?", version.id]).each {|c,s| h[c][0] = s} # Open issues count Issue.count(:group => criteria, :include => :status, Index: app/models/issue.rb =================================================================== --- app/models/issue.rb (revision 3735) +++ app/models/issue.rb (working copy) @@ -21,7 +21,7 @@ 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' + has_and_belongs_to_many :versions belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id' belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' @@ -121,9 +121,11 @@ # reassign to the category with same name if any new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name) issue.category = new_category - # Keep the fixed_version if it's still valid in the new_project - unless new_project.shared_versions.include?(issue.fixed_version) - issue.fixed_version = nil + # Keep the versions that are still valid in the new_project + issue.versions.each do |version| + unless new_project.shared_versions.include?(version) + issue.versions.delete(version) + end end issue.project = new_project if issue.parent && issue.parent.project_id != issue.project_id @@ -204,7 +206,6 @@ category_id assigned_to_id priority_id - fixed_version_id subject description start_date @@ -273,14 +274,6 @@ errors.add :start_date, :invalid end - if fixed_version - if !assignable_versions.include?(fixed_version) - errors.add :fixed_version_id, :inclusion - elsif reopened? && fixed_version.closed? - errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version) - end - end - # Checks that the issue can not be added/moved to a disabled tracker if project && (tracker_id_changed? || project_id_changed?) unless project.trackers.include?(tracker) @@ -365,7 +358,7 @@ # Versions that the issue can be assigned to def assignable_versions - @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort + @assignable_versions ||= (project.shared_versions.open).compact.uniq.sort end # Returns true if this issue is blocked by another issue that is still open @@ -424,7 +417,7 @@ # Returns the due date or the target due date if any # Used on gantt chart def due_before - due_date || (fixed_version ? fixed_version.effective_date : nil) + due_date || (versions.empty? ? nil : versions.map {|version| version.effective_date}.min) end # Returns the time scheduled for this issue. @@ -520,7 +513,7 @@ # Unassigns issues from +version+ if it's no longer shared with issue's project def self.update_versions_from_sharing_change(version) # Update issues assigned to the version - update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) + update_versions(["#{Version.table_name}.id = ?", version.id]) end # Unassigns issues from versions that are no longer shared @@ -557,11 +550,22 @@ end def self.by_version(project) - count_and_group_by(:project => project, - :field => 'fixed_version_id', - :joins => Version.table_name) + joins = "issues_versions, #{Version.table_name}" + where = "i.id = issues_versions.issue_id and issues_versions.version_id = j.id" + ActiveRecord::Base.connection.select_all("select + s.id as status_id, + s.is_closed as closed, + j.id as version_id, + count(i.id) as total + from + #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j + where + i.status_id=s.id + and #{where} + and i.project_id=#{project.id} + group by s.id, s.is_closed, j.id") end - + def self.by_priority(project) count_and_group_by(:project => project, :field => 'priority_id', @@ -708,21 +712,22 @@ end # Update issues so their versions are not pointing to a - # fixed_version that is not shared with the issue's project + # version that is not shared with the issue's project def self.update_versions(conditions=nil) - # Only need to update issues with a fixed_version from + # Only need to update issues with a fversion from # a different project and that is not systemwide shared - Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" + - " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" + - " AND #{Version.table_name}.sharing <> 'system'", + Issue.all(:conditions => merge_conditions("#{Issue.table_name}.project_id <> #{Version.table_name}.project_id" + + " AND #{Version.table_name}.sharing <> 'system'", conditions), - :include => [:project, :fixed_version] + :include => [:project, :versions] ).each do |issue| - next if issue.project.nil? || issue.fixed_version.nil? - unless issue.project.shared_versions.include?(issue.fixed_version) - issue.init_journal(User.current) - issue.fixed_version = nil - issue.save + next if issue.project.nil? + issue.versions.each do |version| + unless issue.project.shared_versions.include?(version) + issue.init_journal(User.current) + issue.versions.delete(version) + issue.save + end end end end Index: app/models/project.rb =================================================================== --- app/models/project.rb (revision 3735) +++ app/models/project.rb (working copy) @@ -225,9 +225,9 @@ # Check that there is no issue of a non descendant project that is assigned # to one of the project or descendant versions v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten - if v_ids.any? && Issue.find(:first, :include => :project, + if v_ids.any? && Issue.find(:first, :include => [:project, :versions], :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" + - " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids]) + " AND #{Version.table_name}.id IN (?)", lft, rgt, v_ids]) return false end Project.transaction do @@ -563,10 +563,12 @@ new_issue = Issue.new new_issue.copy_from(issue) new_issue.project = self - # Reassign fixed_versions by name, since names are unique per + # Reassign versions by name, since names are unique per # project and the versions for self are not yet saved - if issue.fixed_version - new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first + issue.versions.each do |issue_version| + self.versions.select {|v| v.name == issue_version.name}.each do |version| + new_issue.versions << version + end end # Reassign the category by name, since names are unique per # project and the categories for self are not yet saved Index: app/models/query.rb =================================================================== --- app/models/query.rb (revision 3735) +++ app/models/query.rb (working copy) @@ -129,7 +129,7 @@ QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true), QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), - QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), + QueryColumn.new(:versions, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), 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"), @@ -206,7 +206,7 @@ @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } end unless @project.shared_versions.empty? - @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } + @available_filters["version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } end unless @project.descendants.active.empty? @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } } @@ -216,7 +216,7 @@ # global filters for cross project issue list system_shared_versions = Version.visible.find_all_by_sharing('system') unless system_shared_versions.empty? - @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } + @available_filters["version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } end add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) end @@ -426,6 +426,11 @@ db_field = 'user_id' sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " sql << sql_for_field(field, '=', v, db_table, db_field) + ')' + elsif field == 'version_id' + db_table = 'issues_versions' + db_field = 'version_id' + sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.issue_id FROM #{db_table} WHERE " + sql << sql_for_field(field, '=', v, db_table, db_field) + ')' else # regular field db_table = Issue.table_name Index: app/models/version.rb =================================================================== --- app/models/version.rb (revision 3735) +++ app/models/version.rb (working copy) @@ -18,7 +18,7 @@ class Version < ActiveRecord::Base after_update :update_issues_from_sharing_change belongs_to :project - has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify + has_and_belongs_to_many :fixed_issues, :class_name => 'Issue', :join_table => 'issues_versions' acts_as_customizable acts_as_attachable :view_permission => :view_files, :delete_permission => :manage_files @@ -58,7 +58,8 @@ # Returns the total reported time for this version def spent_hours - @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f + @spent_hours ||= TimeEntry.sum(:hours, :include => {:issue => :versions}, + :conditions => ["#{Version.table_name}.id = ?", id]).to_f end def closed? @@ -107,12 +108,14 @@ # Returns the total amount of open issues for this version. def open_issues_count - @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status) + @open_issues_count ||= Issue.count(:all, :conditions => ["#{Version.table_name}.id = ? AND is_closed = ?", self.id, false], + :include => [:status, :versions]) end # Returns the total amount of closed issues for this version. def closed_issues_count - @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status) + @closed_issues_count ||= Issue.count(:all, :conditions => ["#{Version.table_name}.id = ? AND is_closed = ?", self.id, true], + :include => [:status, :versions]) end def wiki_page @@ -170,7 +173,7 @@ # Returns the average estimated time of assigned issues # or 1 if no issue has an estimated time - # Used to weigth unestimated issues in progress calculation + # Used to weight unestimated issues in progress calculation def estimated_average if @estimated_average.nil? average = fixed_issues.average(:estimated_hours).to_f Index: app/views/issue_versions/_form.rhtml =================================================================== --- app/views/issue_versions/_form.rhtml (revision 0) +++ app/views/issue_versions/_form.rhtml (revision 0) @@ -0,0 +1,7 @@ +<%= error_messages_for 'version' %> + +

+ <%= select_tag :id, version_options_for_select(@issue.assignable_versions ), :include_blank => false %> + <%= submit_tag l(:button_add) %> + <%= toggle_link l(:button_cancel), 'new-version-form'%> +

\ No newline at end of file Index: app/views/issues/_attributes.rhtml =================================================================== --- app/views/issues/_attributes.rhtml (revision 3735) +++ app/views/issues/_attributes.rhtml (working copy) @@ -18,16 +18,6 @@ :title => l(:label_issue_category_new), :tabindex => 199) if authorize_for('issue_categories', 'new') %>

<% end %> -<% unless @issue.assignable_versions.empty? %> -

<%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %> -<%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'), - l(:label_version_new), - 'version[name]', - {:controller => 'versions', :action => 'new', :project_id => @project}, - :title => l(:label_version_new), - :tabindex => 200) if authorize_for('versions', 'new') %> -

-<% end %>
Index: app/views/issues/_form_update.rhtml =================================================================== --- app/views/issues/_form_update.rhtml (revision 3735) +++ app/views/issues/_form_update.rhtml (working copy) @@ -7,8 +7,5 @@ <% if Issue.use_field_for_done_ratio? %>

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>

<% end %> -<% unless @issue.assignable_versions.empty? %> -

<%= f.select :fixed_version_id, (@issue.assignable_versions.collect {|v| [v.name, v.id]}), :include_blank => true %>

-<% end %>
Index: app/views/issues/_versions.rhtml =================================================================== --- app/views/issues/_versions.rhtml (revision 0) +++ app/views/issues/_versions.rhtml (revision 0) @@ -0,0 +1,28 @@ +
+ <% if authorize_for('issue_versions', 'new') %> + <%= toggle_link l(:button_add), 'new-version-form'%> + <% end %> +
+ +

<%=l(:label_version_plural)%>

+ +<% if @issue.versions.any? %> + +<% @issue.versions.each do |version| %> + + + + + +<% end %> +
<%= link_to_version(version, :truncate => 60) %><%= format_date(version.effective_date) %><%= link_to_remote(image_tag('delete.png'), { :url => {:controller => 'issue_versions', :action => 'destroy', :issue_id => @issue, :id => version}, + :method => :post + }, :title => l(:label_version_delete)) %>
+<% end %> + +<% remote_form_for(:version, @version, + :url => {:controller => 'issue_versions', :action => 'new', :issue_id => @issue}, + :method => :post, + :html => {:id => 'new-version-form', :style => 'display: none;'}) do |f| %> + <%= render :partial => 'issue_versions/form', :locals => {:f => f}%> +<% end %> Index: app/views/issues/bulk_edit.rhtml =================================================================== --- app/views/issues/bulk_edit.rhtml (revision 3735) +++ app/views/issues/bulk_edit.rhtml (working copy) @@ -36,8 +36,8 @@ options_from_collection_for_select(@project.issue_categories, :id, :name)) %>

- - <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') + + + <%= select_tag('issue[version_id]', content_tag('option', l(:label_no_change_option), :value => '') + content_tag('option', l(:label_none), :value => 'none') + version_options_for_select(@project.shared_versions.open)) %>

Index: app/views/issues/context_menu.rhtml =================================================================== --- app/views/issues/context_menu.rhtml (revision 3735) +++ app/views/issues/context_menu.rhtml (working copy) @@ -40,14 +40,14 @@ <% unless @project.nil? || @project.shared_versions.open.empty? -%>
  • - <%= l(:field_fixed_version) %> + <%= l(:label_version_plural) %>
  • <% end %> Index: app/views/issues/index.xml.builder =================================================================== --- app/views/issues/index.xml.builder (revision 3735) +++ app/views/issues/index.xml.builder (working copy) @@ -10,7 +10,11 @@ xml.author(:id => issue.author_id, :name => issue.author.name) unless issue.author.nil? xml.assigned_to(:id => issue.assigned_to_id, :name => issue.assigned_to.name) unless issue.assigned_to.nil? xml.category(:id => issue.category_id, :name => issue.category.name) unless issue.category.nil? - xml.fixed_version(:id => issue.fixed_version_id, :name => issue.fixed_version.name) unless issue.fixed_version.nil? + xml.versions do + issue.versions.each do |version| + xml.version(:id => version.id, :name => version.name) + end + end xml.parent(:id => issue.parent_id) unless issue.parent.nil? xml.subject issue.subject Index: app/views/issues/show.rhtml =================================================================== --- app/views/issues/show.rhtml (revision 3735) +++ app/views/issues/show.rhtml (working copy) @@ -35,21 +35,20 @@ <%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %> <% end %> - - <%=l(:field_fixed_version)%>:<%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %> - <% if @issue.estimated_hours %> - <%=l(:field_estimated_hours)%>:<%= l_hours(@issue.estimated_hours) %> - <% end %> - <%= render_custom_fields_rows(@issue) %> <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
    +
    + <%= render :partial => 'issues/versions' %> +
    +
    +
    <%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %>
    - +

    <%=l(:field_description)%>

    <%= textilizable @issue, :description, :attachments => @issue.attachments %> Index: app/views/issues/show.xml.builder =================================================================== --- app/views/issues/show.xml.builder (revision 3735) +++ app/views/issues/show.xml.builder (working copy) @@ -8,7 +8,9 @@ xml.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil? xml.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil? xml.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil? - xml.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil? + xml.versions each do |version| + xml.version(:id => version.id, :name => version.name) + end xml.parent(:id => @issue.parent_id) unless @issue.parent.nil? xml.subject @issue.subject Index: app/views/mailer/_issue_text_html.rhtml =================================================================== --- app/views/mailer/_issue_text_html.rhtml (revision 3735) +++ app/views/mailer/_issue_text_html.rhtml (working copy) @@ -6,7 +6,7 @@
  • <%=l(:field_priority)%>: <%=h issue.priority %>
  • <%=l(:field_assigned_to)%>: <%=h issue.assigned_to %>
  • <%=l(:field_category)%>: <%=h issue.category %>
  • -
  • <%=l(:field_fixed_version)%>: <%=h issue.fixed_version %>
  • +
  • <%=l(:field_version)%>: <%=h issue.versions.map {|version| version.name}.join(", ") %>
  • <% issue.custom_values.each do |c| %>
  • <%=h c.custom_field.name %>: <%=h show_value(c) %>
  • <% end %> Index: app/views/mailer/_issue_text_plain.rhtml =================================================================== --- app/views/mailer/_issue_text_plain.rhtml (revision 3735) +++ app/views/mailer/_issue_text_plain.rhtml (working copy) @@ -6,7 +6,7 @@ <%=l(:field_priority)%>: <%= issue.priority %> <%=l(:field_assigned_to)%>: <%= issue.assigned_to %> <%=l(:field_category)%>: <%= issue.category %> -<%=l(:field_fixed_version)%>: <%= issue.fixed_version %> +<%=l(:field_version)%>: <%= issue.versions.map {|version| version.name}.join(", ") %> <% issue.custom_values.each do |c| %><%= c.custom_field.name %>: <%= show_value(c) %> <% end %> Index: app/views/reports/issue_report.rhtml =================================================================== --- app/views/reports/issue_report.rhtml (revision 3735) +++ app/views/reports/issue_report.rhtml (working copy) @@ -17,7 +17,7 @@

    <%=l(:field_version)%>  <%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'version' %>

    -<%= render :partial => 'simple', :locals => { :data => @issues_by_version, :field_name => "fixed_version_id", :rows => @versions } %> +<%= render :partial => 'simple', :locals => { :data => @issues_by_version, :field_name => "version_id", :rows => @versions } %>
    <% if @project.children.any? %>

    <%=l(:field_subproject)%>  <%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'subproject' %>

    Index: app/views/versions/_issue_counts.rhtml =================================================================== --- app/views/versions/_issue_counts.rhtml (revision 3735) +++ app/views/versions/_issue_counts.rhtml (working copy) @@ -19,7 +19,7 @@ :action => 'index', :project_id => version.project, :set_filter => 1, - :fixed_version_id => version, + :version_id => version, "#{criteria}_id" => count[:group]} %> Index: app/views/versions/_overview.rhtml =================================================================== --- app/views/versions/_overview.rhtml (revision 3735) +++ app/views/versions/_overview.rhtml (working copy) @@ -16,10 +16,10 @@ <% if version.fixed_issues.count > 0 %> <%= progress_bar([version.closed_pourcent, version.completed_pourcent], :width => '40em', :legend => ('%0.0f%' % version.completed_pourcent)) %>

    - <%= 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) %> + <%= 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', :version_id => version, :set_filter => 1) %> (<%= '%0.0f' % (version.closed_issues_count.to_f / version.fixed_issues.count * 100) %>%)   - <%= 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) %> + <%= 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', :version_id => version, :set_filter => 1) %> (<%= '%0.0f' % (version.open_issues_count.to_f / version.fixed_issues.count * 100) %>%)

    <% else %> Index: config/locales/en.yml =================================================================== --- config/locales/en.yml (revision 3735) +++ config/locales/en.yml (working copy) @@ -240,7 +240,7 @@ field_password: Password field_new_password: New password field_password_confirmation: Confirmation - field_version: Version + field_version: Versions field_type: Type field_host: Host field_port: Port Index: db/migrate/20100511054420_issue_versions_table.rb =================================================================== --- db/migrate/20100511054420_issue_versions_table.rb (revision 0) +++ db/migrate/20100511054420_issue_versions_table.rb (revision 0) @@ -0,0 +1,31 @@ +class IssueVersionsTable < ActiveRecord::Migration + def self.up + execute <<-EOS + CREATE TABLE issues_versions + ( + issue_id integer NOT NULL, + version_id integer NOT NULL, + PRIMARY KEY (issue_id, version_id) + ); + + CREATE INDEX issues_versions_on_version_id + ON issues_versions USING btree (version_id); + EOS + + Version.find(:all).each do |version| + puts "Migrating version #{version.name}" + Issue.find(:all, :conditions => {:fixed_version_id => version.id}).each do |issue| + version.fixed_issues << issue + end + end + + # Rename fixed_version_id for now - so we keep the data but make sure + # that it isn't used + rename_column :issues, :fixed_version_id, :fixed_version_id_old + end + + def self.down + drop_table :version_issues + rename_column :issues, :fixed_version_id_old, :fixed_version_id + end +end Index: lib/redmine.rb =================================================================== --- lib/redmine.rb (revision 3735) +++ lib/redmine.rb (working copy) @@ -81,6 +81,10 @@ map.permission :view_issue_watchers, {} map.permission :add_issue_watchers, {:watchers => :new} map.permission :delete_issue_watchers, {:watchers => :destroy} + # Versions + map.permission :view_issue_versions, {} + map.permission :add_issue_versions, {:issue_versions => :new} + map.permission :delete_issue_versions, {:issue_versions => :destroy} end map.project_module :time_tracking do |map|