@@ -0,0 +1,9 | |||||
|
1 | class AddRolesTimeEntriesVisibility < ActiveRecord::Migration | |||
|
2 | def self.up | |||
|
3 | add_column :roles, :time_entries_visibility, :string, :limit => 30, :default => 'all', :null => false | |||
|
4 | end | |||
|
5 | ||||
|
6 | def self.down | |||
|
7 | remove_column :roles, :time_entries_visibility | |||
|
8 | end | |||
|
9 | end |
@@ -143,7 +143,7 class ProjectsController < ApplicationController | |||||
143 | @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count |
|
143 | @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count | |
144 | @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count |
|
144 | @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count | |
145 |
|
145 | |||
146 |
if User.current.allowed_to?( |
|
146 | if User.current.allowed_to_view_all_time_entries?(@project) | |
147 | @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f |
|
147 | @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f | |
148 | end |
|
148 | end | |
149 |
|
149 |
@@ -39,6 +39,11 class Role < ActiveRecord::Base | |||||
39 | ['own', :label_issues_visibility_own] |
|
39 | ['own', :label_issues_visibility_own] | |
40 | ] |
|
40 | ] | |
41 |
|
41 | |||
|
42 | TIME_ENTRIES_VISIBILITY_OPTIONS = [ | |||
|
43 | ['all', :label_time_entries_visibility_all], | |||
|
44 | ['own', :label_time_entries_visibility_own] | |||
|
45 | ] | |||
|
46 | ||||
42 | USERS_VISIBILITY_OPTIONS = [ |
|
47 | USERS_VISIBILITY_OPTIONS = [ | |
43 | ['all', :label_users_visibility_all], |
|
48 | ['all', :label_users_visibility_all], | |
44 | ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects] |
|
49 | ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects] | |
@@ -75,6 +80,9 class Role < ActiveRecord::Base | |||||
75 | validates_inclusion_of :users_visibility, |
|
80 | validates_inclusion_of :users_visibility, | |
76 | :in => USERS_VISIBILITY_OPTIONS.collect(&:first), |
|
81 | :in => USERS_VISIBILITY_OPTIONS.collect(&:first), | |
77 | :if => lambda {|role| role.respond_to?(:users_visibility) && role.users_visibility_changed?} |
|
82 | :if => lambda {|role| role.respond_to?(:users_visibility) && role.users_visibility_changed?} | |
|
83 | validates_inclusion_of :time_entries_visibility, | |||
|
84 | :in => TIME_ENTRIES_VISIBILITY_OPTIONS.collect(&:first), | |||
|
85 | :if => lambda {|role| role.respond_to?(:time_entries_visibility) && role.time_entries_visibility_changed?} | |||
78 |
|
86 | |||
79 | # Copies attributes from another role, arg can be an id or a Role |
|
87 | # Copies attributes from another role, arg can be an id or a Role | |
80 | def copy_from(arg, options={}) |
|
88 | def copy_from(arg, options={}) |
@@ -46,7 +46,7 class TimeEntry < ActiveRecord::Base | |||||
46 |
|
46 | |||
47 | scope :visible, lambda {|*args| |
|
47 | scope :visible, lambda {|*args| | |
48 | joins(:project). |
|
48 | joins(:project). | |
49 |
where( |
|
49 | where(TimeEntry.visible_condition(args.shift || User.current, *args)) | |
50 | } |
|
50 | } | |
51 | scope :on_issue, lambda {|issue| |
|
51 | scope :on_issue, lambda {|issue| | |
52 | joins(:issue). |
|
52 | joins(:issue). | |
@@ -55,6 +55,32 class TimeEntry < ActiveRecord::Base | |||||
55 |
|
55 | |||
56 | safe_attributes 'hours', 'comments', 'project_id', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields' |
|
56 | safe_attributes 'hours', 'comments', 'project_id', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields' | |
57 |
|
57 | |||
|
58 | # Returns a SQL conditions string used to find all time entries visible by the specified user | |||
|
59 | def self.visible_condition(user, options={}) | |||
|
60 | Project.allowed_to_condition(user, :view_time_entries, options) do |role, user| | |||
|
61 | if role.time_entries_visibility == 'all' | |||
|
62 | nil | |||
|
63 | elsif role.time_entries_visibility == 'own' && user.id && user.logged? | |||
|
64 | "#{table_name}.user_id = #{user.id}" | |||
|
65 | else | |||
|
66 | '1=0' | |||
|
67 | end | |||
|
68 | end | |||
|
69 | end | |||
|
70 | ||||
|
71 | # Returns true if user or current user is allowed to view the time entry | |||
|
72 | def visible?(user=nil) | |||
|
73 | (user || User.current).allowed_to?(:view_time_entries, self.project) do |role, user| | |||
|
74 | if role.time_entries_visibility == 'all' | |||
|
75 | true | |||
|
76 | elsif role.time_entries_visibility == 'own' | |||
|
77 | self.user == user | |||
|
78 | else | |||
|
79 | false | |||
|
80 | end | |||
|
81 | end | |||
|
82 | end | |||
|
83 | ||||
58 | def initialize(attributes=nil, *args) |
|
84 | def initialize(attributes=nil, *args) | |
59 | super |
|
85 | super | |
60 | if new_record? && self.activity.nil? |
|
86 | if new_record? && self.activity.nil? | |
@@ -116,7 +142,9 class TimeEntry < ActiveRecord::Base | |||||
116 |
|
142 | |||
117 | # Returns true if the time entry can be edited by usr, otherwise false |
|
143 | # Returns true if the time entry can be edited by usr, otherwise false | |
118 | def editable_by?(usr) |
|
144 | def editable_by?(usr) | |
119 | (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project) |
|
145 | visible?(usr) && ( | |
|
146 | (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project) | |||
|
147 | ) | |||
120 | end |
|
148 | end | |
121 |
|
149 | |||
122 | # Returns the custom_field_values that can be edited by the given user |
|
150 | # Returns the custom_field_values that can be edited by the given user |
@@ -634,6 +634,12 class User < Principal | |||||
634 | allowed_to?(action, nil, options.reverse_merge(:global => true), &block) |
|
634 | allowed_to?(action, nil, options.reverse_merge(:global => true), &block) | |
635 | end |
|
635 | end | |
636 |
|
636 | |||
|
637 | def allowed_to_view_all_time_entries?(context) | |||
|
638 | allowed_to?(:view_time_entries, context) do |role, user| | |||
|
639 | role.time_entries_visibility == 'all' | |||
|
640 | end | |||
|
641 | end | |||
|
642 | ||||
637 | # Returns true if the user is allowed to delete the user's own account |
|
643 | # Returns true if the user is allowed to delete the user's own account | |
638 | def own_account_deletable? |
|
644 | def own_account_deletable? | |
639 | Setting.unsubscribe? && |
|
645 | Setting.unsubscribe? && |
@@ -62,7 +62,7 | |||||
62 | rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours' |
|
62 | rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours' | |
63 | end |
|
63 | end | |
64 | end |
|
64 | end | |
65 |
if User.current.allowed_to |
|
65 | if User.current.allowed_to_view_all_time_entries?(@project) | |
66 | if @issue.total_spent_hours > 0 |
|
66 | if @issue.total_spent_hours > 0 | |
67 | rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time' |
|
67 | rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time' | |
68 | end |
|
68 | end |
@@ -1,6 +1,8 | |||||
1 | <% if @total_hours.present? %> |
|
1 | <% if User.current.allowed_to?(:view_time_entries, @project) %> | |
2 | <h3><%= l(:label_spent_time) %></h3> |
|
2 | <h3><%= l(:label_spent_time) %></h3> | |
3 | <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p> |
|
3 | <% if @total_hours.present? %> | |
|
4 | <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p> | |||
|
5 | <% end %> | |||
4 | <p> |
|
6 | <p> | |
5 | <% if User.current.allowed_to?(:log_time, @project) %> |
|
7 | <% if User.current.allowed_to?(:log_time, @project) %> | |
6 | <%= link_to l(:button_log_time), new_project_time_entry_path(@project) %> | |
|
8 | <%= link_to l(:button_log_time), new_project_time_entry_path(@project) %> | |
@@ -10,6 +10,10 | |||||
10 | <p><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p> |
|
10 | <p><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p> | |
11 | <% end %> |
|
11 | <% end %> | |
12 |
|
12 | |||
|
13 | <% unless @role.anonymous? %> | |||
|
14 | <p><%= f.select :time_entries_visibility, Role::TIME_ENTRIES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p> | |||
|
15 | <% end %> | |||
|
16 | ||||
13 | <p><%= f.select :users_visibility, Role::USERS_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p> |
|
17 | <p><%= f.select :users_visibility, Role::USERS_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p> | |
14 |
|
18 | |||
15 | <% if @role.new_record? && @roles.any? %> |
|
19 | <% if @role.new_record? && @roles.any? %> |
@@ -19,7 +19,7 | |||||
19 | <th><%= l(:field_estimated_hours) %></th> |
|
19 | <th><%= l(:field_estimated_hours) %></th> | |
20 | <td class="total-hours"><%= html_hours(l_hours(@version.estimated_hours)) %></td> |
|
20 | <td class="total-hours"><%= html_hours(l_hours(@version.estimated_hours)) %></td> | |
21 | </tr> |
|
21 | </tr> | |
22 |
<% if User.current.allowed_to |
|
22 | <% if User.current.allowed_to_view_all_time_entries?(@project) %> | |
23 | <tr> |
|
23 | <tr> | |
24 | <th><%= l(:label_spent_time) %></th> |
|
24 | <th><%= l(:label_spent_time) %></th> | |
25 | <td class="total-hours"><%= html_hours(l_hours(@version.spent_hours)) %></td> |
|
25 | <td class="total-hours"><%= html_hours(l_hours(@version.spent_hours)) %></td> |
@@ -341,6 +341,7 en: | |||||
341 | field_must_change_passwd: Must change password at next logon |
|
341 | field_must_change_passwd: Must change password at next logon | |
342 | field_default_status: Default status |
|
342 | field_default_status: Default status | |
343 | field_users_visibility: Users visibility |
|
343 | field_users_visibility: Users visibility | |
|
344 | field_time_entries_visibility: Time logs visibility | |||
344 |
|
345 | |||
345 | setting_app_title: Application title |
|
346 | setting_app_title: Application title | |
346 | setting_app_subtitle: Application subtitle |
|
347 | setting_app_subtitle: Application subtitle |
@@ -361,6 +361,7 fr: | |||||
361 | field_must_change_passwd: Doit changer de mot de passe Γ la prochaine connexion |
|
361 | field_must_change_passwd: Doit changer de mot de passe Γ la prochaine connexion | |
362 | field_default_status: Statut par dΓ©faut |
|
362 | field_default_status: Statut par dΓ©faut | |
363 | field_users_visibility: VisibilitΓ© des utilisateurs |
|
363 | field_users_visibility: VisibilitΓ© des utilisateurs | |
|
364 | field_time_entries_visibility: VisibilitΓ© du temps passΓ© | |||
364 |
|
365 | |||
365 | setting_app_title: Titre de l'application |
|
366 | setting_app_title: Titre de l'application | |
366 | setting_app_subtitle: Sous-titre de l'application |
|
367 | setting_app_subtitle: Sous-titre de l'application | |
@@ -962,6 +963,8 fr: | |||||
962 | label_disable_notifications: DΓ©sactiver les notifications |
|
963 | label_disable_notifications: DΓ©sactiver les notifications | |
963 | label_blank_value: non renseignΓ© |
|
964 | label_blank_value: non renseignΓ© | |
964 | label_parent_task_attributes: Attributs des tΓ’ches parentes |
|
965 | label_parent_task_attributes: Attributs des tΓ’ches parentes | |
|
966 | label_time_entries_visibility_all: Tous les temps passΓ©s | |||
|
967 | label_time_entries_visibility_own: Ses propres temps passΓ©s | |||
965 |
|
968 | |||
966 | button_login: Connexion |
|
969 | button_login: Connexion | |
967 | button_submit: Soumettre |
|
970 | button_submit: Soumettre |
@@ -27,6 +27,38 class TimeEntryTest < ActiveSupport::TestCase | |||||
27 | :groups_users, |
|
27 | :groups_users, | |
28 | :enabled_modules |
|
28 | :enabled_modules | |
29 |
|
29 | |||
|
30 | def test_visibility_with_permission_to_view_all_time_entries | |||
|
31 | user = User.generate! | |||
|
32 | role = Role.generate!(:permissions => [:view_time_entries], :time_entries_visibility => 'all') | |||
|
33 | Role.non_member.remove_permission! :view_time_entries | |||
|
34 | project = Project.find(1) | |||
|
35 | User.add_to_project user, project, role | |||
|
36 | own = TimeEntry.generate! :user => user, :project => project | |||
|
37 | other = TimeEntry.generate! :user => User.find(2), :project => project | |||
|
38 | ||||
|
39 | assert TimeEntry.visible(user).find_by_id(own.id) | |||
|
40 | assert TimeEntry.visible(user).find_by_id(other.id) | |||
|
41 | ||||
|
42 | assert own.visible?(user) | |||
|
43 | assert other.visible?(user) | |||
|
44 | end | |||
|
45 | ||||
|
46 | def test_visibility_with_permission_to_view_own_time_entries | |||
|
47 | user = User.generate! | |||
|
48 | role = Role.generate!(:permissions => [:view_time_entries], :time_entries_visibility => 'own') | |||
|
49 | Role.non_member.remove_permission! :view_time_entries | |||
|
50 | project = Project.find(1) | |||
|
51 | User.add_to_project user, project, role | |||
|
52 | own = TimeEntry.generate! :user => user, :project => project | |||
|
53 | other = TimeEntry.generate! :user => User.find(2), :project => project | |||
|
54 | ||||
|
55 | assert TimeEntry.visible(user).find_by_id(own.id) | |||
|
56 | assert_nil TimeEntry.visible(user).find_by_id(other.id) | |||
|
57 | ||||
|
58 | assert own.visible?(user) | |||
|
59 | assert_equal false, other.visible?(user) | |||
|
60 | end | |||
|
61 | ||||
30 | def test_hours_format |
|
62 | def test_hours_format | |
31 | assertions = { "2" => 2.0, |
|
63 | assertions = { "2" => 2.0, | |
32 | "21.1" => 21.1, |
|
64 | "21.1" => 21.1, |
General Comments 0
You need to be logged in to leave comments.
Login now