diff options
author | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2012-06-25 17:49:35 +0000 |
---|---|---|
committer | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2012-06-25 17:49:35 +0000 |
commit | ac56c0c99ccd14c7229145fc22d6e9eb13ee0af0 (patch) | |
tree | f54401b77f7195a1795f4a189f9f9d35734c0a2b | |
parent | 5961a1e70d1efdfb5c4fd28c20dc8cc4d9a51bac (diff) | |
download | redmine-ac56c0c99ccd14c7229145fc22d6e9eb13ee0af0.tar.gz redmine-ac56c0c99ccd14c7229145fc22d6e9eb13ee0af0.zip |
Ability to close projects (read-only) (#3640).
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
-rw-r--r-- | app/controllers/application_controller.rb | 2 | ||||
-rw-r--r-- | app/controllers/projects_controller.rb | 16 | ||||
-rw-r--r-- | app/helpers/admin_helper.rb | 4 | ||||
-rw-r--r-- | app/helpers/application_helper.rb | 8 | ||||
-rw-r--r-- | app/models/principal.rb | 2 | ||||
-rw-r--r-- | app/models/project.rb | 35 | ||||
-rw-r--r-- | app/models/user.rb | 6 | ||||
-rw-r--r-- | app/views/admin/projects.html.erb | 6 | ||||
-rw-r--r-- | app/views/projects/index.html.erb | 8 | ||||
-rw-r--r-- | app/views/projects/show.html.erb | 13 | ||||
-rw-r--r-- | config/locales/en.yml | 9 | ||||
-rw-r--r-- | config/locales/fr.yml | 9 | ||||
-rw-r--r-- | config/routes.rb | 2 | ||||
-rw-r--r-- | lib/redmine.rb | 34 | ||||
-rw-r--r-- | lib/redmine/access_control.rb | 15 | ||||
-rw-r--r-- | lib/redmine/plugin.rb | 8 | ||||
-rw-r--r-- | test/fixtures/roles.yml | 1 | ||||
-rw-r--r-- | test/functional/projects_controller_test.rb | 47 | ||||
-rw-r--r-- | test/integration/routing/projects_test.rb | 8 | ||||
-rw-r--r-- | test/unit/lib/redmine/access_control_test.rb | 10 | ||||
-rw-r--r-- | test/unit/user_test.rb | 12 |
21 files changed, 213 insertions, 42 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 17968bf79..645e8389b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c89167aa3..492e31795 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -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 diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 01f394dd1..bacc7b8ea 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 794cc6f54..8c328326f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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>" + diff --git a/app/models/principal.rb b/app/models/principal.rb index c4968016a..2b89e16a9 100644 --- a/app/models/principal.rb +++ b/app/models/principal.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 1b8f30db3..966806ca9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index d0d1df834..1b7a8ea4f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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? diff --git a/app/views/admin/projects.html.erb b/app/views/admin/projects.html.erb index 6525f8ea5..1d8bcc002 100644 --- a/app/views/admin/projects.html.erb +++ b/app/views/admin/projects.html.erb @@ -27,12 +27,12 @@ <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> diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 42b400ff7..3d527917a 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -25,4 +25,12 @@ <%= 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)) -%> diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 4963ebbe4..7bd37fc7b 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -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> diff --git a/config/locales/en.yml b/config/locales/en.yml index 05ef97b0e..66d1f4952 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 64b65c258..f05afbb48 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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 " diff --git a/config/routes.rb b/config/routes.rb index 3e89a88e4..e79d0226f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/lib/redmine.rb b/lib/redmine.rb index 6b74af731..1b9be57f5 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -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 diff --git a/lib/redmine/access_control.rb b/lib/redmine/access_control.rb index 3845fa5e7..b4f3cca73 100644 --- a/lib/redmine/access_control.rb +++ b/lib/redmine/access_control.rb @@ -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 diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb index 7805c73fb..c3f41501c 100644 --- a/lib/redmine/plugin.rb +++ b/lib/redmine/plugin.rb @@ -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 } diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml index 0eaaafe72..78b35c16e 100644 --- a/test/fixtures/roles.yml +++ b/test/fixtures/roles.yml @@ -8,6 +8,7 @@ roles_001: --- - :add_project - :edit_project + - :close_project - :select_project_modules - :manage_members - :manage_versions diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 299a6e767..5fbb7e20e 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -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 diff --git a/test/integration/routing/projects_test.rb b/test/integration/routing/projects_test.rb index d53c0607c..2d4756f26 100644 --- a/test/integration/routing/projects_test.rb +++ b/test/integration/routing/projects_test.rb @@ -70,6 +70,14 @@ class RoutingProjectsTest < ActionController::IntegrationTest { :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' } ) diff --git a/test/unit/lib/redmine/access_control_test.rb b/test/unit/lib/redmine/access_control_test.rb index 94c83e179..7c0f85475 100644 --- a/test/unit/lib/redmine/access_control_test.rb +++ b/test/unit/lib/redmine/access_control_test.rb @@ -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 diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 2ff55f9f3..51a49de01 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -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"] |