priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
- fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
+ fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
unsaved_issue_ids = []
end
def archive
- @project.archive if request.post? && @project.active?
+ if request.post?
+ unless @project.archive
+ flash[:error] = l(:error_can_not_archive_project)
+ end
+ end
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
end
# Add a new version to @project
def add_version
- @version = @project.versions.build(params[:version])
+ @version = @project.versions.build
+ if params[:version]
+ attributes = params[:version].dup
+ attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
+ @version.attributes = attributes
+ end
if request.post? and @version.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'versions', :id => @project
# Show changelog for @project
def changelog
@trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
- retrieve_selected_tracker_ids(@trackers)
- @versions = @project.versions.sort
+ retrieve_selected_tracker_ids(@trackers)
+ @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
+ project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
+
+ @versions = @project.shared_versions.sort
+
+ @issues_by_version = {}
+ unless @selected_tracker_ids.empty?
+ @versions.each do |version|
+ conditions = {:tracker_id => @selected_tracker_ids, "#{IssueStatus.table_name}.is_closed" => true}
+ if !@project.versions.include?(version)
+ conditions.merge!(:project_id => project_ids)
+ end
+ issues = version.fixed_issues.visible.find(:all,
+ :include => [:status, :tracker, :priority],
+ :conditions => conditions,
+ :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
+ @issues_by_version[version] = issues
+ end
+ end
+ @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].empty?}
end
def roadmap
- @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
+ @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position')
retrieve_selected_tracker_ids(@trackers)
- @versions = @project.versions.sort
- @versions = @versions.select {|v| !v.completed? } unless params[:completed]
+ @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
+ project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
+
+ @versions = @project.shared_versions.sort
+ @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
+
+ @issues_by_version = {}
+ unless @selected_tracker_ids.empty?
+ @versions.each do |version|
+ conditions = {:tracker_id => @selected_tracker_ids}
+ if !@project.versions.include?(version)
+ conditions.merge!(:project_id => project_ids)
+ end
+ issues = version.fixed_issues.visible.find(:all,
+ :include => [:status, :tracker, :priority],
+ :conditions => conditions,
+ :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
+ @issues_by_version[version] = issues
+ end
+ end
+ @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].empty?}
end
def activity
before_filter :authorize
helper :custom_fields
+ helper :projects
def show
end
def edit
- if request.post? and @version.update_attributes(params[:version])
- flash[:notice] = l(:notice_successful_update)
- redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+ if request.post? && params[:version]
+ attributes = params[:version].dup
+ attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
+ if @version.update_attributes(attributes)
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
+ end
end
end
h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
end
+ def format_version_name(version)
+ if version.project == @project
+ h(version)
+ else
+ h("#{version.project} - #{version}")
+ end
+ end
+
def due_date_distance_in_words(date)
if date
l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
when 'fixed_version_id'
- v = Version.find_by_id(detail.value) and value = v.name if detail.value
- v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
+ v = Version.find_by_id(detail.value) and value = format_version_name(v) if detail.value
+ v = Version.find_by_id(detail.old_value) and old_value = format_version_name(v) if detail.old_value
when 'estimated_hours'
value = "%0.02f" % detail.value.to_f unless detail.value.blank?
old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
module ProjectsHelper
def link_to_version(version, options = {})
return '' unless version && version.is_a?(Version)
- link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
+ link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
end
def project_settings_tabs
end
s
end
+
+ # Returns a set of options for a select field, grouped by project.
+ def version_options_for_select(versions, selected=nil)
+ grouped = Hash.new {|h,k| h[k] = []}
+ versions.each do |version|
+ grouped[version.project.name] << [h(version.name), version.id]
+ end
+ # Add in the selected
+ if selected && !versions.include?(selected)
+ grouped[selected.project.name] << [h(selected.name), selected.id]
+ end
+
+ if grouped.keys.size > 1
+ grouped_options_for_select(grouped, selected && selected.id)
+ else
+ options_for_select(grouped.values.first, selected && selected.id)
+ end
+ end
+
+ def format_version_sharing(sharing)
+ sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
+ l("label_version_sharing_#{sharing}")
+ end
end
# 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
- issue.fixed_version = nil
+ # 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
+ end
issue.project = new_project
end
if new_tracker
# Versions that the issue can be assigned to
def assignable_versions
- @assignable_versions ||= (project.versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
+ @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
end
# Returns true if this issue is blocked by another issue that is still open
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
s
end
+
+ # Update all issues so their versions are not pointing to a
+ # fixed_version that is outside of the issue's project hierarchy.
+ #
+ # OPTIMIZE: does a full table scan of Issues with a fixed_version.
+ def self.update_fixed_versions_from_project_hierarchy_change
+ Issue.all(:conditions => ['fixed_version_id IS NOT NULL'],
+ :include => [:project, :fixed_version]
+ ).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
+ end
+ end
+ end
private
self.status == STATUS_ACTIVE
end
- # Archives the project and its descendants recursively
+ # Archives the project and its descendants
def archive
- # Archive subprojects if any
- children.each do |subproject|
- subproject.archive
+ # 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,
+ :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
+ " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
+ return false
end
- update_attribute :status, STATUS_ARCHIVED
+ Project.transaction do
+ archive!
+ end
+ true
end
# Unarchives the project
# move_to_child_of adds the project in last (ie.right) position
move_to_child_of(p)
end
+ Issue.update_fixed_versions_from_project_hierarchy_change
true
else
# Can not move to the given target
end
end
+ # Returns a scope of the Versions used by the project
+ def shared_versions
+ @shared_versions ||=
+ Version.scoped(:include => :project,
+ :conditions => "#{Project.table_name}.id = #{id}" +
+ " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
+ " #{Version.table_name}.sharing = 'system'" +
+ " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
+ " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
+ " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
+ "))")
+ end
+
# Returns a hash of project users grouped by role
def users_by_role
members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
self.time_entry_activities.active
end
end
+
+ # Archives subprojects recursively
+ def archive!
+ children.each do |subproject|
+ subproject.send :archive!
+ end
+ update_attribute :status, STATUS_ARCHIVED
+ end
end
unless @project.issue_categories.empty?
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
end
- unless @project.versions.empty?
- @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
+ 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] } }
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] } }
class Version < ActiveRecord::Base
before_destroy :check_integrity
+ after_update :update_issue_versions
belongs_to :project
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
acts_as_customizable
:delete_permission => :manage_files
VERSION_STATUSES = %w(open locked closed)
+ VERSION_SHARINGS = %w(none descendants hierarchy tree system)
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 60
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
validates_inclusion_of :status, :in => VERSION_STATUSES
+ validates_inclusion_of :sharing, :in => VERSION_SHARINGS
named_scope :open, :conditions => {:status => 'open'}
-
+ named_scope :visible, lambda {|*args| { :include => :project,
+ :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
+
+ # Returns true if +user+ or current user is allowed to view the version
+ def visible?(user=User.current)
+ user.allowed_to?(:view_issues, self.project)
+ end
+
def start_date
effective_date
end
def closed?
status == 'closed'
end
+
+ def open?
+ status == 'open'
+ end
# Returns true if the version is completed: due date reached and no open issues
def completed?
end
end
+ # Returns the sharings that +user+ can set the version to
+ def allowed_sharings(user = User.current)
+ VERSION_SHARINGS.select do |s|
+ if sharing == s
+ true
+ else
+ case s
+ when 'system'
+ # Only admin users can set a systemwide sharing
+ user.admin?
+ when 'hierarchy', 'tree'
+ # Only users allowed to manage versions of the root project can
+ # set sharing to hierarchy or tree
+ project.nil? || user.allowed_to?(:manage_versions, project.root)
+ else
+ true
+ end
+ end
+ end
+ end
+
private
def check_integrity
raise "Can't delete version" if self.fixed_issues.find(:first)
end
+
+ # Update the issue's fixed versions. Used if a version's sharing changes.
+ def update_issue_versions
+ if sharing_changed?
+ Issue.update_fixed_versions_from_project_hierarchy_change
+ end
+ end
# Returns the average estimated time of assigned issues
# or 1 if no issue has an estimated time
:tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
<% end %>
<% unless @issue.assignable_versions.empty? %>
-<p><%= f.select :fixed_version_id, (@issue.assignable_versions.collect {|v| [v.name, v.id]}), :include_blank => true %></p>
+<p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %></p>
<% end %>
</div>
<label><%= l(:field_fixed_version) %>:
<%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') +
content_tag('option', l(:label_none), :value => 'none') +
- options_from_collection_for_select(@project.versions.open.sort, :id, :name)) %></label>
+ version_options_for_select(@project.shared_versions.open)) %></label>
</p>
<p>
<% end -%>
</ul>
</li>
- <% unless @project.nil? || @project.versions.open.empty? -%>
+ <% unless @project.nil? || @project.shared_versions.open.empty? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
<ul>
- <% @project.versions.open.sort.each do |v| -%>
- <li><%= context_menu_link v.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => v, :back_to => @back}, :method => :post,
+ <% @project.shared_versions.open.sort.each do |v| -%>
+ <li><%= context_menu_link format_version_name(v), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => v, :back_to => @back}, :method => :post,
:selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li>
<% end -%>
<li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => 'none', :back_to => @back}, :method => :post,
<br />
<% end %></p>
<% if @project && @project.descendants.active.any? %>
+ <%= hidden_field_tag 'with_subprojects', 0 %>
<p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
- <%= hidden_field_tag 'with_subprojects', 0 %>
<% end %>
<%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
<p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
<% @versions.each do |version| %>
<%= tag 'a', :name => version.name %>
- <h3 class="icon22 icon22-package"><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></h3>
+ <h3 class="icon22 icon22-package"><%= link_to_version version %></h3>
<% if version.effective_date %>
<p><%= format_date(version.effective_date) %></p>
<% end %>
<p><%=h version.description %></p>
- <% issues = version.fixed_issues.find(:all,
- :include => [:status, :tracker, :priority],
- :conditions => ["#{IssueStatus.table_name}.is_closed=? AND #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')})", true],
- :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty?
+ <% issues = version.fixed_issues.visible.find(:all,
+ :include => [:status, :tracker, :priority],
+ :conditions => ["#{Issue.table_name}.project_id = ? AND #{IssueStatus.table_name}.is_closed=? AND #{Issue.table_name}.tracker_id in (?)", @project.id, true, @selected_tracker_ids],
+ :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty?
issues ||= []
%>
<% if !issues.empty? %>
<label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %>
<%= tracker.name %></label><br />
<% end %>
+<% if @project.descendants.active.any? %>
+ <%= hidden_field_tag 'with_subprojects', 0 %>
+ <br /><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label>
+<% end %>
<p><%= submit_tag l(:button_apply), :class => 'button-small' %></p>
<% end %>
<h3><%= l(:label_version_plural) %></h3>
<% @versions.each do |version| %>
-<%= link_to version.name, :anchor => version.name %><br />
+<%= link_to format_version_name(version), :anchor => version.name %><br />
<% end %>
<% end %>
<div id="roadmap">
<% @versions.each do |version| %>
<%= tag 'a', :name => version.name %>
- <h3 class="icon22 icon22-package"><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></h3>
+ <h3 class="icon22 icon22-package"><%= link_to_version version %></h3>
<%= render :partial => 'versions/overview', :locals => {:version => version} %>
<%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
- <% issues = version.fixed_issues.find(:all,
- :include => [:status, :tracker, :priority],
- :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"],
- :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") unless @selected_tracker_ids.empty?
- issues ||= []
- %>
- <% if issues.size > 0 %>
+ <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
<fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
<ul>
<%- issues.each do |issue| -%>
<% end %>
<br />
<label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label>
+<% if @project.descendants.active.any? %>
+ <%= hidden_field_tag 'with_subprojects', 0 %>
+ <br /><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label>
+<% end %>
<p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
<% end %>
<h3><%= l(:label_version_plural) %></h3>
<% @versions.each do |version| %>
-<%= link_to version.name, "##{version.name}" %><br />
+<%= link_to format_version_name(version), "##{version.name}" %><br />
<% end %>
<% end %>
<th><%= l(:field_effective_date) %></th>
<th><%= l(:field_description) %></th>
<th><%= l(:field_status) %></th>
+ <th><%= l(:field_sharing) %></th>
<th><%= l(:label_wiki_page) unless @project.wiki.nil? %></th>
<th style="width:15%"></th>
</thead>
<td align="center"><%= format_date(version.effective_date) %></td>
<td><%=h version.description %></td>
<td><%= l("version_status_#{version.status}") %></td>
+ <td><%=h format_version_sharing(version.sharing) %></td>
<td><%= link_to(h(version.wiki_page_title), :controller => 'wiki', :page => Wiki.titleize(version.wiki_page_title)) unless version.wiki_page_title.blank? || @project.wiki.nil? %></td>
<td class="buttons">
<%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => version }, :class => 'icon icon-edit' %>
<p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
<p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
<p><%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
+<p><%= f.select :sharing, @version.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %></p>
<% @version.custom_field_values.each do |value| %>
<p><%= custom_field_tag_with_label :version, value %></p>
<% end %>
+
</div>
error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
+ error_can_not_archive_project: This project can not be archived
warning_attachments_not_saved: "{{count}} file(s) could not be saved."
field_identity_url: OpenID URL
field_content: Content
field_group_by: Group results by
+ field_sharing: Sharing
setting_app_title: Application title
setting_app_subtitle: Application subtitle
label_group_plural: Groups
label_group_new: New group
label_time_entry_plural: Spent time
+ label_version_sharing_none: Not shared
+ label_version_sharing_descendants: With subprojects
+ label_version_sharing_hierarchy: With project hierarchy
+ label_version_sharing_tree: With project tree
+ label_version_sharing_system: With all projects
button_login: Login
button_submit: Submit
error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
+ error_can_not_archive_project: "Ce projet ne peut pas être archivé"
warning_attachments_not_saved: "{{count}} fichier(s) n'ont pas pu être sauvegardés."
field_identity_url: URL OpenID
field_content: Contenu
field_group_by: Grouper par
+ field_sharing: Partage
setting_app_title: Titre de l'application
setting_app_subtitle: Sous-titre de l'application
label_group: Groupe
label_group_new: Nouveau groupe
label_time_entry_plural: Temps passé
+ label_version_sharing_none: Non partagé
+ label_version_sharing_descendants: Avec les sous-projets
+ label_version_sharing_hierarchy: Avec toute la hiérarchie
+ label_version_sharing_tree: Avec tout l'arbre
+ label_version_sharing_system: Avec tous les projets
button_login: Connexion
button_submit: Soumettre
--- /dev/null
+class AddVersionsSharing < ActiveRecord::Migration
+ def self.up
+ add_column :versions, :sharing, :string, :default => 'none', :null => false
+ add_index :versions, :sharing
+ end
+
+ def self.down
+ remove_column :versions, :sharing
+ end
+end
menu.push :overview, { :controller => 'projects', :action => 'show' }
menu.push :activity, { :controller => 'projects', :action => 'activity' }
menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
- :if => Proc.new { |p| p.versions.any? }
+ :if => Proc.new { |p| p.shared_versions.any? }
menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
:html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
-class Version < ActiveRecord::Base
- generator_for :name, :method => :next_name
-
- def self.next_name
- @last_name ||= 'Version 1.0.0'
- @last_name.succ!
- @last_name
- end
-
-end
+class Version < ActiveRecord::Base\r
+ generator_for :name, :method => :next_name\r
+ generator_for :status => 'open'\r
+ \r
+ def self.next_name\r
+ @last_name ||= 'Version 1.0.0'\r
+ @last_name.succ!\r
+ @last_name\r
+ end\r
+\r
+end\r
filename: picture.jpg
author_id: 2
content_type: image/jpeg
-
\ No newline at end of file
+attachments_012:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Version
+ container_id: 1
+ downloads: 0
+ disk_filename: 060719210727_version_file.zip
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 12
+ filesize: 452
+ filename: version_file.zip
+ author_id: 2
+ content_type: application/octet-stream
status_id: 5
start_date: <%= 1.day.ago.to_date.to_s(:db) %>
due_date:
+issues_013:
+ created_on: <%= 5.days.ago.to_date.to_s(:db) %>
+ project_id: 3
+ updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
+ priority_id: 4
+ subject: Subproject issue two
+ id: 13
+ fixed_version_id:
+ category_id:
+ description: This is a second issue on a cookbook subproject
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 1
value: "30"
prop_key: done_ratio
journal_id: 1
+journal_details_003:
+ old_value: nil
+ property: attr
+ id: 3
+ value: "6"
+ prop_key: fixed_version_id
+ journal_id: 4
journalized_type: Issue
user_id: 2
journalized_id: 2
-
\ No newline at end of file
+journals_004:
+ created_on: <%= 1.days.ago.to_date.to_s(:db) %>
+ notes: "A comment with a private version."
+ id: 4
+ journalized_type: Issue
+ user_id: 1
+ journalized_id: 6
project_id: 5
user_id: 8
mail_notification: false
-
\ No newline at end of file
+members_008:
+ created_on: 2006-07-19 19:35:33 +02:00
+ project_id: 5
+ id: 8
+ user_id: 1
+ mail_notification: true
description: Beta
effective_date: 2006-07-01
status: closed
+ sharing: 'none'
versions_002:
created_on: 2006-07-19 21:00:33 +02:00
name: "1.0"
description: Stable release
effective_date: <%= 20.day.from_now.to_date.to_s(:db) %>
status: locked
+ sharing: 'none'
versions_003:
created_on: 2006-07-19 21:00:33 +02:00
name: "2.0"
description: Future version
effective_date:
status: open
-
\ No newline at end of file
+ sharing: 'none'
+versions_004:
+ created_on: 2006-07-19 21:00:33 +02:00
+ name: "2.0"
+ project_id: 3
+ updated_on: 2006-07-19 21:00:33 +02:00
+ id: 4
+ description: Future version on subproject
+ effective_date:
+ status: open
+ sharing: 'tree'
+versions_005:
+ created_on: 2006-07-19 21:00:07 +02:00
+ name: "Alpha"
+ project_id: 2
+ updated_on: 2006-07-19 21:00:07 +02:00
+ id: 5
+ description: Private Alpha
+ effective_date: 2006-07-01
+ status: open
+ sharing: 'none'
+versions_006:
+ created_on: 2006-07-19 21:00:07 +02:00
+ name: "Private Version of public subproject"
+ project_id: 5
+ updated_on: 2006-07-19 21:00:07 +02:00
+ id: 6
+ description: "Should be done any day now..."
+ effective_date:
+ status: open
+ sharing: 'tree'
+versions_007:
+ created_on: 2006-07-19 21:00:07 +02:00
+ name: "Systemwide visible version"
+ project_id: 2
+ updated_on: 2006-07-19 21:00:07 +02:00
+ id: 7
+ description:
+ effective_date:
+ status: open
+ sharing: 'system'
assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
end
+ def test_post_edit_should_allow_fixed_version_to_be_set_to_a_subproject
+ issue = Issue.find(2)
+ @request.session[:user_id] = 2
+
+ post :edit,
+ :id => issue.id,
+ :issue => {
+ :fixed_version_id => 4
+ }
+
+ assert_response :redirect
+ issue.reload
+ assert_equal 4, issue.fixed_version_id
+ assert_not_equal issue.project_id, issue.fixed_version.project_id
+ end
+
def test_get_bulk_edit
@request.session[:user_id] = 2
get :bulk_edit, :ids => [1, 2]
assert_nil Issue.find(2).assigned_to
end
+ def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject
+ @request.session[:user_id] = 2
+
+ post :bulk_edit,
+ :ids => [1,2],
+ :fixed_version_id => 4
+
+ assert_response :redirect
+ issues = Issue.find([1,2])
+ issues.each do |issue|
+ assert_equal 4, issue.fixed_version_id
+ assert_not_equal issue.project_id, issue.fixed_version.project_id
+ end
+ end
+
def test_move_routing
assert_routing(
{:method => :get, :path => '/issues/1/move'},
assert_tag :tag => 'a', :content => 'Immediate',
:attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&priority_id=8',
:class => '' }
+ # Versions
+ assert_tag :tag => 'a', :content => '2.0',
+ :attributes => { :href => '/issues/bulk_edit?fixed_version_id=3&ids%5B%5D=1',
+ :class => '' }
+ assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
+ :attributes => { :href => '/issues/bulk_edit?fixed_version_id=4&ids%5B%5D=1',
+ :class => '' }
+
assert_tag :tag => 'a', :content => 'Dave Lopper',
:attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&ids%5B%5D=1',
:class => '' }
assert_not_nil assigns(:versions)
end
+ def test_changelog_showing_subprojects_versions
+ get :changelog, :id => 1, :with_subprojects => 1
+ assert_response :success
+ assert_template 'changelog'
+ assert_not_nil assigns(:versions)
+ # Version on subproject appears
+ assert assigns(:versions).include?(Version.find(4))
+ end
+
def test_roadmap_routing
assert_routing(
{:method => :get, :path => 'projects/33/roadmap'},
# Completed version appears
assert assigns(:versions).include?(Version.find(1))
end
+
+ def test_roadmap_showing_subprojects_versions
+ get :roadmap, :id => 1, :with_subprojects => 1
+ assert_response :success
+ assert_template 'roadmap'
+ assert_not_nil assigns(:versions)
+ # Version on subproject appears
+ assert assigns(:versions).include?(Version.find(4))
+ end
def test_project_activity_routing
assert_routing(
end
class AccountTest < ActionController::IntegrationTest
- fixtures :users
+ fixtures :users, :roles
# Replace this with your real tests.
def test_login
# Add more helper methods to be used by all tests here...
def log_user(login, password)
+ User.anonymous
get "/login"
assert_equal nil, session[:user_id]
assert_response :success
def test_objects_count
# low priority
- assert_equal 5, Enumeration.find(4).objects_count
+ assert_equal 6, Enumeration.find(4).objects_count
# urgent
assert_equal 0, Enumeration.find(7).objects_count
end
def test_destroy_with_reassign
Enumeration.find(4).destroy(Enumeration.find(6))
assert_nil Issue.find(:first, :conditions => {:priority_id => 4})
- assert_equal 5, Enumeration.find(6).objects_count
+ assert_equal 6, Enumeration.find(6).objects_count
end
def test_should_be_customizable
--- /dev/null
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../test_helper'
+
+class ProjectsHelperTest < HelperTestCase
+ include ApplicationHelper
+ include ProjectsHelper
+
+ fixtures :all
+
+ def setup
+ super
+ set_language_if_valid('en')
+ User.current = nil
+ end
+
+ def test_link_to_version_within_project
+ @project = Project.find(2)
+ User.current = User.find(1)
+ assert_equal '<a href="/versions/show/5">Alpha</a>', link_to_version(Version.find(5))
+ end
+
+ def test_link_to_version
+ User.current = User.find(1)
+ assert_equal '<a href="/versions/show/5">OnlineStore - Alpha</a>', link_to_version(Version.find(5))
+ end
+
+ def test_link_to_private_version
+ assert_equal 'OnlineStore - Alpha', link_to_version(Version.find(5))
+ end
+
+ def test_link_to_version_invalid_version
+ assert_equal '', link_to_version(Object)
+ end
+
+ def test_format_version_name_within_project
+ @project = Project.find(1)
+ assert_equal "0.1", format_version_name(Version.find(1))
+ end
+
+ def test_format_version_name
+ assert_equal "eCookbook - 0.1", format_version_name(Version.find(1))
+ end
+
+ def test_format_version_name_for_system_version
+ assert_equal "OnlineStore - Systemwide visible version", format_version_name(Version.find(7))
+ end
+end
def test_objects_count
# low priority
- assert_equal 5, IssuePriority.find(4).objects_count
+ assert_equal 6, IssuePriority.find(4).objects_count
# urgent
assert_equal 0, IssuePriority.find(7).objects_count
end
assert_nil issue.category_id
end
+ def test_move_to_another_project_should_clear_fixed_version_when_not_shared
+ issue = Issue.find(1)
+ issue.update_attribute(:fixed_version_id, 1)
+ assert issue.move_to(Project.find(2))
+ issue.reload
+ assert_equal 2, issue.project_id
+ # Cleared fixed_version
+ assert_equal nil, issue.fixed_version
+ end
+
+ def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
+ issue = Issue.find(1)
+ issue.update_attribute(:fixed_version_id, 4)
+ assert issue.move_to(Project.find(5))
+ issue.reload
+ assert_equal 5, issue.project_id
+ # Keep fixed_version
+ assert_equal 4, issue.fixed_version_id
+ end
+
+ def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
+ issue = Issue.find(1)
+ issue.update_attribute(:fixed_version_id, 1)
+ assert issue.move_to(Project.find(5))
+ issue.reload
+ assert_equal 5, issue.project_id
+ # Cleared fixed_version
+ assert_equal nil, issue.fixed_version
+ end
+
+ def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
+ issue = Issue.find(1)
+ issue.update_attribute(:fixed_version_id, 7)
+ assert issue.move_to(Project.find(2))
+ issue.reload
+ assert_equal 2, issue.project_id
+ # Keep fixed_version
+ assert_equal 7, issue.fixed_version_id
+ end
+
def test_copy_to_the_same_project
issue = Issue.find(1)
copy = nil
require File.dirname(__FILE__) + '/../test_helper'
class ProjectTest < ActiveSupport::TestCase
- fixtures :projects, :enabled_modules,
- :issues, :issue_statuses, :journals, :journal_details,
- :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
- :queries
+ fixtures :all
def setup
@ecookbook = Project.find(1)
assert @ecookbook.descendants.active.empty?
end
+ def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
+ # Assign an issue of a project to a version of a child project
+ Issue.find(4).update_attribute :fixed_version_id, 4
+
+ assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
+ assert_equal false, @ecookbook.archive
+ end
+ @ecookbook.reload
+ assert @ecookbook.active?
+ end
+
def test_unarchive
user = @ecookbook.members.first.user
@ecookbook.archive
assert_equal 4, parent.children.size
assert_equal parent.children.sort_by(&:name), parent.children
end
+
+
+ def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
+ # Parent issue with a hierarchy project's fixed version
+ parent_issue = Issue.find(1)
+ parent_issue.update_attribute(:fixed_version_id, 4)
+ parent_issue.reload
+ assert_equal 4, parent_issue.fixed_version_id
+
+ # Should keep fixed versions for the issues
+ issue_with_local_fixed_version = Issue.find(5)
+ issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
+ issue_with_local_fixed_version.reload
+ assert_equal 4, issue_with_local_fixed_version.fixed_version_id
+
+ # Local issue with hierarchy fixed_version
+ issue_with_hierarchy_fixed_version = Issue.find(13)
+ issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
+ issue_with_hierarchy_fixed_version.reload
+ assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
+
+ # Move project out of the issue's hierarchy
+ moved_project = Project.find(3)
+ moved_project.set_parent!(Project.find(2))
+ parent_issue.reload
+ issue_with_local_fixed_version.reload
+ issue_with_hierarchy_fixed_version.reload
+
+ assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
+ assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
+ assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
+ end
def test_parent
p = Project.find(6).parent
assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
end
+
+ def test_shared_versions
+ parent = Project.find(1)
+ child = parent.children.find(3)
+ private_child = parent.children.find(5)
+
+ assert_equal [1,2,3], parent.version_ids.sort
+ assert_equal [4], child.version_ids
+ assert_equal [6], private_child.version_ids
+ assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
+
+ assert_equal 6, parent.shared_versions.size
+ parent.shared_versions.each do |version|
+ assert_kind_of Version, version
+ end
+
+ assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
+ end
+
+ def test_shared_versions_should_ignore_archived_subprojects
+ parent = Project.find(1)
+ child = parent.children.find(3)
+ child.archive
+ parent.reload
+
+ assert_equal [1,2,3], parent.version_ids.sort
+ assert_equal [4], child.version_ids
+ assert !parent.shared_versions.collect(&:id).include?(4)
+ end
+
+ def test_shared_versions_visible_to_user
+ user = User.find(3)
+ parent = Project.find(1)
+ child = parent.children.find(5)
+
+ assert_equal [1,2,3], parent.version_ids.sort
+ assert_equal [6], child.version_ids
+
+ versions = parent.shared_versions.visible(user)
+
+ assert_equal 4, versions.size
+ versions.each do |version|
+ assert_kind_of Version, version
+ end
+
+ assert !versions.collect(&:id).include?(6)
+ end
+
def test_next_identifier
ProjectCustomField.delete_all
Project.create!(:name => 'last', :identifier => 'p2008040')
assert_equal 'p2008041', Project.next_identifier
end
-
+
def test_next_identifier_first_project
Project.delete_all
assert_nil Project.next_identifier
end
should "change the new issues to use the copied version" do
- assigned_version = Version.generate!(:name => "Assigned Issues")
+ User.current = User.find(1)
+ assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
@source_project.versions << assigned_version
- assert_equal 1, @source_project.versions.size
- @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
- :subject => "change the new issues to use the copied version",
- :tracker_id => 1,
- :project_id => @source_project.id)
+ assert_equal 3, @source_project.versions.size
+ Issue.generate_for_project!(@source_project,
+ :fixed_version_id => assigned_version.id,
+ :subject => "change the new issues to use the copied version",
+ :tracker_id => 1,
+ :project_id => @source_project.id)
assert @project.copy(@source_project)
@project.reload
:include => [ :assigned_to, :status, :tracker, :project, :priority ],
:conditions => query.statement
end
+
+ def test_query_should_allow_shared_versions_for_a_project_query
+ subproject_version = Version.find(4)
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
+
+ assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
+ end
def test_query_with_multiple_custom_fields
query = Query.find(1)
assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent
assert_progress_equal 25.0/100.0*100, v.closed_pourcent
end
+
+ test "should update all issue's fixed_version associations in case the hierarchy changed XXX" do
+ User.current = User.find(1) # Need the admin's permissions
+
+ @version = Version.find(7)
+ # Separate hierarchy
+ project_1_issue = Issue.find(1)
+ project_1_issue.fixed_version = @version
+ assert project_1_issue.save, project_1_issue.errors.full_messages
+
+ project_5_issue = Issue.find(6)
+ project_5_issue.fixed_version = @version
+ assert project_5_issue.save
+
+ # Project
+ project_2_issue = Issue.find(4)
+ project_2_issue.fixed_version = @version
+ assert project_2_issue.save
+
+ # Update the sharing
+ @version.sharing = 'none'
+ assert @version.save
+
+ # Project 1 now out of the shared scope
+ project_1_issue.reload
+ assert_equal nil, project_1_issue.fixed_version, "Fixed version is still set after changing the Version's sharing"
+
+ # Project 5 now out of the shared scope
+ project_5_issue.reload
+ assert_equal nil, project_5_issue.fixed_version, "Fixed version is still set after changing the Version's sharing"
+
+ # Project 2 issue remains
+ project_2_issue.reload
+ assert_equal @version, project_2_issue.fixed_version
+ end
private