mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-23 07:10:55 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
InvenTree
InvenTree
metadata.pymodels.pytest_urls.pyurls.py
static
css
script
jquery-ui
images
ui-icons_444444_256x240.pngui-icons_555555_256x240.pngui-icons_777620_256x240.pngui-icons_777777_256x240.pngui-icons_cc0000_256x240.pngui-icons_ffffff_256x240.png
index.htmljquery-ui.cssjquery-ui.jsjquery-ui.min.cssjquery-ui.min.jsjquery-ui.structure.cssjquery-ui.structure.min.cssjquery-ui.theme.min.cssbuild
common
company
label
order
part
report
stock
templates
@ -32,6 +32,9 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
def determine_metadata(self, request, view):
|
||||
|
||||
self.request = request
|
||||
self.view = view
|
||||
|
||||
metadata = super().determine_metadata(request, view)
|
||||
|
||||
user = request.user
|
||||
@ -136,6 +139,42 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Try to extract 'instance' information
|
||||
instance = None
|
||||
|
||||
# Extract extra information if an instance is available
|
||||
if hasattr(serializer, 'instance'):
|
||||
instance = serializer.instance
|
||||
|
||||
if instance is None:
|
||||
try:
|
||||
instance = self.view.get_object()
|
||||
except:
|
||||
pass
|
||||
|
||||
if instance is not None:
|
||||
"""
|
||||
If there is an instance associated with this API View,
|
||||
introspect that instance to find any specific API info.
|
||||
"""
|
||||
|
||||
if hasattr(instance, 'api_instance_filters'):
|
||||
|
||||
instance_filters = instance.api_instance_filters()
|
||||
|
||||
for field_name, field_filters in instance_filters.items():
|
||||
|
||||
if field_name not in serializer_info.keys():
|
||||
# The field might be missing, but is added later on
|
||||
# This function seems to get called multiple times?
|
||||
continue
|
||||
|
||||
if 'instance_filters' not in serializer_info[field_name].keys():
|
||||
serializer_info[field_name]['instance_filters'] = {}
|
||||
|
||||
for key, value in field_filters.items():
|
||||
serializer_info[field_name]['instance_filters'][key] = value
|
||||
|
||||
return serializer_info
|
||||
|
||||
def get_field_info(self, field):
|
||||
|
@ -93,6 +93,17 @@ class InvenTreeTree(MPTTModel):
|
||||
parent: The item immediately above this one. An item with a null parent is a top-level item
|
||||
"""
|
||||
|
||||
def api_instance_filters(self):
|
||||
"""
|
||||
Instance filters for InvenTreeTree models
|
||||
"""
|
||||
|
||||
return {
|
||||
'parent': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
try:
|
||||
|
@ -1037,3 +1037,10 @@ a.anchor {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.search-menu {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.search-menu .ui-menu-item {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
Binary file not shown.
Before ![]() (image error) Size: 6.9 KiB After ![]() (image error) Size: 6.9 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 6.9 KiB After ![]() (image error) Size: 6.9 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 4.5 KiB After ![]() (image error) Size: 4.5 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 6.9 KiB After ![]() (image error) Size: 6.9 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 4.5 KiB After ![]() (image error) Size: 4.5 KiB ![]() ![]() |
Binary file not shown.
Before ![]() (image error) Size: 6.3 KiB After ![]() (image error) Size: 6.3 KiB ![]() ![]() |
@ -59,6 +59,11 @@
|
||||
<h1>YOUR COMPONENTS:</h1>
|
||||
|
||||
|
||||
<!-- Autocomplete -->
|
||||
<h2 class="demoHeaders">Autocomplete</h2>
|
||||
<div>
|
||||
<input id="autocomplete" title="type "a"">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -248,6 +253,23 @@
|
||||
|
||||
|
||||
|
||||
<!-- Menu -->
|
||||
<h2 class="demoHeaders">Menu</h2>
|
||||
<ul style="width:100px;" id="menu">
|
||||
<li><div>Item 1</div></li>
|
||||
<li><div>Item 2</div></li>
|
||||
<li><div>Item 3</div>
|
||||
<ul>
|
||||
<li><div>Item 3-1</div></li>
|
||||
<li><div>Item 3-2</div></li>
|
||||
<li><div>Item 3-3</div></li>
|
||||
<li><div>Item 3-4</div></li>
|
||||
<li><div>Item 3-5</div></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><div>Item 4</div></li>
|
||||
<li><div>Item 5</div></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<!-- Highlight / Error -->
|
||||
@ -270,6 +292,33 @@
|
||||
<script src="jquery-ui.js"></script>
|
||||
<script>
|
||||
|
||||
var availableTags = [
|
||||
"ActionScript",
|
||||
"AppleScript",
|
||||
"Asp",
|
||||
"BASIC",
|
||||
"C",
|
||||
"C++",
|
||||
"Clojure",
|
||||
"COBOL",
|
||||
"ColdFusion",
|
||||
"Erlang",
|
||||
"Fortran",
|
||||
"Groovy",
|
||||
"Haskell",
|
||||
"Java",
|
||||
"JavaScript",
|
||||
"Lisp",
|
||||
"Perl",
|
||||
"PHP",
|
||||
"Python",
|
||||
"Ruby",
|
||||
"Scala",
|
||||
"Scheme"
|
||||
];
|
||||
$( "#autocomplete" ).autocomplete({
|
||||
source: availableTags
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -280,6 +329,7 @@
|
||||
|
||||
|
||||
|
||||
$( "#menu" ).menu();
|
||||
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*! jQuery UI - v1.12.1 - 2021-02-23
|
||||
/*! jQuery UI - v1.12.1 - 2021-07-18
|
||||
* http://jqueryui.com
|
||||
* Includes: core.css, resizable.css, theme.css
|
||||
* Includes: core.css, resizable.css, autocomplete.css, menu.css, theme.css
|
||||
* To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
@ -160,6 +160,66 @@
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.ui-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
.ui-menu .ui-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.ui-menu .ui-menu-item {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
/* support: IE10, see #8844 */
|
||||
list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
|
||||
}
|
||||
.ui-menu .ui-menu-item-wrapper {
|
||||
position: relative;
|
||||
padding: 3px 1em 3px .4em;
|
||||
}
|
||||
.ui-menu .ui-menu-divider {
|
||||
margin: 5px 0;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
.ui-menu .ui-state-focus,
|
||||
.ui-menu .ui-state-active {
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
/* icon support */
|
||||
.ui-menu-icons {
|
||||
position: relative;
|
||||
}
|
||||
.ui-menu-icons .ui-menu-item-wrapper {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* left-aligned */
|
||||
.ui-menu .ui-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: .2em;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
/* right-aligned */
|
||||
.ui-menu .ui-menu-icon {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Component containers
|
||||
----------------------------------*/
|
||||
|
1915
InvenTree/InvenTree/static/script/jquery-ui/jquery-ui.js
vendored
1915
InvenTree/InvenTree/static/script/jquery-ui/jquery-ui.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -164,3 +164,63 @@
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.ui-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
.ui-menu .ui-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.ui-menu .ui-menu-item {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
/* support: IE10, see #8844 */
|
||||
list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
|
||||
}
|
||||
.ui-menu .ui-menu-item-wrapper {
|
||||
position: relative;
|
||||
padding: 3px 1em 3px .4em;
|
||||
}
|
||||
.ui-menu .ui-menu-divider {
|
||||
margin: 5px 0;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
.ui-menu .ui-state-focus,
|
||||
.ui-menu .ui-state-active {
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
/* icon support */
|
||||
.ui-menu-icons {
|
||||
position: relative;
|
||||
}
|
||||
.ui-menu-icons .ui-menu-item-wrapper {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* left-aligned */
|
||||
.ui-menu .ui-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: .2em;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
/* right-aligned */
|
||||
.ui-menu .ui-menu-icon {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*! jQuery UI - v1.12.1 - 2021-02-23
|
||||
/*! jQuery UI - v1.12.1 - 2021-07-18
|
||||
* http://jqueryui.com
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}
|
||||
.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}
|
@ -1,4 +1,4 @@
|
||||
/*! jQuery UI - v1.12.1 - 2021-02-23
|
||||
/*! jQuery UI - v1.12.1 - 2021-07-18
|
||||
* http://jqueryui.com
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
|
142
InvenTree/InvenTree/test_urls.py
Normal file
142
InvenTree/InvenTree/test_urls.py
Normal file
@ -0,0 +1,142 @@
|
||||
"""
|
||||
Validate that all URLs specified in template files are correct.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class URLTest(TestCase):
|
||||
|
||||
# Need fixture data in the database
|
||||
fixtures = [
|
||||
'settings',
|
||||
'build',
|
||||
'company',
|
||||
'manufacturer_part',
|
||||
'price_breaks',
|
||||
'supplier_part',
|
||||
'order',
|
||||
'sales_order',
|
||||
'bom',
|
||||
'category',
|
||||
'params',
|
||||
'part_pricebreaks',
|
||||
'part',
|
||||
'test_templates',
|
||||
'location',
|
||||
'stock_tests',
|
||||
'stock',
|
||||
'users',
|
||||
]
|
||||
|
||||
def find_files(self, suffix):
|
||||
"""
|
||||
Search for all files in the template directories,
|
||||
which can have URLs rendered
|
||||
"""
|
||||
|
||||
template_dirs = [
|
||||
('build', 'templates'),
|
||||
('common', 'templates'),
|
||||
('company', 'templates'),
|
||||
('label', 'templates'),
|
||||
('order', 'templates'),
|
||||
('part', 'templates'),
|
||||
('report', 'templates'),
|
||||
('stock', 'templates'),
|
||||
('templates', ),
|
||||
]
|
||||
|
||||
template_files = []
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
tld = os.path.join(here, '..')
|
||||
|
||||
for directory in template_dirs:
|
||||
|
||||
template_dir = os.path.join(tld, *directory)
|
||||
|
||||
for path in Path(template_dir).rglob(suffix):
|
||||
|
||||
f = os.path.abspath(path)
|
||||
|
||||
if f not in template_files:
|
||||
template_files.append(f)
|
||||
|
||||
return template_files
|
||||
|
||||
def find_urls(self, input_file):
|
||||
"""
|
||||
Search for all instances of {% url %} in supplied template file
|
||||
"""
|
||||
|
||||
urls = []
|
||||
|
||||
pattern = "{% url ['\"]([^'\"]+)['\"]([^%]*)%}"
|
||||
|
||||
with open(input_file, 'r') as f:
|
||||
|
||||
data = f.read()
|
||||
|
||||
results = re.findall(pattern, data)
|
||||
|
||||
for result in results:
|
||||
if len(result) == 2:
|
||||
urls.append([
|
||||
result[0].strip(),
|
||||
result[1].strip()
|
||||
])
|
||||
elif len(result) == 1:
|
||||
urls.append([
|
||||
result[0].strip(),
|
||||
''
|
||||
])
|
||||
|
||||
return urls
|
||||
|
||||
def reverse_url(self, url_pair):
|
||||
"""
|
||||
Perform lookup on the URL
|
||||
"""
|
||||
|
||||
url, pk = url_pair
|
||||
|
||||
# TODO: Handle reverse lookup of admin URLs!
|
||||
if url.startswith("admin:"):
|
||||
return
|
||||
|
||||
if pk:
|
||||
# We will assume that there is at least one item in the database
|
||||
reverse(url, kwargs={"pk": 1})
|
||||
else:
|
||||
reverse(url)
|
||||
|
||||
def check_file(self, f):
|
||||
"""
|
||||
Run URL checks for the provided file
|
||||
"""
|
||||
|
||||
urls = self.find_urls(f)
|
||||
|
||||
for url in urls:
|
||||
self.reverse_url(url)
|
||||
|
||||
def test_html_templates(self):
|
||||
|
||||
template_files = self.find_files("*.html")
|
||||
|
||||
for f in template_files:
|
||||
self.check_file(f)
|
||||
|
||||
def test_js_templates(self):
|
||||
|
||||
template_files = self.find_files("*.js")
|
||||
|
||||
for f in template_files:
|
||||
self.check_file(f)
|
@ -111,6 +111,7 @@ dynamic_javascript_urls = [
|
||||
url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'),
|
||||
url(r'^filters.js', DynamicJsView.as_view(template_name='js/filters.js'), name='filters.js'),
|
||||
url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'),
|
||||
url(r'^inventree.js', DynamicJsView.as_view(template_name='js/inventree.js'), name='inventree.js'),
|
||||
url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'),
|
||||
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/model_renderers.js'), name='model_renderers.js'),
|
||||
url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'),
|
||||
|
@ -104,6 +104,21 @@ class BuildList(generics.ListCreateAPIView):
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# exclude parent tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
|
||||
try:
|
||||
build = Build.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[bld.pk for bld in build.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, Build.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by "parent"
|
||||
parent = params.get('parent', None)
|
||||
|
||||
|
@ -96,6 +96,14 @@ class Build(MPTTModel):
|
||||
def get_api_url():
|
||||
return reverse('api-build-list')
|
||||
|
||||
def api_instance_filters(self):
|
||||
|
||||
return {
|
||||
'parent': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
try:
|
||||
|
@ -277,6 +277,13 @@ class InvenTreeSetting(models.Model):
|
||||
'validator': bool,
|
||||
},
|
||||
|
||||
'SEARCH_PREVIEW_RESULTS': {
|
||||
'name': _('Search Preview Results'),
|
||||
'description': _('Number of results to show in search preview window'),
|
||||
'default': 10,
|
||||
'validator': [int, MinValueValidator(1)]
|
||||
},
|
||||
|
||||
'STOCK_ENABLE_EXPIRY': {
|
||||
'name': _('Stock Expiry'),
|
||||
'description': _('Enable stock expiry functionality'),
|
||||
|
@ -24,14 +24,14 @@
|
||||
|
||||
{% comment "for later" %}
|
||||
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Manufacturer Part Stock" %}'>
|
||||
<a href='{% url "manufacturer-part-stock" part.id %}'>
|
||||
<a href='#'>
|
||||
<span class='fas fa-boxes sidebar-icon'></span>
|
||||
{% trans "Stock" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "orders" %}active{% endif %}' title='{% trans "Manufacturer Part Orders" %}'>
|
||||
<a href='{% url "manufacturer-part-orders" part.id %}'>
|
||||
<a href='#'>
|
||||
<span class='fas fa-shopping-cart sidebar-icon'></span>
|
||||
{% trans "Orders" %}
|
||||
</a>
|
||||
|
@ -30,11 +30,9 @@ company_urls = [
|
||||
|
||||
manufacturer_part_urls = [
|
||||
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
||||
])),
|
||||
url(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
||||
]
|
||||
|
||||
supplier_part_urls = [
|
||||
url('^.*$', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
|
||||
url(r'^(?P<pk>\d+)/', views.SupplierPartDetail.as_view(template_name='company/supplier_part.html'), name='supplier-part-detail'),
|
||||
]
|
||||
|
@ -296,7 +296,9 @@ class StockItemLabel(LabelTemplate):
|
||||
'uid': stock_item.uid,
|
||||
'qr_data': stock_item.format_barcode(brief=True),
|
||||
'qr_url': stock_item.format_barcode(url=True, request=request),
|
||||
'tests': stock_item.testResultMap()
|
||||
'tests': stock_item.testResultMap(),
|
||||
'parameters': stock_item.part.parameters_map(),
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -398,4 +400,5 @@ class PartLabel(LabelTemplate):
|
||||
'revision': part.revision,
|
||||
'qr_data': part.format_barcode(brief=True),
|
||||
'qr_url': part.format_barcode(url=True, request=request),
|
||||
'parameters': part.parameters_map(),
|
||||
}
|
||||
|
@ -84,9 +84,9 @@ class ReceivePurchaseOrderForm(HelperForm):
|
||||
|
||||
location = TreeNodeChoiceField(
|
||||
queryset=StockLocation.objects.all(),
|
||||
required=True,
|
||||
required=False,
|
||||
label=_("Destination"),
|
||||
help_text=_("Receive parts to this location"),
|
||||
help_text=_("Set all received parts listed above to this location (if left blank, use \"Destination\" column value in above table)"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -12,7 +12,7 @@
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<label class='control-label'>{% trans "Parts" %}</label>
|
||||
<p class='help-block'>{% trans "Select parts to receive against this order" %}</p>
|
||||
<p class='help-block'>{% trans "Fill out number of parts received, the status and destination" %}</p>
|
||||
|
||||
<table class='table table-striped'>
|
||||
<tr>
|
||||
@ -55,7 +55,14 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{ line.get_destination }}
|
||||
<div class='control-group'>
|
||||
<select class='select' name='destination-{{ line.id }}'>
|
||||
<option value="">----------</option>
|
||||
{% for location in stock_locations %}
|
||||
<option value="{{ location.pk }}" {% if location == line.get_destination %}selected="selected"{% endif %}>{{ location }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='{% trans "Remove line" %}' type='button'>
|
||||
@ -67,6 +74,8 @@
|
||||
</table>
|
||||
|
||||
{% crispy form %}
|
||||
|
||||
<div id='form-errors'>{{ form_errors }}</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -491,6 +491,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
ctx = {
|
||||
'order': self.order,
|
||||
'lines': self.lines,
|
||||
'stock_locations': StockLocation.objects.all(),
|
||||
}
|
||||
|
||||
return ctx
|
||||
@ -543,6 +544,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
|
||||
self.request = request
|
||||
self.order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk'])
|
||||
errors = False
|
||||
|
||||
self.lines = []
|
||||
self.destination = None
|
||||
@ -557,12 +559,6 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
except (StockLocation.DoesNotExist, ValueError):
|
||||
pass
|
||||
|
||||
errors = False
|
||||
|
||||
if self.destination is None:
|
||||
errors = True
|
||||
msg = _("No destination set")
|
||||
|
||||
# Extract information on all submitted line items
|
||||
for item in request.POST:
|
||||
if item.startswith('line-'):
|
||||
@ -587,6 +583,21 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
else:
|
||||
line.status_code = StockStatus.OK
|
||||
|
||||
# Check the destination field
|
||||
line.destination = None
|
||||
if self.destination:
|
||||
# If global destination is set, overwrite line value
|
||||
line.destination = self.destination
|
||||
else:
|
||||
destination_key = f'destination-{pk}'
|
||||
destination = request.POST.get(destination_key, None)
|
||||
|
||||
if destination:
|
||||
try:
|
||||
line.destination = StockLocation.objects.get(pk=destination)
|
||||
except (StockLocation.DoesNotExist, ValueError):
|
||||
pass
|
||||
|
||||
# Check that line matches the order
|
||||
if not line.order == self.order:
|
||||
# TODO - Display a non-field error?
|
||||
@ -645,7 +656,7 @@ class PurchaseOrderReceive(AjaxUpdateView):
|
||||
|
||||
self.order.receive_line_item(
|
||||
line,
|
||||
self.destination,
|
||||
line.destination,
|
||||
line.receive_quantity,
|
||||
self.request.user,
|
||||
status=line.status_code,
|
||||
|
@ -105,6 +105,20 @@ class CategoryList(generics.ListCreateAPIView):
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Exclude PartCategory tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
try:
|
||||
cat = PartCategory.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[c.pk for c in cat.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
filter_backends = [
|
||||
@ -361,7 +375,6 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
|
||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
@ -619,8 +632,6 @@ class PartList(generics.ListCreateAPIView):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
|
||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
@ -633,10 +644,6 @@ class PartList(generics.ListCreateAPIView):
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# Annotate calculated data to the queryset
|
||||
# (This will be used for further filtering)
|
||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
# Filter by "uses" query - Limit to parts which use the provided part
|
||||
@ -651,6 +658,20 @@ class PartList(generics.ListCreateAPIView):
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Exclude part variant tree?
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
try:
|
||||
top_level_part = Part.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[prt.pk for prt in top_level_part.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by 'ancestor'?
|
||||
ancestor = params.get('ancestor', None)
|
||||
|
||||
|
@ -27,6 +27,8 @@ from markdownx.models import MarkdownxField
|
||||
from django_cleanup import cleanup
|
||||
|
||||
from mptt.models import TreeForeignKey, MPTTModel
|
||||
from mptt.exceptions import InvalidMove
|
||||
from mptt.managers import TreeManager
|
||||
|
||||
from stdimage.models import StdImageField
|
||||
|
||||
@ -284,6 +286,24 @@ def match_part_names(match, threshold=80, reverse=True, compare_length=False):
|
||||
return matches
|
||||
|
||||
|
||||
class PartManager(TreeManager):
|
||||
"""
|
||||
Defines a custom object manager for the Part model.
|
||||
|
||||
The main purpose of this manager is to reduce the number of database hits,
|
||||
as the Part model has a large number of ForeignKey fields!
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
return super().get_queryset().prefetch_related(
|
||||
'category',
|
||||
'category__parent',
|
||||
'stock_items',
|
||||
'builds',
|
||||
)
|
||||
|
||||
|
||||
@cleanup.ignore
|
||||
class Part(MPTTModel):
|
||||
""" The Part object represents an abstract part, the 'concept' of an actual entity.
|
||||
@ -321,6 +341,8 @@ class Part(MPTTModel):
|
||||
responsible: User who is responsible for this part (optional)
|
||||
"""
|
||||
|
||||
objects = PartManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Part")
|
||||
verbose_name_plural = _("Parts")
|
||||
@ -338,6 +360,17 @@ class Part(MPTTModel):
|
||||
|
||||
return reverse('api-part-list')
|
||||
|
||||
def api_instance_filters(self):
|
||||
"""
|
||||
Return API query filters for limiting field results against this instance
|
||||
"""
|
||||
|
||||
return {
|
||||
'variant_of': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
def get_context_data(self, request, **kwargs):
|
||||
"""
|
||||
Return some useful context data about this part for template rendering
|
||||
@ -393,7 +426,12 @@ class Part(MPTTModel):
|
||||
|
||||
self.full_clean()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
try:
|
||||
super().save(*args, **kwargs)
|
||||
except InvalidMove:
|
||||
raise ValidationError({
|
||||
'variant_of': _('Invalid choice for parent part'),
|
||||
})
|
||||
|
||||
if add_category_templates:
|
||||
# Get part category
|
||||
@ -1866,6 +1904,23 @@ class Part(MPTTModel):
|
||||
|
||||
return self.parameters.order_by('template__name')
|
||||
|
||||
def parameters_map(self):
|
||||
"""
|
||||
Return a map (dict) of parameter values assocaited with this Part instance,
|
||||
of the form:
|
||||
{
|
||||
"name_1": "value_1",
|
||||
"name_2": "value_2",
|
||||
}
|
||||
"""
|
||||
|
||||
params = {}
|
||||
|
||||
for parameter in self.parameters.all():
|
||||
params[parameter.template.name] = parameter.data
|
||||
|
||||
return params
|
||||
|
||||
@property
|
||||
def has_variants(self):
|
||||
""" Check if this Part object has variants underneath it. """
|
||||
|
@ -215,25 +215,6 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
if category_detail is not True:
|
||||
self.fields.pop('category_detail')
|
||||
|
||||
@staticmethod
|
||||
def prefetch_queryset(queryset):
|
||||
"""
|
||||
Prefetch related database tables,
|
||||
to reduce database hits.
|
||||
"""
|
||||
|
||||
return queryset.prefetch_related(
|
||||
'category',
|
||||
'category__parts',
|
||||
'category__parent',
|
||||
'stock_items',
|
||||
'bom_items',
|
||||
'builds',
|
||||
'supplier_parts',
|
||||
'supplier_parts__purchase_order_line_items',
|
||||
'supplier_parts__purchase_order_line_items__order',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
|
@ -99,7 +99,7 @@ class CategoryTest(TestCase):
|
||||
""" Test that the Category parameters are correctly fetched """
|
||||
|
||||
# Check number of SQL queries to iterate other parameters
|
||||
with self.assertNumQueries(3):
|
||||
with self.assertNumQueries(7):
|
||||
# Prefetch: 3 queries (parts, parameters and parameters_template)
|
||||
fasteners = self.fasteners.prefetch_parts_parameters()
|
||||
# Iterate through all parts and parameters
|
||||
|
@ -356,6 +356,7 @@ class TestReport(ReportTemplateBase):
|
||||
'stock_item': stock_item,
|
||||
'serial': stock_item.serial,
|
||||
'part': stock_item.part,
|
||||
'parameters': stock_item.part.parameters_map(),
|
||||
'results': stock_item.testResultMap(include_installed=self.include_installed),
|
||||
'result_list': stock_item.testResultList(include_installed=self.include_installed),
|
||||
'installed_items': stock_item.get_installed_items(cascade=True),
|
||||
|
@ -84,7 +84,6 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
queryset = StockItemSerializer.prefetch_queryset(queryset)
|
||||
queryset = StockItemSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
@ -344,6 +343,20 @@ class StockLocationList(generics.ListCreateAPIView):
|
||||
except (ValueError, StockLocation.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Exclude StockLocation tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
try:
|
||||
loc = StockLocation.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[subloc.pk for subloc in loc.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, StockLocation.DoesNotExist):
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
filter_backends = [
|
||||
@ -637,7 +650,6 @@ class StockList(generics.ListCreateAPIView):
|
||||
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
queryset = StockItemSerializer.prefetch_queryset(queryset)
|
||||
queryset = StockItemSerializer.annotate_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
@ -721,6 +733,20 @@ class StockList(generics.ListCreateAPIView):
|
||||
if customer:
|
||||
queryset = queryset.filter(customer=customer)
|
||||
|
||||
# Exclude stock item tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
try:
|
||||
item = StockItem.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[it.pk for it in item.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by 'allocated' parts?
|
||||
allocated = params.get('allocated', None)
|
||||
|
||||
|
@ -23,6 +23,7 @@ from django.dispatch import receiver
|
||||
from markdownx.models import MarkdownxField
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from mptt.managers import TreeManager
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from datetime import datetime, timedelta
|
||||
@ -130,6 +131,31 @@ def before_delete_stock_location(sender, instance, using, **kwargs):
|
||||
child.save()
|
||||
|
||||
|
||||
class StockItemManager(TreeManager):
|
||||
"""
|
||||
Custom database manager for the StockItem class.
|
||||
|
||||
StockItem querysets will automatically prefetch related fields.
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
return super().get_queryset().prefetch_related(
|
||||
'belongs_to',
|
||||
'build',
|
||||
'customer',
|
||||
'purchase_order',
|
||||
'sales_order',
|
||||
'supplier_part',
|
||||
'supplier_part__supplier',
|
||||
'allocations',
|
||||
'sales_order_allocations',
|
||||
'location',
|
||||
'part',
|
||||
'tracking_info'
|
||||
)
|
||||
|
||||
|
||||
class StockItem(MPTTModel):
|
||||
"""
|
||||
A StockItem object represents a quantity of physical instances of a part.
|
||||
@ -165,6 +191,17 @@ class StockItem(MPTTModel):
|
||||
def get_api_url():
|
||||
return reverse('api-stock-list')
|
||||
|
||||
def api_instance_filters(self):
|
||||
"""
|
||||
Custom API instance filters
|
||||
"""
|
||||
|
||||
return {
|
||||
'parent': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
# A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock"
|
||||
IN_STOCK_FILTER = Q(
|
||||
quantity__gt=0,
|
||||
|
@ -70,29 +70,6 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
- Includes serialization for the item location
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def prefetch_queryset(queryset):
|
||||
"""
|
||||
Prefetch related database tables,
|
||||
to reduce database hits.
|
||||
"""
|
||||
|
||||
return queryset.prefetch_related(
|
||||
'belongs_to',
|
||||
'build',
|
||||
'customer',
|
||||
'purchase_order',
|
||||
'sales_order',
|
||||
'supplier_part',
|
||||
'supplier_part__supplier',
|
||||
'supplier_part__manufacturer_part__manufacturer',
|
||||
'allocations',
|
||||
'sales_order_allocations',
|
||||
'location',
|
||||
'part',
|
||||
'tracking_info',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
|
@ -272,7 +272,7 @@
|
||||
<tr>
|
||||
<td><span class='fas fa-user-tie'></span></td>
|
||||
<td>{% trans "Customer" %}</td>
|
||||
<td><a href="{% url 'company-detail-assigned-stock' item.customer.id %}">{{ item.customer.name }}</a></td>
|
||||
<td><a href="{% url 'company-detail' item.customer.id %}?display=assigned-stock">{{ item.customer.name }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.belongs_to %}
|
||||
|
@ -31,4 +31,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>{% trans "Search Settings" %}</h4>
|
||||
<table class='table table-striped table-condensed'>
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" icon="fa-search" %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -142,11 +142,11 @@
|
||||
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
|
||||
|
||||
<!-- general InvenTree -->
|
||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
|
||||
|
||||
<!-- translated -->
|
||||
<script type='text/javascript' src="{% i18n_static 'inventree.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'api.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
|
||||
|
@ -350,6 +350,12 @@ function constructFormBody(fields, options) {
|
||||
for(field in fields) {
|
||||
fields[field].name = field;
|
||||
|
||||
|
||||
// If any "instance_filters" are defined for the endpoint, copy them across (overwrite)
|
||||
if (fields[field].instance_filters) {
|
||||
fields[field].filters = Object.assign(fields[field].filters || {}, fields[field].instance_filters);
|
||||
}
|
||||
|
||||
var field_options = displayed_fields[field];
|
||||
|
||||
// Copy custom options across to the fields object
|
||||
|
@ -1,3 +1,5 @@
|
||||
{% load inventree_extras %}
|
||||
|
||||
function attachClipboard(selector, containerselector, textElement) {
|
||||
// set container
|
||||
if (containerselector){
|
||||
@ -13,7 +15,8 @@ function attachClipboard(selector, containerselector, textElement) {
|
||||
}
|
||||
} else {
|
||||
text = function(trigger) {
|
||||
var content = trigger.parentElement.parentElement.textContent;return content.trim();
|
||||
var content = trigger.parentElement.parentElement.textContent;
|
||||
return content.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +83,45 @@ function inventreeDocReady() {
|
||||
attachClipboard('.clip-btn');
|
||||
attachClipboard('.clip-btn', 'modal-about'); // modals
|
||||
attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text'); // version-text
|
||||
|
||||
// Add autocomplete to the search-bar
|
||||
$("#search-bar" ).autocomplete({
|
||||
source: function (request, response) {
|
||||
$.ajax({
|
||||
url: '/api/part/',
|
||||
data: {
|
||||
search: request.term,
|
||||
limit: {% settings_value 'SEARCH_PREVIEW_RESULTS' %},
|
||||
offset: 0
|
||||
},
|
||||
success: function (data) {
|
||||
var transformed = $.map(data.results, function (el) {
|
||||
return {
|
||||
label: el.name,
|
||||
id: el.pk,
|
||||
thumbnail: el.thumbnail
|
||||
};
|
||||
});
|
||||
response(transformed);
|
||||
},
|
||||
error: function () {
|
||||
response([]);
|
||||
}
|
||||
});
|
||||
},
|
||||
create: function () {
|
||||
$(this).data('ui-autocomplete')._renderItem = function (ul, item) {
|
||||
return $('<li>')
|
||||
.append('<span>' + imageHoverIcon(item.thumbnail) + item.label + '</span>')
|
||||
.appendTo(ul);
|
||||
};
|
||||
},
|
||||
select: function( event, ui ) {
|
||||
window.location = '/part/' + ui.item.id + '/';
|
||||
},
|
||||
minLength: 2,
|
||||
classes: {'ui-autocomplete': 'dropdown-menu search-menu'},
|
||||
});
|
||||
}
|
||||
|
||||
function isFileTransfer(transfer) {
|
@ -3,7 +3,7 @@
|
||||
<form class="navbar-form navbar-left" action="{% url 'search' %}" method='post'>
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<input type="text" name='search' class="form-control" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}>
|
||||
<input type="text" name='search' class="form-control" id="search-bar" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}>
|
||||
</div>
|
||||
<button type="submit" id='search-submit' class="btn btn-default" title='{% trans "Search" %}'>
|
||||
<span class='fas fa-search'></span>
|
||||
|
Reference in New Issue
Block a user