Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2021-03-02 08:04:18 +11:00
commit a604c828e4
24 changed files with 260 additions and 113 deletions

View File

@ -18,7 +18,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from .views import AjaxView from .views import AjaxView
from .version import inventreeVersion, inventreeInstanceName from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
from plugins import plugins as inventree_plugins from plugins import plugins as inventree_plugins
@ -43,11 +43,29 @@ class InfoView(AjaxView):
'server': 'InvenTree', 'server': 'InvenTree',
'version': inventreeVersion(), 'version': inventreeVersion(),
'instance': inventreeInstanceName(), 'instance': inventreeInstanceName(),
'apiVersion': inventreeApiVersion(),
} }
return JsonResponse(data) 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: class AttachmentMixin:
""" """
Mixin for creating attachment objects, Mixin for creating attachment objects,

View File

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

View File

@ -725,6 +725,7 @@ input[type="submit"] {
top: 70px; top: 70px;
position: sticky; position: sticky;
font-size: 115%; font-size: 115%;
margin-left: 5px;
} }
.sidenav-right svg { .sidenav-right svg {

View File

@ -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'; * Initialize navigation tree display
*/
function initNavTree(options) {
var resize = true;
if ('resize' in options) {
resize = options.resize;
} }
sessionStorage.setItem('inventree-sidenav-state', 'open'); var label = options.label || 'nav';
$(navId).animate({ var stateLabel = `${label}-tree-state`;
width: '250px', var widthLabel = `${label}-tree-width`;
'min-width': '200px',
display: 'block'
}, 50);
var treeId = options.treeId || '#sidenav-left';
var toggleId = options.toggleId;
} // Initially hide the tree
$(treeId).animate({
function closeSideNav(navId) {
if (!navId) {
navId = '#sidenav-left';
}
sessionStorage.setItem('inventree-sidenav-state', 'closed');
$(navId).animate({
width: '0px', width: '0px',
'min-width': '0px', }, 0, function() {
display: 'none',
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); }, 50);
//document.getElementById("sidenav").style.display = "none"; sessionStorage.setItem(stateLabel, 'closed');
//document.getElementById("sidenav").style.width = "0"; } else {
//document.getElementById("inventree-content").style.marginLeft = "0px"; sessionStorage.setItem(stateLabel, 'open');
sessionStorage.setItem(widthLabel, `${width}px`);
}
function toggleSideNav(nav) {
if ($(nav).width() <= 0) {
openSideNav(nav);
} }
else {
closeSideNav(nav);
} }
} });
function initSideNav(navId) {
// Make it resizable
if (!navId) {
navId = '#sidenav-left';
} }
$(navId).resizable({ var state = sessionStorage.getItem(stateLabel);
minWidth: '100px', var width = sessionStorage.getItem(widthLabel) || '300px';
maxWidth: '500px',
stop: function(event, ui) { if (state && state == 'open') {
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') { // Register callback for 'toggle' button
openSideNav(navId); 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');
} }
else { });
closeSideNav(navId);
} }
} }
/** /**
* Handle left-hand icon menubar display * Handle left-hand icon menubar display
*/ */

View File

@ -43,7 +43,7 @@ from .views import DynamicJsView
from common.views import SettingEdit from common.views import SettingEdit
from .api import InfoView from .api import InfoView, NotFoundView
from .api import ActionPluginView from .api import ActionPluginView
from users.urls import user_urls from users.urls import user_urls
@ -70,6 +70,9 @@ apipatterns = [
# InvenTree information endpoint # InvenTree information endpoint
url(r'^$', InfoView.as_view(), name='api-inventree-info'), url(r'^$', InfoView.as_view(), name='api-inventree-info'),
# Unknown endpoint
url(r'^.*$', NotFoundView.as_view(), name='api-404'),
] ]
settings_urls = [ settings_urls = [

View File

@ -9,6 +9,9 @@ import common.models
INVENTREE_SW_VERSION = "0.1.7 pre" 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(): def inventreeInstanceName():
""" Returns the InstanceName settings for the current database """ """ Returns the InstanceName settings for the current database """
@ -20,6 +23,10 @@ def inventreeVersion():
return INVENTREE_SW_VERSION return INVENTREE_SW_VERSION
def inventreeApiVersion():
return INVENTREE_API_VERSION
def inventreeDjangoVersion(): def inventreeDjangoVersion():
""" Return the version of Django library """ """ Return the version of Django library """
return django.get_version() return django.get_version()

View File

@ -22,7 +22,7 @@ InvenTree | {% trans "Supplier List" %}
</div> </div>
{% endif %} {% 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> </table>
{% endblock %} {% endblock %}

View File

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

View File

@ -29,8 +29,6 @@ InvenTree | {% trans "Part List" %}
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
closeSideNav();
loadTree("{% url 'api-part-tree' %}", loadTree("{% url 'api-part-tree' %}",
"#part-tree", "#part-tree",
{ {
@ -38,11 +36,9 @@ InvenTree | {% trans "Part List" %}
} }
); );
$("#toggle-part-tree").click(function() { initNavTree({
toggleSideNav("#sidenav-left"); label: 'part',
return false; treeId: '#sidenav-left',
toggleId: '#toggle-part-tree',
}); });
initSideNav();
{% endblock %} {% endblock %}

View File

@ -235,6 +235,21 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) 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): class PartAPIAggregationTest(InvenTreeAPITestCase):
""" """

View File

@ -381,6 +381,11 @@ class StockList(generics.ListCreateAPIView):
queryset = self.filter_queryset(self.get_queryset()) 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) serializer = self.get_serializer(queryset, many=True)
data = serializer.data 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 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) return JsonResponse(data, safe=False)
else: else:
return Response(data) return Response(data)
@ -806,16 +813,15 @@ class StockList(generics.ListCreateAPIView):
print("After error:", str(updated_after)) print("After error:", str(updated_after))
pass pass
# Limit number of results # Optionally, limit the maximum number of returned results
limit = params.get('limit', None) max_results = params.get('max_results', None)
if limit is not None: if max_results is not None:
try: try:
limit = int(limit) max_results = int(max_results)
if limit > 0:
queryset = queryset[:limit]
if max_results > 0:
queryset = queryset[:max_results]
except (ValueError): except (ValueError):
pass pass
@ -839,9 +845,12 @@ class StockList(generics.ListCreateAPIView):
ordering_fields = [ ordering_fields = [
'part__name', 'part__name',
'part__IPN',
'updated', 'updated',
'stocktake_date', 'stocktake_date',
'expiry_date', 'expiry_date',
'quantity',
'status',
] ]
ordering = ['part__name'] ordering = ['part__name']
@ -851,7 +860,8 @@ class StockList(generics.ListCreateAPIView):
'batch', 'batch',
'part__name', 'part__name',
'part__IPN', 'part__IPN',
'part__description' 'part__description',
'location__name',
] ]

View File

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

View File

@ -11,7 +11,9 @@ InvenTree | Stock
{% endblock %} {% endblock %}
{% block sidenav %} {% block sidenav %}
<div id='stock-tree'></div> <div id='stock-tree'>
{% trans "Loading..." %}
</div>
{% endblock %} {% endblock %}
{% block pre_content %} {% block pre_content %}
@ -24,6 +26,7 @@ InvenTree | Stock
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
loadTree("{% url 'api-stock-tree' %}", loadTree("{% url 'api-stock-tree' %}",
"#stock-tree", "#stock-tree",
{ {
@ -31,10 +34,10 @@ InvenTree | Stock
} }
); );
$("#toggle-stock-tree").click(function() { initNavTree({
toggleSideNav("#sidenav-left"); label: 'stock',
return false; treeId: '#sidenav-left',
}) toggleId: '#toggle-stock-tree',
});
initSideNav();
{% endblock %} {% endblock %}

View File

@ -244,6 +244,19 @@ class StockItemListTest(StockAPITestCase):
response = self.get_stock(expired=0) response = self.get_stock(expired=0)
self.assertEqual(len(response), 16) 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): 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' %}", { loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
params: { params: {
ordering: "-creation_date", ordering: "-creation_date",
limit: {% settings_value "PART_RECENT_COUNT" %}, max_results: {% settings_value "PART_RECENT_COUNT" %},
}, },
name: 'latest_parts', name: 'latest_parts',
}); });
@ -132,7 +132,7 @@ addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa
loadStockTable($('#table-recently-updated-stock'), { loadStockTable($('#table-recently-updated-stock'), {
params: { params: {
ordering: "-updated", ordering: "-updated",
limit: {% settings_value "STOCK_RECENT_COUNT" %}, max_results: {% settings_value "STOCK_RECENT_COUNT" %},
}, },
name: 'recently-updated-stock', name: 'recently-updated-stock',
grouping: false, grouping: false,

View File

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

View File

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

View File

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

View File

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

View File

@ -325,6 +325,12 @@ function loadStockTable(table, options) {
grouping = options.grouping; 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({ table.inventreeTable({
method: 'get', method: 'get',
formatNoMatches: function() { formatNoMatches: function() {
@ -332,7 +338,7 @@ function loadStockTable(table, options) {
}, },
url: options.url || "{% url 'api-stock-list' %}", url: options.url || "{% url 'api-stock-list' %}",
queryParams: filters, queryParams: filters,
customSort: customGroupSorter, sidePagination: 'server',
name: 'stock', name: 'stock',
original: original, original: original,
showColumns: true, showColumns: true,
@ -516,6 +522,7 @@ function loadStockTable(table, options) {
{ {
field: 'part_detail.full_name', field: 'part_detail.full_name',
title: '{% trans "Part" %}', title: '{% trans "Part" %}',
sortName: 'part__name',
sortable: true, sortable: true,
switchable: false, switchable: false,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
@ -534,6 +541,7 @@ function loadStockTable(table, options) {
{ {
field: 'part_detail.IPN', field: 'part_detail.IPN',
title: 'IPN', title: 'IPN',
sortName: 'part__IPN',
sortable: true, sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
return row.part_detail.IPN; return row.part_detail.IPN;
@ -542,7 +550,6 @@ function loadStockTable(table, options) {
{ {
field: 'part_detail.description', field: 'part_detail.description',
title: '{% trans "Description" %}', title: '{% trans "Description" %}',
sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
return row.part_detail.description; return row.part_detail.description;
} }
@ -654,8 +661,6 @@ function loadStockTable(table, options) {
{ {
field: 'packaging', field: 'packaging',
title: '{% trans "Packaging" %}', title: '{% trans "Packaging" %}',
sortable: true,
searchable: true,
}, },
{ {
field: 'notes', field: 'notes',
@ -927,7 +932,6 @@ function loadStockTrackingTable(table, options) {
cols.push({ cols.push({
field: 'title', field: 'title',
title: '{% trans "Description" %}', title: '{% trans "Description" %}',
sortable: true,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var html = "<b>" + value + "</b>"; var html = "<b>" + value + "</b>";
@ -952,7 +956,6 @@ function loadStockTrackingTable(table, options) {
}); });
cols.push({ cols.push({
sortable: true,
field: 'user', field: 'user',
title: '{% trans "User" %}', title: '{% trans "User" %}',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {

View File

@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load status_codes %} {% load status_codes %}
{% load inventree_extras %}
{% include "status_codes.html" with label='stock' options=StockStatus.list %} {% include "status_codes.html" with label='stock' options=StockStatus.list %}
{% include "status_codes.html" with label='build' options=BuildStatus.list %} {% include "status_codes.html" with label='build' options=BuildStatus.list %}
@ -110,6 +111,8 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Depleted" %}', title: '{% trans "Depleted" %}',
description: '{% trans "Show stock items which are depleted" %}', description: '{% trans "Show stock items which are depleted" %}',
}, },
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
expired: { expired: {
type: 'bool', type: 'bool',
title: '{% trans "Expired" %}', title: '{% trans "Expired" %}',
@ -120,6 +123,7 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Stale" %}', title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}', description: '{% trans "Show stock which is close to expiring" %}',
}, },
{% endif %}
in_stock: { in_stock: {
type: 'bool', type: 'bool',
title: '{% trans "In Stock" %}', 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('refreshOptions', options);
table.bootstrapTable('refresh'); table.bootstrapTable('refresh');
@ -126,9 +133,45 @@ $.fn.inventreeTable = function(options) {
var varName = tableName + '-pagesize'; var varName = tableName + '-pagesize';
// Pagingation options (can be server-side or client-side as specified by the caller)
options.pagination = true; options.pagination = true;
options.paginationVAlign = 'both';
options.pageSize = inventreeLoad(varName, 25); options.pageSize = inventreeLoad(varName, 25);
options.pageList = [25, 50, 100, 250, 'all']; 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; options.rememberOrder = true;

View File

@ -5,8 +5,8 @@ from . import api
user_urls = [ user_urls = [
url(r'^(?P<pk>[0-9]+)/?$', api.UserDetail.as_view(), name='user-detail'), url(r'^(?P<pk>[0-9]+)/?$', api.UserDetail.as_view(), name='user-detail'),
url(r'roles', api.RoleDetails.as_view(), name='api-user-roles'), url(r'roles/?$', api.RoleDetails.as_view(), name='api-user-roles'),
url(r'token', api.GetAuthToken.as_view(), name='api-token'), url(r'token/?$', api.GetAuthToken.as_view(), name='api-token'),
url(r'^$', api.UserList.as_view()), url(r'^$', api.UserList.as_view()),
] ]

View File

@ -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. 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 # Documentation
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/). For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).