@@ -165,6 +165,14 class IssueQuery < Query | |||||
165 |
|
165 | |||
166 | add_available_filter "issue_id", :type => :integer, :label => :label_issue |
|
166 | add_available_filter "issue_id", :type => :integer, :label => :label_issue | |
167 |
|
167 | |||
|
168 | add_available_filter("updated_by", | |||
|
169 | :type => :list, :values => lambda { author_values } | |||
|
170 | ) | |||
|
171 | ||||
|
172 | add_available_filter("last_updated_by", | |||
|
173 | :type => :list, :values => lambda { author_values } | |||
|
174 | ) | |||
|
175 | ||||
168 | Tracker.disabled_core_fields(trackers).each {|field| |
|
176 | Tracker.disabled_core_fields(trackers).each {|field| | |
169 | delete_available_filter field |
|
177 | delete_available_filter field | |
170 | } |
|
178 | } | |
@@ -341,6 +349,27 class IssueQuery < Query | |||||
341 | raise StatementInvalid.new(e.message) |
|
349 | raise StatementInvalid.new(e.message) | |
342 | end |
|
350 | end | |
343 |
|
351 | |||
|
352 | def sql_for_updated_by_field(field, operator, value) | |||
|
353 | neg = (operator == '!' ? 'NOT' : '') | |||
|
354 | subquery = "SELECT 1 FROM #{Journal.table_name}" + | |||
|
355 | " WHERE #{Journal.table_name}.journalized_type='Issue' AND #{Journal.table_name}.journalized_id=#{Issue.table_name}.id" + | |||
|
356 | " AND (#{sql_for_field field, '=', value, Journal.table_name, 'user_id'})" + | |||
|
357 | " AND (#{Journal.visible_notes_condition(User.current, :skip_pre_condition => true)})" | |||
|
358 | ||||
|
359 | "#{neg} EXISTS (#{subquery})" | |||
|
360 | end | |||
|
361 | ||||
|
362 | def sql_for_last_updated_by_field(field, operator, value) | |||
|
363 | neg = (operator == '!' ? 'NOT' : '') | |||
|
364 | subquery = "SELECT 1 FROM #{Journal.table_name} sj" + | |||
|
365 | " WHERE sj.journalized_type='Issue' AND sj.journalized_id=#{Issue.table_name}.id AND (#{sql_for_field field, '=', value, 'sj', 'user_id'})" + | |||
|
366 | " AND sj.id = (SELECT MAX(#{Journal.table_name}.id) FROM #{Journal.table_name}" + | |||
|
367 | " WHERE #{Journal.table_name}.journalized_type='Issue' AND #{Journal.table_name}.journalized_id=#{Issue.table_name}.id" + | |||
|
368 | " AND (#{Journal.visible_notes_condition(User.current, :skip_pre_condition => true)}))" | |||
|
369 | ||||
|
370 | "#{neg} EXISTS (#{subquery})" | |||
|
371 | end | |||
|
372 | ||||
344 | def sql_for_watcher_id_field(field, operator, value) |
|
373 | def sql_for_watcher_id_field(field, operator, value) | |
345 | db_table = Watcher.table_name |
|
374 | db_table = Watcher.table_name | |
346 | "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " + |
|
375 | "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " + |
@@ -47,10 +47,11 class Journal < ActiveRecord::Base | |||||
47 |
|
47 | |||
48 | scope :visible, lambda {|*args| |
|
48 | scope :visible, lambda {|*args| | |
49 | user = args.shift || User.current |
|
49 | user = args.shift || User.current | |
50 | private_notes_condition = Project.allowed_to_condition(user, :view_private_notes, *args) |
|
50 | options = args.shift || {} | |
|
51 | ||||
51 | joins(:issue => :project). |
|
52 | joins(:issue => :project). | |
52 |
where(Issue.visible_condition(user, |
|
53 | where(Issue.visible_condition(user, options)). | |
53 | where("(#{Journal.table_name}.private_notes = ? OR #{Journal.table_name}.user_id = ? OR (#{private_notes_condition}))", false, user.id) |
|
54 | where(Journal.visible_notes_condition(user, :skip_pre_condition => true)) | |
54 | } |
|
55 | } | |
55 |
|
56 | |||
56 | safe_attributes 'notes', |
|
57 | safe_attributes 'notes', | |
@@ -58,6 +59,12 class Journal < ActiveRecord::Base | |||||
58 | safe_attributes 'private_notes', |
|
59 | safe_attributes 'private_notes', | |
59 | :if => lambda {|journal, user| user.allowed_to?(:set_notes_private, journal.project)} |
|
60 | :if => lambda {|journal, user| user.allowed_to?(:set_notes_private, journal.project)} | |
60 |
|
61 | |||
|
62 | # Returns a SQL condition to filter out journals with notes that are not visible to user | |||
|
63 | def self.visible_notes_condition(user=User.current, options={}) | |||
|
64 | private_notes_permission = Project.allowed_to_condition(user, :view_private_notes, options) | |||
|
65 | sanitize_sql_for_conditions(["(#{table_name}.private_notes = ? OR #{table_name}.user_id = ? OR (#{private_notes_permission}))", false, user.id]) | |||
|
66 | end | |||
|
67 | ||||
61 | def initialize(*args) |
|
68 | def initialize(*args) | |
62 | super |
|
69 | super | |
63 | if journalized |
|
70 | if journalized |
@@ -173,13 +173,14 class Project < ActiveRecord::Base | |||||
173 | # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+ |
|
173 | # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+ | |
174 | # |
|
174 | # | |
175 | # Valid options: |
|
175 | # Valid options: | |
176 | # * :project => limit the condition to project |
|
176 | # * :skip_pre_condition => true don't check that the module is enabled (eg. when the condition is already set elsewhere in the query) | |
177 |
# * : |
|
177 | # * :project => project limit the condition to project | |
178 |
# * : |
|
178 | # * :with_subprojects => true limit the condition to project and its subprojects | |
|
179 | # * :member => true limit the condition to the user projects | |||
179 | def self.allowed_to_condition(user, permission, options={}) |
|
180 | def self.allowed_to_condition(user, permission, options={}) | |
180 | perm = Redmine::AccessControl.permission(permission) |
|
181 | perm = Redmine::AccessControl.permission(permission) | |
181 | base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}") |
|
182 | base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}") | |
182 | if perm && perm.project_module |
|
183 | if !options[:skip_pre_condition] && perm && perm.project_module | |
183 | # If the permission belongs to a project module, make sure the module is enabled |
|
184 | # If the permission belongs to a project module, make sure the module is enabled | |
184 | base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" |
|
185 | base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" | |
185 | end |
|
186 | end |
@@ -802,7 +802,7 class Query < ActiveRecord::Base | |||||
802 | operator = operator_for(field) |
|
802 | operator = operator_for(field) | |
803 |
|
803 | |||
804 | # "me" value substitution |
|
804 | # "me" value substitution | |
805 | if %w(assigned_to_id author_id user_id watcher_id).include?(field) |
|
805 | if %w(assigned_to_id author_id user_id watcher_id updated_by last_updated_by).include?(field) | |
806 | if v.delete("me") |
|
806 | if v.delete("me") | |
807 | if User.current.logged? |
|
807 | if User.current.logged? | |
808 | v.push(User.current.id.to_s) |
|
808 | v.push(User.current.id.to_s) |
@@ -370,6 +370,8 en: | |||||
370 | field_default_version: Default version |
|
370 | field_default_version: Default version | |
371 | field_remote_ip: IP address |
|
371 | field_remote_ip: IP address | |
372 | field_textarea_font: Font used for text areas |
|
372 | field_textarea_font: Font used for text areas | |
|
373 | field_updated_by: Updated by | |||
|
374 | field_last_updated_by: Last updated by | |||
373 |
|
375 | |||
374 | setting_app_title: Application title |
|
376 | setting_app_title: Application title | |
375 | setting_app_subtitle: Application subtitle |
|
377 | setting_app_subtitle: Application subtitle |
@@ -382,6 +382,8 fr: | |||||
382 | field_total_estimated_hours: Temps estimΓ© total |
|
382 | field_total_estimated_hours: Temps estimΓ© total | |
383 | field_default_version: Version par dΓ©faut |
|
383 | field_default_version: Version par dΓ©faut | |
384 | field_textarea_font: Police utilisΓ©e pour les champs texte |
|
384 | field_textarea_font: Police utilisΓ©e pour les champs texte | |
|
385 | field_updated_by: Mise Γ jour par | |||
|
386 | field_last_updated_by: Dernière mise à jour par | |||
385 |
|
387 | |||
386 | setting_app_title: Titre de l'application |
|
388 | setting_app_title: Titre de l'application | |
387 | setting_app_subtitle: Sous-titre de l'application |
|
389 | setting_app_subtitle: Sous-titre de l'application |
@@ -719,6 +719,93 class QueryTest < ActiveSupport::TestCase | |||||
719 | end |
|
719 | end | |
720 | end |
|
720 | end | |
721 |
|
721 | |||
|
722 | def test_filter_updated_by | |||
|
723 | user = User.generate! | |||
|
724 | Journal.create!(:user_id => user.id, :journalized => Issue.find(2), :notes => 'Notes') | |||
|
725 | Journal.create!(:user_id => user.id, :journalized => Issue.find(3), :notes => 'Notes') | |||
|
726 | Journal.create!(:user_id => 2, :journalized => Issue.find(3), :notes => 'Notes') | |||
|
727 | ||||
|
728 | query = IssueQuery.new(:name => '_') | |||
|
729 | filter_name = "updated_by" | |||
|
730 | assert_include filter_name, query.available_filters.keys | |||
|
731 | ||||
|
732 | query.filters = {filter_name => {:operator => '=', :values => [user.id]}} | |||
|
733 | assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort | |||
|
734 | ||||
|
735 | query.filters = {filter_name => {:operator => '!', :values => [user.id]}} | |||
|
736 | assert_equal (Issue.ids.sort - [2, 3]), find_issues_with_query(query).map(&:id).sort | |||
|
737 | end | |||
|
738 | ||||
|
739 | def test_filter_updated_by_should_ignore_private_notes_that_are_not_visible | |||
|
740 | user = User.generate! | |||
|
741 | Journal.create!(:user_id => user.id, :journalized => Issue.find(2), :notes => 'Notes', :private_notes => true) | |||
|
742 | Journal.create!(:user_id => user.id, :journalized => Issue.find(3), :notes => 'Notes') | |||
|
743 | ||||
|
744 | query = IssueQuery.new(:name => '_') | |||
|
745 | filter_name = "updated_by" | |||
|
746 | assert_include filter_name, query.available_filters.keys | |||
|
747 | ||||
|
748 | with_current_user User.anonymous do | |||
|
749 | query.filters = {filter_name => {:operator => '=', :values => [user.id]}} | |||
|
750 | assert_equal [3], find_issues_with_query(query).map(&:id).sort | |||
|
751 | end | |||
|
752 | end | |||
|
753 | ||||
|
754 | def test_filter_updated_by_me | |||
|
755 | user = User.generate! | |||
|
756 | Journal.create!(:user_id => user.id, :journalized => Issue.find(2), :notes => 'Notes') | |||
|
757 | ||||
|
758 | with_current_user user do | |||
|
759 | query = IssueQuery.new(:name => '_') | |||
|
760 | filter_name = "updated_by" | |||
|
761 | assert_include filter_name, query.available_filters.keys | |||
|
762 | ||||
|
763 | query.filters = {filter_name => {:operator => '=', :values => ['me']}} | |||
|
764 | assert_equal [2], find_issues_with_query(query).map(&:id).sort | |||
|
765 | end | |||
|
766 | end | |||
|
767 | ||||
|
768 | def test_filter_last_updated_by | |||
|
769 | user = User.generate! | |||
|
770 | Journal.create!(:user_id => user.id, :journalized => Issue.find(2), :notes => 'Notes') | |||
|
771 | Journal.create!(:user_id => user.id, :journalized => Issue.find(3), :notes => 'Notes') | |||
|
772 | Journal.create!(:user_id => 2, :journalized => Issue.find(3), :notes => 'Notes') | |||
|
773 | ||||
|
774 | query = IssueQuery.new(:name => '_') | |||
|
775 | filter_name = "last_updated_by" | |||
|
776 | assert_include filter_name, query.available_filters.keys | |||
|
777 | ||||
|
778 | query.filters = {filter_name => {:operator => '=', :values => [user.id]}} | |||
|
779 | assert_equal [2], find_issues_with_query(query).map(&:id).sort | |||
|
780 | end | |||
|
781 | ||||
|
782 | def test_filter_last_updated_by_should_ignore_private_notes_that_are_not_visible | |||
|
783 | user1 = User.generate! | |||
|
784 | user2 = User.generate! | |||
|
785 | Journal.create!(:user_id => user1.id, :journalized => Issue.find(2), :notes => 'Notes') | |||
|
786 | Journal.create!(:user_id => user2.id, :journalized => Issue.find(2), :notes => 'Notes', :private_notes => true) | |||
|
787 | ||||
|
788 | query = IssueQuery.new(:name => '_') | |||
|
789 | filter_name = "last_updated_by" | |||
|
790 | assert_include filter_name, query.available_filters.keys | |||
|
791 | ||||
|
792 | with_current_user User.anonymous do | |||
|
793 | query.filters = {filter_name => {:operator => '=', :values => [user1.id]}} | |||
|
794 | assert_equal [2], find_issues_with_query(query).map(&:id).sort | |||
|
795 | ||||
|
796 | query.filters = {filter_name => {:operator => '=', :values => [user2.id]}} | |||
|
797 | assert_equal [], find_issues_with_query(query).map(&:id).sort | |||
|
798 | end | |||
|
799 | ||||
|
800 | with_current_user User.find(2) do | |||
|
801 | query.filters = {filter_name => {:operator => '=', :values => [user1.id]}} | |||
|
802 | assert_equal [], find_issues_with_query(query).map(&:id).sort | |||
|
803 | ||||
|
804 | query.filters = {filter_name => {:operator => '=', :values => [user2.id]}} | |||
|
805 | assert_equal [2], find_issues_with_query(query).map(&:id).sort | |||
|
806 | end | |||
|
807 | end | |||
|
808 | ||||
722 | def test_user_custom_field_filtered_on_me |
|
809 | def test_user_custom_field_filtered_on_me | |
723 | User.current = User.find(2) |
|
810 | User.current = User.find(2) | |
724 | cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1]) |
|
811 | cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1]) |
General Comments 0
You need to be logged in to leave comments.
Login now