]> source.dussan.org Git - redmine.git/commitdiff
Default issue query (#7360).
authorGo MAEDA <maeda@farend.jp>
Tue, 6 Jul 2021 06:01:50 +0000 (06:01 +0000)
committerGo MAEDA <maeda@farend.jp>
Tue, 6 Jul 2021 06:01:50 +0000 (06:01 +0000)
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

21 files changed:
app/controllers/issues_controller.rb
app/helpers/projects_helper.rb
app/helpers/queries_helper.rb
app/helpers/settings_helper.rb
app/helpers/users_helper.rb
app/models/issue_query.rb
app/models/project.rb
app/models/query.rb
app/models/user_preference.rb
app/views/projects/settings/_issues.html.erb
app/views/settings/_issues.html.erb
app/views/users/_preferences.html.erb
config/locales/de.yml
config/locales/en.yml
config/locales/fr.yml
config/settings.yml
db/migrate/20210705111300_add_projects_default_issue_query_id.rb [new file with mode: 0644]
public/stylesheets/application.css
test/functional/issues_controller_test.rb
test/unit/project_copy_test.rb
test/unit/query_test.rb

index 62124db427b80d7b4982c6f4ce90a61ebef44070..080520ef5d2c5baa302717623e7007a4403f2a3f 100644 (file)
@@ -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)
index f87c5b91791d81029c297ae7fdd3db8599b8fcde..618650b997246fa0854ff17be4f8b046c55d4f6b 100644 (file)
@@ -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}")
index 4085673775fea9c4928d1155076f49d8c3b3c843..465e79a09869c4a0549dcc45ecb8766d1ac5ed95 100644 (file)
@@ -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)
     )
index 3c807f1f85bbf9deb434584e028ae1800b2e6f52..c3b8e7a252b986efed4ef34cd559d27603d9a0b4 100644 (file)
@@ -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, ''],
index 63e0f75fc679dae0703fde26fd4666190b5169fc..5c1c964f076e20d52fb0e96abed41f00ff8f02ce 100644 (file)
@@ -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
index e4da8d8c7be356a0d677506075f71ebe71f91a51..efd0ad0f1db22166d556ed86487f93b941def7e7 100644 (file)
@@ -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 => [""]}}
index 36029b0ed30325bf88983d91973e51495a14cfdf..0afb4bdda040e97f00e5977f7952a7373e5c5b07 100644 (file)
@@ -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
 
index 1810764a5088a1fc2e3483e0486e92819b7627db..c49e4f4d115228c4c6c8a1ea893ac88efb38f1ef 100644 (file)
@@ -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.
index 930effb6736b1053b52ecbc518a9b5061a5a1419..1675fb0ae7a97015e609e8a30728e62d358225d5 100644 (file)
@@ -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
index 27e792d22fe9e7a5d38a9b459c75bec3593bc789..a0f0c14d5fd9784698bdd62778a26664dffad257 100644 (file)
   <% 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>
index b4e50d8e3c331d717723a4f03340a235d92fd9ed..3eb7a07a1faeef5e7f906d99056bf3b490d5db8b 100644 (file)
@@ -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) %>
index 3734c3064760040f8878473c247b3c90822f600e..8074cb5690cafed2dcf903c399edc176524d47fb 100644 (file)
@@ -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 %>
index a58ded34ab3affbf023b938ce4c315b3c43b761c..9aa5acbd10511b25828e881c054bb29f9bff0f42 100644 (file)
@@ -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."
index 488a9b4eeedf50e7c05e2b372da0bb50ca3ed9f9..e72e1bf8977aaa492d9bb80df498c3522de11c24 100644 (file)
@@ -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
index 7186187b82de115625dd771ce7cba6b00d4baa3f..a2ac4d3d318de8a03665f77ec571b9df53bdb1ed 100644 (file)
@@ -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é
index 9d7a3ad946cbced2c3a0ef88cf9731adc9ee62b1..7960a3f29d3d7d6e69835d79c8c4987ea0410aa8 100644 (file)
@@ -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 (file)
index 0000000..88e625b
--- /dev/null
@@ -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
index 3ed896b064eb259fa20f6ff10312fe9996900e2f..e117bc1d0d8393d1bbf1c21ba1171de62184e78d 100644 (file)
@@ -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%;}
 
index ccebb43116295828777b0b060fa0f9c237836d58..87a6ec7045745625f96857bd29e52eb4197cde99 100644 (file)
@@ -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
index aca9cbf3fca4fa443c0e7ebadf9038e6205c6a0f..50cc1c9c9806395d61b635915d94f3ec7bcb2fc6 100644 (file)
@@ -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!
index 07921a951045d98899cdb0014e3abdd88e013a05..ccf30f4773e5b34c1746a6fd431d937504722792 100644 (file)
@@ -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