diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index c204c0befb..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,11 +43,29 @@ class InfoView(AjaxView):
'server': 'InvenTree',
'version': inventreeVersion(),
'instance': inventreeInstanceName(),
+ 'apiVersion': inventreeApiVersion(),
}
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'),
+ 'url': request.build_absolute_uri(),
+ }
+
+ return JsonResponse(data, status=404)
+
+
class AttachmentMixin:
"""
Mixin for creating attachment objects,
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/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/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 = [
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/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/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/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
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;
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()),
]
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/).