Project

General

Profile

Feature #7360 » 0001-implements-global-per-project-and-per-user-default-i.patch

Olivier's patch, improved and rebased on r21043 - Jens Krämer, 2021-06-22 08:24

View differences:

app/controllers/issues_controller.rb
44 44

  
45 45
  def index
46 46
    use_session = !request.format.csv?
47
    retrieve_default_query(use_session)
47 48
    retrieve_query(IssueQuery, use_session)
48 49

  
49 50
    if @query.valid?
......
476 477
    super
477 478
  end
478 479

  
480
  def retrieve_default_query(use_session)
481
    return if params[:query_id].present?
482
    return if api_request?
483
    return if params[:set_filter] && (params.key?(:op) || params.key?(:f))
484

  
485
    if params[:without_default].present?
486
      params[:set_filter] = 1
487
      return
488
    end
489
    if !params[:set_filter] && use_session && session[:issue_query]
490
      query_id, project_id = session[:issue_query].values_at(:id, :project_id)
491
      return if IssueQuery.where(id: query_id).exists? && project_id == @project&.id
492
    end
493
    if default_query = IssueQuery.default(project: @project)
494
      params[:query_id] = default_query.id
495
    end
496
  end
497

  
479 498
  def retrieve_previous_and_next_issue_ids
480 499
    if params[:prev_issue_id].present? || params[:next_issue_id].present?
481 500
      @prev_issue_id = params[:prev_issue_id].presence.try(:to_i)
app/helpers/projects_helper.rb
114 114
    principals_options_for_select(assignable_users, project.default_assigned_to)
115 115
  end
116 116

  
117
  def project_default_issue_query_options(project)
118
    public_queries = IssueQuery.only_public
119
    grouped = {
120
      l('label_default_queries.for_all_projects')    => public_queries.where(project_id: nil).pluck(:name, :id),
121
      l('label_default_queries.for_current_project') => public_queries.where(project: project).pluck(:name, :id)
122
    }
123
    grouped_options_for_select(grouped, project.default_issue_query_id)
124
  end
125

  
117 126
  def format_version_sharing(sharing)
118 127
    sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
119 128
    l("label_version_sharing_#{sharing}")
app/helpers/queries_helper.rb
395 395
        @query.project = @project
396 396
      end
397 397
      @query
398
    else
399
      @query = klass.default project: @project
398 400
    end
399 401
  end
400 402

  
......
457 459
        queries.collect do |query|
458 460
          css = +'query'
459 461
          clear_link = +''
462
          clear_link_param = {:set_filter => 1, :sort => '', :project_id => @project}
463

  
464
          if query == query.class.default(project: @project)
465
            css << ' default'
466
            clear_link_param[:without_default] = 1
467
          end
468

  
460 469
          if query == @query
461 470
            css << ' selected'
462
            clear_link += link_to_clear_query
471
            clear_link += link_to_clear_query(clear_link_param)
463 472
          end
464 473
          content_tag('li',
465 474
                      link_to(query.name,
......
471 480
      ) + "\n"
472 481
  end
473 482

  
474
  def link_to_clear_query
483
  def link_to_clear_query(params = {:set_filter => 1, :sort => '', :project_id => @project})
475 484
    link_to(
476 485
      l(:button_clear),
477
      {:set_filter => 1, :sort => '', :project_id => @project},
486
      params,
478 487
      :class => 'icon-only icon-clear-query',
479 488
      :title => l(:button_clear)
480 489
    )
app/helpers/settings_helper.rb
166 166
    options.map {|label, value| [l(label), value.to_s]}
167 167
  end
168 168

  
169
  def default_global_issue_query_options
170
    [[l(:label_none), '']] + IssueQuery.only_public.where(project_id: nil).pluck(:name, :id)
171
  end
172

  
169 173
  def cross_project_subtasks_options
170 174
    options = [
171 175
      [:label_disabled, ''],
app/helpers/users_helper.rb
29 29
    user.valid_notification_options.collect {|o| [l(o.last), o.first]}
30 30
  end
31 31

  
32
  def default_issue_query_options(user)
33
    global_queries = IssueQuery.for_all_projects
34
    global_public_queries = global_queries.only_public
35
    global_user_queries = global_queries.where(user_id: user.id).where.not(id: global_public_queries.pluck(:id))
36
    label = user == User.current ? 'label_my_queries' : 'label_default_queries.for_this_user'
37
    grouped = {
38
      l('label_default_queries.for_all_users') => global_public_queries.pluck(:name, :id),
39
      l(".#{label}") => global_user_queries.pluck(:name, :id),
40
    }
41
    grouped_options_for_select(grouped, user.pref.default_issue_query)
42
  end
43

  
32 44
  def textarea_font_options
33 45
    [[l(:label_font_default), '']] + UserPreference::TEXTAREA_FONT_OPTIONS.map {|o| [l("label_font_#{o}"), o]}
34 46
  end
app/models/issue_query.rb
73 73
    QueryColumn.new(:last_notes, :caption => :label_last_notes, :inline => false)
74 74
  ]
75 75

  
76
  has_many :projects, foreign_key: 'default_issue_query_id', dependent: :nullify, inverse_of: 'default_issue_query'
77
  after_update { projects.clear unless visibility == VISIBILITY_PUBLIC }
78
  scope :only_public, ->{ where(visibility: VISIBILITY_PUBLIC) }
79
  scope :for_all_projects, ->{ where(project_id: nil) }
80

  
81
  def self.default(project: nil, user: User.current)
82
    query = nil
83
    if user&.logged?
84
      query = find_by_id user.pref.default_issue_query
85
    end
86
    query ||= project&.default_issue_query
87
    query || find_by_id(Setting.default_issue_query)
88
  end
89

  
76 90
  def initialize(attributes=nil, *args)
77 91
    super attributes
78 92
    self.filters ||= {'status_id' => {:operator => "o", :values => [""]}}
app/models/project.rb
58 58
                          :class_name => 'IssueCustomField',
59 59
                          :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
60 60
                          :association_foreign_key => 'custom_field_id'
61
  # Default Custom Query
62
  belongs_to :default_issue_query, :class_name => 'IssueQuery'
61 63

  
62 64
  acts_as_attachable :view_permission => :view_files,
63 65
                     :edit_permission => :manage_files,
......
824 826
    'issue_custom_field_ids',
825 827
    'parent_id',
826 828
    'default_version_id',
829
    'default_issue_query_id',
827 830
    'default_assigned_to_id')
828 831

  
829 832
  safe_attributes(
......
1221 1224
      new_query.user_id = query.user_id
1222 1225
      new_query.role_ids = query.role_ids if query.visibility == ::Query::VISIBILITY_ROLES
1223 1226
      self.queries << new_query
1227
      if query == project.default_issue_query
1228
        self.default_issue_query = new_query
1229
      end
1224 1230
    end
1225 1231
  end
1226 1232

  
app/models/query.rb
342 342

  
343 343
  scope :sorted, lambda {order(:name, :id)}
344 344

  
345
  # to be implemented in subclasses that have a way to determine a default
346
  # query for the given options
347
  def self.default(**_)
348
    nil
349
  end
350

  
345 351
  # Scope of visible queries, can be used from subclasses only.
346 352
  # Unlike other visible scopes, a class methods is used as it
347 353
  # let handle inheritance more nicely than scope DSL.
app/models/user_preference.rb
37 37
    'textarea_font',
38 38
    'recently_used_projects',
39 39
    'history_default_tab',
40
    'default_issue_query',
40 41
    'toolbar_language_options')
41 42

  
42 43
  TEXTAREA_FONT_OPTIONS = ['monospace', 'proportional']
......
116 117
    self[:toolbar_language_options] = languages.join(',')
117 118
  end
118 119

  
120
  def default_issue_query; self[:default_issue_query] end
121
  def default_issue_query=(value); self[:default_issue_query]=value; end
122

  
119 123
  # Returns the names of groups that are displayed on user's page
120 124
  # Example:
121 125
  #   preferences.my_page_groups
app/views/projects/settings/_issues.html.erb
41 41
  <% if @project.safe_attribute?('default_assigned_to_id') %>
42 42
    <p><%= f.select :default_assigned_to_id, project_default_assigned_to_options(@project), include_blank: l(:label_none) %></p>
43 43
  <% end %>
44

  
45
  <% if @project.safe_attribute?('default_issue_query_id') %>
46
    <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>
47
  <% end %>
44 48
  </div>
45 49

  
46 50
  <p><%= submit_tag l(:button_save) %></p>
app/views/settings/_issues.html.erb
24 24
<p><%= setting_text_field :gantt_items_limit, :size => 6 %></p>
25 25

  
26 26
<p><%= setting_text_field :gantt_months_limit, :size => 6 %></p>
27

  
28
<p><%= setting_select :default_issue_query, default_global_issue_query_options %></p>
27 29
</div>
28 30

  
29 31
<fieldset class="box">
app/views/users/_preferences.html.erb
7 7
<p><%= pref_fields.text_field :recently_used_projects, :size => 2 %></p>
8 8
<p><%= pref_fields.select :history_default_tab, history_default_tab_options %></p>
9 9
<p><%= pref_fields.text_area :toolbar_language_options, :rows => 4 %></p>
10
<p><%= pref_fields.select :default_issue_query, default_issue_query_options(@user), include_blank: l(:label_none) %></p>
10 11
<% end %>
config/locales/de.yml
514 514
  label_day_plural: Tage
515 515
  label_default: Standard
516 516
  label_default_columns: Standard-Spalten
517
  label_default_queries:
518
    for_all_projects: Für alle Projekte
519
    for_current_project: Für das aktuelle Projekt
520
    for_all_users: Für alle Benutzer
517 521
  label_deleted: gelöscht
518 522
  label_descending: Absteigend
519 523
  label_details: Details
......
997 1001
  setting_cross_project_subtasks: Projektübergreifende untergeordnete Tickets erlauben
998 1002
  setting_date_format: Datumsformat
999 1003
  setting_default_issue_start_date_to_creation_date: Aktuelles Datum als Beginn für neue Tickets verwenden
1004
  setting_default_issue_query: Standardabfrage
1000 1005
  setting_default_language: Standardsprache
1001 1006
  setting_default_notification_option: Standard Benachrichtigungsoptionen
1002 1007
  setting_default_projects_modules: Standardmäßig aktivierte Module für neue Projekte
......
1064 1069
  status_registered: nicht aktivierte
1065 1070

  
1066 1071
  text_account_destroy_confirmation: "Möchten Sie wirklich fortfahren?\nIhr Benutzerkonto wird für immer gelöscht und kann nicht wiederhergestellt werden."
1072
  text_allowed_queries_to_select: Nur für alle sichtbare Abfragen können ausgewählt werden
1067 1073
  text_are_you_sure: Sind Sie sicher?
1068 1074
  text_assign_time_entries_to_project: Gebuchte Aufwände dem Projekt zuweisen
1069 1075
  text_caracters_maximum: "Max. %{count} Zeichen."
config/locales/en.yml
408 408
  field_history_default_tab: Issue's history default tab
409 409
  field_unique_id: Unique ID
410 410
  field_toolbar_language_options: Code highlighting toolbar languages
411
  field_default_issue_query: Default issue query
411 412

  
412 413
  setting_app_title: Application title
413 414
  setting_welcome_text: Welcome text
......
508 509
  setting_show_status_changes_in_mail_subject: Show status changes in issue mail notifications subject
509 510
  setting_project_list_defaults: Projects list defaults
510 511
  setting_twofa: Two-factor authentication
512
  setting_default_issue_query: Default Query
511 513

  
512 514
  permission_add_project: Create project
513 515
  permission_add_subprojects: Create subprojects
......
1096 1098
  label_optgroup_others: Other projects
1097 1099
  label_optgroup_recents: Recently used
1098 1100
  label_last_notes: Last notes
1101
  label_default_queries:
1102
    for_all_projects: For all projects
1103
    for_current_project: For current project
1104
    for_all_users: For all users
1099 1105
  label_nothing_to_preview: Nothing to preview
1100 1106
  label_inherited_from_parent_project: "Inherited from parent project"
1101 1107
  label_inherited_from_group: "Inherited from group %{name}"
......
1276 1282
  text_select_apply_tracker: "Select tracker"
1277 1283
  text_avatar_server_config_html: The current avatar server is <a href="%{url}">%{url}</a>. You can configure it in config/configuration.yml.
1278 1284
  text_no_subject: no subject
1285
  text_allowed_queries_to_select: Public (to any users) queries only selectable
1279 1286

  
1280 1287

  
1281 1288
  default_role_manager: Manager
config/locales/fr.yml
391 391
  field_full_width_layout: Afficher sur toute la largeur
392 392
  field_digest: Checksum
393 393
  field_default_assigned_to: Assigné par défaut
394
  field_default_issue_query: Rapport par défaut
394 395

  
395 396
  setting_app_title: Titre de l'application
396 397
  setting_welcome_text: Texte d'accueil
......
481 482
  setting_time_entry_list_defaults: Affichage par défaut de la liste des temps passés
482 483
  setting_timelog_accept_0_hours: Autoriser la saisie de temps avec 0 heure
483 484
  setting_timelog_max_hours_per_day: Maximum d'heures pouvant être saisies par un utilisateur sur un jour
485
  setting_default_issue_query: Rapport par défaut
484 486

  
485 487
  permission_add_project: Créer un projet
486 488
  permission_add_subprojects: Créer des sous-projets
......
1026 1028
  label_font_monospace: Police non proportionnelle
1027 1029
  label_font_proportional: Police proportionnelle
1028 1030
  label_last_notes: Dernières notes
1031
  label_default_queries:
1032
    for_all_projects: Pour tous les projets
1033
    for_current_project: Pour le projet en cours
1034
    for_all_users: Pour tous les utilisateurs
1029 1035
  label_trackers_description: Description des trackers
1030 1036
  label_open_trackers_description: Afficher la description des trackers
1031 1037

  
......
1217 1223
  description_issue_category_reassign: Choisir une catégorie
1218 1224
  description_wiki_subpages_reassign: Choisir une nouvelle page parent
1219 1225
  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é.'
1226
  text_allowed_queries_to_select: Seuls les rapports publics (pour tous les utilisateurs) sont sélectionnables
1220 1227
  label_parent_task_attributes_derived: Calculé à partir des sous-tâches
1221 1228
  label_parent_task_attributes_independent: Indépendent des sous-tâches
1222 1229
  mail_subject_security_notification: Notification de sécurité
config/settings.yml
122 122
gantt_months_limit:
123 123
  format: int
124 124
  default: 24
125
default_issue_query:
126
  default: ''
125 127
# Maximum size of files that can be displayed
126 128
# inline through the file viewer (in KB)
127 129
file_max_size_displayed:
db/migrate/20200806105730_add_projects_default_issue_query_id.rb
1
class AddProjectsDefaultIssueQueryId < ActiveRecord::Migration[4.2]
2
  def self.up
3
    add_column :projects, :default_issue_query_id, :integer, :default => nil
4
  end
5

  
6
  def self.down
7
    remove_column :projects, :default_issue_query_id
8
  end
9
end
public/stylesheets/application.css
152 152

  
153 153
#sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
154 154
#sidebar a.selected:hover {text-decoration:none;}
155
#sidebar .query.default {font-weight: bold;}
155 156
#admin-menu a {line-height:1.7em;}
156 157
#admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
157 158

  
test/functional/issues_controller_test.rb
8225 8225
      end
8226 8226
    end
8227 8227
  end
8228

  
8229
  def test_index_should_retrieve_default_query
8230
    query = IssueQuery.find(4)
8231
    IssueQuery.stubs(:default).returns query
8232

  
8233
    [nil, 1].each do |user_id|
8234
      @request.session[:user_id] = user_id
8235
      get :index
8236
      assert_select 'h2', text: query.name
8237

  
8238
      get :index, params: { project_id: 1 }
8239
      assert_select 'h2', text: query.name
8240
    end
8241
  end
8242

  
8243
  def test_index_should_ignore_default_query_with_without_default
8244
    query = IssueQuery.find(4)
8245
    IssueQuery.stubs(:default).returns query
8246

  
8247
    [nil, 1].each do |user_id|
8248
      @request.session[:user_id] = user_id
8249
      get :index, params: { set_filter: '1', without_default: '1' }
8250
      assert_select 'h2', text: I18n.t(:label_issue_plural)
8251

  
8252
      get :index, params: { project_id: 1, set_filter: '1', without_default: '1' }
8253
      assert_select 'h2', text: I18n.t(:label_issue_plural)
8254
    end
8255
  end
8256

  
8257
  def test_index_should_ignore_default_query_with_session_query
8258
    query = IssueQuery.find 4
8259
    IssueQuery.stubs(:default).returns query
8260
    session_query = IssueQuery.find 1
8261

  
8262
    @request.session[:issue_query] = { id: 1, project_id: 1}
8263
    @request.session[:user_id] = 1
8264
    get :index, params: { project_id: '1' }
8265
    assert_select 'h2', text: session_query.name
8266
  end
8267

  
8268
  def test_index_global_should_ignore_default_query_with_session_query
8269
    query = IssueQuery.find 4
8270
    IssueQuery.stubs(:default).returns query
8271
    session_query = IssueQuery.find 5
8272

  
8273
    @request.session[:issue_query] = { id: 5, project_id: nil}
8274
    @request.session[:user_id] = 1
8275
    get :index
8276
    assert_select 'h2', text: session_query.name
8277
  end
8278

  
8279
  def test_index_should_use_default_query_with_invalid_session_query
8280
    query = IssueQuery.find 4
8281
    IssueQuery.stubs(:default).returns query
8282

  
8283
    @request.session[:issue_query] = { id: 1, project_id: 1}
8284
    @request.session[:user_id] = 1
8285
    get :index
8286
    assert_select 'h2', text: query.name
8287
  end
8288

  
8289
  def test_index_should_not_load_default_query_for_api_request
8290
    query = IssueQuery.find 4
8291
    IssueQuery.stubs(:default).returns query
8292

  
8293
    @request.session[:user_id] = 1
8294
    get :index, params: { format: 'json' }
8295

  
8296
    assert results = JSON.parse(@response.body)['issues']
8297
    # query filters for tracker_id == 3
8298
    assert results.detect{ |i| i['tracker_id'] != 3 }
8299
  end
8228 8300
end
test/unit/project_copy_test.rb
283 283
    assert_equal [1, 3], query.role_ids.sort
284 284
  end
285 285

  
286
  test "#copy should copy default issue query assignment" do
287
    source = Project.generate!
288
    query = IssueQuery.generate!(:project => source, :user => User.find(2))
289
    source.update_column :default_issue_query_id, query.id
290

  
291
    target = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
292
    assert target.copy(source)
293

  
294
    assert target.default_issue_query.present?
295
    assert_equal 1, target.queries.size
296
    assert_equal query.name, target.default_issue_query.name
297
  end
298

  
286 299
  test "#copy should copy versions" do
287 300
    @source_project.versions << Version.generate!
288 301
    @source_project.versions << Version.generate!
test/unit/query_test.rb
2750 2750
    # Non-paginated issue ids and paginated issue ids should be in the same order.
2751 2751
    assert_equal issue_ids, paginated_issue_ids
2752 2752
  end
2753

  
2754
  def test_destruction_of_default_query_should_remove_reference_from_project
2755
    project = Project.find('ecookbook')
2756
    project_query = IssueQuery.find(1)
2757
    project.update_column :default_issue_query_id, project_query.id
2758

  
2759
    project_query.destroy
2760
    project.reload
2761
    assert_nil project.default_issue_query_id
2762
  end
2763

  
2764
  def test_should_determine_default_issue_query
2765
    project = Project.find('ecookbook')
2766
    user = project.users.first
2767

  
2768
    project_query = IssueQuery.find(1)
2769
    query = IssueQuery.find(4)
2770
    user_query = IssueQuery.find(3)
2771
    user_query.update_column :user_id, user.id
2772

  
2773
    [nil, user, User.anonymous].each do |u|
2774
      [nil, project].each do |p|
2775
        assert_nil IssueQuery.default(project: p, user: u)
2776
      end
2777
    end
2778

  
2779
    # only global default is set
2780
    with_settings :default_issue_query => query.id do
2781
      [nil, user, User.anonymous].each do |u|
2782
        [nil, project].each do |p|
2783
          assert_equal query, IssueQuery.default(project: p, user: u)
2784
        end
2785
      end
2786
    end
2787

  
2788
    # with project default
2789
    assert_equal project.id, project_query.project_id
2790
    project.update_column :default_issue_query_id, project_query.id
2791
    [nil, user, User.anonymous].each do |u|
2792
      assert_nil IssueQuery.default(project: nil, user: u)
2793
      assert_equal project_query, IssueQuery.default(project: project, user: u)
2794
    end
2795

  
2796
    # project default should override global default
2797
    with_settings :default_issue_query => query.id do
2798
      [nil, user, User.anonymous].each do |u|
2799
        assert_equal query, IssueQuery.default(project: nil, user: u)
2800
        assert_equal project_query, IssueQuery.default(project: project, user: u)
2801
      end
2802
    end
2803

  
2804
    # user default, overrides project and global default
2805
    user.pref.default_issue_query = user_query.id
2806
    user.pref.save
2807
    with_settings :default_issue_query => query.id do
2808
      [nil, project].each do |p|
2809
        assert_equal user_query, IssueQuery.default(project: p, user: user)
2810
        assert_equal user_query, IssueQuery.default(project: p, user: user)
2811
      end
2812
    end
2813
  end
2753 2814
end
(7-7/14)