From 0068cd9825223e0676d82d7b22bf014d7a9a26da Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 11 Jun 2020 11:09:07 +1000 Subject: [PATCH] Refactor barcode endoint - Moved code into 'barcode' directory --- InvenTree/InvenTree/api.py | 66 --------- InvenTree/InvenTree/plugins.py | 44 ++++++ InvenTree/InvenTree/urls.py | 6 +- InvenTree/{plugins => }/barcode/__init__.py | 0 InvenTree/barcode/api.py | 132 ++++++++++++++++++ InvenTree/barcode/barcode.py | 126 +++++++++++++++++ InvenTree/barcode/plugins/digikey_barcode.py | 5 + .../barcode/plugins/inventree_barcode.py | 108 ++++++++++++++ InvenTree/plugins/barcode/barcode.py | 79 ----------- InvenTree/plugins/barcode/digikey.py | 8 -- InvenTree/plugins/barcode/inventree.py | 94 ------------- InvenTree/plugins/plugins.py | 21 --- 12 files changed, 419 insertions(+), 270 deletions(-) create mode 100644 InvenTree/InvenTree/plugins.py rename InvenTree/{plugins => }/barcode/__init__.py (100%) create mode 100644 InvenTree/barcode/api.py create mode 100644 InvenTree/barcode/barcode.py create mode 100644 InvenTree/barcode/plugins/digikey_barcode.py create mode 100644 InvenTree/barcode/plugins/inventree_barcode.py delete mode 100644 InvenTree/plugins/barcode/barcode.py delete mode 100644 InvenTree/plugins/barcode/digikey.py delete mode 100644 InvenTree/plugins/barcode/inventree.py diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index eb87b8f77a..44e7ec383f 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -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) diff --git a/InvenTree/InvenTree/plugins.py b/InvenTree/InvenTree/plugins.py new file mode 100644 index 0000000000..d0acb3fd8f --- /dev/null +++ b/InvenTree/InvenTree/plugins.py @@ -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 diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 711df33798..92ea257ce2 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -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 diff --git a/InvenTree/plugins/barcode/__init__.py b/InvenTree/barcode/__init__.py similarity index 100% rename from InvenTree/plugins/barcode/__init__.py rename to InvenTree/barcode/__init__.py diff --git a/InvenTree/barcode/api.py b/InvenTree/barcode/api.py new file mode 100644 index 0000000000..bc89b395dd --- /dev/null +++ b/InvenTree/barcode/api.py @@ -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'), +] \ No newline at end of file diff --git a/InvenTree/barcode/barcode.py b/InvenTree/barcode/barcode.py new file mode 100644 index 0000000000..77fc64c4c6 --- /dev/null +++ b/InvenTree/barcode/barcode.py @@ -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 \ No newline at end of file diff --git a/InvenTree/barcode/plugins/digikey_barcode.py b/InvenTree/barcode/plugins/digikey_barcode.py new file mode 100644 index 0000000000..3a65ff8b23 --- /dev/null +++ b/InvenTree/barcode/plugins/digikey_barcode.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +""" +DigiKey barcode decoding +""" diff --git a/InvenTree/barcode/plugins/inventree_barcode.py b/InvenTree/barcode/plugins/inventree_barcode.py new file mode 100644 index 0000000000..821cdc9c88 --- /dev/null +++ b/InvenTree/barcode/plugins/inventree_barcode.py @@ -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': + } + + """ + + # 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 diff --git a/InvenTree/plugins/barcode/barcode.py b/InvenTree/plugins/barcode/barcode.py deleted file mode 100644 index 94a6c34df4..0000000000 --- a/InvenTree/plugins/barcode/barcode.py +++ /dev/null @@ -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 diff --git a/InvenTree/plugins/barcode/digikey.py b/InvenTree/plugins/barcode/digikey.py deleted file mode 100644 index 2542fe964a..0000000000 --- a/InvenTree/plugins/barcode/digikey.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- - -from . import barcode - - -class DigikeyBarcodePlugin(barcode.BarcodePlugin): - - PLUGIN_NAME = "DigikeyBarcodePlugin" diff --git a/InvenTree/plugins/barcode/inventree.py b/InvenTree/plugins/barcode/inventree.py deleted file mode 100644 index 93b86d42b7..0000000000 --- a/InvenTree/plugins/barcode/inventree.py +++ /dev/null @@ -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': - } - - """ - - # 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 diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index f913c1f295..0a1c78f0b8 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -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(): """