Merge pull request #1373 from SchrodingersGat/server-pagination

Server pagination
This commit is contained in:
Oliver 2021-03-01 13:31:50 +11:00 committed by GitHub
commit 7aed696451
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 148 additions and 41 deletions

View File

@ -18,7 +18,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from .views import AjaxView
from .version import inventreeVersion, inventreeInstanceName
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
from plugins import plugins as inventree_plugins
@ -43,6 +43,7 @@ class InfoView(AjaxView):
'server': 'InvenTree',
'version': inventreeVersion(),
'instance': inventreeInstanceName(),
'apiVersion': inventreeApiVersion(),
}
return JsonResponse(data)

View File

@ -275,12 +275,13 @@ REST_FRAMEWORK = {
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.DjangoModelPermissions',
'InvenTree.permissions.RolePermission',
),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
}
WSGI_APPLICATION = 'InvenTree.wsgi.application'

View File

@ -9,6 +9,9 @@ import common.models
INVENTREE_SW_VERSION = "0.1.7 pre"
# Increment this number whenever there is a significant change to the API that any clients need to know about
INVENTREE_API_VERSION = 2
def inventreeInstanceName():
""" Returns the InstanceName settings for the current database """
@ -20,6 +23,10 @@ def inventreeVersion():
return INVENTREE_SW_VERSION
def inventreeApiVersion():
return INVENTREE_API_VERSION
def inventreeDjangoVersion():
""" Return the version of Django library """
return django.get_version()

View File

@ -22,7 +22,7 @@ InvenTree | {% trans "Supplier List" %}
</div>
{% endif %}
<table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'>
<table class='table table-striped table-condensed' id='company-table' data-toolbar='#button-toolbar'>
</table>
{% endblock %}

View File

@ -397,10 +397,10 @@ class PartList(generics.ListCreateAPIView):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
else:
serializer = self.get_serializer(queryset, many=True)
data = serializer.data
@ -445,7 +445,9 @@ class PartList(generics.ListCreateAPIView):
a) For HTTP requests (e.g. via the browseable API) return a DRF response
b) For AJAX requests, simply return a JSON rendered response.
"""
if request.is_ajax():
if page is not None:
return self.get_paginated_response(data)
elif request.is_ajax():
return JsonResponse(data, safe=False)
else:
return Response(data)
@ -641,15 +643,18 @@ class PartList(generics.ListCreateAPIView):
queryset = queryset.filter(pk__in=parts_need_stock)
# Limit number of results
limit = params.get('limit', None)
# Optionally limit the maximum number of returned results
# e.g. for displaying "recent part" list
max_results = params.get('max_results', None)
if limit is not None:
if max_results is not None:
try:
limit = int(limit)
if limit > 0:
queryset = queryset[:limit]
except ValueError:
max_results = int(max_results)
if max_results > 0:
queryset = queryset[:max_results]
except (ValueError):
pass
return queryset
@ -674,6 +679,8 @@ class PartList(generics.ListCreateAPIView):
ordering_fields = [
'name',
'creation_date',
'IPN',
'in_stock',
]
# Default ordering
@ -685,6 +692,7 @@ class PartList(generics.ListCreateAPIView):
'IPN',
'revision',
'keywords',
'category__name',
]

View File

@ -235,6 +235,21 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_paginate(self):
"""
Test pagination of the Part list API
"""
for n in [1, 5, 10]:
response = self.get(reverse('api-part-list'), {'limit': n})
data = response.data
self.assertIn('count', data)
self.assertIn('results', data)
self.assertEqual(len(data['results']), n)
class PartAPIAggregationTest(InvenTreeAPITestCase):
"""

View File

@ -381,6 +381,11 @@ class StockList(generics.ListCreateAPIView):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
else:
serializer = self.get_serializer(queryset, many=True)
data = serializer.data
@ -465,7 +470,9 @@ class StockList(generics.ListCreateAPIView):
Note: b) is about 100x quicker than a), because the DRF framework adds a lot of cruft
"""
if request.is_ajax():
if page is not None:
return self.get_paginated_response(data)
elif request.is_ajax():
return JsonResponse(data, safe=False)
else:
return Response(data)
@ -806,16 +813,15 @@ class StockList(generics.ListCreateAPIView):
print("After error:", str(updated_after))
pass
# Limit number of results
limit = params.get('limit', None)
# Optionally, limit the maximum number of returned results
max_results = params.get('max_results', None)
if limit is not None:
if max_results is not None:
try:
limit = int(limit)
if limit > 0:
queryset = queryset[:limit]
max_results = int(max_results)
if max_results > 0:
queryset = queryset[:max_results]
except (ValueError):
pass
@ -839,9 +845,12 @@ class StockList(generics.ListCreateAPIView):
ordering_fields = [
'part__name',
'part__IPN',
'updated',
'stocktake_date',
'expiry_date',
'quantity',
'status',
]
ordering = ['part__name']
@ -851,7 +860,8 @@ class StockList(generics.ListCreateAPIView):
'batch',
'part__name',
'part__IPN',
'part__description'
'part__description',
'location__name',
]

View File

@ -52,12 +52,10 @@
loadStockTrackingTable($("#track-table"), {
params: function(p) {
return {
params: {
ordering: '-date',
item: {{ item.pk }},
user_detail: true,
};
},
url: "{% url 'api-stock-track' %}",
});

View File

@ -244,6 +244,19 @@ class StockItemListTest(StockAPITestCase):
response = self.get_stock(expired=0)
self.assertEqual(len(response), 16)
def test_paginate(self):
"""
Test that we can paginate results correctly
"""
for n in [1, 5, 10]:
response = self.get_stock(limit=n)
self.assertIn('count', response)
self.assertIn('results', response)
self.assertEqual(len(response['results']), n)
class StockItemTest(StockAPITestCase):
"""

View File

@ -102,7 +102,7 @@ addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-ti
loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
params: {
ordering: "-creation_date",
limit: {% settings_value "PART_RECENT_COUNT" %},
max_results: {% settings_value "PART_RECENT_COUNT" %},
},
name: 'latest_parts',
});
@ -132,7 +132,7 @@ addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa
loadStockTable($('#table-recently-updated-stock'), {
params: {
ordering: "-updated",
limit: {% settings_value "STOCK_RECENT_COUNT" %},
max_results: {% settings_value "STOCK_RECENT_COUNT" %},
},
name: 'recently-updated-stock',
grouping: false,

View File

@ -630,6 +630,7 @@ function loadBuildTable(table, options) {
url: options.url,
queryParams: filters,
groupBy: false,
sidePagination: 'server',
name: 'builds',
original: params,
columns: [

View File

@ -93,6 +93,7 @@ function loadCompanyTable(table, url, options={}) {
method: 'get',
queryParams: filters,
groupBy: false,
sidePagination: 'server',
formatNoMatches: function() { return "{% trans "No company information found" %}"; },
showColumns: true,
name: options.pagetype || 'company',

View File

@ -131,6 +131,7 @@ function loadPurchaseOrderTable(table, options) {
queryParams: filters,
name: 'purchaseorder',
groupBy: false,
sidePagination: 'server',
original: options.params,
formatNoMatches: function() { return '{% trans "No purchase orders found" %}'; },
columns: [
@ -225,6 +226,7 @@ function loadSalesOrderTable(table, options) {
queryParams: filters,
name: 'salesorder',
groupBy: false,
sidePagination: 'server',
original: options.params,
formatNoMatches: function() { return '{% trans "No sales orders found" %}'; },
columns: [

View File

@ -366,7 +366,6 @@ function loadPartTable(table, url, options={}) {
});
columns.push({
sortable: true,
field: 'description',
title: '{% trans "Description" %}',
formatter: function(value, row, index, field) {
@ -442,12 +441,13 @@ function loadPartTable(table, url, options={}) {
$(table).inventreeTable({
url: url,
sortName: 'name',
method: 'get',
queryParams: filters,
groupBy: false,
name: options.name || 'part',
original: params,
sidePagination: 'server',
pagination: 'true',
formatNoMatches: function() { return '{% trans "No parts found" %}'; },
columns: columns,
showColumns: true,

View File

@ -325,6 +325,12 @@ function loadStockTable(table, options) {
grouping = options.grouping;
}
// Explicitly disable part grouping functionality
// Might be able to add this in later on,
// but there is a bug which makes this crash if paginating on the server side.
// Ref: https://github.com/wenzhixin/bootstrap-table/issues/3250
grouping = false;
table.inventreeTable({
method: 'get',
formatNoMatches: function() {
@ -332,7 +338,7 @@ function loadStockTable(table, options) {
},
url: options.url || "{% url 'api-stock-list' %}",
queryParams: filters,
customSort: customGroupSorter,
sidePagination: 'server',
name: 'stock',
original: original,
showColumns: true,
@ -516,6 +522,7 @@ function loadStockTable(table, options) {
{
field: 'part_detail.full_name',
title: '{% trans "Part" %}',
sortName: 'part__name',
sortable: true,
switchable: false,
formatter: function(value, row, index, field) {
@ -534,6 +541,7 @@ function loadStockTable(table, options) {
{
field: 'part_detail.IPN',
title: 'IPN',
sortName: 'part__IPN',
sortable: true,
formatter: function(value, row, index, field) {
return row.part_detail.IPN;
@ -542,7 +550,6 @@ function loadStockTable(table, options) {
{
field: 'part_detail.description',
title: '{% trans "Description" %}',
sortable: true,
formatter: function(value, row, index, field) {
return row.part_detail.description;
}
@ -654,8 +661,6 @@ function loadStockTable(table, options) {
{
field: 'packaging',
title: '{% trans "Packaging" %}',
sortable: true,
searchable: true,
},
{
field: 'notes',
@ -927,7 +932,6 @@ function loadStockTrackingTable(table, options) {
cols.push({
field: 'title',
title: '{% trans "Description" %}',
sortable: true,
formatter: function(value, row, index, field) {
var html = "<b>" + value + "</b>";
@ -952,7 +956,6 @@ function loadStockTrackingTable(table, options) {
});
cols.push({
sortable: true,
field: 'user',
title: '{% trans "User" %}',
formatter: function(value, row, index, field) {

View File

@ -1,5 +1,6 @@
{% load i18n %}
{% load status_codes %}
{% load inventree_extras %}
{% include "status_codes.html" with label='stock' options=StockStatus.list %}
{% include "status_codes.html" with label='build' options=BuildStatus.list %}
@ -110,6 +111,8 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Depleted" %}',
description: '{% trans "Show stock items which are depleted" %}',
},
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
expired: {
type: 'bool',
title: '{% trans "Expired" %}',
@ -120,6 +123,7 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}',
},
{% endif %}
in_stock: {
type: 'bool',
title: '{% trans "In Stock" %}',

View File

@ -93,7 +93,14 @@ function reloadTable(table, filters) {
}
}
options.queryParams = params;
options.queryParams = function(tableParams) {
for (key in params) {
tableParams[key] = params[key];
}
return tableParams;
}
table.bootstrapTable('refreshOptions', options);
table.bootstrapTable('refresh');
@ -126,9 +133,45 @@ $.fn.inventreeTable = function(options) {
var varName = tableName + '-pagesize';
// Pagingation options (can be server-side or client-side as specified by the caller)
options.pagination = true;
options.paginationVAlign = 'both';
options.pageSize = inventreeLoad(varName, 25);
options.pageList = [25, 50, 100, 250, 'all'];
options.totalField = 'count';
options.dataField = 'results';
// Extract query params
var filters = options.queryParams || options.filters || {};
options.queryParams = function(params) {
for (var key in filters) {
params[key] = filters[key];
}
// Override the way that we ask the server to sort results
// It seems bootstrap-table does not offer a "native" way to do this...
if ('sort' in params) {
var order = params['order'];
var ordering = params['sort'] || null;
if (ordering) {
if (order == 'desc') {
ordering = `-${ordering}`;
}
params['ordering'] = ordering;
}
delete params['sort'];
delete params['order'];
}
return params;
}
options.rememberOrder = true;