Refactor barcode endoint

- Moved code into 'barcode' directory
This commit is contained in:
Oliver Walters 2020-06-11 11:09:07 +10:00
parent 290c0eb225
commit 0068cd9825
12 changed files with 419 additions and 270 deletions

View File

@ -20,9 +20,6 @@ from .version import inventreeVersion, inventreeInstanceName
from plugins import plugins as inventree_plugins
# Load barcode plugins
print("Loading barcode plugins")
barcode_plugins = inventree_plugins.load_barcode_plugins()
print("Loading action plugins")
action_plugins = inventree_plugins.load_action_plugins()
@ -100,66 +97,3 @@ class ActionPluginView(APIView):
'error': _("No matching action found"),
"action": action,
})
class BarcodePluginView(APIView):
"""
Endpoint for handling barcode scan requests.
Barcode data are decoded by the client application,
and sent to this endpoint (as a JSON object) for validation.
A barcode could follow the internal InvenTree barcode format,
or it could match to a third-party barcode format (e.g. Digikey).
"""
permission_classes = [
permissions.IsAuthenticated,
]
def post(self, request, *args, **kwargs):
response = {}
barcode_data = request.data.get('barcode', None)
print("Barcode data:")
print(barcode_data)
if barcode_data is None:
response['error'] = _('No barcode data provided')
else:
# Look for a barcode plugin that knows how to handle the data
for plugin_class in barcode_plugins:
# Instantiate the plugin with the provided plugin data
plugin = plugin_class(barcode_data)
if plugin.validate():
# Plugin should return a dict response
response = plugin.decode()
if type(response) is dict:
if 'success' not in response.keys() and 'error' not in response.keys():
response['success'] = _('Barcode successfully decoded')
else:
response = {
'error': _('Barcode plugin returned incorrect response')
}
response['plugin'] = plugin.plugin_name()
response['hash'] = plugin.hash()
break
if 'error' not in response and 'success' not in response:
response = {
'error': _('Unknown barcode format'),
}
# Include the original barcode data
response['barcode_data'] = barcode_data
return Response(response)

View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
import inspect
import importlib
import pkgutil
def iter_namespace(pkg):
return pkgutil.iter_modules(pkg.__path__, pkg.__name__ + ".")
def get_modules(pkg):
# Return all modules in a given package
return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(pkg)]
def get_classes(module):
# Return all classes in a given module
return inspect.getmembers(module, inspect.isclass)
def get_plugins(pkg, baseclass):
"""
Return a list of all modules under a given package.
- Modules must be a subclass of the provided 'baseclass'
- Modules must have a non-empty PLUGIN_NAME parameter
"""
plugins = []
modules = get_modules(pkg)
# Iterate through each module in the package
for mod in modules:
print("mod:", mod)
# Iterate through each class in the module
for item in get_classes(mod):
plugin = item[1]
if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME:
plugins.append(plugin)
return plugins

View File

@ -20,6 +20,7 @@ from stock.urls import stock_urls
from build.urls import build_urls
from order.urls import order_urls
from barcode.api import barcode_api_urls
from common.api import common_api_urls
from part.api import part_api_urls, bom_api_urls
from company.api import company_api_urls
@ -37,13 +38,15 @@ from .views import IndexView, SearchView, DatabaseStatsView
from .views import SettingsView, EditUserView, SetPasswordView
from .views import DynamicJsView
from .api import InfoView, BarcodePluginView, ActionPluginView
from .api import InfoView
from .api import ActionPluginView
from users.urls import user_urls
admin.site.site_header = "InvenTree Admin"
apipatterns = [
url(r'^barcode/', include(barcode_api_urls)),
url(r'^common/', include(common_api_urls)),
url(r'^part/', include(part_api_urls)),
url(r'^bom/', include(bom_api_urls)),
@ -56,7 +59,6 @@ apipatterns = [
url(r'^user/', include(user_urls)),
# Plugin endpoints
url(r'^barcode/', BarcodePluginView.as_view(), name='api-barcode-plugin'),
url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
# InvenTree information endpoint

132
InvenTree/barcode/api.py Normal file
View File

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
import hashlib
from django.conf.urls import url
from rest_framework.exceptions import ValidationError
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.views import APIView
import InvenTree.plugins as InvenTreePlugins
from stock.models import StockItem
from stock.serializers import StockItemSerializer
import barcode.plugins as BarcodePlugins
from barcode.barcode import BarcodePlugin, load_barcode_plugins, hash_barcode
class BarcodeScan(APIView):
"""
Endpoint for handling generic barcode scan requests.
Barcode data are decoded by the client application,
and sent to this endpoint (as a JSON object) for validation.
A barcode could follow the internal InvenTree barcode format,
or it could match to a third-party barcode format (e.g. Digikey).
When a barcode is sent to the server, the following parameters must be provided:
- barcode: The raw barcode data
plugins:
Third-party barcode formats may be supported using 'plugins'
(more information to follow)
hashing:
Barcode hashes are calculated using MD5
"""
permission_classes = [
permissions.IsAuthenticated,
]
def post(self, request, *args, **kwargs):
"""
Respond to a barcode POST request
"""
data = request.data
if 'barcode' not in data:
raise ValidationError({'barcode': 'Must provide barcode_data parameter'})
plugins = load_barcode_plugins()
barcode_data = data.get('barcode')
# Look for a barcode plugin which knows how to deal with this barcode
plugin = None
for plugin_class in plugins:
plugin_instance = plugin_class(barcode_data)
if plugin_instance.validate():
plugin = plugin_instance
break
match_found = False
response = {}
response['barcode_data'] = barcode_data
# A plugin has been found!
if plugin is not None:
# Try to associate with a stock item
item = plugin.getStockItem()
if item is not None:
response['stockitem'] = plugin.renderStockItem(item)
match_found = True
# Try to associate with a stock location
loc = plugin.getStockLocation()
if loc is not None:
response['stocklocation'] = plugin.renderStockLocation(loc)
match_found = True
# Try to associate with a part
part = plugin.getPart()
if part is not None:
response['part'] = plugin.renderPart(part)
match_found = True
response['hash'] = plugin.hash()
response['plugin'] = plugin.name
# No plugin is found!
# However, the hash of the barcode may still be associated with a StockItem!
else:
hash = hash_barcode(barcode_data)
response['hash'] = hash
response['plugin'] = None
# Try to look for a matching StockItem
try:
item = StockItem.objects.get(uid=hash)
serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True)
response['stockitem'] = serializer.data
match_found = True
except StockItem.DoesNotExist:
pass
if not match_found:
response['error'] = 'No match found for barcode data'
else:
response['success'] = 'Match found for barcode data'
return Response(response)
barcode_api_urls = [
# Catch-all performs barcode 'scan'
url(r'^.*$', BarcodeScan.as_view(), name='api-barcode-plugin'),
]

View File

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
import hashlib
import json
from InvenTree import plugins as InvenTreePlugins
from barcode import plugins as BarcodePlugins
from stock.serializers import StockItemSerializer, LocationSerializer
from part.serializers import PartSerializer
def hash_barcode(barcode_data):
"""
Calculate an MD5 hash of barcode data
"""
hash = hashlib.md5(str(barcode_data).encode())
return str(hash.hexdigest())
class BarcodePlugin:
"""
Base class for barcode handling.
Custom barcode plugins should extend this class as necessary.
"""
# Override the barcode plugin name for each sub-class
PLUGIN_NAME = ""
@property
def name(self):
return self.PLUGIN_NAME
def __init__(self, barcode_data):
self.data = barcode_data
def getStockItem(self):
"""
Attempt to retrieve a StockItem associated with this barcode.
Default implementation returns None
"""
return None
def renderStockItem(self, item):
"""
Render a stock item to JSON response
"""
serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True)
return serializer.data
def getStockLocation(self):
"""
Attempt to retrieve a StockLocation associated with this barcode.
Default implementation returns None
"""
return None
def renderStockLocation(self, loc):
"""
Render a stock location to a JSON response
"""
serializer = LocationSerializer(loc)
return serializer.data
def getPart(self):
"""
Attempt to retrieve a Part associated with this barcode.
Default implementation returns None
"""
return None
def renderPart(self, part):
"""
Render a part to JSON response
"""
serializer = PartSerializer(part)
return serializer.data
def hash(self):
"""
Calculate a hash for the barcode data.
This is supposed to uniquely identify the barcode contents,
at least within the bardcode sub-type.
The default implementation simply returns an MD5 hash of the barcode data,
encoded to a string.
This may be sufficient for most applications, but can obviously be overridden
by a subclass.
"""
return hash_barcode(self.data)
def validate(self):
"""
Default implementation returns False
"""
return False
def load_barcode_plugins():
"""
Function to load all barcode plugins
"""
print("Loading barcode plugins")
plugins = InvenTreePlugins.get_plugins(BarcodePlugins, BarcodePlugin)
if len(plugins) > 0:
print("Discovered {n} plugins:".format(n=len(plugins)))
for p in plugins:
print(" - {p}".format(p=p.PLUGIN_NAME))
return plugins

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""
DigiKey barcode decoding
"""

View File

@ -0,0 +1,108 @@
"""
The InvenTreeBarcodePlugin validates barcodes generated by InvenTree itself.
It can be used as a template for developing third-party barcode plugins.
The data format is very simple, and maps directly to database objects,
via the "id" parameter.
Parsing an InvenTree barcode simply involves validating that the
references model objects actually exist in the database.
"""
# -*- coding: utf-8 -*-
import json
from barcode.barcode import BarcodePlugin
from stock.models import StockItem, StockLocation
from part.models import Part
from rest_framework.exceptions import ValidationError
class InvenTreeBarcodePlugin(BarcodePlugin):
PLUGIN_NAME = "InvenTreeBarcode"
def validate(self):
"""
An "InvenTree" barcode must be a jsonnable-dict with the following tags:
{
'tool': 'InvenTree',
'version': <anything>
}
"""
# The data must either be dict or be able to dictified
if type(self.data) is dict:
pass
elif type(self.data) is str:
try:
self.data = json.loads(self.data)
except json.JSONDecodeError:
return False
else:
return False
for key in ['tool', 'version']:
if key not in self.data.keys():
return False
if not self.data['tool'] == 'InvenTree':
return False
return True
def getStockItem(self):
for k in self.data.keys():
if k.lower() == 'stockitem':
try:
pk = self.data[k]['id']
except (AttributeError, KeyError):
raise ValidationError({k: "id parameter not supplied"})
try:
item = StockItem.objects.get(pk=pk)
return item
except (ValueError, StockItem.DoesNotExist):
raise ValidationError({k, "Stock item does not exist"})
return None
def getStockLocation(self):
for k in self.data.keys():
if k.lower() == 'stocklocation':
try:
pk = self.data[k]['id']
except (AttributeError, KeyError):
raise ValidationError({k: "id parameter not supplied"})
try:
loc = StockLocation.objects.get(pk=pk)
return loc
except (ValueError, StockLocation.DoesNotExist):
raise ValidationError({k, "Stock location does not exist"})
return None
def getPart(self):
for k in self.data.keys():
if k.lower() == 'part':
try:
pk = self.data[k]['id']
except (AttributeError, KeyError):
raise ValidationError({k, 'id parameter not supplied'})
try:
part = Part.objects.get(pk=pk)
return part
except (ValueError, Part.DoesNotExist):
raise ValidationError({k, 'Part does not exist'})
return None

View File

@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
import hashlib
from stock.serializers import StockItemSerializer, LocationSerializer
from part.serializers import PartSerializer
import plugins.plugin as plugin
class BarcodePlugin(plugin.InvenTreePlugin):
"""
The BarcodePlugin class is the base class for any barcode plugin.
"""
def __init__(self, barcode_data):
plugin.InvenTreePlugin.__init__(self)
self.data = barcode_data
def hash(self):
"""
Calculate a hash for the barcode data.
This is supposed to uniquely identify the barcode contents,
at least within the bardcode sub-type.
The default implementation simply returns an MD5 hash of the barcode data,
encoded to a string.
This may be sufficient for most applications, but can obviously be overridden
by a subclass.
"""
hash = hashlib.md5(str(self.data).encode())
return str(hash.hexdigest())
def validate(self):
"""
Default implementation returns False
"""
return False
def decode(self):
"""
Decode the barcode, and craft a response
"""
return None
def render_part(self, part):
"""
Render a Part object to JSON
Use the existing serializer to do this.
"""
serializer = PartSerializer(part)
return serializer.data
def render_stock_location(self, loc):
"""
Render a StockLocation object to JSON
Use the existing serializer to do this.
"""
serializer = LocationSerializer(loc)
return serializer.data
def render_stock_item(self, item):
"""
Render a StockItem object to JSON.
Use the existing serializer to do this
"""
serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True)
return serializer.data

View File

@ -1,8 +0,0 @@
# -*- coding: utf-8 -*-
from . import barcode
class DigikeyBarcodePlugin(barcode.BarcodePlugin):
PLUGIN_NAME = "DigikeyBarcodePlugin"

View File

@ -1,94 +0,0 @@
"""
The InvenTreeBarcodePlugin validates barcodes generated by InvenTree itself.
It can be used as a template for developing third-party barcode plugins.
The data format is very simple, and maps directly to database objects,
via the "id" parameter.
Parsing an InvenTree barcode simply involves validating that the
references model objects actually exist in the database.
"""
# -*- coding: utf-8 -*-
import json
from . import barcode
from stock.models import StockItem, StockLocation
from part.models import Part
from django.utils.translation import ugettext as _
class InvenTreeBarcodePlugin(barcode.BarcodePlugin):
PLUGIN_NAME = "InvenTreeBarcodePlugin"
def validate(self):
"""
An "InvenTree" barcode must include the following tags:
{
'tool': 'InvenTree',
'version': <anything>
}
"""
# The data must either be dict or be able to dictified
if type(self.data) is dict:
pass
elif type(self.data) is str:
try:
self.data = json.loads(self.data)
except json.JSONDecodeError:
return False
else:
return False
for key in ['tool', 'version']:
if key not in self.data.keys():
return False
if not self.data['tool'] == 'InvenTree':
return False
return True
def decode(self):
response = {}
if 'part' in self.data.keys():
id = self.data['part'].get('id', None)
try:
part = Part.objects.get(id=id)
response['part'] = self.render_part(part)
except (ValueError, Part.DoesNotExist):
response['error'] = _('Part does not exist')
elif 'stocklocation' in self.data.keys():
id = self.data['stocklocation'].get('id', None)
try:
loc = StockLocation.objects.get(id=id)
response['stocklocation'] = self.render_stock_location(loc)
except (ValueError, StockLocation.DoesNotExist):
response['error'] = _('StockLocation does not exist')
elif 'stockitem' in self.data.keys():
id = self.data['stockitem'].get('id', None)
try:
item = StockItem.objects.get(id=id)
response['stockitem'] = self.render_stock_item(item)
except (ValueError, StockItem.DoesNotExist):
response['error'] = _('StockItem does not exist')
else:
response['error'] = _('No matching data')
return response

View File

@ -4,10 +4,6 @@ import inspect
import importlib
import pkgutil
# Barcode plugins
import plugins.barcode as barcode
from plugins.barcode.barcode import BarcodePlugin
# Action plugins
import plugins.action as action
from plugins.action.action import ActionPlugin
@ -51,23 +47,6 @@ def get_plugins(pkg, baseclass):
return plugins
def load_barcode_plugins():
"""
Return a list of all registered barcode plugins
"""
print("Loading barcode plugins")
plugins = get_plugins(barcode, BarcodePlugin)
if len(plugins) > 0:
print("Discovered {n} barcode plugins:".format(n=len(plugins)))
for bp in plugins:
print(" - {bp}".format(bp=bp.PLUGIN_NAME))
return plugins
def load_action_plugins():
"""