##// END OF EJS Templates
Private issues (#7414)....
Jean-Philippe Lang -
r5346:f16cddd57ae8
parent child
Show More
@@ -0,0 +1,9
1 class AddIssuesIsPrivate < ActiveRecord::Migration
2 def self.up
3 add_column :issues, :is_private, :boolean, :default => false, :null => false
4 end
5
6 def self.down
7 remove_column :issues, :is_private
8 end
9 end
@@ -61,18 +61,23 module IssuesHelper
61 61
62 62 def render_issue_subject_with_tree(issue)
63 63 s = ''
64 ancestors = issue.root? ? [] : issue.ancestors.all
64 ancestors = issue.root? ? [] : issue.ancestors.visible.all
65 65 ancestors.each do |ancestor|
66 66 s << '<div>' + content_tag('p', link_to_issue(ancestor))
67 67 end
68 s << '<div>' + content_tag('h3', h(issue.subject))
68 s << '<div>'
69 subject = h(issue.subject)
70 if issue.is_private?
71 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
72 end
73 s << content_tag('h3', subject)
69 74 s << '</div>' * (ancestors.size + 1)
70 75 s
71 76 end
72 77
73 78 def render_descendants_tree(issue)
74 79 s = '<form><table class="list issues">'
75 issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
80 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
76 81 s << content_tag('tr',
77 82 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
78 83 content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
@@ -159,6 +164,10 module IssuesHelper
159 164 label = l(:field_parent_issue)
160 165 value = "##{detail.value}" unless detail.value.blank?
161 166 old_value = "##{detail.old_value}" unless detail.old_value.blank?
167
168 when detail.prop_key == 'is_private'
169 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
170 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
162 171 end
163 172 when 'cf'
164 173 custom_field = CustomField.find_by_id(detail.prop_key)
@@ -90,8 +90,10 class Issue < ActiveRecord::Base
90 90 def self.visible_condition(user, options={})
91 91 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
92 92 case role.issues_visibility
93 when 'default'
93 when 'all'
94 94 nil
95 when 'default'
96 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
95 97 when 'own'
96 98 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
97 99 else
@@ -104,8 +106,10 class Issue < ActiveRecord::Base
104 106 def visible?(usr=nil)
105 107 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
106 108 case role.issues_visibility
107 when 'default'
109 when 'all'
108 110 true
111 when 'default'
112 !self.is_private? || self.author == user || self.assigned_to == user
109 113 when 'own'
110 114 self.author == user || self.assigned_to == user
111 115 else
@@ -257,6 +261,12 class Issue < ActiveRecord::Base
257 261 'done_ratio',
258 262 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
259 263
264 safe_attributes 'is_private',
265 :if => lambda {|issue, user|
266 user.allowed_to?(:set_issues_private, issue.project) ||
267 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
268 }
269
260 270 # Safely sets attributes
261 271 # Should be called from controllers instead of #attributes=
262 272 # attr_accessible is too rough because we still want things like
@@ -552,6 +562,7 class Issue < ActiveRecord::Base
552 562 s << ' overdue' if overdue?
553 563 s << ' child' if child?
554 564 s << ' parent' unless leaf?
565 s << ' private' if is_private?
555 566 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
556 567 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
557 568 s
@@ -17,4 +17,22
17 17
18 18 class JournalDetail < ActiveRecord::Base
19 19 belongs_to :journal
20 before_save :normalize_values
21
22 private
23
24 def normalize_values
25 self.value = normalize(value)
26 self.old_value = normalize(old_value)
27 end
28
29 def normalize(v)
30 if v == true
31 "1"
32 elsif v == false
33 "0"
34 else
35 v
36 end
37 end
20 38 end
@@ -21,7 +21,8 class Role < ActiveRecord::Base
21 21 BUILTIN_ANONYMOUS = 2
22 22
23 23 ISSUES_VISIBILITY_OPTIONS = [
24 ['default', :label_issues_visibility_all],
24 ['all', :label_issues_visibility_all],
25 ['default', :label_issues_visibility_public],
25 26 ['own', :label_issues_visibility_own]
26 27 ]
27 28
@@ -1,6 +1,11
1 1 <%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
2 2
3 3 <div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
4 <% if @issue.safe_attribute_names.include?('is_private') %>
5 <p style="float:right; margin-right:1em;">
6 <label class="inline" for="issue_is_private"><%= f.check_box :is_private, :no_label => true %> <%= l(:field_is_private) %></label>
7 </p>
8 <% end %>
4 9 <p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
5 10 <%= observe_field :issue_tracker_id, :url => { :action => :new, :project_id => @project, :id => @issue },
6 11 :update => :attributes,
@@ -305,6 +305,7 en:
305 305 field_visible: Visible
306 306 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
307 307 field_issues_visibility: Issues visibility
308 field_is_private: Private
308 309
309 310 setting_app_title: Application title
310 311 setting_app_subtitle: Application subtitle
@@ -377,6 +378,8 en:
377 378 permission_add_issues: Add issues
378 379 permission_edit_issues: Edit issues
379 380 permission_manage_issue_relations: Manage issue relations
381 permission_set_issues_private: Set issues public or private
382 permission_set_own_issues_private: Set own issues public or private
380 383 permission_add_issue_notes: Add notes
381 384 permission_edit_issue_notes: Edit notes
382 385 permission_edit_own_issue_notes: Edit own notes
@@ -806,6 +809,7 en:
806 809 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
807 810 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
808 811 label_issues_visibility_all: All issues
812 label_issues_visibility_public: All non private issues
809 813 label_issues_visibility_own: Issues created by or assigned to the user
810 814
811 815 button_login: Login
@@ -309,6 +309,7 fr:
309 309 field_visible: Visible
310 310 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
311 311 field_issues_visibility: Visibilité des demandes
312 field_is_private: Privée
312 313
313 314 setting_app_title: Titre de l'application
314 315 setting_app_subtitle: Sous-titre de l'application
@@ -378,6 +379,8 fr:
378 379 permission_add_issues: Créer des demandes
379 380 permission_edit_issues: Modifier les demandes
380 381 permission_manage_issue_relations: Gérer les relations
382 permission_set_issues_private: Rendre les demandes publiques ou privées
383 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
381 384 permission_add_issue_notes: Ajouter des notes
382 385 permission_edit_issue_notes: Modifier les notes
383 386 permission_edit_own_issue_notes: Modifier ses propres notes
@@ -793,6 +796,7 fr:
793 796 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
794 797 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
795 798 label_issues_visibility_all: Toutes les demandes
799 label_issues_visibility_public: Toutes les demandes non privées
796 800 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
797 801
798 802 button_login: Connexion
@@ -71,6 +71,8 Redmine::AccessControl.map do |map|
71 71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
72 72 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
73 73 map.permission :manage_subtasks, {}
74 map.permission :set_issues_private, {}
75 map.permission :set_own_issues_private, {}
74 76 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
75 77 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
76 78 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
@@ -1,5 +1,5
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
@@ -41,6 +41,7 module Redmine
41 41 Role.transaction do
42 42 # Roles
43 43 manager = Role.create! :name => l(:default_role_manager),
44 :issues_visibility => 'all',
44 45 :position => 1
45 46 manager.permissions = manager.setable_permissions.collect {|p| p.name}
46 47 manager.save!
@@ -278,6 +278,7 div.issue div.subject div div { padding-left: 16px; }
278 278 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
279 279 div.issue div.subject>div>p { margin-top: 0.5em; }
280 280 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
281 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;}
281 282
282 283 #issue_tree table.issues, #relations table.issues { border: 0; }
283 284 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
@@ -169,3 +169,16 attachments_014:
169 169 filename: changeset_utf8.diff
170 170 author_id: 2
171 171 content_type: text/x-diff
172 attachments_015:
173 id: 15
174 created_on: 2010-07-19 21:07:27 +02:00
175 container_type: Issue
176 container_id: 14
177 downloads: 0
178 disk_filename: 060719210727_changeset_utf8.diff
179 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
180 filesize: 687
181 filename: private.diff
182 author_id: 2
183 content_type: text/x-diff
184 description: attachement of a private issue
@@ -244,3 +244,21 issues_013:
244 244 root_id: 13
245 245 lft: 1
246 246 rgt: 2
247 issues_014:
248 id: 14
249 created_on: <%= 15.days.ago.to_date.to_s(:db) %>
250 project_id: 3
251 updated_on: <%= 15.days.ago.to_date.to_s(:db) %>
252 priority_id: 5
253 subject: Private issue on public project
254 fixed_version_id:
255 category_id:
256 description: This is a private issue
257 tracker_id: 1
258 assigned_to_id:
259 author_id: 2
260 status_id: 1
261 is_private: true
262 root_id: 14
263 lft: 1
264 rgt: 2
@@ -3,7 +3,7 roles_001:
3 3 name: Manager
4 4 id: 1
5 5 builtin: 0
6 issues_visibility: default
6 issues_visibility: all
7 7 permissions: |
8 8 ---
9 9 - :add_project
@@ -86,6 +86,18 class AttachmentsControllerTest < ActionController::TestCase
86 86 assert_equal 'application/octet-stream', @response.content_type
87 87 end
88 88
89 def test_show_file_from_private_issue_without_permission
90 get :show, :id => 15
91 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
92 end
93
94 def test_show_file_from_private_issue_with_permission
95 @request.session[:user_id] = 2
96 get :show, :id => 15
97 assert_response :success
98 assert_tag 'h2', :content => /private.diff/
99 end
100
89 101 def test_download_text_file
90 102 get :download, :id => 4
91 103 assert_response :success
@@ -91,6 +91,13 class IssuesControllerTest < ActionController::TestCase
91 91 assert_no_tag :tag => 'a', :content => /Can't print recipes/
92 92 assert_tag :tag => 'a', :content => /Subproject issue/
93 93 end
94
95 def test_index_should_list_visible_issues_only
96 get :index, :per_page => 100
97 assert_response :success
98 assert_not_nil assigns(:issues)
99 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
100 end
94 101
95 102 def test_index_with_project
96 103 Setting.display_subprojects_issues = 0
@@ -317,6 +324,12 class IssuesControllerTest < ActionController::TestCase
317 324 assert_response :redirect
318 325 end
319 326
327 def test_show_should_deny_anonymous_access_to_private_issue
328 Issue.update_all(["is_private = ?", true], "id = 1")
329 get :show, :id => 1
330 assert_response :redirect
331 end
332
320 333 def test_show_should_deny_non_member_access_without_permission
321 334 Role.non_member.remove_permission!(:view_issues)
322 335 @request.session[:user_id] = 9
@@ -324,6 +337,13 class IssuesControllerTest < ActionController::TestCase
324 337 assert_response 403
325 338 end
326 339
340 def test_show_should_deny_non_member_access_to_private_issue
341 Issue.update_all(["is_private = ?", true], "id = 1")
342 @request.session[:user_id] = 9
343 get :show, :id => 1
344 assert_response 403
345 end
346
327 347 def test_show_should_deny_member_access_without_permission
328 348 Role.find(1).remove_permission!(:view_issues)
329 349 @request.session[:user_id] = 2
@@ -331,6 +351,35 class IssuesControllerTest < ActionController::TestCase
331 351 assert_response 403
332 352 end
333 353
354 def test_show_should_deny_member_access_to_private_issue_without_permission
355 Issue.update_all(["is_private = ?", true], "id = 1")
356 @request.session[:user_id] = 3
357 get :show, :id => 1
358 assert_response 403
359 end
360
361 def test_show_should_allow_author_access_to_private_issue
362 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
363 @request.session[:user_id] = 3
364 get :show, :id => 1
365 assert_response :success
366 end
367
368 def test_show_should_allow_assignee_access_to_private_issue
369 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
370 @request.session[:user_id] = 3
371 get :show, :id => 1
372 assert_response :success
373 end
374
375 def test_show_should_allow_member_access_to_private_issue_with_permission
376 Issue.update_all(["is_private = ?", true], "id = 1")
377 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
378 @request.session[:user_id] = 3
379 get :show, :id => 1
380 assert_response :success
381 end
382
334 383 def test_show_should_not_disclose_relations_to_invisible_issues
335 384 Setting.cross_project_issue_relations = '1'
336 385 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
@@ -74,6 +74,7 class IssueTest < ActiveSupport::TestCase
74 74 issues = Issue.visible(User.anonymous).all
75 75 assert issues.any?
76 76 assert_nil issues.detect {|issue| !issue.project.is_public?}
77 assert_nil issues.detect {|issue| issue.is_private?}
77 78 assert_visibility_match User.anonymous, issues
78 79 end
79 80
@@ -102,6 +103,7 class IssueTest < ActiveSupport::TestCase
102 103 issues = Issue.visible(user).all
103 104 assert issues.any?
104 105 assert_nil issues.detect {|issue| !issue.project.is_public?}
106 assert_nil issues.detect {|issue| issue.is_private?}
105 107 assert_visibility_match user, issues
106 108 end
107 109
@@ -130,10 +132,11 class IssueTest < ActiveSupport::TestCase
130 132 user = User.find(9)
131 133 # User should see issues of projects for which he has view_issues permissions only
132 134 Role.non_member.remove_permission!(:view_issues)
133 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
135 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
134 136 issues = Issue.visible(user).all
135 137 assert issues.any?
136 assert_nil issues.detect {|issue| issue.project_id != 2}
138 assert_nil issues.detect {|issue| issue.project_id != 3}
139 assert_nil issues.detect {|issue| issue.is_private?}
137 140 assert_visibility_match user, issues
138 141 end
139 142
@@ -145,6 +148,8 class IssueTest < ActiveSupport::TestCase
145 148 assert issues.any?
146 149 # Admin should see issues on private projects that he does not belong to
147 150 assert issues.detect {|issue| !issue.project.is_public?}
151 # Admin should see private issues of other users
152 assert issues.detect {|issue| issue.is_private? && issue.author != user}
148 153 assert_visibility_match user, issues
149 154 end
150 155
@@ -1,5 +1,5
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
@@ -44,11 +44,13 module Redmine
44 44 end
45 45
46 46 def attachments_visible?(user=User.current)
47 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
47 (respond_to?(:visible?) ? visible?(user) : true) &&
48 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
48 49 end
49 50
50 51 def attachments_deletable?(user=User.current)
51 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
52 (respond_to?(:visible?) ? visible?(user) : true) &&
53 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
52 54 end
53 55
54 56 def initialize_unsaved_attachments
General Comments 0
You need to be logged in to leave comments. Login now