diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 3489056865..2fc85ef653 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -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) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 5dbfb845bc..3abb99010f 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -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' diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index c51398e182..1ffab1be96 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -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() diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index ae3e191bc1..ca2fa619d9 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -22,7 +22,7 @@ InvenTree | {% trans "Supplier List" %} {% endif %} - +
{% endblock %} diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 9a0e3a2a68..a245f9d67c 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -397,11 +397,11 @@ 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) - - serializer = self.get_serializer(queryset, many=True) + 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', ] diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index d917b6ebb2..faadf26c15 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -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): """ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index fe669da5b9..15edfed066 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -381,7 +381,12 @@ class StockList(generics.ListCreateAPIView): queryset = self.filter_queryset(self.get_queryset()) - serializer = self.get_serializer(queryset, many=True) + 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', ] diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 6f13de30a3..a1101f1bdb 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -52,12 +52,10 @@ loadStockTrackingTable($("#track-table"), { - params: function(p) { - return { - ordering: '-date', - item: {{ item.pk }}, - user_detail: true, - }; + params: { + ordering: '-date', + item: {{ item.pk }}, + user_detail: true, }, url: "{% url 'api-stock-track' %}", }); diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index 17cab7ffe3..ae0d6fd862 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -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): """ diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index cb2cda0a30..2023227bce 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -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, diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index 01d5fcdef0..45ebd133d1 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -630,6 +630,7 @@ function loadBuildTable(table, options) { url: options.url, queryParams: filters, groupBy: false, + sidePagination: 'server', name: 'builds', original: params, columns: [ diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index 388a79668b..b497609db1 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -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', diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js index 53063cd709..c4ca4acd08 100644 --- a/InvenTree/templates/js/order.js +++ b/InvenTree/templates/js/order.js @@ -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: [ diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index 9f770f452e..cefc2af8a7 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -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, diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index b115b8171c..bfb539c3c5 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -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 = "" + value + ""; @@ -952,7 +956,6 @@ function loadStockTrackingTable(table, options) { }); cols.push({ - sortable: true, field: 'user', title: '{% trans "User" %}', formatter: function(value, row, index, field) { diff --git a/InvenTree/templates/js/table_filters.js b/InvenTree/templates/js/table_filters.js index 39224c4ffe..fb76d2731b 100644 --- a/InvenTree/templates/js/table_filters.js +++ b/InvenTree/templates/js/table_filters.js @@ -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" %}', diff --git a/InvenTree/templates/js/tables.js b/InvenTree/templates/js/tables.js index ceb38690a8..f09c683bff 100644 --- a/InvenTree/templates/js/tables.js +++ b/InvenTree/templates/js/tables.js @@ -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;