Project

General

Profile

Patch #5510 » versions.patch

Charlie Savage, 2010-05-12 08:14

View differences:

app/controllers/gantts_controller.rb (working copy)
23 23
                              :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_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]
24 24
                              )
25 25
      # Issues that don't have a due date but that are assigned to a version with a date
26
      events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
26
      events += @query.issues(:include => [:tracker, :assigned_to, :priority, :versions],
27 27
                              :order => "start_date, effective_date",
28 28
                              :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_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]
29 29
                              )
app/controllers/issue_versions_controller.rb (revision 0)
1
# redMine - project management software
2
# Copyright (C) 2006-2007  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 IssueVersionsController < ApplicationController
19
  before_filter :find_issue, :find_project_from_association, :authorize
20
  helper :projects
21
  
22
  def new
23
    @version = Version.find(params[:id])
24
    @issue.versions << @version if request.post?
25
    respond_to do |format|
26
      format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
27
      format.js do
28
        render :update do |page|
29
          page.replace_html "versions", :partial => 'issues/versions'
30
        end
31
      end
32
    end
33
  end
34
  
35
  def destroy
36
    @version = Version.find(params[:id])
37
    if request.post? && @issue.versions.include?(@version)
38
      @issue.versions.delete(@version)
39
      @issue.reload
40
    end
41
    respond_to do |format|
42
      format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
43
      format.js { render(:update) {|page| page.replace_html "versions", :partial => 'issues/versions'} }
44
    end
45
  end
46
  
47
private
48
  def find_issue
49
    @issue = @object = Issue.find(params[:issue_id])
50
  rescue ActiveRecord::RecordNotFound
51
    render_404
52
  end
53
end
app/controllers/issues_controller.rb (working copy)
73 73
      
74 74
      @issue_count = @query.issue_count
75 75
      @issue_pages = Paginator.new self, @issue_count, limit, params['page']
76
      @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
76
      @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :versions],
77 77
                              :order => sort_clause, 
78 78
                              :offset => @issue_pages.current.offset, 
79 79
                              :limit => limit)
......
158 158
  
159 159
  # Attributes that can be updated on workflow transition (without :edit permission)
160 160
  # TODO: make it configurable (at least per role)
161
  UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
161
  UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
162 162
  
163 163
  def edit
164 164
    update_issue_from_params
......
229 229
      @issues.each do |issue|
230 230
        issue.reload
231 231
        journal = issue.init_journal(User.current, params[:notes])
232
        if attributes.include?(:version_id)
233
          if attributes[:version_id].blank?
234
            issue.versions.clear
235
          else
236
            issue.versions << Version.find(attributes[:version_id])
237
          end
238
        end
232 239
        issue.safe_attributes = attributes
233 240
        call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
234 241
        unless issue.save
app/controllers/projects_controller.rb (working copy)
302 302
    unless @selected_tracker_ids.empty?
303 303
      @versions.each do |version|
304 304
        issues = version.fixed_issues.visible.find(:all,
305
                                                   :include => [:project, :status, :tracker, :priority],
306
                                                   :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
307
                                                   :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
305
                                             :include => [:project, :status, :tracker, :priority],
306
                                             :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
307
                                             :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
308 308
        @issues_by_version[version] = issues
309 309
      end
310 310
    end
app/controllers/reports_controller.rb (working copy)
47 47
      @data = Issue.by_tracker(@project)
48 48
      @report_title = l(:field_tracker)
49 49
    when "version"
50
      @field = "fixed_version_id"
50
      @field = "version_id"
51 51
      @rows = @project.shared_versions.sort
52 52
      @data = Issue.by_version(@project)
53 53
      @report_title = l(:field_version)
app/controllers/timelog_controller.rb (working copy)
33 33
    @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
34 34
                                          :klass => Project,
35 35
                                          :label => :label_project},
36
                             'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
36
                             'version' => {:sql => "#{Version.table_name}.id",
37 37
                                          :klass => Version,
38 38
                                          :label => :label_version},
39 39
                             'category' => {:sql => "#{Issue.table_name}.category_id",
......
101 101
      sql << " FROM #{TimeEntry.table_name}"
102 102
      sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
103 103
      sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
104
      # Note - If an issue is in multiple versions and if the user asks to group times
105
      # by version, then that issue's time will be included for each version.  So if the
106
      # issue is in 3 versions, then its time will be counted 3 times over, once per
107
      # version.  That is intentional for the breakdown by versions, but means the
108
      # total time will be too high.
109
      if @criterias.include?('version')
110
        sql << " LEFT JOIN issues_versions ON #{Issue.table_name}.id = issues_versions.issue_id"
111
        sql << " LEFT JOIN #{Version.table_name} ON issues_versions.version_id = #{Version.table_name}.id"
112
      end
104 113
      sql << " WHERE"
105 114
      sql << " (%s) AND" % sql_condition
106 115
      sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
app/controllers/versions_controller.rb (working copy)
43 43
            flash[:notice] = l(:notice_successful_create)
44 44
            redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
45 45
          end
46
          format.js do
47
            # IE doesn't support the replace_html rjs method for select box options
48
            render(:update) {|page| page.replace "issue_fixed_version_id",
49
              content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
50
            }
51
          end
52 46
        end
53 47
      else
54 48
        respond_to do |format|
app/helpers/issues_helper.rb (working copy)
110 110
        value = format_date(detail.value.to_date) if detail.value
111 111
        old_value = format_date(detail.old_value.to_date) if detail.old_value
112 112

  
113
      when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key)
113
      when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'version_id'].include?(detail.prop_key)
114 114
        value = find_name_by_reflection(field, detail.value)
115 115
        old_value = find_name_by_reflection(field, detail.old_value)
116 116

  
......
189 189
                  l(:field_subject),
190 190
                  l(:field_assigned_to),
191 191
                  l(:field_category),
192
                  l(:field_fixed_version),
192
                  l(:field_version),
193 193
                  l(:field_author),
194 194
                  l(:field_start_date),
195 195
                  l(:field_due_date),
......
216 216
                  issue.subject,
217 217
                  issue.assigned_to,
218 218
                  issue.category,
219
                  issue.fixed_version,
219
                  issue.versions.map {|v| v.id}.join("|"),
220 220
                  issue.author.name,
221 221
                  format_date(issue.start_date),
222 222
                  format_date(issue.due_date),
app/helpers/versions_helper.rb (working copy)
27 27
    begin
28 28
      # Total issue count
29 29
      Issue.count(:group => criteria,
30
                  :conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s}
30
                  :include => :versions,
31
                  :conditions => ["#{Version.table_name}.id = ?", version.id]).each {|c,s| h[c][0] = s}
31 32
      # Open issues count
32 33
      Issue.count(:group => criteria,
33 34
                  :include => :status,
app/models/issue.rb (working copy)
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
  belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24
  has_and_belongs_to_many :versions
25 25
  belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 26
  belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27

  
......
121 121
      # reassign to the category with same name if any
122 122
      new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
123 123
      issue.category = new_category
124
      # Keep the fixed_version if it's still valid in the new_project
125
      unless new_project.shared_versions.include?(issue.fixed_version)
126
        issue.fixed_version = nil
124
      # Keep the versions that are still valid in the new_project
125
      issue.versions.each do |version|
126
        unless new_project.shared_versions.include?(version)
127
          issue.versions.delete(version)
128
        end
127 129
      end
128 130
      issue.project = new_project
129 131
      if issue.parent && issue.parent.project_id != issue.project_id
......
204 206
    category_id
205 207
    assigned_to_id
206 208
    priority_id
207
    fixed_version_id
208 209
    subject
209 210
    description
210 211
    start_date
......
273 274
      errors.add :start_date, :invalid
274 275
    end
275 276
    
276
    if fixed_version
277
      if !assignable_versions.include?(fixed_version)
278
        errors.add :fixed_version_id, :inclusion
279
      elsif reopened? && fixed_version.closed?
280
        errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
281
      end
282
    end
283
    
284 277
    # Checks that the issue can not be added/moved to a disabled tracker
285 278
    if project && (tracker_id_changed? || project_id_changed?)
286 279
      unless project.trackers.include?(tracker)
......
365 358
  
366 359
  # Versions that the issue can be assigned to
367 360
  def assignable_versions
368
    @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
361
    @assignable_versions ||= (project.shared_versions.open).compact.uniq.sort
369 362
  end
370 363
  
371 364
  # Returns true if this issue is blocked by another issue that is still open
......
424 417
  # Returns the due date or the target due date if any
425 418
  # Used on gantt chart
426 419
  def due_before
427
    due_date || (fixed_version ? fixed_version.effective_date : nil)
420
    due_date || (versions.empty? ? nil : versions.map {|version| version.effective_date}.min)
428 421
  end
429 422
  
430 423
  # Returns the time scheduled for this issue.
......
520 513
  # Unassigns issues from +version+ if it's no longer shared with issue's project
521 514
  def self.update_versions_from_sharing_change(version)
522 515
    # Update issues assigned to the version
523
    update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
516
    update_versions(["#{Version.table_name}.id = ?", version.id])
524 517
  end
525 518
  
526 519
  # Unassigns issues from versions that are no longer shared
......
557 550
  end
558 551

  
559 552
  def self.by_version(project)
560
    count_and_group_by(:project => project,
561
                       :field => 'fixed_version_id',
562
                       :joins => Version.table_name)
553
    joins = "issues_versions,  #{Version.table_name}"
554
    where = "i.id = issues_versions.issue_id and issues_versions.version_id = j.id"
555
    ActiveRecord::Base.connection.select_all("select
556
                                                s.id as status_id,
557
                                                s.is_closed as closed,
558
                                                j.id as version_id,
559
                                                count(i.id) as total
560
                                              from
561
                                                #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
562
                                              where
563
                                                i.status_id=s.id
564
                                                and #{where}
565
                                                and i.project_id=#{project.id}
566
                                              group by s.id, s.is_closed, j.id")
563 567
  end
564

  
568
  
565 569
  def self.by_priority(project)
566 570
    count_and_group_by(:project => project,
567 571
                       :field => 'priority_id',
......
708 712
  end
709 713
  
710 714
  # Update issues so their versions are not pointing to a
711
  # fixed_version that is not shared with the issue's project
715
  # version that is not shared with the issue's project
712 716
  def self.update_versions(conditions=nil)
713
    # Only need to update issues with a fixed_version from
717
    # Only need to update issues with a fversion from
714 718
    # a different project and that is not systemwide shared
715
    Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
716
                                                " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
717
                                                " AND #{Version.table_name}.sharing <> 'system'",
719
    Issue.all(:conditions => merge_conditions("#{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
720
                                              " AND #{Version.table_name}.sharing <> 'system'",
718 721
                                                conditions),
719
              :include => [:project, :fixed_version]
722
              :include => [:project, :versions]
720 723
              ).each do |issue|
721
      next if issue.project.nil? || issue.fixed_version.nil?
722
      unless issue.project.shared_versions.include?(issue.fixed_version)
723
        issue.init_journal(User.current)
724
        issue.fixed_version = nil
725
        issue.save
724
      next if issue.project.nil?
725
      issue.versions.each do |version|
726
        unless issue.project.shared_versions.include?(version)
727
          issue.init_journal(User.current)
728
          issue.versions.delete(version)
729
          issue.save
730
        end
726 731
      end
727 732
    end
728 733
  end
app/models/project.rb (working copy)
225 225
    # Check that there is no issue of a non descendant project that is assigned
226 226
    # to one of the project or descendant versions
227 227
    v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228
    if v_ids.any? && Issue.find(:first, :include => :project,
228
    if v_ids.any? && Issue.find(:first, :include => [:project, :versions],
229 229
                                        :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230
                                                        " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
230
                                                        " AND #{Version.table_name}.id IN (?)", lft, rgt, v_ids])
231 231
      return false
232 232
    end
233 233
    Project.transaction do
......
563 563
      new_issue = Issue.new
564 564
      new_issue.copy_from(issue)
565 565
      new_issue.project = self
566
      # Reassign fixed_versions by name, since names are unique per
566
      # Reassign versions by name, since names are unique per
567 567
      # project and the versions for self are not yet saved
568
      if issue.fixed_version
569
        new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
568
      issue.versions.each do |issue_version|
569
        self.versions.select {|v| v.name == issue_version.name}.each do |version|
570
          new_issue.versions << version
571
        end
570 572
      end
571 573
      # Reassign the category by name, since names are unique per
572 574
      # project and the categories for self are not yet saved
app/models/query.rb (working copy)
129 129
    QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
130 130
    QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
131 131
    QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
132
    QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
132
    QueryColumn.new(:versions, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
133 133
    QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
134 134
    QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
135 135
    QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
......
206 206
        @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
207 207
      end
208 208
      unless @project.shared_versions.empty?
209
        @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] } }
209
        @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] } }
210 210
      end
211 211
      unless @project.descendants.active.empty?
212 212
        @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
......
216 216
      # global filters for cross project issue list
217 217
      system_shared_versions = Version.visible.find_all_by_sharing('system')
218 218
      unless system_shared_versions.empty?
219
        @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] } }
219
        @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] } }
220 220
      end
221 221
      add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
222 222
    end
......
426 426
        db_field = 'user_id'
427 427
        sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
428 428
        sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
429
      elsif field == 'version_id'
430
        db_table = 'issues_versions'
431
        db_field = 'version_id'
432
        sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.issue_id FROM #{db_table} WHERE "
433
        sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
429 434
      else
430 435
        # regular field
431 436
        db_table = Issue.table_name
app/models/version.rb (working copy)
18 18
class Version < ActiveRecord::Base
19 19
  after_update :update_issues_from_sharing_change
20 20
  belongs_to :project
21
  has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
21
  has_and_belongs_to_many :fixed_issues, :class_name => 'Issue', :join_table => 'issues_versions'
22 22
  acts_as_customizable
23 23
  acts_as_attachable :view_permission => :view_files,
24 24
                     :delete_permission => :manage_files
......
58 58
  
59 59
  # Returns the total reported time for this version
60 60
  def spent_hours
61
    @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
61
    @spent_hours ||= TimeEntry.sum(:hours, :include => {:issue => :versions},
62
                                   :conditions => ["#{Version.table_name}.id = ?", id]).to_f
62 63
  end
63 64
  
64 65
  def closed?
......
107 108
  
108 109
  # Returns the total amount of open issues for this version.
109 110
  def open_issues_count
110
    @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
111
    @open_issues_count ||= Issue.count(:all, :conditions => ["#{Version.table_name}.id = ? AND is_closed = ?", self.id, false],
112
                                       :include => [:status, :versions])
111 113
  end
112 114

  
113 115
  # Returns the total amount of closed issues for this version.
114 116
  def closed_issues_count
115
    @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
117
    @closed_issues_count ||= Issue.count(:all, :conditions => ["#{Version.table_name}.id = ? AND is_closed = ?", self.id, true],
118
                                         :include => [:status, :versions])
116 119
  end
117 120
  
118 121
  def wiki_page
......
170 173
  
171 174
  # Returns the average estimated time of assigned issues
172 175
  # or 1 if no issue has an estimated time
173
  # Used to weigth unestimated issues in progress calculation
176
  # Used to weight unestimated issues in progress calculation
174 177
  def estimated_average
175 178
    if @estimated_average.nil?
176 179
      average = fixed_issues.average(:estimated_hours).to_f
app/views/issue_versions/_form.rhtml (revision 0)
1
<%= error_messages_for 'version' %>
2

  
3
<p>
4
  <%= select_tag :id, version_options_for_select(@issue.assignable_versions ), :include_blank => false %>
5
  <%= submit_tag l(:button_add) %>
6
  <%= toggle_link l(:button_cancel), 'new-version-form'%>
7
</p>
app/views/issues/_attributes.rhtml (working copy)
18 18
                     :title => l(:label_issue_category_new), 
19 19
                     :tabindex => 199) if authorize_for('issue_categories', 'new') %></p>
20 20
<% end %>
21
<% unless @issue.assignable_versions.empty? %>
22
<p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %>
23
<%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
24
                     l(:label_version_new),
25
                     'version[name]', 
26
                     {:controller => 'versions', :action => 'new', :project_id => @project},
27
                     :title => l(:label_version_new), 
28
                     :tabindex => 200) if authorize_for('versions', 'new') %>
29
</p>
30
<% end %>
31 21
</div>
32 22

  
33 23
<div class="splitcontentright">
app/views/issues/_form_update.rhtml (working copy)
7 7
<% if Issue.use_field_for_done_ratio? %>
8 8
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
9 9
<% end %>
10
<% unless @issue.assignable_versions.empty? %>
11
<p><%= f.select :fixed_version_id, (@issue.assignable_versions.collect {|v| [v.name, v.id]}), :include_blank => true %></p>
12
<% end %>
13 10
</div>
14 11
</div>
app/views/issues/_versions.rhtml (revision 0)
1
<div class="contextual">
2
  <% if authorize_for('issue_versions', 'new') %>
3
      <%= toggle_link l(:button_add), 'new-version-form'%>
4
  <% end %>
5
</div>
6

  
7
<p><strong><%=l(:label_version_plural)%></strong></p>
8

  
9
<% if @issue.versions.any? %>
10
<table style="width:100%">
11
<% @issue.versions.each do |version| %>
12
  <tr>
13
    <td><%= link_to_version(version, :truncate => 60) %></td>
14
    <td><%= format_date(version.effective_date) %></td>
15
    <td><%= link_to_remote(image_tag('delete.png'), { :url => {:controller => 'issue_versions', :action => 'destroy', :issue_id => @issue, :id => version},
16
                                                      :method => :post
17
                                                    }, :title => l(:label_version_delete)) %></td>
18
  </tr>
19
<% end %>
20
</table>
21
<% end %>
22

  
23
<% remote_form_for(:version, @version,
24
                   :url => {:controller => 'issue_versions', :action => 'new', :issue_id => @issue},
25
                   :method => :post,
26
                   :html => {:id => 'new-version-form', :style => 'display: none;'}) do |f| %>
27
  <%= render :partial => 'issue_versions/form', :locals => {:f => f}%>
28
<% end %>
app/views/issues/bulk_edit.rhtml (working copy)
36 36
                                options_from_collection_for_select(@project.issue_categories, :id, :name)) %>
37 37
</p>
38 38
<p>
39
	<label><%= l(:field_fixed_version) %></label> 
40
	<%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
39
	<label><%= l(:field_version) %></label> 
40
	<%= select_tag('issue[version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
41 41
                                   content_tag('option', l(:label_none), :value => 'none') +
42 42
                                   version_options_for_select(@project.shared_versions.open)) %>
43 43
</p>
app/views/issues/context_menu.rhtml (working copy)
40 40
	</li>
41 41
	<% unless @project.nil? || @project.shared_versions.open.empty? -%>
42 42
	<li class="folder">			
43
		<a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
43
		<a href="#" class="submenu"><%= l(:label_version_plural) %></a>
44 44
		<ul>
45 45
		<% @project.shared_versions.open.sort.each do |v| -%>
46
		    <li><%= context_menu_link format_version_name(v), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => v}, :back_url => @back}, :method => :post,
47
		                              :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li>
46
		    <li><%= context_menu_link format_version_name(v), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'version_id' => v}, :back_url => @back}, :method => :post,
47
		                              :selected => (@issue && @issue.versions.include?(v)), :disabled => !@can[:update] %></li>
48 48
		<% end -%>
49
		    <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => 'none'}, :back_url => @back}, :method => :post,
50
		                              :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li>
49
		    <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'version_id' => 'none'}, :back_url => @back}, :method => :post,
50
		                              :selected => (@issue && @issue.versions.empty?), :disabled => !@can[:update] %></li>
51 51
		</ul>
52 52
	</li>
53 53
	<% end %>
app/views/issues/index.xml.builder (working copy)
10 10
		 	xml.author(:id => issue.author_id, :name => issue.author.name) unless issue.author.nil?
11 11
		 	xml.assigned_to(:id => issue.assigned_to_id, :name => issue.assigned_to.name) unless issue.assigned_to.nil?
12 12
		  xml.category(:id => issue.category_id, :name => issue.category.name) unless issue.category.nil?
13
		  xml.fixed_version(:id => issue.fixed_version_id, :name => issue.fixed_version.name) unless issue.fixed_version.nil?
13
		  xml.versions do
14
        issue.versions.each do |version|
15
          xml.version(:id => version.id, :name => version.name)
16
        end
17
      end
14 18
      xml.parent(:id => issue.parent_id) unless issue.parent.nil?
15 19
      
16 20
      xml.subject 		issue.subject
app/views/issues/show.rhtml (working copy)
35 35
    <td class="spent-time"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %></td>
36 36
    <% end %>
37 37
</tr>
38
<tr>
39
    <th class="fixed-version"><%=l(:field_fixed_version)%>:</th><td class="fixed-version"><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
40
    <% if @issue.estimated_hours %>
41
    <th class="estimated-hours"><%=l(:field_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.estimated_hours) %></td>
42
    <% end %>
43
</tr>
44 38
<%= render_custom_fields_rows(@issue) %>
45 39
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
46 40
</table>
47 41
<hr />
48 42

  
43
<div id="versions">
44
  <%= render :partial => 'issues/versions' %>
45
</div>
46
<hr />
47

  
49 48
<div class="contextual">
50 49
<%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %>
51 50
</div>
52
                              
51

  
53 52
<p><strong><%=l(:field_description)%></strong></p>
54 53
<div class="wiki">
55 54
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
app/views/issues/show.xml.builder (working copy)
8 8
 	xml.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil?
9 9
 	xml.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil?
10 10
  xml.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil?
11
  xml.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil?
11
  xml.versions each do |version|
12
    xml.version(:id => version.id, :name => version.name)
13
  end
12 14
  xml.parent(:id => @issue.parent_id) unless @issue.parent.nil?
13 15
  
14 16
  xml.subject 		@issue.subject
app/views/mailer/_issue_text_html.rhtml (working copy)
6 6
<li><%=l(:field_priority)%>: <%=h issue.priority %></li>
7 7
<li><%=l(:field_assigned_to)%>: <%=h issue.assigned_to %></li>
8 8
<li><%=l(:field_category)%>: <%=h issue.category %></li>
9
<li><%=l(:field_fixed_version)%>: <%=h issue.fixed_version %></li>
9
<li><%=l(:field_version)%>: <%=h issue.versions.map {|version| version.name}.join(", ") %></li>
10 10
<% issue.custom_values.each do |c| %>
11 11
  <li><%=h c.custom_field.name %>: <%=h show_value(c) %></li>
12 12
<% end %>
app/views/mailer/_issue_text_plain.rhtml (working copy)
6 6
<%=l(:field_priority)%>: <%= issue.priority %>
7 7
<%=l(:field_assigned_to)%>: <%= issue.assigned_to %>
8 8
<%=l(:field_category)%>: <%= issue.category %>
9
<%=l(:field_fixed_version)%>: <%= issue.fixed_version %>
9
<%=l(:field_version)%>: <%= issue.versions.map {|version| version.name}.join(", ") %>
10 10
<% issue.custom_values.each do |c| %><%= c.custom_field.name %>: <%= show_value(c) %>
11 11
<% end %>
12 12

  
app/views/reports/issue_report.rhtml (working copy)
17 17

  
18 18
<div class="splitcontentright">
19 19
<h3><%=l(:field_version)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'version' %></h3>
20
<%= render :partial => 'simple', :locals => { :data => @issues_by_version, :field_name => "fixed_version_id", :rows => @versions } %>
20
<%= render :partial => 'simple', :locals => { :data => @issues_by_version, :field_name => "version_id", :rows => @versions } %>
21 21
<br />
22 22
<% if @project.children.any? %>
23 23
<h3><%=l(:field_subproject)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'subproject' %></h3>
app/views/versions/_issue_counts.rhtml (working copy)
19 19
                                        :action => 'index',
20 20
                                        :project_id => version.project,
21 21
                                        :set_filter => 1,
22
                                        :fixed_version_id => version,
22
                                        :version_id => version,
23 23
                                        "#{criteria}_id" => count[:group]} %>
24 24
        </td>
25 25
        <td width="240px">
app/views/versions/_overview.rhtml (working copy)
16 16
<% if version.fixed_issues.count > 0 %>
17 17
    <%= progress_bar([version.closed_pourcent, version.completed_pourcent], :width => '40em', :legend => ('%0.0f%' % version.completed_pourcent)) %>
18 18
    <p class="progress-info">
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) %>
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', :version_id => version, :set_filter => 1) %>
20 20
        (<%= '%0.0f' % (version.closed_issues_count.to_f / version.fixed_issues.count * 100) %>%)
21 21
        &#160;
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) %>
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', :version_id => version, :set_filter => 1) %>
23 23
        (<%= '%0.0f' % (version.open_issues_count.to_f / version.fixed_issues.count * 100) %>%)
24 24
    </p>
25 25
<% else %>
config/locales/en.yml (working copy)
240 240
  field_password: Password
241 241
  field_new_password: New password
242 242
  field_password_confirmation: Confirmation
243
  field_version: Version
243
  field_version: Versions
244 244
  field_type: Type
245 245
  field_host: Host
246 246
  field_port: Port
db/migrate/20100511054420_issue_versions_table.rb (revision 0)
1
class IssueVersionsTable < ActiveRecord::Migration
2
  def self.up
3
    execute <<-EOS
4
      CREATE TABLE issues_versions
5
      (
6
        issue_id integer NOT NULL,
7
        version_id integer NOT NULL,
8
        PRIMARY KEY (issue_id, version_id)
9
      );
10

  
11
      CREATE INDEX issues_versions_on_version_id
12
      ON issues_versions USING btree (version_id);
13
    EOS
14

  
15
    Version.find(:all).each do |version|
16
      puts "Migrating version #{version.name}"
17
      Issue.find(:all, :conditions => {:fixed_version_id => version.id}).each do |issue|
18
        version.fixed_issues << issue
19
      end
20
    end
21

  
22
    # Rename fixed_version_id for now - so we keep the data but make sure
23
    # that it isn't used
24
    rename_column :issues, :fixed_version_id, :fixed_version_id_old
25
  end
26

  
27
  def self.down
28
    drop_table :version_issues
29
    rename_column :issues, :fixed_version_id_old, :fixed_version_id
30
  end
31
end
lib/redmine.rb (working copy)
81 81
    map.permission :view_issue_watchers, {}
82 82
    map.permission :add_issue_watchers, {:watchers => :new}
83 83
    map.permission :delete_issue_watchers, {:watchers => :destroy}
84
    # Versions
85
    map.permission :view_issue_versions, {}
86
    map.permission :add_issue_versions, {:issue_versions => :new}
87
    map.permission :delete_issue_versions, {:issue_versions => :destroy}
84 88
  end
85 89
  
86 90
  map.project_module :time_tracking do |map|
(1-1/2)