From: Go MAEDA
Date: Tue, 6 Jul 2021 06:01:50 +0000 (+0000)
Subject: Default issue query (#7360).
X-Git-Tag: 5.0.0~350
X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=7f965286ed6b1d7e906e8662daba0d49b1cf7666;p=redmine.git
Default issue query (#7360).
Patch by Katsuya HIDAKA (the author of https://github.com/hidakatsuya/redmine_default_custom_query), Takenori TAKAKI, Olivier Chabert, and Jens Krämer.
git-svn-id: http://svn.redmine.org/redmine/trunk@21061 e93f8b46-1217-0410-a6f0-8f06a7374b81
---
diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb
index 62124db42..080520ef5 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?
@@ -480,6 +481,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 1810764a5..c49e4f4d1 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -340,6 +340,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..3eb7a07a1 100644
--- a/app/views/settings/_issues.html.erb
+++ b/app/views/settings/_issues.html.erb
@@ -47,6 +47,8 @@
IssueQuery.new(:totalable_names => Setting.issue_list_default_totals).available_totalable_columns.map {|c| [c.caption, c.name.to_s]},
:inline => true,
:label => :label_total_plural %>
+
+ <%= setting_select :default_issue_query, default_global_issue_query_options %>
<%= submit_tag l(:button_save) %>
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 488a9b4ee..e72e1bf89 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -409,6 +409,7 @@ en:
field_unique_id: Unique ID
field_toolbar_language_options: Code highlighting toolbar languages
field_twofa_required: Require two factor authentication
+ field_default_issue_query: Default issue query
setting_app_title: Application title
setting_welcome_text: Welcome text
@@ -509,6 +510,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
@@ -1097,6 +1099,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}"
@@ -1277,6 +1283,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/20210705111300_add_projects_default_issue_query_id.rb b/db/migrate/20210705111300_add_projects_default_issue_query_id.rb
new file mode 100644
index 000000000..88e625b2d
--- /dev/null
+++ b/db/migrate/20210705111300_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