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-8f06a7374b81tags/5.0.0
@@ -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) |
@@ -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}") |
@@ -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) | |||
) |
@@ -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, ''], |
@@ -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 |
@@ -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 => [""]}} |
@@ -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 | |||
@@ -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. |
@@ -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 |
@@ -41,6 +41,10 @@ | |||
<% if @project.safe_attribute?('default_assigned_to_id') %> | |||
<p><%= f.select :default_assigned_to_id, project_default_assigned_to_options(@project), include_blank: l(:label_none) %></p> | |||
<% end %> | |||
<% if @project.safe_attribute?('default_issue_query_id') %> | |||
<p><%= f.select :default_issue_query_id, project_default_issue_query_options(@project), include_blank: l(:label_none) %><em class="info"><%=l 'text_allowed_queries_to_select' %></em></p> | |||
<% end %> | |||
</div> | |||
<p><%= submit_tag l(:button_save) %></p> |
@@ -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 %></p> | |||
<p><%= setting_select :default_issue_query, default_global_issue_query_options %></p> | |||
</fieldset> | |||
<%= submit_tag l(:button_save) %> |
@@ -7,4 +7,5 @@ | |||
<p><%= pref_fields.text_field :recently_used_projects, :size => 2 %></p> | |||
<p><%= pref_fields.select :history_default_tab, history_default_tab_options %></p> | |||
<p><%= pref_fields.text_area :toolbar_language_options, :rows => 4 %></p> | |||
<p><%= pref_fields.select :default_issue_query, default_issue_query_options(@user), include_blank: l(:label_none) %></p> | |||
<% end %> |
@@ -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." |
@@ -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 <a href="%{url}">%{url}</a>. 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 |
@@ -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.<br />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é |
@@ -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: |
@@ -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 |
@@ -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%;} | |||
@@ -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 |
@@ -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! |
@@ -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 |