@@ -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 ' |
|
|
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 ' |
|
|
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 |
[' |
|
|
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 |
# |
|
|
2 |
# Copyright (C) 2006-20 |
|
|
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: |
|
|
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 => |
|
|
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 != |
|
|
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-20 |
|
|
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