Skip to content

Commit

Permalink
Private issues (#7414).
Browse files Browse the repository at this point in the history
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@5466 e93f8b46-1217-0410-a6f0-8f06a7374b81
  • Loading branch information
jplang committed Apr 15, 2011
1 parent 37205a8 commit f16cddd
Show file tree
Hide file tree
Showing 18 changed files with 178 additions and 14 deletions.
15 changes: 12 additions & 3 deletions app/helpers/issues_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,23 @@ def issue_heading(issue)

def render_issue_subject_with_tree(issue)
s = ''
ancestors = issue.root? ? [] : issue.ancestors.all
ancestors = issue.root? ? [] : issue.ancestors.visible.all
ancestors.each do |ancestor|
s << '<div>' + content_tag('p', link_to_issue(ancestor))
end
s << '<div>' + content_tag('h3', h(issue.subject))
s << '<div>'
subject = h(issue.subject)
if issue.is_private?
subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
end
s << content_tag('h3', subject)
s << '</div>' * (ancestors.size + 1)
s
end

def render_descendants_tree(issue)
s = '<form><table class="list issues">'
issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
s << content_tag('tr',
content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
Expand Down Expand Up @@ -159,6 +164,10 @@ def show_detail(detail, no_html=false)
label = l(:field_parent_issue)
value = "##{detail.value}" unless detail.value.blank?
old_value = "##{detail.old_value}" unless detail.old_value.blank?

when detail.prop_key == 'is_private'
value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
end
when 'cf'
custom_field = CustomField.find_by_id(detail.prop_key)
Expand Down
15 changes: 13 additions & 2 deletions app/models/issue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ class Issue < ActiveRecord::Base
def self.visible_condition(user, options={})
Project.allowed_to_condition(user, :view_issues, options) do |role, user|
case role.issues_visibility
when 'default'
when 'all'
nil
when 'default'
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
when 'own'
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
else
Expand All @@ -104,8 +106,10 @@ def self.visible_condition(user, options={})
def visible?(usr=nil)
(usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
case role.issues_visibility
when 'default'
when 'all'
true
when 'default'
!self.is_private? || self.author == user || self.assigned_to == user
when 'own'
self.author == user || self.assigned_to == user
else
Expand Down Expand Up @@ -257,6 +261,12 @@ def estimated_hours=(h)
'done_ratio',
:if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }

safe_attributes 'is_private',
:if => lambda {|issue, user|
user.allowed_to?(:set_issues_private, issue.project) ||
(issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
}

# Safely sets attributes
# Should be called from controllers instead of #attributes=
# attr_accessible is too rough because we still want things like
Expand Down Expand Up @@ -552,6 +562,7 @@ def css_classes
s << ' overdue' if overdue?
s << ' child' if child?
s << ' parent' unless leaf?
s << ' private' if is_private?
s << ' created-by-me' if User.current.logged? && author_id == User.current.id
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
s
Expand Down
18 changes: 18 additions & 0 deletions app/models/journal_detail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,22 @@

class JournalDetail < ActiveRecord::Base
belongs_to :journal
before_save :normalize_values

private

def normalize_values
self.value = normalize(value)
self.old_value = normalize(old_value)
end

def normalize(v)
if v == true
"1"
elsif v == false
"0"
else
v
end
end
end
3 changes: 2 additions & 1 deletion app/models/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class Role < ActiveRecord::Base
BUILTIN_ANONYMOUS = 2

ISSUES_VISIBILITY_OPTIONS = [
['default', :label_issues_visibility_all],
['all', :label_issues_visibility_all],
['default', :label_issues_visibility_public],
['own', :label_issues_visibility_own]
]

Expand Down
5 changes: 5 additions & 0 deletions app/views/issues/_form.rhtml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>

<div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
<% if @issue.safe_attribute_names.include?('is_private') %>
<p style="float:right; margin-right:1em;">
<label class="inline" for="issue_is_private"><%= f.check_box :is_private, :no_label => true %> <%= l(:field_is_private) %></label>
</p>
<% end %>
<p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
<%= observe_field :issue_tracker_id, :url => { :action => :new, :project_id => @project, :id => @issue },
:update => :attributes,
Expand Down
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ en:
field_visible: Visible
field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
field_issues_visibility: Issues visibility
field_is_private: Private

setting_app_title: Application title
setting_app_subtitle: Application subtitle
Expand Down Expand Up @@ -377,6 +378,8 @@ en:
permission_add_issues: Add issues
permission_edit_issues: Edit issues
permission_manage_issue_relations: Manage issue relations
permission_set_issues_private: Set issues public or private
permission_set_own_issues_private: Set own issues public or private
permission_add_issue_notes: Add notes
permission_edit_issue_notes: Edit notes
permission_edit_own_issue_notes: Edit own notes
Expand Down Expand Up @@ -806,6 +809,7 @@ en:
label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
label_issues_visibility_all: All issues
label_issues_visibility_public: All non private issues
label_issues_visibility_own: Issues created by or assigned to the user

button_login: Login
Expand Down
4 changes: 4 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ fr:
field_visible: Visible
field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
field_issues_visibility: Visibilité des demandes
field_is_private: Privée

setting_app_title: Titre de l'application
setting_app_subtitle: Sous-titre de l'application
Expand Down Expand Up @@ -378,6 +379,8 @@ fr:
permission_add_issues: Créer des demandes
permission_edit_issues: Modifier les demandes
permission_manage_issue_relations: Gérer les relations
permission_set_issues_private: Rendre les demandes publiques ou privées
permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
permission_add_issue_notes: Ajouter des notes
permission_edit_issue_notes: Modifier les notes
permission_edit_own_issue_notes: Modifier ses propres notes
Expand Down Expand Up @@ -793,6 +796,7 @@ fr:
label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
label_issues_visibility_all: Toutes les demandes
label_issues_visibility_public: Toutes les demandes non privées
label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur

button_login: Connexion
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20110412065600_add_issues_is_private.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AddIssuesIsPrivate < ActiveRecord::Migration
def self.up
add_column :issues, :is_private, :boolean, :default => false, :null => false
end

def self.down
remove_column :issues, :is_private
end
end
2 changes: 2 additions & 0 deletions lib/redmine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
map.permission :manage_subtasks, {}
map.permission :set_issues_private, {}
map.permission :set_own_issues_private, {}
map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
Expand Down
5 changes: 3 additions & 2 deletions lib/redmine/default_data/loader.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 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
Expand Down Expand Up @@ -41,6 +41,7 @@ def load(lang=nil)
Role.transaction do
# Roles
manager = Role.create! :name => l(:default_role_manager),
:issues_visibility => 'all',
:position => 1
manager.permissions = manager.setable_permissions.collect {|p| p.name}
manager.save!
Expand Down
1 change: 1 addition & 0 deletions public/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ div.issue div.subject div div { padding-left: 16px; }
div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
div.issue div.subject>div>p { margin-top: 0.5em; }
div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px; -moz-border-radius: 2px;}

#issue_tree table.issues, #relations table.issues { border: 0; }
#issue_tree td.checkbox, #relations td.checkbox {display:none;}
Expand Down
13 changes: 13 additions & 0 deletions test/fixtures/attachments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,16 @@ attachments_014:
filename: changeset_utf8.diff
author_id: 2
content_type: text/x-diff
attachments_015:
id: 15
created_on: 2010-07-19 21:07:27 +02:00
container_type: Issue
container_id: 14
downloads: 0
disk_filename: 060719210727_changeset_utf8.diff
digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
filesize: 687
filename: private.diff
author_id: 2
content_type: text/x-diff
description: attachement of a private issue
18 changes: 18 additions & 0 deletions test/fixtures/issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,21 @@ issues_013:
root_id: 13
lft: 1
rgt: 2
issues_014:
id: 14
created_on: <%= 15.days.ago.to_date.to_s(:db) %>
project_id: 3
updated_on: <%= 15.days.ago.to_date.to_s(:db) %>
priority_id: 5
subject: Private issue on public project
fixed_version_id:
category_id:
description: This is a private issue
tracker_id: 1
assigned_to_id:
author_id: 2
status_id: 1
is_private: true
root_id: 14
lft: 1
rgt: 2
2 changes: 1 addition & 1 deletion test/fixtures/roles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ roles_001:
name: Manager
id: 1
builtin: 0
issues_visibility: default
issues_visibility: all
permissions: |
---
- :add_project
Expand Down
12 changes: 12 additions & 0 deletions test/functional/attachments_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ def test_show_other
assert_equal 'application/octet-stream', @response.content_type
end

def test_show_file_from_private_issue_without_permission
get :show, :id => 15
assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
end

def test_show_file_from_private_issue_with_permission
@request.session[:user_id] = 2
get :show, :id => 15
assert_response :success
assert_tag 'h2', :content => /private.diff/
end

def test_download_text_file
get :download, :id => 4
assert_response :success
Expand Down
49 changes: 49 additions & 0 deletions test/functional/issues_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ def test_index_should_not_list_issues_when_module_disabled
assert_no_tag :tag => 'a', :content => /Can't print recipes/
assert_tag :tag => 'a', :content => /Subproject issue/
end

def test_index_should_list_visible_issues_only
get :index, :per_page => 100
assert_response :success
assert_not_nil assigns(:issues)
assert_nil assigns(:issues).detect {|issue| !issue.visible?}
end

def test_index_with_project
Setting.display_subprojects_issues = 0
Expand Down Expand Up @@ -317,20 +324,62 @@ def test_show_should_deny_anonymous_access_without_permission
assert_response :redirect
end

def test_show_should_deny_anonymous_access_to_private_issue
Issue.update_all(["is_private = ?", true], "id = 1")
get :show, :id => 1
assert_response :redirect
end

def test_show_should_deny_non_member_access_without_permission
Role.non_member.remove_permission!(:view_issues)
@request.session[:user_id] = 9
get :show, :id => 1
assert_response 403
end

def test_show_should_deny_non_member_access_to_private_issue
Issue.update_all(["is_private = ?", true], "id = 1")
@request.session[:user_id] = 9
get :show, :id => 1
assert_response 403
end

def test_show_should_deny_member_access_without_permission
Role.find(1).remove_permission!(:view_issues)
@request.session[:user_id] = 2
get :show, :id => 1
assert_response 403
end

def test_show_should_deny_member_access_to_private_issue_without_permission
Issue.update_all(["is_private = ?", true], "id = 1")
@request.session[:user_id] = 3
get :show, :id => 1
assert_response 403
end

def test_show_should_allow_author_access_to_private_issue
Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
@request.session[:user_id] = 3
get :show, :id => 1
assert_response :success
end

def test_show_should_allow_assignee_access_to_private_issue
Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
@request.session[:user_id] = 3
get :show, :id => 1
assert_response :success
end

def test_show_should_allow_member_access_to_private_issue_with_permission
Issue.update_all(["is_private = ?", true], "id = 1")
User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
@request.session[:user_id] = 3
get :show, :id => 1
assert_response :success
end

def test_show_should_not_disclose_relations_to_invisible_issues
Setting.cross_project_issue_relations = '1'
IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
Expand Down
Loading

0 comments on commit f16cddd

Please sign in to comment.