From a0aff3691dce21cda37cb5149d364d482af06408 Mon Sep 17 00:00:00 2001 From: Jens Kraemer Date: Mon, 14 Jun 2021 17:02:39 +0800 Subject: [PATCH] implements global, per project and per user default issue queries #7360 patch by Olivier Chabert, with changes by Jens Kraemer --- app/controllers/issues_controller.rb | 19 +++++ app/helpers/projects_helper.rb | 9 +++ app/helpers/queries_helper.rb | 15 +++- app/helpers/settings_helper.rb | 4 ++ app/helpers/users_helper.rb | 12 ++++ app/models/issue_query.rb | 14 ++++ app/models/project.rb | 6 ++ app/models/query.rb | 6 ++ app/models/user_preference.rb | 4 ++ app/views/projects/settings/_issues.html.erb | 4 ++ app/views/settings/_issues.html.erb | 2 + app/views/users/_preferences.html.erb | 1 + config/locales/de.yml | 6 ++ config/locales/en.yml | 7 ++ config/locales/fr.yml | 7 ++ config/settings.yml | 2 + ...730_add_projects_default_issue_query_id.rb | 9 +++ public/stylesheets/application.css | 1 + test/functional/issues_controller_test.rb | 72 +++++++++++++++++++ test/unit/project_copy_test.rb | 13 ++++ test/unit/query_test.rb | 61 ++++++++++++++++ 21 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20200806105730_add_projects_default_issue_query_id.rb diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 0278b3088..da2b4f3eb 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -44,6 +44,7 @@ class IssuesController < ApplicationController def index use_session = !request.format.csv? + retrieve_default_query(use_session) retrieve_query(IssueQuery, use_session) if @query.valid? @@ -476,6 +477,24 @@ class IssuesController < ApplicationController super end + def retrieve_default_query(use_session) + return if params[:query_id].present? + return if api_request? + return if params[:set_filter] && (params.key?(:op) || params.key?(:f)) + + if params[:without_default].present? + params[:set_filter] = 1 + return + end + if !params[:set_filter] && use_session && session[:issue_query] + query_id, project_id = session[:issue_query].values_at(:id, :project_id) + return if IssueQuery.where(id: query_id).exists? && project_id == @project&.id + end + if default_query = IssueQuery.default(project: @project) + params[:query_id] = default_query.id + end + end + def retrieve_previous_and_next_issue_ids if params[:prev_issue_id].present? || params[:next_issue_id].present? @prev_issue_id = params[:prev_issue_id].presence.try(:to_i) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index f87c5b917..618650b99 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -114,6 +114,15 @@ module ProjectsHelper principals_options_for_select(assignable_users, project.default_assigned_to) end + def project_default_issue_query_options(project) + public_queries = IssueQuery.only_public + grouped = { + l('label_default_queries.for_all_projects') => public_queries.where(project_id: nil).pluck(:name, :id), + l('label_default_queries.for_current_project') => public_queries.where(project: project).pluck(:name, :id) + } + grouped_options_for_select(grouped, project.default_issue_query_id) + end + def format_version_sharing(sharing) sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing) l("label_version_sharing_#{sharing}") diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 408567377..465e79a09 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -395,6 +395,8 @@ module QueriesHelper @query.project = @project end @query + else + @query = klass.default project: @project end end @@ -457,9 +459,16 @@ module QueriesHelper queries.collect do |query| css = +'query' clear_link = +'' + clear_link_param = {:set_filter => 1, :sort => '', :project_id => @project} + + if query == query.class.default(project: @project) + css << ' default' + clear_link_param[:without_default] = 1 + end + if query == @query css << ' selected' - clear_link += link_to_clear_query + clear_link += link_to_clear_query(clear_link_param) end content_tag('li', link_to(query.name, @@ -471,10 +480,10 @@ module QueriesHelper ) + "\n" end - def link_to_clear_query + def link_to_clear_query(params = {:set_filter => 1, :sort => '', :project_id => @project}) link_to( l(:button_clear), - {:set_filter => 1, :sort => '', :project_id => @project}, + params, :class => 'icon-only icon-clear-query', :title => l(:button_clear) ) diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 3c807f1f8..c3b8e7a25 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -166,6 +166,10 @@ module SettingsHelper options.map {|label, value| [l(label), value.to_s]} end + def default_global_issue_query_options + [[l(:label_none), '']] + IssueQuery.only_public.where(project_id: nil).pluck(:name, :id) + end + def cross_project_subtasks_options options = [ [:label_disabled, ''], diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 63e0f75fc..5c1c964f0 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -29,6 +29,18 @@ module UsersHelper user.valid_notification_options.collect {|o| [l(o.last), o.first]} end + def default_issue_query_options(user) + global_queries = IssueQuery.for_all_projects + global_public_queries = global_queries.only_public + global_user_queries = global_queries.where(user_id: user.id).where.not(id: global_public_queries.pluck(:id)) + label = user == User.current ? 'label_my_queries' : 'label_default_queries.for_this_user' + grouped = { + l('label_default_queries.for_all_users') => global_public_queries.pluck(:name, :id), + l(".#{label}") => global_user_queries.pluck(:name, :id), + } + grouped_options_for_select(grouped, user.pref.default_issue_query) + end + def textarea_font_options [[l(:label_font_default), '']] + UserPreference::TEXTAREA_FONT_OPTIONS.map {|o| [l("label_font_#{o}"), o]} end diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index e4da8d8c7..efd0ad0f1 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -73,6 +73,20 @@ class IssueQuery < Query QueryColumn.new(:last_notes, :caption => :label_last_notes, :inline => false) ] + has_many :projects, foreign_key: 'default_issue_query_id', dependent: :nullify, inverse_of: 'default_issue_query' + after_update { projects.clear unless visibility == VISIBILITY_PUBLIC } + scope :only_public, ->{ where(visibility: VISIBILITY_PUBLIC) } + scope :for_all_projects, ->{ where(project_id: nil) } + + def self.default(project: nil, user: User.current) + query = nil + if user&.logged? + query = find_by_id user.pref.default_issue_query + end + query ||= project&.default_issue_query + query || find_by_id(Setting.default_issue_query) + end + def initialize(attributes=nil, *args) super attributes self.filters ||= {'status_id' => {:operator => "o", :values => [""]}} diff --git a/app/models/project.rb b/app/models/project.rb index 36029b0ed..0afb4bdda 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -58,6 +58,8 @@ class Project < ActiveRecord::Base :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :association_foreign_key => 'custom_field_id' + # Default Custom Query + belongs_to :default_issue_query, :class_name => 'IssueQuery' acts_as_attachable :view_permission => :view_files, :edit_permission => :manage_files, @@ -824,6 +826,7 @@ class Project < ActiveRecord::Base 'issue_custom_field_ids', 'parent_id', 'default_version_id', + 'default_issue_query_id', 'default_assigned_to_id') safe_attributes( @@ -1221,6 +1224,9 @@ class Project < ActiveRecord::Base new_query.user_id = query.user_id new_query.role_ids = query.role_ids if query.visibility == ::Query::VISIBILITY_ROLES self.queries << new_query + if query == project.default_issue_query + self.default_issue_query = new_query + end end end diff --git a/app/models/query.rb b/app/models/query.rb index 50c53c821..3636e2c83 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -342,6 +342,12 @@ class Query < ActiveRecord::Base scope :sorted, lambda {order(:name, :id)} + # to be implemented in subclasses that have a way to determine a default + # query for the given options + def self.default(**_) + nil + end + # Scope of visible queries, can be used from subclasses only. # Unlike other visible scopes, a class methods is used as it # let handle inheritance more nicely than scope DSL. diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 930effb67..1675fb0ae 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -37,6 +37,7 @@ class UserPreference < ActiveRecord::Base 'textarea_font', 'recently_used_projects', 'history_default_tab', + 'default_issue_query', 'toolbar_language_options') TEXTAREA_FONT_OPTIONS = ['monospace', 'proportional'] @@ -116,6 +117,9 @@ class UserPreference < ActiveRecord::Base self[:toolbar_language_options] = languages.join(',') end + def default_issue_query; self[:default_issue_query] end + def default_issue_query=(value); self[:default_issue_query]=value; end + # Returns the names of groups that are displayed on user's page # Example: # preferences.my_page_groups diff --git a/app/views/projects/settings/_issues.html.erb b/app/views/projects/settings/_issues.html.erb index 27e792d22..a0f0c14d5 100644 --- a/app/views/projects/settings/_issues.html.erb +++ b/app/views/projects/settings/_issues.html.erb @@ -41,6 +41,10 @@ <% if @project.safe_attribute?('default_assigned_to_id') %>

<%= f.select :default_assigned_to_id, project_default_assigned_to_options(@project), include_blank: l(:label_none) %>

<% end %> + + <% if @project.safe_attribute?('default_issue_query_id') %> +

<%= f.select :default_issue_query_id, project_default_issue_query_options(@project), include_blank: l(:label_none) %><%=l 'text_allowed_queries_to_select' %>

+ <% end %>

<%= submit_tag l(:button_save) %>

diff --git a/app/views/settings/_issues.html.erb b/app/views/settings/_issues.html.erb index b4e50d8e3..d9dd26060 100644 --- a/app/views/settings/_issues.html.erb +++ b/app/views/settings/_issues.html.erb @@ -24,6 +24,8 @@

<%= setting_text_field :gantt_items_limit, :size => 6 %>

<%= setting_text_field :gantt_months_limit, :size => 6 %>

+ +

<%= setting_select :default_issue_query, default_global_issue_query_options %>

diff --git a/app/views/users/_preferences.html.erb b/app/views/users/_preferences.html.erb index 3734c3064..8074cb569 100644 --- a/app/views/users/_preferences.html.erb +++ b/app/views/users/_preferences.html.erb @@ -7,4 +7,5 @@

<%= pref_fields.text_field :recently_used_projects, :size => 2 %>

<%= pref_fields.select :history_default_tab, history_default_tab_options %>

<%= pref_fields.text_area :toolbar_language_options, :rows => 4 %>

+

<%= pref_fields.select :default_issue_query, default_issue_query_options(@user), include_blank: l(:label_none) %>

<% end %> diff --git a/config/locales/de.yml b/config/locales/de.yml index a58ded34a..9aa5acbd1 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -514,6 +514,10 @@ de: label_day_plural: Tage label_default: Standard label_default_columns: Standard-Spalten + label_default_queries: + for_all_projects: Für alle Projekte + for_current_project: Für das aktuelle Projekt + for_all_users: Für alle Benutzer label_deleted: gelöscht label_descending: Absteigend label_details: Details @@ -997,6 +1001,7 @@ de: setting_cross_project_subtasks: Projektübergreifende untergeordnete Tickets erlauben setting_date_format: Datumsformat setting_default_issue_start_date_to_creation_date: Aktuelles Datum als Beginn für neue Tickets verwenden + setting_default_issue_query: Standardabfrage setting_default_language: Standardsprache setting_default_notification_option: Standard Benachrichtigungsoptionen setting_default_projects_modules: Standardmäßig aktivierte Module für neue Projekte @@ -1064,6 +1069,7 @@ de: status_registered: nicht aktivierte text_account_destroy_confirmation: "Möchten Sie wirklich fortfahren?\nIhr Benutzerkonto wird für immer gelöscht und kann nicht wiederhergestellt werden." + text_allowed_queries_to_select: Nur für alle sichtbare Abfragen können ausgewählt werden text_are_you_sure: Sind Sie sicher? text_assign_time_entries_to_project: Gebuchte Aufwände dem Projekt zuweisen text_caracters_maximum: "Max. %{count} Zeichen." diff --git a/config/locales/en.yml b/config/locales/en.yml index 0aeafe516..94cb02412 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -408,6 +408,7 @@ en: field_history_default_tab: Issue's history default tab field_unique_id: Unique ID field_toolbar_language_options: Code highlighting toolbar languages + field_default_issue_query: Default issue query setting_app_title: Application title setting_welcome_text: Welcome text @@ -508,6 +509,7 @@ en: setting_show_status_changes_in_mail_subject: Show status changes in issue mail notifications subject setting_project_list_defaults: Projects list defaults setting_twofa: Two-factor authentication + setting_default_issue_query: Default Query permission_add_project: Create project permission_add_subprojects: Create subprojects @@ -1096,6 +1098,10 @@ en: label_optgroup_others: Other projects label_optgroup_recents: Recently used label_last_notes: Last notes + label_default_queries: + for_all_projects: For all projects + for_current_project: For current project + for_all_users: For all users label_nothing_to_preview: Nothing to preview label_inherited_from_parent_project: "Inherited from parent project" label_inherited_from_group: "Inherited from group %{name}" @@ -1276,6 +1282,7 @@ en: text_select_apply_tracker: "Select tracker" text_avatar_server_config_html: The current avatar server is %{url}. You can configure it in config/configuration.yml. text_no_subject: no subject + text_allowed_queries_to_select: Public (to any users) queries only selectable default_role_manager: Manager diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 7186187b8..a2ac4d3d3 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -391,6 +391,7 @@ fr: field_full_width_layout: Afficher sur toute la largeur field_digest: Checksum field_default_assigned_to: Assigné par défaut + field_default_issue_query: Rapport par défaut setting_app_title: Titre de l'application setting_welcome_text: Texte d'accueil @@ -481,6 +482,7 @@ fr: setting_time_entry_list_defaults: Affichage par défaut de la liste des temps passés setting_timelog_accept_0_hours: Autoriser la saisie de temps avec 0 heure setting_timelog_max_hours_per_day: Maximum d'heures pouvant être saisies par un utilisateur sur un jour + setting_default_issue_query: Rapport par défaut permission_add_project: Créer un projet permission_add_subprojects: Créer des sous-projets @@ -1026,6 +1028,10 @@ fr: label_font_monospace: Police non proportionnelle label_font_proportional: Police proportionnelle label_last_notes: Dernières notes + label_default_queries: + for_all_projects: Pour tous les projets + for_current_project: Pour le projet en cours + for_all_users: Pour tous les utilisateurs label_trackers_description: Description des trackers label_open_trackers_description: Afficher la description des trackers @@ -1217,6 +1223,7 @@ fr: description_issue_category_reassign: Choisir une catégorie description_wiki_subpages_reassign: Choisir une nouvelle page parent text_repository_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés.
Un fois sauvegardé, l''identifiant ne pourra plus être modifié.' + text_allowed_queries_to_select: Seuls les rapports publics (pour tous les utilisateurs) sont sélectionnables label_parent_task_attributes_derived: Calculé à partir des sous-tâches label_parent_task_attributes_independent: Indépendent des sous-tâches mail_subject_security_notification: Notification de sécurité diff --git a/config/settings.yml b/config/settings.yml index 9d7a3ad94..7960a3f29 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -122,6 +122,8 @@ gantt_items_limit: gantt_months_limit: format: int default: 24 +default_issue_query: + default: '' # Maximum size of files that can be displayed # inline through the file viewer (in KB) file_max_size_displayed: diff --git a/db/migrate/20200806105730_add_projects_default_issue_query_id.rb b/db/migrate/20200806105730_add_projects_default_issue_query_id.rb new file mode 100644 index 000000000..88e625b2d --- /dev/null +++ b/db/migrate/20200806105730_add_projects_default_issue_query_id.rb @@ -0,0 +1,9 @@ +class AddProjectsDefaultIssueQueryId < ActiveRecord::Migration[4.2] + def self.up + add_column :projects, :default_issue_query_id, :integer, :default => nil + end + + def self.down + remove_column :projects, :default_issue_query_id + end +end diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 3ed896b06..e117bc1d0 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -152,6 +152,7 @@ a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;} #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;} #sidebar a.selected:hover {text-decoration:none;} +#sidebar .query.default {font-weight: bold;} #admin-menu a {line-height:1.7em;} #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;} diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index ccebb4311..87a6ec704 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -8225,4 +8225,76 @@ class IssuesControllerTest < Redmine::ControllerTest end end end + + def test_index_should_retrieve_default_query + query = IssueQuery.find(4) + IssueQuery.stubs(:default).returns query + + [nil, 1].each do |user_id| + @request.session[:user_id] = user_id + get :index + assert_select 'h2', text: query.name + + get :index, params: { project_id: 1 } + assert_select 'h2', text: query.name + end + end + + def test_index_should_ignore_default_query_with_without_default + query = IssueQuery.find(4) + IssueQuery.stubs(:default).returns query + + [nil, 1].each do |user_id| + @request.session[:user_id] = user_id + get :index, params: { set_filter: '1', without_default: '1' } + assert_select 'h2', text: I18n.t(:label_issue_plural) + + get :index, params: { project_id: 1, set_filter: '1', without_default: '1' } + assert_select 'h2', text: I18n.t(:label_issue_plural) + end + end + + def test_index_should_ignore_default_query_with_session_query + query = IssueQuery.find 4 + IssueQuery.stubs(:default).returns query + session_query = IssueQuery.find 1 + + @request.session[:issue_query] = { id: 1, project_id: 1} + @request.session[:user_id] = 1 + get :index, params: { project_id: '1' } + assert_select 'h2', text: session_query.name + end + + def test_index_global_should_ignore_default_query_with_session_query + query = IssueQuery.find 4 + IssueQuery.stubs(:default).returns query + session_query = IssueQuery.find 5 + + @request.session[:issue_query] = { id: 5, project_id: nil} + @request.session[:user_id] = 1 + get :index + assert_select 'h2', text: session_query.name + end + + def test_index_should_use_default_query_with_invalid_session_query + query = IssueQuery.find 4 + IssueQuery.stubs(:default).returns query + + @request.session[:issue_query] = { id: 1, project_id: 1} + @request.session[:user_id] = 1 + get :index + assert_select 'h2', text: query.name + end + + def test_index_should_not_load_default_query_for_api_request + query = IssueQuery.find 4 + IssueQuery.stubs(:default).returns query + + @request.session[:user_id] = 1 + get :index, params: { format: 'json' } + + assert results = JSON.parse(@response.body)['issues'] + # query filters for tracker_id == 3 + assert results.detect{ |i| i['tracker_id'] != 3 } + end end diff --git a/test/unit/project_copy_test.rb b/test/unit/project_copy_test.rb index aca9cbf3f..50cc1c9c9 100644 --- a/test/unit/project_copy_test.rb +++ b/test/unit/project_copy_test.rb @@ -283,6 +283,19 @@ class ProjectCopyTest < ActiveSupport::TestCase assert_equal [1, 3], query.role_ids.sort end + test "#copy should copy default issue query assignment" do + source = Project.generate! + query = IssueQuery.generate!(:project => source, :user => User.find(2)) + source.update_column :default_issue_query_id, query.id + + target = Project.new(:name => 'Copy Test', :identifier => 'copy-test') + assert target.copy(source) + + assert target.default_issue_query.present? + assert_equal 1, target.queries.size + assert_equal query.name, target.default_issue_query.name + end + test "#copy should copy versions" do @source_project.versions << Version.generate! @source_project.versions << Version.generate! diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 07921a951..ccf30f477 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -2750,4 +2750,65 @@ class QueryTest < ActiveSupport::TestCase # Non-paginated issue ids and paginated issue ids should be in the same order. assert_equal issue_ids, paginated_issue_ids end + + def test_destruction_of_default_query_should_remove_reference_from_project + project = Project.find('ecookbook') + project_query = IssueQuery.find(1) + project.update_column :default_issue_query_id, project_query.id + + project_query.destroy + project.reload + assert_nil project.default_issue_query_id + end + + def test_should_determine_default_issue_query + project = Project.find('ecookbook') + user = project.users.first + + project_query = IssueQuery.find(1) + query = IssueQuery.find(4) + user_query = IssueQuery.find(3) + user_query.update_column :user_id, user.id + + [nil, user, User.anonymous].each do |u| + [nil, project].each do |p| + assert_nil IssueQuery.default(project: p, user: u) + end + end + + # only global default is set + with_settings :default_issue_query => query.id do + [nil, user, User.anonymous].each do |u| + [nil, project].each do |p| + assert_equal query, IssueQuery.default(project: p, user: u) + end + end + end + + # with project default + assert_equal project.id, project_query.project_id + project.update_column :default_issue_query_id, project_query.id + [nil, user, User.anonymous].each do |u| + assert_nil IssueQuery.default(project: nil, user: u) + assert_equal project_query, IssueQuery.default(project: project, user: u) + end + + # project default should override global default + with_settings :default_issue_query => query.id do + [nil, user, User.anonymous].each do |u| + assert_equal query, IssueQuery.default(project: nil, user: u) + assert_equal project_query, IssueQuery.default(project: project, user: u) + end + end + + # user default, overrides project and global default + user.pref.default_issue_query = user_query.id + user.pref.save + with_settings :default_issue_query => query.id do + [nil, project].each do |p| + assert_equal user_query, IssueQuery.default(project: p, user: user) + assert_equal user_query, IssueQuery.default(project: p, user: user) + end + end + end end -- 2.20.1