diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 2610ca6..6158042 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -97,6 +97,9 @@ class ProjectsController < ApplicationController @open_issues_by_tracker = Issue.count(:group => :tracker, :include => [:project, :status, :tracker], :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false]) + @private_issues_by_tracker = Issue.count(:group => :tracker, + :include => [:project, :status, :tracker], + :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=? AND #{Issue.table_name}.private=?", false, true]) @total_issues_by_tracker = Issue.count(:group => :tracker, :include => [:project, :status, :tracker], :conditions => cond) @@ -247,7 +250,12 @@ class ProjectsController < ApplicationController @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty? events = @activity.events(@date_from, @date_to) - + + # The private issues should be removed from events + events.each do |event| + events.delete(event) if event.kind_of?(Issue) && !event.visible?(User.current, Project.find(event.project)) + end + respond_to do |format| format.html { @events_by_day = events.group_by(&:event_date) diff --git a/app/models/issue.rb b/app/models/issue.rb index e3a9e17..0828260 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -262,6 +262,14 @@ class Issue < ActiveRecord::Base yield end end + + def visible? (usr, project) + private==false || private==true && + ( + usr.allowed_to?(:view_private_issues, project) || + author == usr + ) + end def to_s "#{tracker} ##{id}: #{subject}" diff --git a/app/views/issues/_form.rhtml b/app/views/issues/_form.rhtml index 0ca1255..4faf8f0 100644 --- a/app/views/issues/_form.rhtml +++ b/app/views/issues/_form.rhtml @@ -41,6 +41,10 @@

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>

+<% if User.current.allowed_to?(:add_private_issues, @project) %> +

<%=f.check_box :private %>

+<% end%> +
<%= render :partial => 'form_custom_fields' %> diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml index 9326760..32da387 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -11,11 +11,13 @@ <% issues.each do |issue| -%> - - <%= check_box_tag("ids[]", issue.id, false, :id => nil) %> - <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> - <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> - + <% if issue.visible? User.current, @project %> + + <%= check_box_tag("ids[]", issue.id, false, :id => nil) %> + <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> + <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> + + <% end %> <% end -%> diff --git a/app/views/issues/_list_simple.rhtml b/app/views/issues/_list_simple.rhtml index e401a4a..ff436ff 100644 --- a/app/views/issues/_list_simple.rhtml +++ b/app/views/issues/_list_simple.rhtml @@ -8,17 +8,19 @@ <% for issue in issues %> - - - <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> - <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> - - <%=h issue.project.name %> - <%= issue.tracker.name %>
- <%= issue.status.name %> - <%= format_time(issue.updated_on) %> - - <%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %> - - + <% if issue.visible? User.current, @project %> + + + <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> + <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> + + <%=h issue.project.name %> - <%= issue.tracker.name %>
+ <%= issue.status.name %> - <%= format_time(issue.updated_on) %> + + <%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %> + + + <% end %> <% end %> diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 28d6566..928080b 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -1,3 +1,5 @@ +<% if @issue.visible? User.current, @project%> +
<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %> <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> @@ -42,6 +44,9 @@ <% if @issue.estimated_hours %> <%=l(:field_estimated_hours)%>:<%= lwr(:label_f_hour, @issue.estimated_hours) %> <% end %> + <% if @issue.private %> + <%=l(:field_private)%> + <% end %> <% n = 0 -%> @@ -124,3 +129,7 @@ end %> <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %> <%= stylesheet_link_tag 'scm' %> <% end %> + +<% else %> +

<%=l(:label_access_denied)%>

+<% end %> diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index fa65713..7283b67 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -26,6 +26,7 @@ :set_filter => 1, "tracker_id" => tracker.id %>: <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %> + (<%= @private_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_private_issues, @private_issues_by_tracker[tracker] || 0)%>) <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %> <% end %> diff --git a/db/migrate/102_add_issues_private_flag.rb b/db/migrate/102_add_issues_private_flag.rb new file mode 100644 index 0000000..148611f --- /dev/null +++ b/db/migrate/102_add_issues_private_flag.rb @@ -0,0 +1,9 @@ +class AddIssuesPrivateFlag < ActiveRecord::Migration + def self.up + add_column :issues, :private, :boolean, :default => false, :null => false + end + + def self.down + remove_column :issues, :private + end +end diff --git a/lang/en.yml b/lang/en.yml index dad948e..8ee850c 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -186,6 +186,7 @@ field_searchable: Searchable field_default_value: Default value field_comments_sorting: Display comments field_parent_title: Parent page +field_private: Private issue setting_app_title: Application title setting_app_subtitle: Application subtitle @@ -233,6 +234,8 @@ permission_manage_members: Manage members permission_manage_versions: Manage versions permission_manage_categories: Manage issue categories permission_add_issues: Add issues +permission_add_private_issues: Add private issues +permission_view_private_issues: View private issues permission_edit_issues: Edit issues permission_manage_issue_relations: Manage issue relations permission_add_issue_notes: Add notes @@ -285,6 +288,7 @@ project_module_wiki: Wiki project_module_repository: Repository project_module_boards: Boards +label_access_denied: Access denied label_user: User label_user_plural: Users label_user_new: New user @@ -397,6 +401,8 @@ label_read: Read... label_public_projects: Public projects label_open_issues: open label_open_issues_plural: open +label_private_issues: private +label_private_issues_plural: private label_closed_issues: closed label_closed_issues_plural: closed label_total: Total diff --git a/lang/ru.yml b/lang/ru.yml index 4943eea..f6d5dad 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -179,6 +179,7 @@ field_password: Пароль field_port: Порт field_possible_values: Возможные значения field_priority: Приоритет +field_private: Конфиденциальная задача field_project: Проект field_redirect_existing_links: Перенаправить существующие ссылки field_regexp: Регулярное выражение @@ -227,6 +228,7 @@ gui_validation_error_plural5: %d ошибок gui_validation_error_plural: %d ошибок label_activity: Активность +label_access_denied: Доступ запрещен label_add_another_file: Добавить ещё один файл label_added_time_by: Добавил(а) %s %s назад label_added: добавлено @@ -430,6 +432,10 @@ label_open_issues_plural2: открыто label_open_issues_plural5: открыто label_open_issues_plural: открыто label_open_issues: открыт +label_private_issues_plural2: конфиденциальных +label_private_issues_plural5: конфиденциальных +label_private_issues_plural: конфиденциальных +label_private_issues: конфиденциальная label_optional_description: Описание (опционально) label_options: Опции label_overall_activity: Сводная активность @@ -622,6 +628,8 @@ permission_browse_repository: Просмотр хранилища permission_view_documents: Просмотр документов permission_edit_project: Редактирование проектов permission_add_issue_notes: Добавление примечаний +permission_add_private_issues: Добавление конфиденциальных задач +permission_view_private_issues: Просмотр конфиденциальных задач permission_save_queries: Сохранение запросов permission_view_wiki_pages: Просмотр wiki permission_rename_wiki_pages: Переименование страниц wiki diff --git a/lib/redmine.rb b/lib/redmine.rb index c8d64b8..c4a31e9 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -35,6 +35,12 @@ Redmine::AccessControl.map do |map| :queries => :index, :reports => :issue_report}, :public => true map.permission :add_issues, {:issues => :new} + map.permission :add_private_issues, {:issues => :new} + map.permission :view_private_issues, {:projects => [:changelog, :roadmap], + :issues => [:index, :changes, :show, :context_menu], + :versions => [:show, :status_by], + :queries => :index, + :reports => :issue_report} map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit]} map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} map.permission :add_issue_notes, {:issues => [:edit, :reply]} diff --git a/test/fixtures/issues.yml b/test/fixtures/issues.yml index 4e14163..ce5c9a7 100644 --- a/test/fixtures/issues.yml +++ b/test/fixtures/issues.yml @@ -108,4 +108,19 @@ issues_007: start_date: <%= 10.days.ago.to_s(:db) %> due_date: <%= Date.today.to_s(:db) %> lock_version: 0 - \ No newline at end of file +issues_008: + created_on: <%= 5.days.ago.to_date.to_s(:db) %> + project_id: 1 + updated_on: <%= 2.days.ago.to_date.to_s(:db) %> + priority_id: 5 + subject: Private Issue on project 2 + id: 8 + fixed_version_id: + category_id: + description: Priavte Issue on project 2 + tracker_id: 1 + assigned_to_id: + author_id: 7 + status_id: 1 + private: 1 + diff --git a/test/fixtures/members.yml b/test/fixtures/members.yml index 32c65c6..e0ad561 100644 --- a/test/fixtures/members.yml +++ b/test/fixtures/members.yml @@ -30,4 +30,10 @@ members_005: project_id: 5 role_id: 1 user_id: 2 +members_006: + id: 6 + created_on: 2006-07-19 19:35:37 +02:00 + project_id: 1 + role_id: 3 + user_id: 7 \ No newline at end of file diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml index d8ae2c8..28af18d 100644 --- a/test/fixtures/roles.yml +++ b/test/fixtures/roles.yml @@ -45,6 +45,8 @@ roles_001: - :browse_repository - :manage_repository - :view_changesets + - :add_private_issues + - :view_private_issues position: 1 roles_002: @@ -87,6 +89,7 @@ roles_002: - :manage_files - :browse_repository - :view_changesets + - :view_private_issues position: 2 roles_003: @@ -124,6 +127,7 @@ roles_003: - :manage_files - :browse_repository - :view_changesets + - :add_private_issues position: 3 roles_004: @@ -154,6 +158,7 @@ roles_004: - :browse_repository - :view_changesets + position: 4 roles_005: name: Anonymous @@ -173,4 +178,4 @@ roles_005: - :view_changesets position: 5 - \ No newline at end of file + diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index de35531..9e85aa7 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -96,5 +96,20 @@ users_006: mail_notification: false login: '' type: AnonymousUser - +users_007: + id: 7 + created_on: 2009-01-27 01:20:19 +03:00 + status: 1 + last_login_on: + language: ru + hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415 + updated_on: 2009-01-27 19:33:19 +03:00 + admin: false + mail: vasja@somenet.foo + lastname: Pupkin + firstname: Vasja + auth_source_id: + mail_notification: true + login: vasiliy + type: User \ No newline at end of file diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index d282d09..b66525b 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -260,6 +260,54 @@ class IssuesControllerTest < Test::Unit::TestCase assert_not_nil assigns(:issue) end + def test_show_private_issue_by_manager + @request.session[:user_id] = 2 + get :show, :id => 8 + assert_response :success + assert_tag :td, :attributes => { :class => 'private-issue'} + end + + def test_show_private_issue_by_admin + @request.session[:user_id] = 1 + get :show, :id => 8 + assert_response :success + assert_tag :td, :attributes => { :class => 'private-issue'} + end + + def test_show_private_issue_by_developer + @request.session[:user_id] = 3 + get :show, :id => 8 + assert_response :success + # Developer can view private issues + assert_tag :td, :attributes => { :class => 'private-issue'} + end + + def test_show_private_issue_by_issue_author + # issue author always can browse his issue + @request.session[:user_id] = 7 + get :show, :id => 8 + assert_response :success + assert_tag :input, :attributes => { :name => 'issue[private]'} + assert_tag :td, :attributes => { :class => 'private-issue'} + end + + def test_show_private_issue_by_non_member + @request.session[:user_id] = 4 + get :show, :id => 8 + assert_response :success + assert_no_tag :input, :attributes => { :name => 'issue[private]'} + assert_no_tag :td, :attributes => { :class => 'private-issue'} + assert_tag :p, :attributes => { :class => 'nodata'} + end + + def test_show_private_issue_by_anonymous + get :show, :id => 8 + assert_response :success + assert_no_tag :input, :attributes => { :name => 'issue[private]'} + assert_no_tag :td, :attributes => { :class => 'private-issue'} + assert_tag :p, :attributes => { :class => 'nodata'} + end + def test_get_new @request.session[:user_id] = 2 get :new, :project_id => 1, :tracker_id => 1 @@ -270,6 +318,41 @@ class IssuesControllerTest < Test::Unit::TestCase :value => 'Default string' } end + def test_get_new_manager + # Manager have add_private_issue permission + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + assert_tag :input, :attributes => { :name => 'issue[private]'} + end + + def test_get_new_developer + @request.session[:user_id] = 3 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + # Developer can't change issue type + assert_no_tag :input, :attributes => { :name => 'issue[private]'} + end + + def test_get_new_reporter + @request.session[:user_id] = 7 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + # Reporter can add private issue + assert_tag :input, :attributes => { :name => 'issue[private]'} + end + + def test_get_new_admin + @request.session[:user_id] = 1 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + assert_tag :input, :attributes => { :name => 'issue[private]'} + end + def test_get_new_without_tracker_id @request.session[:user_id] = 2 get :new, :project_id => 1 diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 8737b3c..77a7d82 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -247,7 +247,83 @@ class ProjectsControllerTest < Test::Unit::TestCase } } end + + #private issue are not visible for Anonymous user in global Activity + def test_private_issue_global_activity_for_anonymous + get :activity + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events_by_day) + + assert_no_tag :tag => "h3", + :content => /#{5.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /#{Issue.find(8).subject}/, + } + } + } + end + + def test_private_issue_global_activity_for_manager + @request.session[:user_id] = 2 # manager + get :activity + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events_by_day) + + assert_tag :tag => "h3", + :content => /#{5.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /#{Issue.find(8).subject}/, + } + } + } + end + def test_private_issue_global_activity_for_developer + @request.session[:user_id] = 3 # developer + get :activity + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events_by_day) + + assert_tag :tag => "h3", + :content => /#{5.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /#{Issue.find(8).subject}/, + } + } + } + end + + def test_private_issue_global_activity_for_non_member + @request.session[:user_id] = 4 # does not have any role in project #1 + get :activity + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events_by_day) + + assert_no_tag :tag => "h3", + :content => /#{5.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /#{Issue.find(8).subject}/, + } + } + } + end + def test_user_activity get :activity, :user_id => 2 assert_response :success diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index ed7593a..7579d68 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -203,4 +203,21 @@ class IssueTest < Test::Unit::TestCase assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue? assert !Issue.new(:due_date => nil).overdue? end + + def test_visible + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 2, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_private_create', :description => 'IssueTest#test_private_create', :estimated_hours => '5:30', :private => true) + assert issue.save + issue.reload + assert_equal true, issue.private + # Test fixtures contain "add_private_issues" and "view_private_issues" + # permissions for Manager role and only "view_private_issues" for Developer. + # User with id #3 in project with id #1 has Developer role + assert_equal true, issue.visible?(User.find(3), Project.find(1)) + # User with id #2 in project with id #1 has Manager role + assert_equal true, issue.visible?(User.find(2), Project.find(1)) + # User with id #6 has Anonymous role + assert_equal false, issue.visible?(User.find(6), Project.find(1)) + # User with id #4 does not have any role in project #1 + assert_equal false, issue.visible?(User.find(4), Project.find(1)) + end end diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index 6e32c02..7c8b381 100644 --- a/test/unit/project_test.rb +++ b/test/unit/project_test.rb @@ -81,9 +81,9 @@ class ProjectTest < Test::Unit::TestCase def test_destroy # 2 active members - assert_equal 2, @ecookbook.members.size + assert_equal 3, @ecookbook.members.size # and 1 is locked - assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size + assert_equal 4, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size # some boards assert @ecookbook.boards.any?