Fixes #2337 - Zammad stops Organization pagination after 500 entries.

This commit is contained in:
Rolf Schmidt 2020-10-30 08:59:32 +01:00 committed by Thorsten Eckel
parent 5b4c7ab886
commit 3fd0609f61
30 changed files with 590 additions and 251 deletions

View file

@ -138,7 +138,7 @@ class App.ControllerGenericIndex extends App.Controller
@render() @render()
# fetch all # fetch all
if !@disableInitFetch if !@disableInitFetch && !@pageData.pagerAjax
App[ @genericObject ].fetchFull( App[ @genericObject ].fetchFull(
-> ->
clear: true clear: true
@ -156,12 +156,45 @@ class App.ControllerGenericIndex extends App.Controller
if @subscribeId if @subscribeId
App[ @genericObject ].unsubscribe(@subscribeId) App[ @genericObject ].unsubscribe(@subscribeId)
paginate: (page) =>
return if page is @pageData.pagerSelected
@pageData.pagerSelected = page
@render()
render: => render: =>
if @pageData.pagerAjax
sortBy = @table?.customOrderBy || @table?.orderBy || @defaultSortBy || 'id'
orderBy = @table?.customOrderDirection || @table?.orderDirection || @defaultOrder || 'ASC'
fallbackSortBy = sortBy
fallbackOrderBy = orderBy
if sortBy isnt 'id'
fallbackSortBy = "#{sortBy}, id"
fallbackOrderBy = "#{orderBy}, ASC"
@startLoading()
App[@genericObject].indexFull(
(collection, data) =>
@pageData.pagerTotalCount = data.total_count
@stopLoading()
@renderObjects(collection)
{
refresh: false
sort_by: fallbackSortBy
order_by: fallbackOrderBy
page: @pageData.pagerSelected
per_page: @pageData.pagerPerPage
}
)
return
objects = App[@genericObject].search( objects = App[@genericObject].search(
sortBy: @defaultSortBy || 'name' sortBy: @defaultSortBy || 'name'
order: @defaultOrder order: @defaultOrder
) )
@renderObjects(objects)
renderObjects: (objects) =>
# remove ignored items from collection # remove ignored items from collection
if @ignoreObjectIDs if @ignoreObjectIDs
@ -208,10 +241,24 @@ class App.ControllerGenericIndex extends App.Controller
}, },
@pageData.tableExtend @pageData.tableExtend
) )
if @pageData.pagerAjax
params = _.extend(
{
pagerAjax: @pageData.pagerAjax
pagerBaseUrl: @pageData.pagerBaseUrl
pagerSelected: @pageData.pagerSelected
pagerPerPage: @pageData.pagerPerPage
pagerTotalCount: @pageData.pagerTotalCount
sortRenderCallback: @render
},
params
)
if !@table if !@table
@table = new App.ControllerTable(params) @table = new App.ControllerTable(params)
else else
@table.update(objects: objects) @table.update(objects: objects, pagerSelected: @pageData.pagerSelected, pagerTotalCount: @pageData.pagerTotalCount)
edit: (id, e) => edit: (id, e) =>
e.preventDefault() e.preventDefault()

View file

@ -196,6 +196,29 @@ class App.ControllerTable extends App.Controller
App.QueueManager.run('tableRender') App.QueueManager.run('tableRender')
renderPager: (el, find = false) => renderPager: (el, find = false) =>
if @pagerAjax
@renderPagerAjax(el, find)
else
@renderPagerStatic(el, find)
renderPagerAjax: (el, find = false) =>
pages = parseInt((@pagerTotalCount - 1) / @pagerPerPage)
if pages < 1
if find
el.find('.js-pager').html('')
else
el.filter('.js-pager').html('')
return
pager = App.view('generic/table_pager')(
page: @pagerSelected - 1
pages: pages
)
if find
el.find('.js-pager').html(pager)
else
el.filter('.js-pager').html(pager)
renderPagerStatic: (el, find = false) =>
pages = parseInt(((@objects.length - 1) / @shownPerPage)) pages = parseInt(((@objects.length - 1) / @shownPerPage))
if pages < 1 if pages < 1
if find if find
@ -214,6 +237,11 @@ class App.ControllerTable extends App.Controller
render: => render: =>
@setMaxPage() @setMaxPage()
# always render pager in case of ajax pagination
if @pagerTotalCount
@renderPager(@el, true)
if @renderState is undefined if @renderState is undefined
# check if table is empty # check if table is empty
@ -451,11 +479,14 @@ class App.ControllerTable extends App.Controller
container.delegate('.js-page', 'click', (e) => container.delegate('.js-page', 'click', (e) =>
e.stopPropagation() e.stopPropagation()
page = $(e.currentTarget).attr 'data-page' page = $(e.currentTarget).attr 'data-page'
render = => if @pagerAjax
@shownPage = page @navigate "#{@pagerBaseUrl}#{(parseInt(page) + 1)}"
@renderTableFull() else
App.QueueManager.add('tableRender', render) render = =>
App.QueueManager.run('tableRender') @shownPage = page
@renderTableFull()
App.QueueManager.add('tableRender', render)
App.QueueManager.run('tableRender')
) )
@el.html(container) @el.html(container)
@ -660,6 +691,19 @@ class App.ControllerTable extends App.Controller
@lastOrderDirection = orderDirection @lastOrderDirection = orderDirection
@lastOrderBy = orderBy @lastOrderBy = orderBy
# sorting for ajax pagination will be made in the backend
# so we only set the arrow for the sort direction
if @pagerAjax
for header in @headers
if header.name is orderBy || "#{header.name}_id" is orderBy || header.name is "#{orderBy}_id"
if orderDirection is 'DESC'
header.sortOrderIcon = ['arrow-down', 'table-sort-arrow']
else
header.sortOrderIcon = ['arrow-up', 'table-sort-arrow']
else
header.sortOrderIcon = undefined
return
# Underscore's sortBy cannot deal with null values, so we replace null values with a place holder string # Underscore's sortBy cannot deal with null values, so we replace null values with a place holder string
sortBy = (list, iteratee) -> sortBy = (list, iteratee) ->
_.sortBy( _.sortBy(
@ -964,6 +1008,10 @@ class App.ControllerTable extends App.Controller
sortByColumn: (event) => sortByColumn: (event) =>
column = $(event.currentTarget).closest('[data-column-key]').attr('data-column-key') column = $(event.currentTarget).closest('[data-column-key]').attr('data-column-key')
# for ajax pagination we only accept valid attributes for sorting
if @model && @pagerAjax
return if !@attributesList[column]
orderBy = @customOrderBy || @orderBy orderBy = @customOrderBy || @orderBy
orderDirection = @customOrderDirection || @orderDirection orderDirection = @customOrderDirection || @orderDirection
@ -988,6 +1036,10 @@ class App.ControllerTable extends App.Controller
render = => render = =>
@renderTableFull(false, skipHeadersResize: true) @renderTableFull(false, skipHeadersResize: true)
App.QueueManager.add('tableRender', render) App.QueueManager.add('tableRender', render)
if @sortRenderCallback
App.QueueManager.add('tableRender', @sortRenderCallback)
App.QueueManager.run('tableRender') App.QueueManager.run('tableRender')
preferencesStore: (type, key, value) -> preferencesStore: (type, key, value) ->

View file

@ -8,10 +8,15 @@ class Index extends App.ControllerSubContent
el: @el el: @el
id: @id id: @id
genericObject: 'Group' genericObject: 'Group'
defaultSortBy: 'name'
pageData: pageData:
home: 'groups' home: 'groups'
object: 'Group' object: 'Group'
objects: 'Groups' objects: 'Groups'
pagerAjax: true
pagerBaseUrl: '#manage/groups/'
pagerSelected: ( @page || 1 )
pagerPerPage: 150
navupdate: '#groups' navupdate: '#groups'
notes: [ notes: [
'Groups are ...' 'Groups are ...'
@ -22,4 +27,11 @@ class Index extends App.ControllerSubContent
container: @el.closest('.content') container: @el.closest('.content')
) )
show: (params) =>
for key, value of params
if key isnt 'el' && key isnt 'shown' && key isnt 'match'
@[key] = value
@genericController.paginate( @page || 1 )
App.Config.set('Group', { prio: 1500, name: 'Groups', parent: '#manage', target: '#manage/groups', controller: Index, permission: ['admin.group'] }, 'NavBarAdmin') App.Config.set('Group', { prio: 1500, name: 'Groups', parent: '#manage', target: '#manage/groups', controller: Index, permission: ['admin.group'] }, 'NavBarAdmin')

View file

@ -13,6 +13,10 @@ class Index extends App.ControllerSubContent
home: 'Jobs' home: 'Jobs'
object: 'Scheduler' object: 'Scheduler'
objects: 'Schedulers' objects: 'Schedulers'
pagerAjax: true
pagerBaseUrl: '#manage/job/'
pagerSelected: ( @page || 1 )
pagerPerPage: 150
navupdate: '#Jobs' navupdate: '#Jobs'
notes: [ notes: [
'Scheduler are ...' 'Scheduler are ...'
@ -24,4 +28,11 @@ class Index extends App.ControllerSubContent
large: true large: true
) )
show: (params) =>
for key, value of params
if key isnt 'el' && key isnt 'shown' && key isnt 'match'
@[key] = value
@genericController.paginate( @page || 1 )
App.Config.set('Job', { prio: 3400, name: 'Scheduler', parent: '#manage', target: '#manage/job', controller: Index, permission: ['admin.scheduler'] }, 'NavBarAdmin') App.Config.set('Job', { prio: 3400, name: 'Scheduler', parent: '#manage', target: '#manage/job', controller: Index, permission: ['admin.scheduler'] }, 'NavBarAdmin')

View file

@ -8,10 +8,15 @@ class Index extends App.ControllerSubContent
el: @el el: @el
id: @id id: @id
genericObject: 'Macro' genericObject: 'Macro'
defaultSortBy: 'name'
pageData: pageData:
home: 'macros' home: 'macros'
object: 'Macro' object: 'Macro'
objects: 'Macros' objects: 'Macros'
pagerAjax: true
pagerBaseUrl: '#manage/macros/'
pagerSelected: ( @page || 1 )
pagerPerPage: 150
navupdate: '#macros' navupdate: '#macros'
notes: [ notes: [
'Text modules are ...' 'Text modules are ...'
@ -22,4 +27,11 @@ class Index extends App.ControllerSubContent
container: @el.closest('.content') container: @el.closest('.content')
) )
show: (params) =>
for key, value of params
if key isnt 'el' && key isnt 'shown' && key isnt 'match'
@[key] = value
@genericController.paginate( @page || 1 )
App.Config.set('Macros', { prio: 2310, name: 'Macros', parent: '#manage', target: '#manage/macros', controller: Index, permission: ['admin.macro'] }, 'NavBarAdmin') App.Config.set('Macros', { prio: 2310, name: 'Macros', parent: '#manage', target: '#manage/macros', controller: Index, permission: ['admin.macro'] }, 'NavBarAdmin')

View file

@ -21,6 +21,7 @@ class ManageRouter extends App.ControllerPermanent
App.Config.set('manage', ManageRouter, 'Routes') App.Config.set('manage', ManageRouter, 'Routes')
App.Config.set('manage/:target', ManageRouter, 'Routes') App.Config.set('manage/:target', ManageRouter, 'Routes')
App.Config.set('manage/:target/:page', ManageRouter, 'Routes')
App.Config.set('settings/:target', ManageRouter, 'Routes') App.Config.set('settings/:target', ManageRouter, 'Routes')
App.Config.set('channels/:target', ManageRouter, 'Routes') App.Config.set('channels/:target', ManageRouter, 'Routes')
App.Config.set('channels/:target/:channel_id', ManageRouter, 'Routes') App.Config.set('channels/:target/:channel_id', ManageRouter, 'Routes')

View file

@ -13,10 +13,15 @@ class Index extends App.ControllerSubContent
baseUrl: '/api/v1/organizations' baseUrl: '/api/v1/organizations'
container: @el.closest('.content') container: @el.closest('.content')
) )
defaultSortBy: 'name'
pageData: pageData:
home: 'organizations' home: 'organizations'
object: 'Organization' object: 'Organization'
objects: 'Organizations' objects: 'Organizations'
pagerAjax: true
pagerBaseUrl: '#manage/organizations/'
pagerSelected: ( @page || 1 )
pagerPerPage: 150
navupdate: '#organizations' navupdate: '#organizations'
notes: [ notes: [
'Organizations are for any person in the system. Agents (Owners, Resposbiles, ...) and Customers.' 'Organizations are for any person in the system. Agents (Owners, Resposbiles, ...) and Customers.'
@ -28,4 +33,12 @@ class Index extends App.ControllerSubContent
container: @el.closest('.content') container: @el.closest('.content')
) )
show: (params) =>
for key, value of params
if key isnt 'el' && key isnt 'shown' && key isnt 'match'
@[key] = value
@genericController.paginate( @page || 1 )
App.Config.set('Organization', { prio: 2000, name: 'Organizations', parent: '#manage', target: '#manage/organizations', controller: Index, permission: ['admin.organization'] }, 'NavBarAdmin') App.Config.set('Organization', { prio: 2000, name: 'Organizations', parent: '#manage', target: '#manage/organizations', controller: Index, permission: ['admin.organization'] }, 'NavBarAdmin')

View file

@ -8,10 +8,15 @@ class Index extends App.ControllerSubContent
el: @el el: @el
id: @id id: @id
genericObject: 'ReportProfile' genericObject: 'ReportProfile'
defaultSortBy: 'name'
pageData: pageData:
home: 'report_profiles' home: 'report_profiles'
object: 'Report Profile' object: 'Report Profile'
objects: 'Report Profiles' objects: 'Report Profiles'
pagerAjax: true
pagerBaseUrl: '#manage/report_profiles/'
pagerSelected: ( @page || 1 )
pagerPerPage: 150
navupdate: '#report_profiles' navupdate: '#report_profiles'
notes: [ notes: [
# 'Report Profile are ...' # 'Report Profile are ...'
@ -22,4 +27,11 @@ class Index extends App.ControllerSubContent
container: @el.closest('.content') container: @el.closest('.content')
) )
show: (params) =>
for key, value of params
if key isnt 'el' && key isnt 'shown' && key isnt 'match'
@[key] = value
@genericController.paginate( @page || 1 )
App.Config.set('ReportProfile', { prio: 8000, name: 'Report Profiles', parent: '#manage', target: '#manage/report_profiles', controller: Index, permission: ['admin.report_profile'] }, 'NavBarAdmin') App.Config.set('ReportProfile', { prio: 8000, name: 'Report Profiles', parent: '#manage', target: '#manage/report_profiles', controller: Index, permission: ['admin.report_profile'] }, 'NavBarAdmin')

View file

@ -8,10 +8,15 @@ class Index extends App.ControllerSubContent
el: @el el: @el
id: @id id: @id
genericObject: 'Role' genericObject: 'Role'
defaultSortBy: 'name'
pageData: pageData:
home: 'roles' home: 'roles'
object: 'Role' object: 'Role'
objects: 'Roles' objects: 'Roles'
pagerAjax: true
pagerBaseUrl: '#manage/roles/'
pagerSelected: ( @page || 1 )
pagerPerPage: 150
navupdate: '#roles' navupdate: '#roles'
notes: [ notes: [
'Roles are ...' 'Roles are ...'
@ -22,4 +27,11 @@ class Index extends App.ControllerSubContent
container: @el.closest('.content') container: @el.closest('.content')
) )
show: (params) =>
for key, value of params
if key isnt 'el' && key isnt 'shown' && key isnt 'match'
@[key] = value
@genericController.paginate( @page || 1 )
App.Config.set('Role', { prio: 1600, name: 'Roles', parent: '#manage', target: '#manage/roles', controller: Index, permission: ['admin.role'] }, 'NavBarAdmin') App.Config.set('Role', { prio: 1600, name: 'Roles', parent: '#manage', target: '#manage/roles', controller: Index, permission: ['admin.role'] }, 'NavBarAdmin')

View file

@ -8,6 +8,7 @@ class Index extends App.ControllerSubContent
el: @el el: @el
id: @id id: @id
genericObject: 'TextModule' genericObject: 'TextModule'
defaultSortBy: 'name'
importCallback: -> importCallback: ->
new App.Import( new App.Import(
baseUrl: '/api/v1/text_modules' baseUrl: '/api/v1/text_modules'
@ -18,6 +19,10 @@ class Index extends App.ControllerSubContent
home: 'text_modules' home: 'text_modules'
object: 'TextModule' object: 'TextModule'
objects: 'Text modules' objects: 'Text modules'
pagerAjax: true
pagerBaseUrl: '#manage/text_modules/'
pagerSelected: ( @page || 1 )
pagerPerPage: 150
navupdate: '#text_modules' navupdate: '#text_modules'
notes: [ notes: [
'Text modules are ...' 'Text modules are ...'
@ -29,4 +34,11 @@ class Index extends App.ControllerSubContent
container: @el.closest('.content') container: @el.closest('.content')
) )
show: (params) =>
for key, value of params
if key isnt 'el' && key isnt 'shown' && key isnt 'match'
@[key] = value
@genericController.paginate( @page || 1 )
App.Config.set('TextModule', { prio: 2300, name: 'Text modules', parent: '#manage', target: '#manage/text_modules', controller: Index, permission: ['admin.text_module'] }, 'NavBarAdmin') App.Config.set('TextModule', { prio: 2300, name: 'Text modules', parent: '#manage', target: '#manage/text_modules', controller: Index, permission: ['admin.text_module'] }, 'NavBarAdmin')

View file

@ -13,6 +13,10 @@ class Index extends App.ControllerSubContent
home: 'triggers' home: 'triggers'
object: 'Trigger' object: 'Trigger'
objects: 'Triggers' objects: 'Triggers'
pagerAjax: true
pagerBaseUrl: '#manage/trigger/'
pagerSelected: ( @page || 1 )
pagerPerPage: 150
navupdate: '#triggers' navupdate: '#triggers'
notes: [ notes: [
'Triggers are ...' 'Triggers are ...'
@ -24,4 +28,11 @@ class Index extends App.ControllerSubContent
veryLarge: true veryLarge: true
) )
show: (params) =>
for key, value of params
if key isnt 'el' && key isnt 'shown' && key isnt 'match'
@[key] = value
@genericController.paginate( @page || 1 )
App.Config.set('Trigger', { prio: 3300, name: 'Trigger', parent: '#manage', target: '#manage/trigger', controller: Index, permission: ['admin.trigger'] }, 'NavBarAdmin') App.Config.set('Trigger', { prio: 3300, name: 'Trigger', parent: '#manage', target: '#manage/trigger', controller: Index, permission: ['admin.trigger'] }, 'NavBarAdmin')

View file

@ -621,6 +621,74 @@ set new attributes of model (remove already available attributes)
App.Log.error('Model', statusText, error, url) App.Log.error('Model', statusText, error, url)
) )
###
index full collection (with assets)
App.Model.indexFull(@callback)
App.Model.indexFull(
@callback
page: 1
per_page: 10
sort_by: 'name'
order_by: 'ASC'
)
###
@indexFull: (callback, params = {}) ->
url = "#{@url}?full=true"
for key in ['page', 'per_page', 'sort_by', 'order_by']
continue if !params[key]
url += "&#{key}=#{params[key]}"
App.Log.debug('Model', "indexFull collection #{@className}", url)
# request already active, queue callback
queueManagerName = "#{@className}::indexFull"
if params.refresh is undefined
params.refresh = true
App.Ajax.request(
type: 'GET'
url: url
processData: true,
success: (data, status, xhr) =>
App.Log.debug('Model', "got indexFull collection #{@className}", data)
recordIds = data.record_ids
if data.record_ids is undefined
recordIds = data[ @className.toLowerCase() + '_ids' ]
# full / load assets
if data.assets
App.Collection.loadAssets(data.assets, targetModel: @className)
# if no record_ids are found, no initial render is fired
if data.record_ids && _.isEmpty(data.record_ids) && params.refresh
App[@className].trigger('refresh', [])
# find / load object
else if params.refresh
App[@className].refresh(data)
if callback
localCallback = =>
collection = []
for id in recordIds
collection.push App[@className].find(id)
callback(collection, data)
App.QueueManager.add(queueManagerName, localCallback)
App.QueueManager.run(queueManagerName)
error: (xhr, statusText, error) =>
@indexFullActive = false
App.Log.error('Model', statusText, error, url)
)
@_bindsEmpty: -> @_bindsEmpty: ->
if @SUBSCRIPTION_ITEM if @SUBSCRIPTION_ITEM
for id, keys of @SUBSCRIPTION_ITEM for id, keys of @SUBSCRIPTION_ITEM
@ -667,8 +735,9 @@ set new attributes of model (remove already available attributes)
# just show this values in result, all filters need to match to get shown # just show this values in result, all filters need to match to get shown
filter: filter:
some_attribute1: ['only_this_value1', 'only_that_value1']
some_attribute2: ['only_this_value2', 'only_that_value2'] # check single value
some_attribute1: 'only_this_value1'
# just show this values in result, all filters need to match to get shown # just show this values in result, all filters need to match to get shown
filterExtended: filterExtended:

View file

@ -105,7 +105,12 @@ module ApplicationController::RendersModels
per_page = (params[:per_page] || 500).to_i per_page = (params[:per_page] || 500).to_i
offset = (page - 1) * per_page offset = (page - 1) * per_page
generic_objects = object.order(id: :asc).offset(offset).limit(per_page) sql_helper = ::SqlHelper.new(object: object)
sort_by = sql_helper.get_sort_by(params, 'id')
order_by = sql_helper.get_order_by(params, 'ASC')
order_sql = sql_helper.get_order(sort_by, order_by)
generic_objects = object.order(Arel.sql(order_sql)).offset(offset).limit(per_page)
if response_expand? if response_expand?
list = [] list = []
@ -124,8 +129,9 @@ module ApplicationController::RendersModels
assets = item.assets(assets) assets = item.assets(assets)
end end
render json: { render json: {
record_ids: item_ids, record_ids: item_ids,
assets: assets, assets: assets,
total_count: object.count
}, status: :ok }, status: :ok
return return
end end

View file

@ -48,48 +48,7 @@ curl http://localhost/api/v1/organizations -v -u #{login}:#{password}
=end =end
def index def index
offset = 0 model_index_render(policy_scope(Organization), params)
per_page = 500
if params[:page] && params[:per_page]
offset = (params[:page].to_i - 1) * params[:per_page].to_i
per_page = params[:per_page].to_i
end
if per_page > 500
per_page = 500
end
organizations = policy_scope(Organization).order(id: :asc).offset(offset).limit(per_page)
if response_expand?
list = []
organizations.each do |organization|
list.push organization.attributes_with_association_names
end
render json: list, status: :ok
return
end
if response_full?
assets = {}
item_ids = []
organizations.each do |item|
item_ids.push item.id
assets = item.assets(assets)
end
render json: {
record_ids: item_ids,
assets: assets,
}, status: :ok
return
end
list = []
organizations.each do |organization|
list.push organization.attributes_with_association_ids
end
render json: list
end end
=begin =begin

View file

@ -1,166 +0,0 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
module HasSearchSortable
extend ActiveSupport::Concern
# methods defined here are going to extend the class, not the instance of it
class_methods do
=begin
This function will check the params for the "sort_by" attribute
and validate its values.
sort_by = search_get_sort_by(params, default)
returns
sort_by = [
'created_at',
'updated_at',
]
=end
def search_get_sort_by(params, default)
sort_by = []
if params[:sort_by].present? && params[:sort_by].is_a?(String)
params[:sort_by] = [params[:sort_by]]
elsif params[:sort_by].blank?
params[:sort_by] = []
end
# check order
params[:sort_by].each do |value|
# only accept values which are set for the db schema
raise "Found invalid column '#{value}' for sorting." if columns_hash[value].blank?
sort_by.push(value)
end
if sort_by.blank?
if default.is_a?(Array)
sort_by = default
else
sort_by.push(default)
end
end
sort_by
end
=begin
This function will check the params for the "order_by" attribute
and validate its values.
order_by = search_get_order_by(params, default)
returns
order_by = [
'asc',
'desc',
]
=end
def search_get_order_by(params, default)
order_by = []
if params[:order_by].present? && params[:order_by].is_a?(String)
params[:order_by] = [ params[:order_by] ]
elsif params[:order_by].blank?
params[:order_by] = []
end
# check order
params[:order_by].each do |value|
raise "Found invalid order by value #{value}. Please use 'asc' or 'desc'." if !value.match?(/\A(asc|desc)\z/i)
order_by.push(value.downcase)
end
if order_by.blank?
if default.is_a?(Array)
order_by = default
else
order_by.push(default)
end
end
order_by
end
=begin
This function will use the evaluated values for sort_by and
order_by to generate the ORDER-SELECT sql statement for the sorting
of the result.
sort_by = [ 'created_at', 'updated_at' ]
order_by = [ 'asc', 'desc' ]
default = 'tickets.created_at'
sql = search_get_order_select_sql(sort_by, order_by, default)
returns
sql = 'tickets.created_at, tickets.updated_at'
=end
def search_get_order_select_sql(sort_by, order_by, default)
sql = []
sort_by.each_with_index do |value, index|
next if value.blank?
next if order_by[index].blank?
sql.push( "#{ActiveRecord::Base.connection.quote_table_name(table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)}" )
end
if sql.blank?
sql.push("#{ActiveRecord::Base.connection.quote_table_name(table_name)}.#{ActiveRecord::Base.connection.quote_column_name(default)}")
end
sql.join(', ')
end
=begin
This function will use the evaluated values for sort_by and
order_by to generate the ORDER- sql statement for the sorting
of the result.
sort_by = [ 'created_at', 'updated_at' ]
order_by = [ 'asc', 'desc' ]
default = 'tickets.created_at DESC'
sql = search_get_order_sql(sort_by, order_by, default)
returns
sql = 'tickets.created_at ASC, tickets.updated_at DESC'
=end
def search_get_order_sql(sort_by, order_by, default)
sql = []
sort_by.each_with_index do |value, index|
next if value.blank?
next if order_by[index].blank?
sql.push( "#{ActiveRecord::Base.connection.quote_table_name(table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)} #{order_by[index]}" )
end
if sql.blank?
sql.push("#{ActiveRecord::Base.connection.quote_table_name(table_name)}.#{ActiveRecord::Base.connection.quote_column_name(default)}")
end
sql.join(', ')
end
end
end

View file

@ -2,21 +2,19 @@ class KnowledgeBase
module Search module Search
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
include HasSearchSortable
end
class_methods do class_methods do
def search(params) def search(params)
current_user = params[:current_user] current_user = params[:current_user]
# enable search only for agents and admins # enable search only for agents and admins
return [] if !search_preferences(current_user) return [] if !search_preferences(current_user)
sql_helper = ::SqlHelper.new(object: self)
options = { options = {
limit: params[:limit] || 10, limit: params[:limit] || 10,
from: params[:offset] || 0, from: params[:offset] || 0,
sort_by: search_get_sort_by(params, 'updated_at'), sort_by: sql_helper.get_sort_by(params, 'updated_at'),
order_by: search_get_order_by(params, 'desc'), order_by: sql_helper.get_order_by(params, 'desc'),
user: current_user user: current_user
} }
@ -41,8 +39,9 @@ class KnowledgeBase
end end
def search_sql(query, kb_locales, options) def search_sql(query, kb_locales, options)
table_name = arel_table.name table_name = arel_table.name
order_sql = search_get_order_sql(options[:sort_by], options[:order_by], "#{table_name}.updated_at ASC") sql_helper = ::SqlHelper.new(object: self)
order_sql = sql_helper.get_order(options[:sort_by], options[:order_by], "#{table_name}.updated_at ASC")
# - stip out * we already search for *query* - # - stip out * we already search for *query* -
query.delete! '*' query.delete! '*'

View file

@ -4,10 +4,6 @@ class Organization
module Search module Search
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
include HasSearchSortable
end
# methods defined here are going to extend the class, not the instance of it # methods defined here are going to extend the class, not the instance of it
class_methods do class_methods do
@ -72,11 +68,13 @@ returns
offset = params[:offset] || 0 offset = params[:offset] || 0
current_user = params[:current_user] current_user = params[:current_user]
sql_helper = ::SqlHelper.new(object: self)
# check sort - positions related to order by # check sort - positions related to order by
sort_by = search_get_sort_by(params, %w[active updated_at]) sort_by = sql_helper.get_sort_by(params, %w[active updated_at])
# check order - positions related to sort by # check order - positions related to sort by
order_by = search_get_order_by(params, %w[desc desc]) order_by = sql_helper.get_order_by(params, %w[desc desc])
# enable search only for agents and admins # enable search only for agents and admins
return [] if !search_preferences(current_user) return [] if !search_preferences(current_user)
@ -97,8 +95,8 @@ returns
return organizations return organizations
end end
order_select_sql = search_get_order_select_sql(sort_by, order_by, 'organizations.updated_at') order_select_sql = sql_helper.get_order_select(sort_by, order_by, 'organizations.updated_at')
order_sql = search_get_order_sql(sort_by, order_by, 'organizations.updated_at ASC') order_sql = sql_helper.get_order(sort_by, order_by, 'organizations.updated_at ASC')
# fallback do sql query # fallback do sql query
# - stip out * we already search for *query* - # - stip out * we already search for *query* -

View file

@ -2,10 +2,6 @@
module Ticket::Search module Ticket::Search
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
include HasSearchSortable
end
# methods defined here are going to extend the class, not the instance of it # methods defined here are going to extend the class, not the instance of it
class_methods do class_methods do
@ -119,11 +115,13 @@ returns
full = true full = true
end end
sql_helper = ::SqlHelper.new(object: self)
# check sort # check sort
sort_by = search_get_sort_by(params, 'updated_at') sort_by = sql_helper.get_sort_by(params, 'updated_at')
# check order # check order
order_by = search_get_order_by(params, 'desc') order_by = sql_helper.get_order_by(params, 'desc')
# try search index backend # try search index backend
if condition.blank? && SearchIndexBackend.enabled? if condition.blank? && SearchIndexBackend.enabled?
@ -197,8 +195,8 @@ returns
# do query # do query
# - stip out * we already search for *query* - # - stip out * we already search for *query* -
order_select_sql = search_get_order_select_sql(sort_by, order_by, 'tickets.updated_at') order_select_sql = sql_helper.get_order_select(sort_by, order_by, 'tickets.updated_at')
order_sql = search_get_order_sql(sort_by, order_by, 'tickets.updated_at DESC') order_sql = sql_helper.get_order(sort_by, order_by, 'tickets.updated_at DESC')
if query if query
query.delete! '*' query.delete! '*'
tickets_all = Ticket.select("DISTINCT(tickets.id), #{order_select_sql}") tickets_all = Ticket.select("DISTINCT(tickets.id), #{order_select_sql}")

View file

@ -4,10 +4,6 @@ class User
module Search module Search
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
include HasSearchSortable
end
# methods defined here are going to extend the class, not the instance of it # methods defined here are going to extend the class, not the instance of it
class_methods do class_methods do
@ -83,11 +79,13 @@ returns
offset = params[:offset] || 0 offset = params[:offset] || 0
current_user = params[:current_user] current_user = params[:current_user]
sql_helper = ::SqlHelper.new(object: self)
# check sort - positions related to order by # check sort - positions related to order by
sort_by = search_get_sort_by(params, %w[active updated_at]) sort_by = sql_helper.get_sort_by(params, %w[active updated_at])
# check order - positions related to sort by # check order - positions related to sort by
order_by = search_get_order_by(params, %w[desc desc]) order_by = sql_helper.get_order_by(params, %w[desc desc])
# enable search only for agents and admins # enable search only for agents and admins
return [] if !search_preferences(current_user) return [] if !search_preferences(current_user)
@ -129,7 +127,7 @@ returns
return users return users
end end
order_sql = search_get_order_sql(sort_by, order_by, 'users.updated_at DESC') order_sql = sql_helper.get_order(sort_by, order_by, 'users.updated_at DESC')
# fallback do sql query # fallback do sql query
# - stip out * we already search for *query* - # - stip out * we already search for *query* -

172
lib/sql_helper.rb Normal file
View file

@ -0,0 +1,172 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class SqlHelper
def initialize(object:)
@object = object
end
def get_param_key(key, params)
sort_by = []
if params[key].present? && params[key].is_a?(String)
params[key] = params[key].split(/\s*,\s*/)
elsif params[key].blank?
params[key] = []
end
sort_by
end
=begin
This function will check the params for the "sort_by" attribute
and validate its values.
sql_helper = SqlHelper.new(object: Ticket)
sort_by = sql_helper.get_sort_by(params, default)
returns
sort_by = [
'created_at',
'updated_at',
]
=end
def get_sort_by(params, default = nil)
sort_by = get_param_key(:sort_by, params)
# check order
params[:sort_by].each do |value|
# only accept values which are set for the db schema
raise "Found invalid column '#{value}' for sorting." if @object.columns_hash[value].blank?
sort_by.push(value)
end
if sort_by.blank? && default.present?
if default.is_a?(Array)
sort_by = default
else
sort_by.push(default)
end
end
sort_by
end
=begin
This function will check the params for the "order_by" attribute
and validate its values.
sql_helper = SqlHelper.new(object: Ticket)
order_by = sql_helper.get_order_by(params, default)
returns
order_by = [
'asc',
'desc',
]
=end
def get_order_by(params, default = nil)
order_by = get_param_key(:order_by, params)
# check order
params[:order_by].each do |value|
raise "Found invalid order by value #{value}. Please use 'asc' or 'desc'." if !value.match?(/\A(asc|desc)\z/i)
order_by.push(value.downcase)
end
if order_by.blank? && default.present?
if default.is_a?(Array)
order_by = default
else
order_by.push(default)
end
end
order_by
end
def set_sql_order_default(sql, default)
if sql.blank? && default.present?
sql.push("#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(default)}")
end
sql
end
=begin
This function will use the evaluated values for sort_by and
order_by to generate the ORDER-SELECT sql statement for the sorting
of the result.
sort_by = [ 'created_at', 'updated_at' ]
order_by = [ 'asc', 'desc' ]
default = 'tickets.created_at'
sql_helper = SqlHelper.new(object: Ticket)
sql = sql_helper.get_order_select(sort_by, order_by, default)
returns
sql = 'tickets.created_at, tickets.updated_at'
=end
def get_order_select(sort_by, order_by, default = nil)
sql = []
sort_by.each_with_index do |value, index|
next if value.blank?
next if order_by[index].blank?
sql.push( "#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)}" )
end
sql = set_sql_order_default(sql, default)
sql.join(', ')
end
=begin
This function will use the evaluated values for sort_by and
order_by to generate the ORDER- sql statement for the sorting
of the result.
sort_by = [ 'created_at', 'updated_at' ]
order_by = [ 'asc', 'desc' ]
default = 'tickets.created_at DESC'
sql_helper = SqlHelper.new(object: Ticket)
sql = sql_helper.get_order(sort_by, order_by, default)
returns
sql = 'tickets.created_at ASC, tickets.updated_at DESC'
=end
def get_order(sort_by, order_by, default = nil)
sql = []
sort_by.each_with_index do |value, index|
next if value.blank?
next if order_by[index].blank?
sql.push( "#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)} #{order_by[index]}" )
end
sql = set_sql_order_default(sql, default)
sql.join(', ')
end
end

View file

@ -0,0 +1,8 @@
FactoryBot.define do
factory :'report/profile', aliases: %i[report_profile] do
sequence(:name) { |n| "Report #{n}" }
active { true }
created_by_id { 1 }
updated_by_id { 1 }
end
end

View file

@ -0,0 +1,45 @@
RSpec.shared_examples 'pagination' do |model:, klass:, path:, sort_by: :name|
def prepare(model)
create_list(model, 500)
end
it 'does paginate' do
prepare(model)
visit path
refresh # more stability
expect(page).to have_css('.js-pager', wait: 10)
class_page1 = klass.order(sort_by => :asc, id: :asc).offset(50).first
expect(page).to have_text(class_page1.name, wait: 10)
page.first('.js-page', text: '2').click
await_empty_ajax_queue
class_page2 = klass.order(sort_by => :asc, id: :asc).offset(175).first
expect(page).to have_text(class_page2.name, wait: 10)
page.first('.js-page', text: '3').click
await_empty_ajax_queue
class_page3 = klass.order(sort_by => :asc, id: :asc).offset(325).first
expect(page).to have_text(class_page3.name, wait: 10)
page.first('.js-page', text: '4').click
await_empty_ajax_queue
class_page4 = klass.order(sort_by => :asc, id: :asc).offset(475).first
expect(page).to have_text(class_page4.name, wait: 10)
page.first('.js-page', text: '1').click
await_empty_ajax_queue
page.first('.js-tableHead[data-column-key=name]').click
await_empty_ajax_queue
expect(page).to have_text(class_page1.name, wait: 10)
page.first('.js-tableHead[data-column-key=name]').click
await_empty_ajax_queue
class_last = klass.order(sort_by => :desc).first
expect(page).to have_text(class_last.name, wait: 10)
end
end

View file

@ -0,0 +1,8 @@
require 'rails_helper'
require 'system/examples/pagination_examples'
RSpec.describe 'Manage > Groups', type: :system do
context 'ajax pagination' do
include_examples 'pagination', model: :group, klass: Group, path: 'manage/groups'
end
end

View file

@ -0,0 +1,8 @@
require 'rails_helper'
require 'system/examples/pagination_examples'
RSpec.describe 'Manage > Job', type: :system do
context 'ajax pagination' do
include_examples 'pagination', model: :job, klass: Job, path: 'manage/job'
end
end

View file

@ -0,0 +1,8 @@
require 'rails_helper'
require 'system/examples/pagination_examples'
RSpec.describe 'Manage > Macro', type: :system do
context 'ajax pagination' do
include_examples 'pagination', model: :macro, klass: Macro, path: 'manage/macros'
end
end

View file

@ -1,4 +1,5 @@
require 'rails_helper' require 'rails_helper'
require 'system/examples/pagination_examples'
RSpec.describe 'Manage > Organizations', type: :system do RSpec.describe 'Manage > Organizations', type: :system do
@ -48,4 +49,8 @@ RSpec.describe 'Manage > Organizations', type: :system do
end end
end end
end end
context 'ajax pagination' do
include_examples 'pagination', model: :organization, klass: Organization, path: 'manage/organizations'
end
end end

View file

@ -0,0 +1,8 @@
require 'rails_helper'
require 'system/examples/pagination_examples'
RSpec.describe 'Manage > Report Profiles', type: :system do
context 'ajax pagination' do
include_examples 'pagination', model: :report_profile, klass: Report::Profile, path: 'manage/report_profiles'
end
end

View file

@ -0,0 +1,8 @@
require 'rails_helper'
require 'system/examples/pagination_examples'
RSpec.describe 'Manage > Role', type: :system do
context 'ajax pagination' do
include_examples 'pagination', model: :role, klass: Role, path: 'manage/roles'
end
end

View file

@ -0,0 +1,8 @@
require 'rails_helper'
require 'system/examples/pagination_examples'
RSpec.describe 'Manage > Text Module', type: :system do
context 'ajax pagination' do
include_examples 'pagination', model: :text_module, klass: TextModule, path: 'manage/text_modules'
end
end

View file

@ -1,4 +1,5 @@
require 'rails_helper' require 'rails_helper'
require 'system/examples/pagination_examples'
RSpec.describe 'Manage > Trigger', type: :system do RSpec.describe 'Manage > Trigger', type: :system do
@ -57,4 +58,8 @@ RSpec.describe 'Manage > Trigger', type: :system do
end end
end end
end end
context 'ajax pagination' do
include_examples 'pagination', model: :trigger, klass: Trigger, path: 'manage/trigger'
end
end end