Merge pull request #1856 from SchrodingersGat/query-filters

Add instance-specific filters to API OPTIONS data
This commit is contained in:
Oliver 2021-07-21 21:50:17 +10:00 committed by GitHub
commit 20a30f317f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 164 additions and 1 deletions

View File

@ -32,6 +32,9 @@ class InvenTreeMetadata(SimpleMetadata):
def determine_metadata(self, request, view): def determine_metadata(self, request, view):
self.request = request
self.view = view
metadata = super().determine_metadata(request, view) metadata = super().determine_metadata(request, view)
user = request.user user = request.user
@ -136,6 +139,42 @@ class InvenTreeMetadata(SimpleMetadata):
except AttributeError: except AttributeError:
pass 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 return serializer_info
def get_field_info(self, field): def get_field_info(self, field):

View File

@ -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 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): def save(self, *args, **kwargs):
try: try:

View File

@ -104,6 +104,21 @@ class BuildList(generics.ListCreateAPIView):
params = self.request.query_params 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" # Filter by "parent"
parent = params.get('parent', None) parent = params.get('parent', None)

View File

@ -96,6 +96,14 @@ class Build(MPTTModel):
def get_api_url(): def get_api_url():
return reverse('api-build-list') return reverse('api-build-list')
def api_instance_filters(self):
return {
'parent': {
'exclude_tree': self.pk,
}
}
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
try: try:

View File

@ -105,6 +105,20 @@ class CategoryList(generics.ListCreateAPIView):
except (ValueError, PartCategory.DoesNotExist): except (ValueError, PartCategory.DoesNotExist):
pass 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 return queryset
filter_backends = [ filter_backends = [
@ -644,6 +658,20 @@ class PartList(generics.ListCreateAPIView):
except (ValueError, Part.DoesNotExist): except (ValueError, Part.DoesNotExist):
pass 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'? # Filter by 'ancestor'?
ancestor = params.get('ancestor', None) ancestor = params.get('ancestor', None)

View File

@ -27,6 +27,7 @@ from markdownx.models import MarkdownxField
from django_cleanup import cleanup from django_cleanup import cleanup
from mptt.models import TreeForeignKey, MPTTModel from mptt.models import TreeForeignKey, MPTTModel
from mptt.exceptions import InvalidMove
from mptt.managers import TreeManager from mptt.managers import TreeManager
from stdimage.models import StdImageField from stdimage.models import StdImageField
@ -359,6 +360,17 @@ class Part(MPTTModel):
return reverse('api-part-list') 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): def get_context_data(self, request, **kwargs):
""" """
Return some useful context data about this part for template rendering Return some useful context data about this part for template rendering
@ -414,7 +426,12 @@ class Part(MPTTModel):
self.full_clean() self.full_clean()
try:
super().save(*args, **kwargs) super().save(*args, **kwargs)
except InvalidMove:
raise ValidationError({
'variant_of': _('Invalid choice for parent part'),
})
if add_category_templates: if add_category_templates:
# Get part category # Get part category

View File

@ -343,6 +343,20 @@ class StockLocationList(generics.ListCreateAPIView):
except (ValueError, StockLocation.DoesNotExist): except (ValueError, StockLocation.DoesNotExist):
pass 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 return queryset
filter_backends = [ filter_backends = [
@ -719,6 +733,20 @@ class StockList(generics.ListCreateAPIView):
if customer: if customer:
queryset = queryset.filter(customer=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? # Filter by 'allocated' parts?
allocated = params.get('allocated', None) allocated = params.get('allocated', None)

View File

@ -191,6 +191,17 @@ class StockItem(MPTTModel):
def get_api_url(): def get_api_url():
return reverse('api-stock-list') 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" # A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock"
IN_STOCK_FILTER = Q( IN_STOCK_FILTER = Q(
quantity__gt=0, quantity__gt=0,

View File

@ -350,6 +350,12 @@ function constructFormBody(fields, options) {
for(field in fields) { for(field in fields) {
fields[field].name = field; 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]; var field_options = displayed_fields[field];
// Copy custom options across to the fields object // Copy custom options across to the fields object