]> source.dussan.org Git - redmine.git/commitdiff
Ability to close projects (read-only) (#3640).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Mon, 25 Jun 2012 17:49:35 +0000 (17:49 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Mon, 25 Jun 2012 17:49:35 +0000 (17:49 +0000)
A new permission (Close/reopen project) is available to give non-admin users the ability to close their projects.

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@9883 e93f8b46-1217-0410-a6f0-8f06a7374b81

21 files changed:
app/controllers/application_controller.rb
app/controllers/projects_controller.rb
app/helpers/admin_helper.rb
app/helpers/application_helper.rb
app/models/principal.rb
app/models/project.rb
app/models/user.rb
app/views/admin/projects.html.erb
app/views/projects/index.html.erb
app/views/projects/show.html.erb
config/locales/en.yml
config/locales/fr.yml
config/routes.rb
lib/redmine.rb
lib/redmine/access_control.rb
lib/redmine/plugin.rb
test/fixtures/roles.yml
test/functional/projects_controller_test.rb
test/integration/routing/projects_test.rb
test/unit/lib/redmine/access_control_test.rb
test/unit/user_test.rb

index 17968bf799c76b8d7791aeb71f7a7ba400fd76ed..645e8389b9130bc2ea5a52942ca575a878acd294 100644 (file)
@@ -276,7 +276,7 @@ class ApplicationController < ActionController::Base
   # make sure that the user is a member of the project (or admin) if project is private
   # used as a before_filter for actions that do not require any particular permission on the project
   def check_project_privacy
-    if @project && @project.active?
+    if @project && !@project.archived?
       if @project.visible?
         true
       else
index c89167aa32fc36f840484ae1c3b40435172f3421..492e31795c8f35aa3860de776b3d76621ec7cc81 100644 (file)
@@ -48,7 +48,11 @@ class ProjectsController < ApplicationController
   def index
     respond_to do |format|
       format.html {
-        @projects = Project.visible.find(:all, :order => 'lft')
+        scope = Project
+        unless params[:closed]
+          scope = scope.active
+        end
+        @projects = scope.visible.order('lft').all
       }
       format.api  {
         @offset, @limit = api_offset_and_limit
@@ -224,6 +228,16 @@ class ProjectsController < ApplicationController
     redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
   end
 
+  def close
+    @project.close
+    redirect_to project_path(@project)
+  end
+
+  def reopen
+    @project.reopen
+    redirect_to project_path(@project)
+  end
+
   # Delete @project
   def destroy
     @project_to_destroy = @project
index 01f394dd1649eb00e83804bfe3c2fbeeecdfae90..bacc7b8eade597d1bad29313d199cb51a3db5e42 100644 (file)
@@ -20,6 +20,8 @@
 module AdminHelper
   def project_status_options_for_select(selected)
     options_for_select([[l(:label_all), ''],
-                        [l(:status_active), '1']], selected.to_s)
+                        [l(:project_status_active), '1'],
+                        [l(:project_status_closed), '5'],
+                        [l(:project_status_archived), '9']], selected.to_s)
   end
 end
index 794cc6f5402cf2b33c2d63eecccc0a0be8c9c01a..8c328326fe78708a54e66ce3fb07bb8321af36e4 100644 (file)
@@ -145,11 +145,11 @@ module ApplicationHelper
   #   link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
   #
   def link_to_project(project, options={}, html_options = nil)
-    if project.active?
+    if project.archived?
+      h(project)
+    else
       url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
       link_to(h(project), url, html_options)
-    else
-      h(project)
     end
   end
 
@@ -237,7 +237,7 @@ module ApplicationHelper
   # Renders the project quick-jump box
   def render_project_jump_box
     return unless User.current.logged?
-    projects = User.current.memberships.collect(&:project).compact.uniq
+    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
     if projects.any?
       s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
             "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
index c4968016a47a5c1feacbf638e3a3b53f18d0b946..2b89e16a977249938433c8cc8c3d89d1ae57cbfc 100644 (file)
@@ -19,7 +19,7 @@ class Principal < ActiveRecord::Base
   self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
 
   has_many :members, :foreign_key => 'user_id', :dependent => :destroy
-  has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
+  has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}", :order => "#{Project.table_name}.name"
   has_many :projects, :through => :memberships
   has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
 
index 1b8f30db38bb0531612150d71b698021476cf966..966806ca950e55d4fe16e0ba2689c54519b5fb12 100644 (file)
@@ -20,6 +20,7 @@ class Project < ActiveRecord::Base
 
   # Project statuses
   STATUS_ACTIVE     = 1
+  STATUS_CLOSED     = 5
   STATUS_ARCHIVED   = 9
 
   # Maximum length for project identifiers
@@ -161,12 +162,11 @@ class Project < ActiveRecord::Base
   # * :with_subprojects => limit the condition to project and its subprojects
   # * :member => limit the condition to the user projects
   def self.allowed_to_condition(user, permission, options={})
-    base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
-    if perm = Redmine::AccessControl.permission(permission)
-      unless perm.project_module.nil?
-        # If the permission belongs to a project module, make sure the module is enabled
-        base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
-      end
+    perm = Redmine::AccessControl.permission(permission)
+    base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
+    if perm && perm.project_module
+      # If the permission belongs to a project module, make sure the module is enabled
+      base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
     end
     if options[:project]
       project_statement = "#{Project.table_name}.id = #{options[:project].id}"
@@ -325,6 +325,14 @@ class Project < ActiveRecord::Base
     update_attribute :status, STATUS_ACTIVE
   end
 
+  def close
+    self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
+  end
+
+  def reopen
+    self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
+  end
+
   # Returns an array of projects the project can be moved to
   # by the current user
   def allowed_parents
@@ -404,7 +412,7 @@ class Project < ActiveRecord::Base
     @rolled_up_trackers ||=
       Tracker.find(:all, :joins => :projects,
                          :select => "DISTINCT #{Tracker.table_name}.*",
-                         :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
+                         :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt],
                          :order => "#{Tracker.table_name}.position")
   end
 
@@ -423,20 +431,20 @@ class Project < ActiveRecord::Base
   def rolled_up_versions
     @rolled_up_versions ||=
       Version.scoped(:include => :project,
-                     :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
+                     :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
   end
 
   # Returns a scope of the Versions used by the project
   def shared_versions
     if new_record?
       Version.scoped(:include => :project,
-                     :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'")
+                     :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
     else
       @shared_versions ||= begin
         r = root? ? self : root
         Version.scoped(:include => :project,
                        :conditions => "#{Project.table_name}.id = #{id}" +
-                                      " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
+                                      " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
                                           " #{Version.table_name}.sharing = 'system'" +
                                           " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
                                           " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
@@ -515,6 +523,13 @@ class Project < ActiveRecord::Base
     s << ' root' if root?
     s << ' child' if child?
     s << (leaf? ? ' leaf' : ' parent')
+    unless active?
+      if archived?
+        s << ' archived'
+      else
+        s << ' closed'
+      end
+    end
     s
   end
 
index d0d1df8348b0e63afbfeb9138a43d0b4ae2555b6..1b7a8ea4ff9dd7ee753f26128de76cf6ae5c0d20 100644 (file)
@@ -394,7 +394,7 @@ class User < Principal
   def roles_for_project(project)
     roles = []
     # No role on archived projects
-    return roles unless project && project.active?
+    return roles if project.nil? || project.archived?
     if logged?
       # Find project membership
       membership = memberships.detect {|m| m.project_id == project.id}
@@ -456,9 +456,11 @@ class User < Principal
   def allowed_to?(action, context, options={}, &block)
     if context && context.is_a?(Project)
       # No action allowed on archived projects
-      return false unless context.active?
+      return false if context.archived?
       # No action allowed on disabled modules
       return false unless context.allows_to?(action)
+      # No write action allowed on closed projects
+      return false unless context.active? || Redmine::AccessControl.read_action?(action)
       # Admin users are authorized for anything else
       return true if admin?
 
index 6525f8ea5baf55b243a30d8766fa30de5dafe97f..1d8bcc002d5a88e620644c926ca62ab149641d0f 100644 (file)
   <tbody>
 <% project_tree(@projects) do |project, level| %>
   <tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
-  <td class="name"><span><%= link_to_project(project, {:action => 'settings'}, :title => project.short_description) %></span></td>
+  <td class="name"><span><%= link_to_project(project, {:action => (project.active? ? 'settings' : 'show')}, :title => project.short_description) %></span></td>
   <td align="center"><%= checked_image project.is_public? %></td>
   <td align="center"><%= format_date(project.created_on) %></td>
   <td class="buttons">
-    <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
-    <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
+    <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') unless project.archived? %>
+    <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if project.archived? && (project.parent.nil? || !project.parent.archived?) %>
     <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
     <%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %>
   </td>
index 42b400ff7b12f5feb281c8ab03d6fb8a57929582..3d527917aefacd6eaa24c24437ea3cc4a04aad8b 100644 (file)
   <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
 <% end %>
 
+<% content_for :sidebar do %>
+  <%= form_tag({}, :method => :get) do %>
+    <h3><%= l(:label_project_plural) %></h3>
+    <label for="closed"><%= check_box_tag 'closed', 1, params[:closed] %> <%= l(:label_show_closed_projects) %></label>
+    <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
+  <% end %>
+<% end %>
+
 <% html_title(l(:label_project_plural)) -%>
index 4963ebbe47705047c7ecf4def472919494623fbf..7bd37fc7b0ec0507db19dd3f16936fa8a598b035 100644 (file)
@@ -2,14 +2,27 @@
   <% if User.current.allowed_to?(:add_subprojects, @project) %>
     <%= link_to l(:label_subproject_new), {:controller => 'projects', :action => 'new', :parent_id => @project}, :class => 'icon icon-add' %>
   <% end %>
+  <% if User.current.allowed_to?(:close_project, @project) %>
+    <% if @project.active? %>
+      <%= link_to l(:button_close), close_project_path(@project), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock' %>
+    <% else %>
+      <%= link_to l(:button_reopen), reopen_project_path(@project), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-unlock' %>
+    <% end %>
+  <% end %>
 </div>
 
 <h2><%=l(:label_overview)%></h2>
 
+<% unless @project.active? %>
+  <p class="warning"><span class="icon icon-lock"><%= l(:text_project_closed) %></span></p>
+<% end %>
+
 <div class="splitcontentleft">
+  <% if @project.description.present? %>
   <div class="wiki">
     <%= textilizable @project.description %>
   </div>
+  <% end %>
   <ul>
   <% unless @project.homepage.blank? %>
     <li><%=l(:field_homepage)%>: <%= link_to h(@project.homepage), @project.homepage %></li>
index 05ef97b0e365a51d5f150991fbff68967eeb6fb4..66d1f4952356a108c746bb53f9948052f8248a60 100644 (file)
@@ -395,6 +395,7 @@ en:
   permission_add_project: Create project
   permission_add_subprojects: Create subprojects
   permission_edit_project: Edit project
+  permission_close_project: Close / reopen the project
   permission_select_project_modules: Select project modules
   permission_manage_members: Manage members
   permission_manage_project_activities: Manage project activities
@@ -854,6 +855,7 @@ en:
   label_completed_versions: Completed versions
   label_search_for_watchers: Search for watchers to add
   label_session_expiration: Session expiration
+  label_show_closed_projects: View closed projects
 
   button_login: Login
   button_submit: Submit
@@ -904,11 +906,17 @@ en:
   button_edit_section: Edit this section
   button_export: Export
   button_delete_my_account: Delete my account
+  button_close: Close
+  button_reopen: Reopen
 
   status_active: active
   status_registered: registered
   status_locked: locked
 
+  project_status_active: active
+  project_status_closed: closed
+  project_status_archived: archived
+
   version_status_open: open
   version_status_locked: locked
   version_status_closed: closed
@@ -990,6 +998,7 @@ en:
   text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
   text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
   text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
+  text_project_closed: This project is closed and read-only.
 
   default_role_manager: Manager
   default_role_developer: Developer
index 64b65c258a7d1a18f4debf3759693d0477f256eb..f05afbb48bde7faf3df10b22726d44b00ee71452 100644 (file)
@@ -391,6 +391,7 @@ fr:
   permission_add_project: Créer un projet
   permission_add_subprojects: Créer des sous-projets
   permission_edit_project: Modifier le projet
+  permission_close_project: Fermer / réouvrir le projet
   permission_select_project_modules: Choisir les modules
   permission_manage_members: Gérer les membres
   permission_manage_versions: Gérer les versions
@@ -829,6 +830,7 @@ fr:
   label_item_position: "%{position} sur %{count}"
   label_completed_versions: Versions passées
   label_session_expiration: Expiration des sessions
+  label_show_closed_projects: Voir les projets fermés
 
   button_login: Connexion
   button_submit: Soumettre
@@ -878,11 +880,17 @@ fr:
   button_edit_section: Modifier cette section
   button_export: Exporter
   button_delete_my_account: Supprimer mon compte
+  button_close: Fermer
+  button_reopen: Réouvrir
 
   status_active: actif
   status_registered: enregistré
   status_locked: verrouillé
 
+  project_status_active: actif
+  project_status_closed: fermé
+  project_status_archived: archivé
+
   version_status_open: ouvert
   version_status_locked: verrouillé
   version_status_closed: fermé
@@ -946,6 +954,7 @@ fr:
   text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
   text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver."
   text_session_expiration_settings: "Attention : le changement de ces paramètres peut entrainer l'expiration des sessions utilisateurs en cours, y compris la vôtre."
+  text_project_closed: Ce projet est fermé et accessible en lecture seule.
 
   default_role_manager: "Manager "
   default_role_developer: "Développeur "
index 3e89a88e4df811e07f486c955f24dcc2f3dd04cd..e79d0226fa0f6e1d851606009f4d6f68bcc43a79 100644 (file)
@@ -93,6 +93,8 @@ RedmineApp::Application.routes.draw do
       post 'modules'
       post 'archive'
       post 'unarchive'
+      post 'close'
+      post 'reopen'
       match 'copy', :via => [:get, :post]
     end
 
index 6b74af7315593dab4344ba8685bf71c1f08ce967..1b9be57f5bf7fcc269364efdbc1ba9da6328fe67 100644 (file)
@@ -47,10 +47,11 @@ end
 
 # Permissions
 Redmine::AccessControl.map do |map|
-  map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
-  map.permission :search_project, {:search => :index}, :public => true
+  map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
+  map.permission :search_project, {:search => :index}, :public => true, :read => true
   map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
   map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
+  map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
   map.permission :select_project_modules, {:projects => :modules}, :require => :member
   map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :create, :update, :destroy, :autocomplete]}, :require => :member
   map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
@@ -66,7 +67,8 @@ Redmine::AccessControl.map do |map|
                                   :versions => [:index, :show, :status_by],
                                   :journals => [:index, :diff],
                                   :queries => :index,
-                                  :reports => [:issue_report, :issue_report_details]}
+                                  :reports => [:issue_report, :issue_report_details]},
+                                  :read => true
     map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
     map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
     map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
@@ -82,14 +84,14 @@ Redmine::AccessControl.map do |map|
     map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
     map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
     # Watchers
-    map.permission :view_issue_watchers, {}
+    map.permission :view_issue_watchers, {}, :read => true
     map.permission :add_issue_watchers, {:watchers => :new}
     map.permission :delete_issue_watchers, {:watchers => :destroy}
   end
 
   map.project_module :time_tracking do |map|
     map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
-    map.permission :view_time_entries, :timelog => [:index, :report, :show]
+    map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
     map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
     map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
     map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
@@ -97,27 +99,27 @@ Redmine::AccessControl.map do |map|
 
   map.project_module :news do |map|
     map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
-    map.permission :view_news, {:news => [:index, :show]}, :public => true
+    map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true
     map.permission :comment_news, {:comments => :create}
   end
 
   map.project_module :documents do |map|
     map.permission :manage_documents, {:documents => [:new, :create, :edit, :update, :destroy, :add_attachment]}, :require => :loggedin
-    map.permission :view_documents, :documents => [:index, :show, :download]
+    map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
   end
 
   map.project_module :files do |map|
     map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
-    map.permission :view_files, :files => :index, :versions => :download
+    map.permission :view_files, {:files => :index, :versions => :download}, :read => true
   end
 
   map.project_module :wiki do |map|
     map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
     map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
     map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
-    map.permission :view_wiki_pages, :wiki => [:index, :show, :special, :date_index]
-    map.permission :export_wiki_pages, :wiki => [:export]
-    map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
+    map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true
+    map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
+    map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true
     map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
     map.permission :delete_wiki_pages_attachments, {}
     map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
@@ -125,15 +127,15 @@ Redmine::AccessControl.map do |map|
 
   map.project_module :repository do |map|
     map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
-    map.permission :browse_repository, :repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]
-    map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
+    map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
+    map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true
     map.permission :commit_access, {}
     map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
   end
 
   map.project_module :boards do |map|
     map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
-    map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
+    map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true
     map.permission :add_messages, {:messages => [:new, :reply, :quote]}
     map.permission :edit_messages, {:messages => :edit}, :require => :member
     map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
@@ -142,11 +144,11 @@ Redmine::AccessControl.map do |map|
   end
 
   map.project_module :calendar do |map|
-    map.permission :view_calendar, :calendars => [:show, :update]
+    map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
   end
 
   map.project_module :gantt do |map|
-    map.permission :view_gantt, :gantts => [:show, :update]
+    map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
   end
 end
 
index 3845fa5e773551a17c1ebff395442468e77ce54e..b4f3cca7306ff6ec1a1e095c4b3e691d3c6c2fc3 100644 (file)
@@ -54,6 +54,16 @@ module Redmine
         @loggedin_only_permissions ||= @permissions.select {|p| p.require_loggedin?}
       end
 
+      def read_action?(action)
+        if action.is_a?(Symbol)
+          perm = permission(action)
+          !perm.nil? && perm.read?
+        else
+          s = "#{action[:controller]}/#{action[:action]}"
+          permissions.detect {|p| p.actions.include?(s) && !p.read?}.nil?
+        end
+      end
+
       def available_project_modules
         @available_project_modules ||= @permissions.collect(&:project_module).uniq.compact
       end
@@ -93,6 +103,7 @@ module Redmine
         @actions = []
         @public = options[:public] || false
         @require = options[:require]
+        @read = options[:read] || false
         @project_module = options[:project_module]
         hash.each do |controller, actions|
           if actions.is_a? Array
@@ -115,6 +126,10 @@ module Redmine
       def require_loggedin?
         @require && (@require == :member || @require == :loggedin)
       end
+
+      def read?
+        @read
+      end
     end
   end
 end
index 7805c73fb824f825f879d313bab05b1e4181cd44..c3f41501c24cc8a0906e63b386c491fdd7c1c5da 100644 (file)
@@ -244,13 +244,15 @@ module Redmine #:nodoc:
     #   permission :destroy_contacts, { :contacts => :destroy }
     #   permission :view_contacts, { :contacts => [:index, :show] }
     #
-    # The +options+ argument can be used to make the permission public (implicitly given to any user)
-    # or to restrict users the permission can be given to.
+    # The +options+ argument is a hash that accept the following keys:
+    # * :public => the permission is public if set to true (implicitly given to any user)
+    # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member
+    # * :read => set it to true so that the permission is still granted on closed projects
     #
     # Examples
     #   # A permission that is implicitly given to any user
     #   # This permission won't appear on the Roles & Permissions setup screen
-    #   permission :say_hello, { :example => :say_hello }, :public => true
+    #   permission :say_hello, { :example => :say_hello }, :public => true, :read => true
     #
     #   # A permission that can be given to any user
     #   permission :say_hello, { :example => :say_hello }
index 0eaaafe7230c64aac11f6a75389e4e470bf21092..78b35c16e28591190dd2ddacee8a2450c153f618 100644 (file)
@@ -8,6 +8,7 @@ roles_001:
     --- 
     - :add_project
     - :edit_project
+    - :close_project
     - :select_project_modules
     - :manage_members
     - :manage_versions
index 299a6e76716bcf4c4ff994853a6f5d4f30d5b1dd..5fbb7e20ebbb4858c7fec1336694bea0434e72b8 100644 (file)
@@ -380,6 +380,21 @@ class ProjectsControllerTest < ActionController::TestCase
     assert_template 'settings'
   end
 
+  def test_settings_should_be_denied_for_member_on_closed_project
+    Project.find(1).close
+    @request.session[:user_id] = 2 # manager
+
+    get :settings, :id => 1
+    assert_response 403
+  end
+
+  def test_settings_should_be_denied_for_anonymous_on_closed_project
+    Project.find(1).close
+
+    get :settings, :id => 1
+    assert_response 302
+  end
+
   def test_update
     @request.session[:user_id] = 2 # manager
     post :update, :id => 1, :project => {:name => 'Test changed name',
@@ -397,6 +412,23 @@ class ProjectsControllerTest < ActionController::TestCase
     assert_error_tag :content => /name can't be blank/i
   end
 
+  def test_update_should_be_denied_for_member_on_closed_project
+    Project.find(1).close
+    @request.session[:user_id] = 2 # manager
+
+    post :update, :id => 1, :project => {:name => 'Closed'}
+    assert_response 403
+    assert_equal 'eCookbook', Project.find(1).name
+  end
+
+  def test_update_should_be_denied_for_anonymous_on_closed_project
+    Project.find(1).close
+
+    post :update, :id => 1, :project => {:name => 'Closed'}
+    assert_response 302
+    assert_equal 'eCookbook', Project.find(1).name
+  end
+
   def test_modules
     @request.session[:user_id] = 2
     Project.find(1).enabled_module_names = ['issue_tracking', 'news']
@@ -444,6 +476,21 @@ class ProjectsControllerTest < ActionController::TestCase
     assert Project.find(1).active?
   end
 
+  def test_close
+    @request.session[:user_id] = 2
+    post :close, :id => 1
+    assert_redirected_to '/projects/ecookbook'
+    assert_equal Project::STATUS_CLOSED, Project.find(1).status
+  end
+
+  def test_reopen
+    Project.find(1).close
+    @request.session[:user_id] = 2
+    post :reopen, :id => 1
+    assert_redirected_to '/projects/ecookbook'
+    assert Project.find(1).active?
+  end
+
   def test_project_breadcrumbs_should_be_limited_to_3_ancestors
     CustomField.delete_all
     parent = nil
index d53c0607cc13757bf965a680b8ab130f8a95ac86..2d4756f266de5f8ce558c82519dc6f80c55e4611 100644 (file)
@@ -69,6 +69,14 @@ class RoutingProjectsTest < ActionController::IntegrationTest
         { :method => 'post', :path => "/projects/64/unarchive" },
         { :controller => 'projects', :action => 'unarchive', :id => '64' }
       )
+    assert_routing(
+        { :method => 'post', :path => "/projects/64/close" },
+        { :controller => 'projects', :action => 'close', :id => '64' }
+      )
+    assert_routing(
+        { :method => 'post', :path => "/projects/64/reopen" },
+        { :controller => 'projects', :action => 'reopen', :id => '64' }
+      )
     assert_routing(
         { :method => 'put', :path => "/projects/4223" },
         { :controller => 'projects', :action => 'update', :id => '4223' }
index 94c83e17996257285e7f4fd3c633d2e85bc82d79..7c0f8547520cf9e4e12400bc59f8c735db61278e 100644 (file)
@@ -46,4 +46,14 @@ class Redmine::AccessControlTest < ActiveSupport::TestCase
     assert perm.actions.is_a?(Array)
     assert perm.actions.include?('projects/settings')
   end
+
+  def test_read_action_should_return_true_for_read_actions
+    assert_equal true, @access_module.read_action?(:view_project)
+    assert_equal true, @access_module.read_action?(:controller => 'projects', :action => 'show')
+  end
+
+  def test_read_action_should_return_false_for_update_actions
+    assert_equal false, @access_module.read_action?(:edit_project)
+    assert_equal false, @access_module.read_action?(:controller => 'projects', :action => 'edit')
+  end
 end
index 2ff55f9f37237b96c33a590d87e3729b64de53a0..51a49de013534228bd78c0b0fd49fa28b5e3dd86 100644 (file)
@@ -874,6 +874,18 @@ class UserTest < ActiveSupport::TestCase
         assert ! @admin.allowed_to?(:view_issues, Project.find(1))
       end
 
+      should "return false for write action if project is closed" do
+        project = Project.find(1)
+        Project.any_instance.stubs(:status).returns(Project::STATUS_CLOSED)
+        assert ! @admin.allowed_to?(:edit_project, Project.find(1))
+      end
+
+      should "return true for read action if project is closed" do
+        project = Project.find(1)
+        Project.any_instance.stubs(:status).returns(Project::STATUS_CLOSED)
+        assert @admin.allowed_to?(:view_project, Project.find(1))
+      end
+
       should "return false if related module is disabled" do
         project = Project.find(1)
         project.enabled_module_names = ["issue_tracking"]