From af91c3b25108065ce108a6f0c9b1d6ecd7e22e5a Mon Sep 17 00:00:00 2001
From: Eric Davis <edavis@littlestreamsoftware.com>
Date: Tue, 1 Sep 2009 15:49:55 -0700
Subject: [PATCH] Added Version sharing. (#465)

Versions can have a sharing value setup to control which project(s) they
will appear on:

* none (default) - version will only appear on it's project
* hierarchy - version will appear to parent/child projects also
* system - version will appear to any project

When a version is displayed and/or linked for unauthorized users,
it is masked to prevent information exposure.  The hidden versions will
still appear as select options though, so they are not cleared on accident.

If a Journal is entered that changes the Version to one that isn't accessible
by all of the email recipients, then the Version text is hidden.  This is
because only one email is sent to everyone.

When a Project is moved or a Version is updated, all the issues are checked
to make sure the version is still reachable according to it's sharing state.
---
 app/controllers/issues_controller.rb               |    2 +-
 app/controllers/projects_controller.rb             |   16 +++-
 app/helpers/application_helper.rb                  |   37 +++++++
 app/helpers/issues_helper.rb                       |   34 ++++++-
 app/helpers/mailer_helper.rb                       |   25 +++++
 app/helpers/projects_helper.rb                     |    6 +-
 app/models/issue.rb                                |   26 +++++-
 app/models/mailer.rb                               |    1 +
 app/models/project.rb                              |   28 ++++++
 app/models/query.rb                                |    4 +-
 app/models/version.rb                              |   30 ++++++-
 app/views/issues/_attributes.rhtml                 |    6 +-
 app/views/issues/bulk_edit.rhtml                   |    2 +-
 app/views/issues/context_menu.rhtml                |    6 +-
 app/views/mailer/issue_edit.text.html.rhtml        |    4 +
 app/views/mailer/issue_edit.text.plain.rhtml       |    4 +
 app/views/projects/changelog.rhtml                 |    2 +
 app/views/projects/list_files.rhtml                |    2 +-
 app/views/projects/roadmap.rhtml                   |    4 +-
 app/views/projects/settings/_versions.rhtml        |    2 +
 app/views/versions/_form.rhtml                     |    2 +
 config/locales/bg.yml                              |    6 +
 config/locales/bs.yml                              |    6 +
 config/locales/ca.yml                              |    6 +
 config/locales/cs.yml                              |    6 +
 config/locales/da.yml                              |    6 +
 config/locales/de.yml                              |    6 +
 config/locales/el.yml                              |    6 +
 config/locales/en.yml                              |    7 ++
 config/locales/es.yml                              |    6 +
 config/locales/fi.yml                              |    6 +
 config/locales/fr.yml                              |    6 +
 config/locales/gl.yml                              |    6 +
 config/locales/he.yml                              |    6 +
 config/locales/hu.yml                              |    6 +
 config/locales/it.yml                              |    6 +
 config/locales/ja.yml                              |    6 +
 config/locales/ko.yml                              |    6 +
 config/locales/lt.yml                              |    6 +
 config/locales/nl.yml                              |    6 +
 config/locales/no.yml                              |    6 +
 config/locales/pl.yml                              |    6 +
 config/locales/pt-BR.yml                           |    6 +
 config/locales/pt.yml                              |    6 +
 config/locales/ro.yml                              |    6 +
 config/locales/ru.yml                              |    6 +
 config/locales/sk.yml                              |    6 +
 config/locales/sl.yml                              |    6 +
 config/locales/sr.yml                              |    6 +
 config/locales/sv.yml                              |    6 +
 config/locales/th.yml                              |    6 +
 config/locales/tr.yml                              |    6 +
 config/locales/uk.yml                              |    6 +
 config/locales/vi.yml                              |    6 +
 config/locales/zh-TW.yml                           |    6 +
 config/locales/zh.yml                              |    6 +
 .../20090907153556_add_shared_to_versions.rb       |   10 ++
 .../20090907170038_populate_version_shared.rb      |    9 ++
 lib/redmine.rb                                     |    2 +-
 test/exemplars/version_exemplar.rb                 |    1 +
 test/fixtures/attachments.yml                      |   13 +++-
 test/fixtures/issues.yml                           |   14 +++
 test/fixtures/journal_details.yml                  |    7 ++
 test/fixtures/journals.yml                         |    8 ++-
 test/fixtures/members.yml                          |    7 +-
 test/fixtures/versions.yml                         |  100 ++++++++++++++------
 test/functional/issues_controller_test.rb          |   42 ++++++++
 test/functional/projects_controller_test.rb        |   28 ++++++
 test/integration/account_test.rb                   |    2 +-
 test/test_helper.rb                                |    1 +
 test/unit/enumeration_test.rb                      |    4 +-
 test/unit/helpers/application_helper_test.rb       |   17 +++-
 test/unit/helpers/projects_helper_test.rb          |   44 +++++++++
 test/unit/issue_priority_test.rb                   |    2 +-
 test/unit/issue_test.rb                            |   40 ++++++++
 test/unit/mailer_test.rb                           |   19 ++++
 test/unit/project_test.rb                          |   98 ++++++++++++++++++--
 test/unit/query_test.rb                            |    8 ++
 test/unit/version_test.rb                          |   41 ++++++++
 79 files changed, 904 insertions(+), 67 deletions(-)
 create mode 100644 app/helpers/mailer_helper.rb
 create mode 100644 db/migrate/20090907153556_add_shared_to_versions.rb
 create mode 100644 db/migrate/20090907170038_populate_version_shared.rb
 create mode 100644 test/unit/helpers/projects_helper_test.rb

diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb
index 336f63b..d4378d5 100644
--- a/app/controllers/issues_controller.rb
+++ b/app/controllers/issues_controller.rb
@@ -239,7 +239,7 @@ class IssuesController < ApplicationController
       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 {|v| v.id.to_s == params[:fixed_version_id] }
       custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
       
       unsaved_issue_ids = []      
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 75affee..82ddde1 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -271,7 +271,9 @@ class ProjectsController < ApplicationController
                 'downloads' => "#{Attachment.table_name}.downloads"
                 
     @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
-    @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
+    # Need get the ids separately in order to add the finder options
+    version_ids = @project.shared_versions_visible_to_user.collect(&:id)
+    @containers += Version.find_all_by_id(version_ids, :include => :attachments, :order => sort_clause).sort.reverse
     render :layout => !request.xhr?
   end
   
@@ -279,13 +281,21 @@ class ProjectsController < ApplicationController
   def changelog
     @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
     retrieve_selected_tracker_ids(@trackers)    
-    @versions = @project.versions.sort
+    if params[:shared_versions]
+      @versions = @project.shared_versions_visible_to_user.sort
+    else
+      @versions = @project.versions.sort
+    end
   end
 
   def roadmap
     @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
     retrieve_selected_tracker_ids(@trackers)
-    @versions = @project.versions.sort
+    if params[:shared_versions]
+      @versions = @project.shared_versions_visible_to_user.sort
+    else
+      @versions = @project.versions.sort
+    end
     @versions = @versions.select {|v| !v.completed? } unless params[:completed]
   end
   
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0b054f0..b0bc19e 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -126,6 +126,43 @@ module ApplicationHelper
     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 User.current.allowed_to?(:view_issues, version.project) || version.systemwide?
+      "#{h(version.project.name)} - #{h(version.name)}"
+    else
+      "#{l(:text_not_authorized)}"
+    end
+  end
+
+  def format_version_sharing_name(version)
+    if version.shared && Version::SharedValues.key?(version.shared)
+      l(Version::SharedValues[version.shared])
+    else
+      l(:label_version_shared_none)
+    end
+  end
+
+  # Given an Array of emails (+recipients+), do they all have
+  # +permission+ on +project+ ?
+  def recipients_all_allowed_to?(recipients, permission, project)
+    recipients.uniq.all? do |recipient|
+      user = User.find_by_mail(recipient)
+      user && user.allowed_to?(permission, project)
+    end
+  end
+
+  # Given an Array of +versions+, are all the email addresses
+  # (+recipients+) allowed to view all of the versions?
+  def recipients_all_allowed_to_see_versions?(recipients, versions)
+    versions.uniq.all? do |version|
+      if version.nil?
+        true
+      else
+        recipients_all_allowed_to?(recipients, :view_issues, version.project)
+      end
+    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))
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 0f28cc0..2e55f1f 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -64,6 +64,36 @@ module IssuesHelper
     @sidebar_queries
   end
 
+  # Returns a set of options for a select field, grouped by project.
+  def shared_version_options(project, selected=nil)
+    grouped = {}
+    project.shared_versions.each do |version|
+      next unless version.open?
+      
+      if User.current.allowed_to?(:view_issues, version.project) || version.systemwide?
+        grouped[version.project.name] ||= []
+        grouped[version.project.name] << [h(version.name), version.id]
+      else
+        grouped[l(:text_not_authorized)] ||=[]
+        grouped[l(:text_not_authorized)] << [l(:text_not_authorized), version.id]
+      end
+    end
+
+    # Add in the selected
+    selected_version = Version.find_by_id(selected)
+    if selected_version && !project.shared_versions.include?(selected_version)
+      if User.current.allowed_to?(:view_issues, selected_version.project) || selected_version.systemwide?
+        grouped[selected_version.project.name] ||= []
+        grouped[selected_version.project.name] << [h(selected_version.name), selected_version.id]
+      else
+        grouped[l(:text_not_authorized)] ||=[]
+        grouped[l(:text_not_authorized)] << [l(:text_not_authorized), selected_version.id]
+      end
+    end
+    
+    grouped_options_for_select(grouped, selected)
+  end
+
   def show_detail(detail, no_html=false)
     case detail.property
     when 'attr'
@@ -91,8 +121,8 @@ module IssuesHelper
         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?
diff --git a/app/helpers/mailer_helper.rb b/app/helpers/mailer_helper.rb
new file mode 100644
index 0000000..a4ff772
--- /dev/null
+++ b/app/helpers/mailer_helper.rb
@@ -0,0 +1,25 @@
+# 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.
+
+module MailerHelper
+  def unauthorized_version?(journal_detail, recipients)
+    return false unless journal_detail.prop_key == "fixed_version_id"
+
+    !recipients_all_allowed_to_see_versions?(recipients,
+                                             Version.find_all_by_id([journal_detail.value, journal_detail.old_value]))
+  end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 07ba23d..e76c42c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -18,7 +18,11 @@
 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
+    if User.current.allowed_to?(:view_issues, version.project)
+      link_to format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
+    else
+      format_version_name(version)
+    end
   end
   
   def project_settings_tabs
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d279f3c..0184a6a 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -100,7 +100,10 @@ class Issue < ActiveRecord::Base
         # 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
@@ -234,7 +237,9 @@ class Issue < ActiveRecord::Base
   
   # 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_visible_to_user(User.current).select {|v| v.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
@@ -318,6 +323,23 @@ class Issue < ActiveRecord::Base
     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
   
diff --git a/app/models/mailer.rb b/app/models/mailer.rb
index 3d5231d..8fd1466 100644
--- a/app/models/mailer.rb
+++ b/app/models/mailer.rb
@@ -20,6 +20,7 @@ class Mailer < ActionMailer::Base
   helper :application
   helper :issues
   helper :custom_fields
+  helper :mailer
 
   include ActionController::UrlWriter
   include Redmine::I18n
diff --git a/app/models/project.rb b/app/models/project.rb
index 8829f04..1d98588 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -297,6 +297,7 @@ class Project < ActiveRecord::Base
         # 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
@@ -324,6 +325,29 @@ class Project < ActiveRecord::Base
     end
   end
   
+  # Returns an array of the Versions used by the project, its active
+  # sub projects, and its active parent projects
+  def shared_versions
+    unless @shared_versions
+      @shared_versions = Version.systemwide_versions
+      @shared_versions += Version.hierarchy_versions(active_projects_in_hierarchy.collect(&:id))
+      @shared_versions += versions
+
+      @shared_versions.uniq!
+      @shared_versions.sort!
+    end
+
+    yield @shared_versions if block_given?
+    @shared_versions
+  end
+
+  # Returns the shared_versions that are visible to +user+
+  def shared_versions_visible_to_user(user=User.current)
+    return shared_versions do |versions|
+      versions.delete_if {|v| !user.allowed_to?(:view_issues, v.project) }
+    end
+  end
+
   # Returns a hash of project users grouped by role
   def users_by_role
     members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
@@ -600,4 +624,8 @@ class Project < ActiveRecord::Base
         self.time_entry_activities.active
     end
   end
+
+  def active_projects_in_hierarchy
+    self.root.self_and_descendants.delete_if {|p| !p.active? }
+  end
 end
diff --git a/app/models/query.rb b/app/models/query.rb
index f7aeb0e..2e1680a 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -200,8 +200,8 @@ class Query < ActiveRecord::Base
       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] } }
diff --git a/app/models/version.rb b/app/models/version.rb
index e63ed46..e5ef314 100644
--- a/app/models/version.rb
+++ b/app/models/version.rb
@@ -16,7 +16,14 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 class Version < ActiveRecord::Base
+  SharedValues = {
+    "none" => :label_version_shared_none,
+    "hierarchy" => :label_version_shared_hierarchy,
+    "system" => :label_version_shared_systemwide
+  }
+
   before_destroy :check_integrity
+  after_save :update_issue_versions
   belongs_to :project
   has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
   acts_as_customizable
@@ -30,9 +37,21 @@ class Version < ActiveRecord::Base
   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 :shared, :in => SharedValues.keys
 
   named_scope :open, :conditions => {:status => 'open'}
-    
+  named_scope :systemwide_versions, :conditions => ["#{Version.table_name}.shared = ?", 'system' ]
+  named_scope :hierarchy_versions, lambda {|project_ids|
+    {
+      :conditions => ["#{Version.table_name}.shared = ? AND #{Version.table_name}.project_id IN (?)",
+                      'hierarchy', project_ids]
+    }
+  }
+
+  def systemwide?
+    shared == 'system'
+  end
+  
   def start_date
     effective_date
   end
@@ -54,6 +73,10 @@ class Version < ActiveRecord::Base
   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?
@@ -124,6 +147,11 @@ 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
+    Issue.update_fixed_versions_from_project_hierarchy_change
+  end
   
   # Returns the average estimated time of assigned issues
   # or 1 if no issue has an estimated time
diff --git a/app/views/issues/_attributes.rhtml b/app/views/issues/_attributes.rhtml
index b27c562..687f2cf 100644
--- a/app/views/issues/_attributes.rhtml
+++ b/app/views/issues/_attributes.rhtml
@@ -18,9 +18,9 @@
                      :title => l(:label_issue_category_new), 
                      :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>
-<% end %>
+<p><%= f.select(:fixed_version_id, 
+                              (shared_version_options(@project, @issue.fixed_version_id)),
+                              { :include_blank => true }) unless @project.shared_versions.empty? %></p>
 </div>
 
 <div class="splitcontentright">
diff --git a/app/views/issues/bulk_edit.rhtml b/app/views/issues/bulk_edit.rhtml
index de82e18..da95abd 100644
--- a/app/views/issues/bulk_edit.rhtml
+++ b/app/views/issues/bulk_edit.rhtml
@@ -31,7 +31,7 @@
 <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>
+                                   options_for_select(shared_version_options(@project))) %></label>
 </p>
 
 <p>
diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml
index d2c673d..7b28bee 100644
--- a/app/views/issues/context_menu.rhtml
+++ b/app/views/issues/context_menu.rhtml
@@ -38,12 +38,12 @@
 		<% end -%>
 		</ul>
 	</li>
-	<% unless @project.nil? || @project.versions.open.empty? -%>
+	<% unless @project.nil? || @project.shared_versions.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.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,
diff --git a/app/views/mailer/issue_edit.text.html.rhtml b/app/views/mailer/issue_edit.text.html.rhtml
index b4a1f95..63f083e 100644
--- a/app/views/mailer/issue_edit.text.html.rhtml
+++ b/app/views/mailer/issue_edit.text.html.rhtml
@@ -2,7 +2,11 @@
 
 <ul>
 <% for detail in @journal.details %>
+<% if unauthorized_version?(detail, @recipients + @cc) %>
+    <li><%= l(:text_journal_set_to, :label => l(:field_fixed_version), :value => l(:text_not_authorized)) %></li>
+<% else %>
     <li><%= show_detail(detail, true) %></li>
+    <% end %>
 <% end %>
 </ul>
 
diff --git a/app/views/mailer/issue_edit.text.plain.rhtml b/app/views/mailer/issue_edit.text.plain.rhtml
index 870f8fa..fb114c9 100644
--- a/app/views/mailer/issue_edit.text.plain.rhtml
+++ b/app/views/mailer/issue_edit.text.plain.rhtml
@@ -1,7 +1,11 @@
 <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
 
 <% for detail in @journal.details -%>
+<% if unauthorized_version?(detail, @recipients + @cc) %>
+<%= l(:text_journal_set_to, :label => l(:field_fixed_version), :value => l(:text_not_authorized)) %>
+<% else %>
 <%= show_detail(detail, true) %>
+<% end %>
 <% end -%>
 
 <%= @journal.notes if @journal.notes? %>
diff --git a/app/views/projects/changelog.rhtml b/app/views/projects/changelog.rhtml
index a1cd99b..d680455 100644
--- a/app/views/projects/changelog.rhtml
+++ b/app/views/projects/changelog.rhtml
@@ -33,6 +33,8 @@
   <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %>
   <%= tracker.name %></label><br />
 <% end %>
+<br />
+<label for="shared_versions"><%= check_box_tag "shared_versions", 1, params[:shared_versions] %> <%= l(:label_show_shared_versions) %></label>
 <p><%= submit_tag l(:button_apply), :class => 'button-small' %></p>
 <% end %>
 
diff --git a/app/views/projects/list_files.rhtml b/app/views/projects/list_files.rhtml
index 2b2e5e8..70e36fe 100644
--- a/app/views/projects/list_files.rhtml
+++ b/app/views/projects/list_files.rhtml
@@ -21,7 +21,7 @@
 	<% if container.is_a?(Version) -%>
   <tr>
   	<th colspan="6" align="left">
-  		<%= link_to(h(container), {:controller => 'versions', :action => 'show', :id => container}, :class => "icon icon-package") %>
+  		<%= link_to(h(container.project.name) + ' - ' + h(container), {:controller => 'versions', :action => 'show', :id => container}, :class => "icon icon-package") %>
 		</th>
 	</tr>
 	<% end -%>
diff --git a/app/views/projects/roadmap.rhtml b/app/views/projects/roadmap.rhtml
index 00d4a21..f2aca2d 100644
--- a/app/views/projects/roadmap.rhtml
+++ b/app/views/projects/roadmap.rhtml
@@ -6,7 +6,7 @@
 <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 h(version.project.name) + " - " + h(version.name), :controller => 'versions', :action => 'show', :id => version %></h3>
     <%= render :partial => 'versions/overview', :locals => {:version => version} %>
     <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
 
@@ -39,6 +39,8 @@
 <% end %>
 <br />
 <label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label>
+<br />
+<label for="shared_versions"><%= check_box_tag "shared_versions", 1, params[:shared_versions] %> <%= l(:label_show_shared_versions) %></label>
 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
 <% end %>
 
diff --git a/app/views/projects/settings/_versions.rhtml b/app/views/projects/settings/_versions.rhtml
index 9bca58c..628fbf7 100644
--- a/app/views/projects/settings/_versions.rhtml
+++ b/app/views/projects/settings/_versions.rhtml
@@ -5,6 +5,7 @@
     <th><%= l(:field_effective_date) %></th>
     <th><%= l(:field_description) %></th>
     <th><%= l(:field_status) %></th>
+    <th><%= l(:field_shared) %></th>
     <th><%= l(:label_wiki_page) unless @project.wiki.nil? %></th>
     <th style="width:15%"></th>
     </thead>
@@ -15,6 +16,7 @@
     <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_name(version) %></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' %>
diff --git a/app/views/versions/_form.rhtml b/app/views/versions/_form.rhtml
index 13579b8..c2ea0ea 100644
--- a/app/views/versions/_form.rhtml
+++ b/app/views/versions/_form.rhtml
@@ -6,8 +6,10 @@
 <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 :shared, Version::SharedValues.collect {|v| [l(v[1]),v[0]]} %></p>
 
 <% @version.custom_field_values.each do |value| %>
 	<p><%= custom_field_tag_with_label :version, value %></p>
 <% end %>
+
 </div>
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index e091f85..f8ed2a8 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -835,3 +835,9 @@ bg:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/bs.yml b/config/locales/bs.yml
index d6b1590..d09489e 100644
--- a/config/locales/bs.yml
+++ b/config/locales/bs.yml
@@ -859,3 +859,9 @@ bs:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 6aacce6..c42539f 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -838,3 +838,9 @@ ca:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 943fb69..4832bfa 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -841,3 +841,9 @@ cs:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/da.yml b/config/locales/da.yml
index 33acc45..ece9d12 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -861,3 +861,9 @@ da:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 29273f3..897088f 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -861,3 +861,9 @@ de:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/el.yml b/config/locales/el.yml
index 6907603..0b6938e 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -841,3 +841,9 @@ el:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 85234cb..3c1b70d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -267,6 +267,7 @@ en:
   field_identity_url: OpenID URL
   field_content: Content
   field_group_by: Group results by
+  field_shared: Shared
   
   setting_app_title: Application title
   setting_app_subtitle: Application subtitle
@@ -639,6 +640,7 @@ en:
   label_stay_logged_in: Stay logged in
   label_disabled: disabled
   label_show_completed_versions: Show completed versions
+  label_show_shared_versions: Show shared versions
   label_me: me
   label_board: Forum
   label_board_new: New forum
@@ -709,6 +711,9 @@ en:
   label_group_plural: Groups
   label_group_new: New group
   label_time_entry_plural: Spent time
+  label_version_shared_none: None
+  label_version_shared_hierarchy: "Parent and child projects"
+  label_version_shared_systemwide: "Systemwide"
   
   button_login: Login
   button_submit: Submit
@@ -814,6 +819,8 @@ en:
   text_wiki_page_nullify_children: "Keep child pages as root pages"
   text_wiki_page_destroy_children: "Delete child pages and all their descendants"
   text_wiki_page_reassign_children: "Reassign child pages to this parent page"
+  text_not_authorized: You are not authorized to view this.
+
   
   default_role_manager: Manager
   default_role_developper: Developer
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 07bb880..07eeac3 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -882,3 +882,9 @@ es:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 4501e63..c525780 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -871,3 +871,9 @@ fi:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 2e2a69b..370df5a 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -864,3 +864,9 @@ fr:
   field_active: Actif
   enumeration_system_activity: Activité système
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index f2af3ed..2884a8d 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -861,3 +861,9 @@ gl:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/he.yml b/config/locales/he.yml
index cae31b3..6cb4547 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -845,3 +845,9 @@ he:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index ef37ef2..3ded7a2 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -866,3 +866,9 @@
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 9460211..4548c40 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -848,3 +848,9 @@ it:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 450cff9..5f02e7d 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -870,3 +870,9 @@ ja:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 260d353..2e4239e 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -901,3 +901,9 @@ ko:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 5d4ff19..16bebe0 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -871,3 +871,9 @@ lt:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index f41e42b..f7a0399 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -823,3 +823,9 @@ nl:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/no.yml b/config/locales/no.yml
index d9f81cf..2006607 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -836,3 +836,9 @@
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 6227892..a94dfd6 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -864,3 +864,9 @@ pl:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 2019c49..8728b30 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -867,3 +867,9 @@ pt-BR:
   button_move_and_follow: Mover e seguir
   setting_default_projects_modules: Módulos habilitados por padrão para novos projetos
   setting_gravatar_default: Imagem Gravatar padrão
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 3b3f104..c33a8cd 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -853,3 +853,9 @@ pt:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/ro.yml b/config/locales/ro.yml
index 19e5a5c..2d331cf 100644
--- a/config/locales/ro.yml
+++ b/config/locales/ro.yml
@@ -838,3 +838,9 @@ ro:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 7555af4..0364ec7 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -949,3 +949,9 @@ ru:
   button_move_and_follow: Переместить и перейти
   setting_default_projects_modules: Включенные по умолчанию модули для новых проектов
   setting_gravatar_default: Изображение Gravatar по умолчанию
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index af8eab1..fc78cae 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -840,3 +840,9 @@ sk:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index 56cd421..194b52c 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -837,3 +837,9 @@ sl:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 3227463..4401191 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -856,3 +856,9 @@
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index a4698fc..29499cd 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -889,3 +889,9 @@ sv:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 7b41412..d1c4815 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -838,3 +838,9 @@ th:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index a0a1c26..376aff7 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -868,3 +868,9 @@ tr:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 7d8d1a9..02460c5 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -837,3 +837,9 @@ uk:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index fef14e4..e8c9a75 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -900,3 +900,9 @@ vi:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 7eab6fa..b083123 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -932,3 +932,9 @@
   enumeration_doc_categories: 文件分類
   enumeration_activities: 活動 (時間追蹤)
   enumeration_system_activity: 系統活動
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index a40851b..36368f4 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -865,3 +865,9 @@ zh:
   button_move_and_follow: Move and follow
   setting_default_projects_modules: Default enabled modules for new projects
   setting_gravatar_default: Default Gravatar image
+  label_version_shared_none: None
+  text_not_authorized: You are not authorized to view this.
+  field_shared: Shared
+  label_version_shared_systemwide: Systemwide
+  label_show_shared_versions: Show shared versions
+  label_version_shared_hierarchy: Parent and child projects
diff --git a/db/migrate/20090907153556_add_shared_to_versions.rb b/db/migrate/20090907153556_add_shared_to_versions.rb
new file mode 100644
index 0000000..f8407ce
--- /dev/null
+++ b/db/migrate/20090907153556_add_shared_to_versions.rb
@@ -0,0 +1,10 @@
+class AddSharedToVersions < ActiveRecord::Migration
+  def self.up
+    add_column :versions, :shared, :string, :default => 'none', :null => false
+    add_index :versions, :shared
+  end
+
+  def self.down
+    remove_column :versions, :shared
+  end
+end
diff --git a/db/migrate/20090907170038_populate_version_shared.rb b/db/migrate/20090907170038_populate_version_shared.rb
new file mode 100644
index 0000000..114eb14
--- /dev/null
+++ b/db/migrate/20090907170038_populate_version_shared.rb
@@ -0,0 +1,9 @@
+class PopulateVersionShared < ActiveRecord::Migration
+  def self.up
+    Version.update_all('shared = \'none\'', ['shared IS NULL OR shared = ?',''])
+  end
+
+  def self.down
+    # No-op
+  end
+end
diff --git a/lib/redmine.rb b/lib/redmine.rb
index 8dbe5d0..71a1f15 100644
--- a/lib/redmine.rb
+++ b/lib/redmine.rb
@@ -142,7 +142,7 @@ Redmine::MenuManager.map :project_menu do |menu|
   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_visible_to_user.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) }
diff --git a/test/exemplars/version_exemplar.rb b/test/exemplars/version_exemplar.rb
index 8aef99f..33e7c3a 100644
--- a/test/exemplars/version_exemplar.rb
+++ b/test/exemplars/version_exemplar.rb
@@ -1,5 +1,6 @@
 class Version < ActiveRecord::Base
   generator_for :name, :method => :next_name
+  generator_for :status => 'open'
   
   def self.next_name
     @last_name ||= 'Version 1.0.0'
diff --git a/test/fixtures/attachments.yml b/test/fixtures/attachments.yml
index c6493bd..002bb09 100644
--- a/test/fixtures/attachments.yml
+++ b/test/fixtures/attachments.yml
@@ -133,4 +133,15 @@ attachments_011:
   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
diff --git a/test/fixtures/issues.yml b/test/fixtures/issues.yml
index 0319745..1cc7837 100644
--- a/test/fixtures/issues.yml
+++ b/test/fixtures/issues.yml
@@ -189,3 +189,17 @@ issues_012:
   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
diff --git a/test/fixtures/journal_details.yml b/test/fixtures/journal_details.yml
index 38e6bf1..046202d 100644
--- a/test/fixtures/journal_details.yml
+++ b/test/fixtures/journal_details.yml
@@ -13,3 +13,10 @@ journal_details_002:
   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
diff --git a/test/fixtures/journals.yml b/test/fixtures/journals.yml
index 5c0b702..2323c1a 100644
--- a/test/fixtures/journals.yml
+++ b/test/fixtures/journals.yml
@@ -20,4 +20,10 @@ journals_003:
   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
diff --git a/test/fixtures/members.yml b/test/fixtures/members.yml
index 65e3fd6..504d64f 100644
--- a/test/fixtures/members.yml
+++ b/test/fixtures/members.yml
@@ -42,4 +42,9 @@ members_007:
   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
diff --git a/test/fixtures/versions.yml b/test/fixtures/versions.yml
index bcecb67..c9e6fd4 100644
--- a/test/fixtures/versions.yml
+++ b/test/fixtures/versions.yml
@@ -1,29 +1,71 @@
---- 
-versions_001: 
-  created_on: 2006-07-19 21:00:07 +02:00
-  name: "0.1"
-  project_id: 1
-  updated_on: 2006-07-19 21:00:07 +02:00
-  id: 1
-  description: Beta
-  effective_date: 2006-07-01
-  status: closed
-versions_002: 
-  created_on: 2006-07-19 21:00:33 +02:00
-  name: "1.0"
-  project_id: 1
-  updated_on: 2006-07-19 21:00:33 +02:00
-  id: 2
-  description: Stable release
-  effective_date: <%= 20.day.from_now.to_date.to_s(:db) %>
-  status: locked
-versions_003: 
-  created_on: 2006-07-19 21:00:33 +02:00
-  name: "2.0"
-  project_id: 1
-  updated_on: 2006-07-19 21:00:33 +02:00
-  id: 3
-  description: Future version
-  effective_date: 
-  status: open
-  
\ No newline at end of file
+--- 
+versions_001: 
+  created_on: 2006-07-19 21:00:07 +02:00
+  name: "0.1"
+  project_id: 1
+  updated_on: 2006-07-19 21:00:07 +02:00
+  id: 1
+  description: Beta
+  effective_date: 2006-07-01
+  status: closed
+  shared: 'none'
+versions_002: 
+  created_on: 2006-07-19 21:00:33 +02:00
+  name: "1.0"
+  project_id: 1
+  updated_on: 2006-07-19 21:00:33 +02:00
+  id: 2
+  description: Stable release
+  effective_date: <%= 20.day.from_now.to_date.to_s(:db) %>
+  status: locked
+  shared: 'none'
+versions_003: 
+  created_on: 2006-07-19 21:00:33 +02:00
+  name: "2.0"
+  project_id: 1
+  updated_on: 2006-07-19 21:00:33 +02:00
+  id: 3
+  description: Future version
+  effective_date: 
+  status: open
+  shared: '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
+  shared: 'hierarchy'
+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
+  shared: '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
+  shared: 'hierarchy'
+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
+  shared: 'system'
diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb
index 93e2995..768189f 100644
--- a/test/functional/issues_controller_test.rb
+++ b/test/functional/issues_controller_test.rb
@@ -925,6 +925,22 @@ class IssuesControllerTest < ActionController::TestCase
     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]
@@ -1005,6 +1021,21 @@ class IssuesControllerTest < ActionController::TestCase
     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'},
@@ -1080,6 +1111,17 @@ class IssuesControllerTest < ActionController::TestCase
     assert_tag :tag => 'a', :content => 'Immediate',
                             :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
                                              :class => '' }
+    # Versions
+    assert_tag :tag => 'a', :content => 'eCookbook - 0.1',
+                            :attributes => { :href => '/issues/bulk_edit?fixed_version_id=1&amp;ids%5B%5D=1',
+                                             :class => '' }
+    assert_tag :tag => 'a', :content => 'eCookbook - 1.0',
+                            :attributes => { :href => '/issues/bulk_edit?fixed_version_id=2&amp;ids%5B%5D=1',
+                                             :class => '' }
+    assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
+                            :attributes => { :href => '/issues/bulk_edit?fixed_version_id=4&amp;ids%5B%5D=1',
+                                             :class => '' }
+
     assert_tag :tag => 'a', :content => 'Dave Lopper',
                             :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
                                              :class => '' }
diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb
index 6704d4f..1c49d0e 100644
--- a/test/functional/projects_controller_test.rb
+++ b/test/functional/projects_controller_test.rb
@@ -343,6 +343,16 @@ class ProjectsControllerTest < ActionController::TestCase
                    :attributes => { :href => '/attachments/download/9/version_file.zip' }
   end
 
+  def test_list_files_with_shared_versions
+    get :list_files, :id => 1
+    assert_response :success
+    assert_not_nil assigns(:containers)
+    
+    # file attached to a subproject's version
+    assert_tag :a, :content => 'version_file.zip',
+                   :attributes => { :href => '/attachments/download/12/version_file.zip' }
+  end
+
   def test_list_files_routing
     assert_routing(
       {:method => :get, :path => '/projects/33/files'},
@@ -364,6 +374,15 @@ class ProjectsControllerTest < ActionController::TestCase
     assert_not_nil assigns(:versions)
   end
   
+  def test_changelog_showing_shared_versions
+    get :changelog, :id => 1, :shared_versions => 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'},
@@ -392,6 +411,15 @@ class ProjectsControllerTest < ActionController::TestCase
     # Completed version appears
     assert assigns(:versions).include?(Version.find(1))
   end
+
+  def test_roadmap_showing_shared_versions
+    get :roadmap, :id => 1, :shared_versions => 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(
diff --git a/test/integration/account_test.rb b/test/integration/account_test.rb
index c612ea2..f82e681 100644
--- a/test/integration/account_test.rb
+++ b/test/integration/account_test.rb
@@ -24,7 +24,7 @@ rescue
 end
 
 class AccountTest < ActionController::IntegrationTest
-  fixtures :users
+  fixtures :users, :roles
 
   # Replace this with your real tests.
   def test_login
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 07d4af7..4b8e03d 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -49,6 +49,7 @@ class ActiveSupport::TestCase
   # 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
diff --git a/test/unit/enumeration_test.rb b/test/unit/enumeration_test.rb
index abb1535..d9d3307 100644
--- a/test/unit/enumeration_test.rb
+++ b/test/unit/enumeration_test.rb
@@ -25,7 +25,7 @@ class EnumerationTest < ActiveSupport::TestCase
   
   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
@@ -79,7 +79,7 @@ class EnumerationTest < ActiveSupport::TestCase
   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
diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb
index 90eaed4..fecf2b2 100644
--- a/test/unit/helpers/application_helper_test.rb
+++ b/test/unit/helpers/application_helper_test.rb
@@ -32,6 +32,21 @@ class ApplicationHelperTest < HelperTestCase
   def setup
     super
   end
+
+  def test_format_version_name_is_authorized
+    User.current = User.find(1)
+    assert_equal "eCookbook - 0.1", format_version_name(Version.find(1))
+  end
+  
+  def test_format_version_name_is_unauthorized
+    User.current = User.find(3)
+    assert_equal "You are not authorized to view this.", format_version_name(Version.find(5))
+  end
+
+  def test_format_version_name_for_system_version
+    User.current = User.find(3)
+    assert_equal "OnlineStore - Systemwide visible version", format_version_name(Version.find(7))
+  end
   
   def test_auto_links
     to_test = {
@@ -135,7 +150,7 @@ RAW
   
   def test_redmine_links
     issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3}, 
-                               :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
+                               :class => 'issue status-1 priority-1 overdue assigned-to-me', :title => 'Error 281 when updating a recipe (New)')
     
     changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
                                    :class => 'changeset', :title => 'My very first commit')
diff --git a/test/unit/helpers/projects_helper_test.rb b/test/unit/helpers/projects_helper_test.rb
new file mode 100644
index 0000000..3b379a9
--- /dev/null
+++ b/test/unit/helpers/projects_helper_test.rb
@@ -0,0 +1,44 @@
+# 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')
+  end
+
+  def test_link_to_version
+    User.current = User.find(1)
+    assert_equal '<a href="/versions/show/1">eCookbook - 0.1</a>', link_to_version(Version.find(1))
+  end
+
+  def test_link_to_version_invalid_version
+    assert_equal '', link_to_version(Object)
+  end
+
+  def test_link_to_version_unauthorized
+    User.current = User.find(3)
+    assert_equal "You are not authorized to view this.", link_to_version(Version.find(5))
+  end
+end
diff --git a/test/unit/issue_priority_test.rb b/test/unit/issue_priority_test.rb
index 6574bf3..51a6b82 100644
--- a/test/unit/issue_priority_test.rb
+++ b/test/unit/issue_priority_test.rb
@@ -26,7 +26,7 @@ class IssuePriorityTest < ActiveSupport::TestCase
   
   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
diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb
index afde6c7..5adad60 100644
--- a/test/unit/issue_test.rb
+++ b/test/unit/issue_test.rb
@@ -329,6 +329,46 @@ class IssueTest < ActiveSupport::TestCase
     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
diff --git a/test/unit/mailer_test.rb b/test/unit/mailer_test.rb
index b8ff4f0..4395156 100644
--- a/test/unit/mailer_test.rb
+++ b/test/unit/mailer_test.rb
@@ -256,4 +256,23 @@ class MailerTest < ActiveSupport::TestCase
     assert mail.bcc.include?('dlopper@somenet.foo')
     assert mail.body.include?('Bug #3: Error 281 when updating a recipe')
   end
+
+  def test_issue_edit_should_not_show_private_version_changes_if_any_recipients_are_unauthorized
+    Setting.default_language = 'en'
+    # Move issue to a public project but with a Journal showing a
+    # Private Version
+    issue = Issue.find(6)
+    issue.update_attribute(:project_id, 1)
+
+    # User is allowed to see the Private Version
+    User.current = User.find(1)
+    # but another Member of Project #1 is not (dlopper@somenet.foo)
+
+    journal = Journal.find(4)
+    Mailer.deliver_issue_edit(journal)
+    mail = ActionMailer::Base.deliveries.last
+    
+    assert !mail.body.include?('Private Version of public subproject'), "Private version exposed to unauthorized user"
+    assert mail.body.include?('You are not authorized to view this.'), "Unauthorized message not displayed"
+  end
 end
diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb
index 93e4bbf..8dbd8ce 100644
--- a/test/unit/project_test.rb
+++ b/test/unit/project_test.rb
@@ -21,7 +21,7 @@ class ProjectTest < ActiveSupport::TestCase
   fixtures :projects, :enabled_modules, 
            :issues, :issue_statuses, :journals, :journal_details,
            :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
-           :queries
+           :queries, :versions
 
   def setup
     @ecookbook = Project.find(1)
@@ -206,6 +206,38 @@ class ProjectTest < ActiveSupport::TestCase
     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
@@ -277,13 +309,61 @@ class ProjectTest < ActiveSupport::TestCase
     
     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_shared('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_to_user(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
@@ -429,13 +509,15 @@ class ProjectTest < ActiveSupport::TestCase
     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
diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb
index 31d3cd1..2044747 100644
--- a/test/unit/query_test.rb
+++ b/test/unit/query_test.rb
@@ -31,6 +31,14 @@ class QueryTest < ActiveSupport::TestCase
       :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)
diff --git a/test/unit/version_test.rb b/test/unit/version_test.rb
index 5702fd1..f05b7ed 100644
--- a/test/unit/version_test.rb
+++ b/test/unit/version_test.rb
@@ -104,6 +104,47 @@ class VersionTest < ActiveSupport::TestCase
     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
+
+  def test_systemwide
+    assert !Version.find(1).systemwide? # not shared
+    assert !Version.find(4).systemwide? # hierarchy
+    assert Version.find(7).systemwide? # system
+  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.shared = '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
   
-- 
1.6.5

