From 0b9cb507c7a4488e6f9e7cc1cbc2c8a5a98b04b8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 27 Feb 2021 22:38:08 +1100 Subject: [PATCH 01/16] Redirect invalid API urls to a 404 page --- InvenTree/InvenTree/api.py | 16 ++++++++++++++++ InvenTree/InvenTree/urls.py | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index c204c0befb..fa8a6739b7 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -48,6 +48,22 @@ class InfoView(AjaxView): return JsonResponse(data) +class NotFoundView(AjaxView): + """ + Simple JSON view when accessing an invalid API view. + """ + + permission_classes = [permissions.AllowAny] + + def get(self, request, *args, **kwargs): + + data = { + 'details': _('API endpoint not found') + } + + return JsonResponse(data, status=404) + + class AttachmentMixin: """ Mixin for creating attachment objects, diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index c5b439c0be..a9f53a7014 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -43,7 +43,7 @@ from .views import DynamicJsView from common.views import SettingEdit -from .api import InfoView +from .api import InfoView, NotFoundView from .api import ActionPluginView from users.urls import user_urls @@ -70,6 +70,9 @@ apipatterns = [ # InvenTree information endpoint url(r'^$', InfoView.as_view(), name='api-inventree-info'), + + # Unknown endpoint + url(r'^.*$', NotFoundView.as_view(), name='api-404'), ] settings_urls = [ From 5069882a7f895de620af4703467c3b197b19ca87 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 27 Feb 2021 22:41:36 +1100 Subject: [PATCH 02/16] URL tweaks --- InvenTree/users/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/users/urls.py b/InvenTree/users/urls.py index 7d8d23883f..7f29bd85cc 100644 --- a/InvenTree/users/urls.py +++ b/InvenTree/users/urls.py @@ -5,8 +5,8 @@ from . import api user_urls = [ url(r'^(?P[0-9]+)/?$', api.UserDetail.as_view(), name='user-detail'), - url(r'roles', api.RoleDetails.as_view(), name='api-user-roles'), - url(r'token', api.GetAuthToken.as_view(), name='api-token'), + url(r'roles/?$', api.RoleDetails.as_view(), name='api-user-roles'), + url(r'token/?$', api.GetAuthToken.as_view(), name='api-token'), url(r'^$', api.UserList.as_view()), ] From 19c76f7842c587a5e1bb03a8ad6620f21c7cbab4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 27 Feb 2021 22:44:38 +1100 Subject: [PATCH 03/16] Include 404 URL in response --- InvenTree/InvenTree/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index fa8a6739b7..3489056865 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -58,7 +58,8 @@ class NotFoundView(AjaxView): def get(self, request, *args, **kwargs): data = { - 'details': _('API endpoint not found') + 'details': _('API endpoint not found'), + 'url': request.build_absolute_uri(), } return JsonResponse(data, status=404) From 6f2ba71339c627b594e4d7d412867a568c91cf47 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Feb 2021 11:48:20 +1100 Subject: [PATCH 04/16] Refactor side navigation tree --- InvenTree/InvenTree/static/css/inventree.css | 1 + .../static/script/inventree/sidenav.js | 122 ++++++++++-------- .../part/templates/part/part_app_base.html | 12 +- .../stock/templates/stock/stock_app_base.html | 15 ++- 4 files changed, 81 insertions(+), 69 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 28b1166e1a..1c10897b6c 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -725,6 +725,7 @@ input[type="submit"] { top: 70px; position: sticky; font-size: 115%; + margin-left: 5px; } .sidenav-right svg { diff --git a/InvenTree/InvenTree/static/script/inventree/sidenav.js b/InvenTree/InvenTree/static/script/inventree/sidenav.js index 376380e09a..ce1214abc4 100644 --- a/InvenTree/InvenTree/static/script/inventree/sidenav.js +++ b/InvenTree/InvenTree/static/script/inventree/sidenav.js @@ -67,79 +67,91 @@ function loadTree(url, tree, options={}) { }); } -function openSideNav(navId) { -// document.getElementById("sidenav").style.display = "block"; -// document.getElementById("sidenav").style.width = "250px"; - if (!navId) { - navId = '#sidenav-left'; - } - - sessionStorage.setItem('inventree-sidenav-state', 'open'); +/** + * Initialize navigation tree display + */ +function initNavTree(options) { - $(navId).animate({ - width: '250px', - 'min-width': '200px', - display: 'block' - }, 50); + var resize = true; - -} - -function closeSideNav(navId) { - - if (!navId) { - navId = '#sidenav-left'; + if ('resize' in options) { + resize = options.resize; } - sessionStorage.setItem('inventree-sidenav-state', 'closed'); - - $(navId).animate({ + var label = options.label || 'nav'; + + var stateLabel = `${label}-tree-state`; + var widthLabel = `${label}-tree-width`; + + var treeId = options.treeId || '#sidenav-left'; + var toggleId = options.toggleId; + + // Initially hide the tree + $(treeId).animate({ width: '0px', - 'min-width': '0px', - display: 'none', - }, 50); + }, 0, function() { - //document.getElementById("sidenav").style.display = "none"; - //document.getElementById("sidenav").style.width = "0"; - //document.getElementById("inventree-content").style.marginLeft = "0px"; + if (resize) { + $(treeId).resizable({ + minWidth: '0px', + maxWidth: '500px', + handles: 'e, se', + grid: [5, 5], + stop: function(event, ui) { + var width = Math.round(ui.element.width()); -} + if (width < 75) { + $(treeId).animate({ + width: '0px' + }, 50); -function toggleSideNav(nav) { - if ($(nav).width() <= 0) { - openSideNav(nav); - } - else { - closeSideNav(nav); - } -} + sessionStorage.setItem(stateLabel, 'closed'); + } else { + sessionStorage.setItem(stateLabel, 'open'); + sessionStorage.setItem(widthLabel, `${width}px`); + } + } + }); + } -function initSideNav(navId) { + var state = sessionStorage.getItem(stateLabel); + var width = sessionStorage.getItem(widthLabel) || '300px'; - // Make it resizable + if (state && state == 'open') { - if (!navId) { - navId = '#sidenav-left'; - } - - $(navId).resizable({ - minWidth: '100px', - maxWidth: '500px', - stop: function(event, ui) { - console.log(ui.element.width()); - //console.log(ui.size.width); + $(treeId).animate({ + width: width, + }, 50); } }); - if (sessionStorage.getItem("inventree-sidenav-state") && sessionStorage.getItem('inventree-sidenav-state') == 'open') { - openSideNav(navId); - } - else { - closeSideNav(navId); + // Register callback for 'toggle' button + if (toggleId) { + + $(toggleId).click(function() { + + var state = sessionStorage.getItem(stateLabel) || 'closed'; + var width = sessionStorage.getItem(widthLabel) || '300px'; + + if (state == 'open') { + $(treeId).animate({ + width: '0px' + }, 50); + + sessionStorage.setItem(stateLabel, 'closed'); + } else { + $(treeId).animate({ + width: width, + }, 50); + + sessionStorage.setItem(stateLabel, 'open'); + } + }); } } + /** * Handle left-hand icon menubar display */ diff --git a/InvenTree/part/templates/part/part_app_base.html b/InvenTree/part/templates/part/part_app_base.html index 045027e063..94773f1e79 100644 --- a/InvenTree/part/templates/part/part_app_base.html +++ b/InvenTree/part/templates/part/part_app_base.html @@ -29,8 +29,6 @@ InvenTree | {% trans "Part List" %} {% block js_ready %} {{ block.super }} - closeSideNav(); - loadTree("{% url 'api-part-tree' %}", "#part-tree", { @@ -38,11 +36,9 @@ InvenTree | {% trans "Part List" %} } ); - $("#toggle-part-tree").click(function() { - toggleSideNav("#sidenav-left"); - return false; + initNavTree({ + label: 'part', + treeId: '#sidenav-left', + toggleId: '#toggle-part-tree', }); - - initSideNav(); - {% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/stock_app_base.html b/InvenTree/stock/templates/stock/stock_app_base.html index d415261d7f..a9ae20830e 100644 --- a/InvenTree/stock/templates/stock/stock_app_base.html +++ b/InvenTree/stock/templates/stock/stock_app_base.html @@ -11,7 +11,9 @@ InvenTree | Stock {% endblock %} {% block sidenav %} -
+
+ {% trans "Loading..." %} +
{% endblock %} {% block pre_content %} @@ -24,6 +26,7 @@ InvenTree | Stock {% block js_ready %} {{ block.super }} + loadTree("{% url 'api-stock-tree' %}", "#stock-tree", { @@ -31,10 +34,10 @@ InvenTree | Stock } ); - $("#toggle-stock-tree").click(function() { - toggleSideNav("#sidenav-left"); - return false; - }) + initNavTree({ + label: 'stock', + treeId: '#sidenav-left', + toggleId: '#toggle-stock-tree', + }); - initSideNav(); {% endblock %} \ No newline at end of file From 8ce7b572cc59a265fa1f76a31178d45d8f2554d2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Feb 2021 16:18:45 +1100 Subject: [PATCH 05/16] Optionally paginate the StockList table on the server - This makes the bootstrap table interface SO FREAKING MUCH FASTER - Search is now performed on the server too! --- InvenTree/stock/api.py | 32 +++++++++--------- InvenTree/templates/js/stock.js | 13 ++++--- InvenTree/templates/js/table_filters.js | 4 +++ InvenTree/templates/js/tables.js | 45 ++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index fe669da5b9..ac4ba8b02e 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -40,6 +40,7 @@ from decimal import Decimal, InvalidOperation from datetime import datetime, timedelta +from rest_framework.pagination import LimitOffsetPagination from rest_framework.serializers import ValidationError from rest_framework.views import APIView from rest_framework.response import Response @@ -337,6 +338,8 @@ class StockList(generics.ListCreateAPIView): serializer_class = StockItemSerializer queryset = StockItem.objects.all() + pagination_class = LimitOffsetPagination + def create(self, request, *args, **kwargs): """ Create a new StockItem object via the API. @@ -381,7 +384,13 @@ 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,6 +474,8 @@ class StockList(generics.ListCreateAPIView): Note: b) is about 100x quicker than a), because the DRF framework adds a lot of cruft """ + if page is not None: + return self.get_paginated_response(data) if request.is_ajax(): return JsonResponse(data, safe=False) else: @@ -806,19 +817,6 @@ class StockList(generics.ListCreateAPIView): print("After error:", str(updated_after)) pass - # Limit number of results - limit = params.get('limit', None) - - if limit is not None: - try: - limit = int(limit) - - if limit > 0: - queryset = queryset[:limit] - - except (ValueError): - pass - # Also ensure that we pre-fecth all the related items queryset = queryset.prefetch_related( 'part', @@ -839,9 +837,12 @@ class StockList(generics.ListCreateAPIView): ordering_fields = [ 'part__name', + 'part__IPN', 'updated', 'stocktake_date', 'expiry_date', + 'quantity', + 'status', ] ordering = ['part__name'] @@ -851,7 +852,8 @@ class StockList(generics.ListCreateAPIView): 'batch', 'part__name', 'part__IPN', - 'part__description' + 'part__description', + 'location__name', ] diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index b115b8171c..660a09e5af 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', 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; From 487794a9382064e08156a5b0b130d8f35152f64e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Feb 2021 16:29:25 +1100 Subject: [PATCH 06/16] Server side pagination for the part list --- InvenTree/part/api.py | 27 ++++++++++++--------------- InvenTree/stock/api.py | 3 +-- InvenTree/templates/js/part.js | 3 ++- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 9a0e3a2a68..ff4c9b3f94 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -9,6 +9,7 @@ from django_filters.rest_framework import DjangoFilterBackend from django.http import JsonResponse from django.db.models import Q, F, Count, Prefetch, Sum +from rest_framework.pagination import LimitOffsetPagination from rest_framework import status from rest_framework.response import Response from rest_framework import filters, serializers @@ -371,6 +372,8 @@ class PartList(generics.ListCreateAPIView): starred_parts = None + pagination_class = LimitOffsetPagination + def get_serializer(self, *args, **kwargs): # Ensure the request context is passed through @@ -397,11 +400,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 +448,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,17 +646,6 @@ class PartList(generics.ListCreateAPIView): queryset = queryset.filter(pk__in=parts_need_stock) - # Limit number of results - limit = params.get('limit', None) - - if limit is not None: - try: - limit = int(limit) - if limit > 0: - queryset = queryset[:limit] - except ValueError: - pass - return queryset filter_backends = [ @@ -674,6 +668,8 @@ class PartList(generics.ListCreateAPIView): ordering_fields = [ 'name', 'creation_date', + 'IPN', + 'in_stock', ] # Default ordering @@ -685,6 +681,7 @@ class PartList(generics.ListCreateAPIView): 'IPN', 'revision', 'keywords', + 'category__name', ] diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index ac4ba8b02e..1bc0817083 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -387,7 +387,6 @@ class StockList(generics.ListCreateAPIView): 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) @@ -476,7 +475,7 @@ class StockList(generics.ListCreateAPIView): if page is not None: return self.get_paginated_response(data) - if request.is_ajax(): + elif request.is_ajax(): return JsonResponse(data, safe=False) else: return Response(data) diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index 9f770f452e..b12788cfe8 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) { @@ -448,6 +447,8 @@ function loadPartTable(table, url, options={}) { groupBy: false, name: options.name || 'part', original: params, + sidePagination: 'server', + pagination: 'true', formatNoMatches: function() { return '{% trans "No parts found" %}'; }, columns: columns, showColumns: true, From 1239d4af162416c90d13b3dd166923ac077ef754 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Feb 2021 20:16:05 +1100 Subject: [PATCH 07/16] Fixes result limiting - Required for index page --- InvenTree/part/api.py | 14 ++++++++++++++ InvenTree/stock/api.py | 12 ++++++++++++ InvenTree/templates/InvenTree/index.html | 4 ++-- InvenTree/templates/js/part.js | 1 - 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index ff4c9b3f94..bf6e49580f 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -646,6 +646,20 @@ class PartList(generics.ListCreateAPIView): queryset = queryset.filter(pk__in=parts_need_stock) + # Optionally limit the maximum number of returned results + # e.g. for displaying "recent part" list + max_results = params.get('max_results', None) + + if max_results is not None: + try: + max_results = int(max_results) + + if max_results > 0: + queryset = queryset[:max_results] + + except (ValueError): + pass + return queryset filter_backends = [ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 1bc0817083..1a470a09b5 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -816,6 +816,18 @@ class StockList(generics.ListCreateAPIView): print("After error:", str(updated_after)) pass + # Optionally, limit the maximum number of returned results + max_results = params.get('max_results', None) + + if max_results is not None: + try: + max_results = int(max_results) + + if max_results > 0: + queryset = queryset[:max_results] + except (ValueError): + pass + # Also ensure that we pre-fecth all the related items queryset = queryset.prefetch_related( 'part', 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/part.js b/InvenTree/templates/js/part.js index b12788cfe8..cefc2af8a7 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -441,7 +441,6 @@ function loadPartTable(table, url, options={}) { $(table).inventreeTable({ url: url, - sortName: 'name', method: 'get', queryParams: filters, groupBy: false, From 90feb6d210632dc086fd226dc00e284173441934 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Feb 2021 20:31:01 +1100 Subject: [PATCH 08/16] Server-side paginatino for build orders --- InvenTree/build/api.py | 3 +++ InvenTree/part/test_api.py | 13 +++++++++++++ InvenTree/stock/test_api.py | 13 +++++++++++++ InvenTree/templates/js/build.js | 1 + 4 files changed, 30 insertions(+) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index cb6b3f6b2b..13c24c6110 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters from rest_framework import generics +from rest_framework.pagination import LimitOffsetPagination from django.conf.urls import url, include @@ -28,6 +29,8 @@ class BuildList(generics.ListCreateAPIView): queryset = Build.objects.all() serializer_class = BuildSerializer + pagination_class = LimitOffsetPagination + filter_backends = [ DjangoFilterBackend, filters.SearchFilter, diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index d917b6ebb2..23865aa0f3 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -235,6 +235,19 @@ 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}) + + self.assertIn('count', response) + self.assertIn('results', response) + + self.assertEqual(len(response['results']), n) + class PartAPIAggregationTest(InvenTreeAPITestCase): """ 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/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: [ From 44635d2499304204548fafb4fb310ba5a5758299 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Feb 2021 20:35:32 +1100 Subject: [PATCH 09/16] Use server-side pagination for more list endpoints --- InvenTree/company/api.py | 2 ++ InvenTree/company/templates/company/index.html | 2 +- InvenTree/order/api.py | 3 +++ InvenTree/templates/js/company.js | 1 + InvenTree/templates/js/order.js | 2 ++ 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 3398760d45..27d67bdfed 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters from rest_framework import generics +from rest_framework.pagination import LimitOffsetPagination from django.conf.urls import url, include from django.db.models import Q @@ -32,6 +33,7 @@ class CompanyList(generics.ListCreateAPIView): serializer_class = CompanySerializer queryset = Company.objects.all() + pagination_class = LimitOffsetPagination def get_queryset(self): 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/order/api.py b/InvenTree/order/api.py index ce75a47697..0d3755384b 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics from rest_framework import filters +from rest_framework.pagination import LimitOffsetPagination from django.conf.urls import url, include @@ -36,6 +37,7 @@ class POList(generics.ListCreateAPIView): queryset = PurchaseOrder.objects.all() serializer_class = POSerializer + pagination_class = LimitOffsetPagination def get_serializer(self, *args, **kwargs): @@ -238,6 +240,7 @@ class SOList(generics.ListCreateAPIView): queryset = SalesOrder.objects.all() serializer_class = SalesOrderSerializer + pagination_class = LimitOffsetPagination def get_serializer(self, *args, **kwargs): 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: [ From 3dad86f004e896e826dff0ee30ed6b6c108c7944 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Feb 2021 20:45:41 +1100 Subject: [PATCH 10/16] Actually just enable (optional) pagination for every list endpoint --- InvenTree/InvenTree/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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' From 04dbfbd892a5e075a4553b10a01348faf1642f30 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Feb 2021 20:50:19 +1100 Subject: [PATCH 11/16] Fix for stock item tracking table --- InvenTree/stock/templates/stock/item.html | 10 ++++------ InvenTree/templates/js/stock.js | 2 -- 2 files changed, 4 insertions(+), 8 deletions(-) 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/templates/js/stock.js b/InvenTree/templates/js/stock.js index 660a09e5af..bfb539c3c5 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -932,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 + ""; @@ -957,7 +956,6 @@ function loadStockTrackingTable(table, options) { }); cols.push({ - sortable: true, field: 'user', title: '{% trans "User" %}', formatter: function(value, row, index, field) { From 34def1076546b3886e4c3ad0f3d8683de9c47aaf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Feb 2021 21:44:10 +1100 Subject: [PATCH 12/16] Delete un-needed code --- InvenTree/build/api.py | 3 --- InvenTree/company/api.py | 2 -- InvenTree/order/api.py | 3 --- InvenTree/part/api.py | 3 --- InvenTree/stock/api.py | 3 --- 5 files changed, 14 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 13c24c6110..cb6b3f6b2b 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -8,7 +8,6 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters from rest_framework import generics -from rest_framework.pagination import LimitOffsetPagination from django.conf.urls import url, include @@ -29,8 +28,6 @@ class BuildList(generics.ListCreateAPIView): queryset = Build.objects.all() serializer_class = BuildSerializer - pagination_class = LimitOffsetPagination - filter_backends = [ DjangoFilterBackend, filters.SearchFilter, diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 27d67bdfed..3398760d45 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -8,7 +8,6 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters from rest_framework import generics -from rest_framework.pagination import LimitOffsetPagination from django.conf.urls import url, include from django.db.models import Q @@ -33,7 +32,6 @@ class CompanyList(generics.ListCreateAPIView): serializer_class = CompanySerializer queryset = Company.objects.all() - pagination_class = LimitOffsetPagination def get_queryset(self): diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 0d3755384b..ce75a47697 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -8,7 +8,6 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics from rest_framework import filters -from rest_framework.pagination import LimitOffsetPagination from django.conf.urls import url, include @@ -37,7 +36,6 @@ class POList(generics.ListCreateAPIView): queryset = PurchaseOrder.objects.all() serializer_class = POSerializer - pagination_class = LimitOffsetPagination def get_serializer(self, *args, **kwargs): @@ -240,7 +238,6 @@ class SOList(generics.ListCreateAPIView): queryset = SalesOrder.objects.all() serializer_class = SalesOrderSerializer - pagination_class = LimitOffsetPagination def get_serializer(self, *args, **kwargs): diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index bf6e49580f..a245f9d67c 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -9,7 +9,6 @@ from django_filters.rest_framework import DjangoFilterBackend from django.http import JsonResponse from django.db.models import Q, F, Count, Prefetch, Sum -from rest_framework.pagination import LimitOffsetPagination from rest_framework import status from rest_framework.response import Response from rest_framework import filters, serializers @@ -372,8 +371,6 @@ class PartList(generics.ListCreateAPIView): starred_parts = None - pagination_class = LimitOffsetPagination - def get_serializer(self, *args, **kwargs): # Ensure the request context is passed through diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 1a470a09b5..15edfed066 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -40,7 +40,6 @@ from decimal import Decimal, InvalidOperation from datetime import datetime, timedelta -from rest_framework.pagination import LimitOffsetPagination from rest_framework.serializers import ValidationError from rest_framework.views import APIView from rest_framework.response import Response @@ -338,8 +337,6 @@ class StockList(generics.ListCreateAPIView): serializer_class = StockItemSerializer queryset = StockItem.objects.all() - pagination_class = LimitOffsetPagination - def create(self, request, *args, **kwargs): """ Create a new StockItem object via the API. From b157690ae0b4930aa51ea6f87d19fc84794f329f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 1 Mar 2021 11:15:53 +1100 Subject: [PATCH 13/16] Add API version to server info endpoint --- InvenTree/InvenTree/api.py | 3 ++- InvenTree/InvenTree/version.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) 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/version.py b/InvenTree/InvenTree/version.py index c51398e182..ce25f1bd14 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -9,6 +9,8 @@ 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 +22,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() From 9f9629edb389278eb935663d2111d894613c44de Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 1 Mar 2021 11:21:18 +1100 Subject: [PATCH 14/16] Fix for unit test --- InvenTree/part/test_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 23865aa0f3..faadf26c15 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -243,10 +243,12 @@ class PartAPITest(InvenTreeAPITestCase): for n in [1, 5, 10]: response = self.get(reverse('api-part-list'), {'limit': n}) - self.assertIn('count', response) - self.assertIn('results', response) + data = response.data + + self.assertIn('count', data) + self.assertIn('results', data) - self.assertEqual(len(response['results']), n) + self.assertEqual(len(data['results']), n) class PartAPIAggregationTest(InvenTreeAPITestCase): From b7cf57c875e2b6cf3accc96f8156217dd7765521 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 1 Mar 2021 11:38:52 +1100 Subject: [PATCH 15/16] PEP fix --- InvenTree/InvenTree/version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index ce25f1bd14..1ffab1be96 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,6 +12,7 @@ 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 """ return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "") From 278466e8309cfab45e23bf13ae14072e9d7b4b2a Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 1 Mar 2021 14:49:13 +1100 Subject: [PATCH 16/16] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 551f6ce43a..7d0560c2e9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,14 @@ InvenTree is designed to be lightweight and easy to use for SME or hobbyist appl However, powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information. +# Companion App + +InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality. + +[**Download InvenTree from the Android Play Store**](https://play.google.com/store/apps/details?id=inventree.inventree_app) + +*Currently the mobile app is only availble for Android* + # Documentation For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).