diff --git a/tailbone/api/batch/__init__.py b/tailbone/api/batch/__init__.py index ba15fd11..bdf58438 100644 --- a/tailbone/api/batch/__init__.py +++ b/tailbone/api/batch/__init__.py @@ -26,4 +26,4 @@ Tailbone Web API - Batches from __future__ import unicode_literals, absolute_import -from .core import BatchAPIMasterView +from .core import APIBatchView, APIBatchRowView, BatchAPIMasterView diff --git a/tailbone/api/batch/core.py b/tailbone/api/batch/core.py index 8e6af1e9..a21e0e86 100644 --- a/tailbone/api/batch/core.py +++ b/tailbone/api/batch/core.py @@ -28,26 +28,22 @@ from __future__ import unicode_literals, absolute_import import six +from rattail.time import localtime from rattail.util import load_object +from cornice import resource + from tailbone.api import APIMasterView -class BatchAPIMasterView(APIMasterView): +class APIBatchMixin(object): """ Base class for all API views which are meant to handle "batch" *and/or* "batch row" data. """ - supports_quick_entry = False - supports_toggle_complete = False - def __init__(self, request, **kwargs): - super(BatchAPIMasterView, self).__init__(request, **kwargs) - self.handler = self.get_handler() - - @classmethod - def get_batch_class(cls): - model_class = cls.get_model_class() + def get_batch_class(self): + model_class = self.get_model_class() if hasattr(model_class, '__batch_class__'): return model_class.__batch_class__ return model_class @@ -74,27 +70,75 @@ class BatchAPIMasterView(APIMasterView): default=self.default_handler_spec) return load_object(spec)(self.rattail_config) - def quick_entry(self): + +class APIBatchView(APIBatchMixin, APIMasterView): + """ + Base class for all API views which are meant to handle "batch" *and/or* + "batch row" data. + """ + supports_toggle_complete = False + + def __init__(self, request, **kwargs): + super(APIBatchView, self).__init__(request, **kwargs) + self.handler = self.get_handler() + + def normalize(self, batch): + + created = batch.created + created = localtime(self.rattail_config, created, from_utc=True) + created = self.pretty_datetime(created) + + executed = batch.executed + if executed: + executed = localtime(self.rattail_config, executed, from_utc=True) + executed = self.pretty_datetime(executed) + + return { + 'uuid': batch.uuid, + '_str': six.text_type(batch), + 'id': batch.id, + 'id_str': batch.id_str, + 'description': batch.description, + 'notes': batch.notes, + 'rowcount': batch.rowcount, + 'created': created, + 'created_by_uuid': batch.created_by.uuid, + 'created_by_display': six.text_type(batch.created_by), + 'complete': batch.complete, + 'executed': executed, + 'executed_by_uuid': batch.executed_by_uuid, + 'executed_by_display': six.text_type(batch.executed_by or ''), + } + + def create_object(self, data): """ - View for handling "quick entry" user input, for a batch. + Create a new object instance and populate it with the given data. + + Here we'll invoke the handler for actual batch creation, instead of + typical logic used for simple records. """ - data = self.request.json_body + kwargs = dict(data) + kwargs['user'] = self.request.user + batch = self.handler.make_batch(self.Session(), **kwargs) + return batch - uuid = data['batch_uuid'] - batch = self.Session.query(self.get_batch_class()).get(uuid) - if not batch: - raise self.notfound() + def update_object(self, batch, data): + """ + Logic for updating a main object record. - entry = data['quick_entry'] + Here we want to make sure we set "created by" to the current user, when + creating a new batch. + """ + # we're only concerned with *new* batches here + if not batch.uuid: - try: - row = self.handler.quick_entry(self.Session(), batch, entry) - except Exception as error: - return {'error': six.text_type(error)} + # assign creator; initialize row count + batch.created_by_uuid = self.request.user.uuid + if batch.rowcount is None: + batch.rowcount = 0 - result = self._get(obj=row) - result['ok'] = True - return result + # then go ahead with usual logic + return super(APIBatchView, self).update_object(batch, data) def mark_complete(self): """ @@ -130,21 +174,24 @@ class BatchAPIMasterView(APIMasterView): batch.complete = False return self._get(obj=batch) + @classmethod + def defaults(cls, config): + cls._batch_defaults(config) + @classmethod def _batch_defaults(cls, config): route_prefix = cls.get_route_prefix() + permission_prefix = cls.get_permission_prefix() collection_url_prefix = cls.get_collection_url_prefix() object_url_prefix = cls.get_object_url_prefix() - permission_prefix = cls.get_permission_prefix() - if cls.supports_quick_entry: - - # quick entry - config.add_route('{}.quick_entry'.format(route_prefix), '{}/quick-entry'.format(collection_url_prefix), - request_method=('OPTIONS', 'POST')) - config.add_view(cls, attr='quick_entry', route_name='{}.quick_entry'.format(route_prefix), - permission='{}.edit'.format(permission_prefix), - renderer='json') + # primary / typical API + resource.add_view(cls.collection_get, permission='{}.list'.format(permission_prefix)) + resource.add_view(cls.collection_post, permission='{}.create'.format(permission_prefix)) + resource.add_view(cls.get, permission='{}.view'.format(permission_prefix)) + batch_resource = resource.add_resource(cls, collection_path=collection_url_prefix, + path='{}/{{uuid}}'.format(object_url_prefix)) + config.add_cornice_resource(batch_resource) if cls.supports_toggle_complete: @@ -160,3 +207,84 @@ class BatchAPIMasterView(APIMasterView): permission='{}.edit'.format(permission_prefix), renderer='json') + +# TODO: deprecate / remove this +BatchAPIMasterView = APIBatchView + + +class APIBatchRowView(APIBatchMixin, APIMasterView): + """ + Base class for all API views which are meant to handle "batch rows" data. + """ + supports_quick_entry = False + + def __init__(self, request, **kwargs): + super(APIBatchRowView, self).__init__(request, **kwargs) + self.handler = self.get_handler() + + def normalize(self, row): + batch = row.batch + return { + 'uuid': row.uuid, + '_str': six.text_type(row), + '_parent_str': six.text_type(batch), + '_parent_uuid': batch.uuid, + 'batch_uuid': batch.uuid, + 'batch_id': batch.id, + 'batch_id_str': batch.id_str, + 'batch_description': batch.description, + 'sequence': row.sequence, + 'status_code': row.status_code, + 'status_display': row.STATUS.get(row.status_code, six.text_type(row.status_code)), + } + + def quick_entry(self): + """ + View for handling "quick entry" user input, for a batch. + """ + data = self.request.json_body + + uuid = data['batch_uuid'] + batch = self.Session.query(self.get_batch_class()).get(uuid) + if not batch: + raise self.notfound() + + entry = data['quick_entry'] + + try: + row = self.handler.quick_entry(self.Session(), batch, entry) + except Exception as error: + msg = six.text_type(error) + if not msg and isinstance(error, NotImplementedError): + msg = "Feature is not implemented" + return {'error': msg} + + result = self._get(obj=row) + result['ok'] = True + return result + + @classmethod + def defaults(cls, config): + cls._batch_row_defaults(config) + + @classmethod + def _batch_row_defaults(cls, config): + route_prefix = cls.get_route_prefix() + permission_prefix = cls.get_permission_prefix() + collection_url_prefix = cls.get_collection_url_prefix() + object_url_prefix = cls.get_object_url_prefix() + + resource.add_view(cls.collection_get, permission='{}.view'.format(permission_prefix)) + resource.add_view(cls.get, permission='{}.view'.format(permission_prefix)) + rows_resource = resource.add_resource(cls, collection_path=collection_url_prefix, + path='{}/{{uuid}}'.format(object_url_prefix)) + config.add_cornice_resource(rows_resource) + + if cls.supports_quick_entry: + + # quick entry + config.add_route('{}.quick_entry'.format(route_prefix), '{}/quick-entry'.format(collection_url_prefix), + request_method=('OPTIONS', 'POST')) + config.add_view(cls, attr='quick_entry', route_name='{}.quick_entry'.format(route_prefix), + permission='{}.edit'.format(permission_prefix), + renderer='json') diff --git a/tailbone/api/batch/labels.py b/tailbone/api/batch/labels.py index 05553c5a..02af03ba 100644 --- a/tailbone/api/batch/labels.py +++ b/tailbone/api/batch/labels.py @@ -29,119 +29,50 @@ from __future__ import unicode_literals, absolute_import import six from rattail.db import model -from rattail.time import localtime -from cornice import resource - -from tailbone.api.batch import BatchAPIMasterView +from tailbone.api.batch import APIBatchView, APIBatchRowView -class LabelBatchViews(BatchAPIMasterView): +class LabelBatchViews(APIBatchView): model_class = model.LabelBatch default_handler_spec = 'rattail.batch.labels:LabelBatchHandler' + route_prefix = 'labelbatchviews' permission_prefix = 'labels.batch' + collection_url_prefix = '/label-batches' object_url_prefix = '/label-batch' supports_toggle_complete = True - def pretty_datetime(self, dt): - if not dt: - return "" - return dt.strftime('%Y-%m-%d @ %I:%M %p') - - def normalize(self, batch): - - created = batch.created - created = localtime(self.rattail_config, created, from_utc=True) - created = self.pretty_datetime(created) - - executed = batch.executed - if executed: - executed = localtime(self.rattail_config, executed, from_utc=True) - executed = self.pretty_datetime(executed) - - return { - 'uuid': batch.uuid, - '_str': six.text_type(batch), - 'id': batch.id, - 'id_str': batch.id_str, - 'description': batch.description, - 'notes': batch.notes, - 'rowcount': batch.rowcount, - 'created': created, - 'created_by_uuid': batch.created_by.uuid, - 'created_by_display': six.text_type(batch.created_by), - 'complete': batch.complete, - 'executed': executed, - 'executed_by_uuid': batch.executed_by_uuid, - 'executed_by_display': six.text_type(batch.executed_by or ''), - } - def collection_get(self): return self._collection_get() def collection_post(self): return self._collection_post() - def update_object(self, batch, data): - - # assign some default values for new batch - if not batch.uuid: - batch.created_by_uuid = self.request.user.uuid - if batch.rowcount is None: - batch.rowcount = 0 - - return super(LabelBatchViews, self).update_object(batch, data) - def get(self): return self._get() - # @view(permission='labels.batch.edit') - # def post(self): - # return self._post() - @classmethod - def defaults(cls, config): - - # label batches - resource.add_view(cls.collection_get, permission='labels.batch.list') - resource.add_view(cls.collection_post, permission='labels.batch.create') - resource.add_view(cls.get, permission='labels.batch.view') - batch_resource = resource.add_resource(cls, collection_path='/label-batches', path='/label-batch/{uuid}') - config.add_cornice_resource(batch_resource) - - cls._batch_defaults(config) - - -class LabelBatchRowViews(BatchAPIMasterView): +class LabelBatchRowViews(APIBatchRowView): model_class = model.LabelBatchRow default_handler_spec = 'rattail.batch.labels:LabelBatchHandler' route_prefix = 'api.label_batch_rows' permission_prefix = 'labels.batch' collection_url_prefix = '/label-batch-rows' + object_url_prefix = '/label-batch-row' supports_quick_entry = True def normalize(self, row): batch = row.batch - return { - 'uuid': row.uuid, - '_str': six.text_type(row), - '_parent_str': six.text_type(batch), - '_parent_uuid': batch.uuid, - 'batch_uuid': batch.uuid, - 'batch_id': batch.id, - 'batch_id_str': batch.id_str, - 'batch_description': batch.description, - 'sequence': row.sequence, - 'item_id': row.item_id, - 'upc': six.text_type(row.upc), - 'upc_pretty': row.upc.pretty() if row.upc else None, - 'description': row.description, - 'full_description': row.product.full_description if row.product else row.description, - 'status_code': row.status_code, - 'status_display': row.STATUS.get(row.status_code, six.text_type(row.status_code)), - } + data = super(LabelBatchRowViews, self).normalize(row) + + data['item_id'] = row.item_id + data['upc'] = six.text_type(row.upc) + data['upc_pretty'] = row.upc.pretty() if row.upc else None + data['description'] = row.description + data['full_description'] = row.product.full_description if row.product else row.description + return data def collection_get(self): return self._collection_get() @@ -149,18 +80,6 @@ class LabelBatchRowViews(BatchAPIMasterView): def get(self): return self._get() - @classmethod - def defaults(cls, config): - permission_prefix = cls.get_permission_prefix() - - # label batch rows - resource.add_view(cls.collection_get, permission='{}.view'.format(permission_prefix)) - resource.add_view(cls.get, permission='{}.view'.format(permission_prefix)) - rows_resource = resource.add_resource(cls, collection_path='/label-batch-rows', path='/label-batch-row/{uuid}') - config.add_cornice_resource(rows_resource) - - cls._batch_defaults(config) - def includeme(config): LabelBatchViews.defaults(config) diff --git a/tailbone/api/batch/receiving.py b/tailbone/api/batch/receiving.py new file mode 100644 index 00000000..b50fa2c7 --- /dev/null +++ b/tailbone/api/batch/receiving.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2019 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Tailbone Web API - Receiving Batches +""" + +from __future__ import unicode_literals, absolute_import + +import six + +from rattail.db import model + +from tailbone.api.batch import APIBatchView, APIBatchRowView + + +class ReceivingBatchViews(APIBatchView): + + model_class = model.PurchaseBatch + default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' + route_prefix = 'receivingbatchviews' + permission_prefix = 'receiving' + collection_url_prefix = '/receiving-batches' + object_url_prefix = '/receiving-batch' + + def normalize(self, batch): + data = super(ReceivingBatchViews, self).normalize(batch) + + data['vendor_uuid'] = batch.vendor.uuid + data['vendor_display'] = six.text_type(batch.vendor) + + return data + + def create_object(self, data): + data = dict(data) + data['mode'] = self.enum.PURCHASE_BATCH_MODE_RECEIVING + batch = super(ReceivingBatchViews, self).create_object(data) + return batch + + def collection_get(self): + return self._collection_get() + + def collection_post(self): + return self._collection_post() + + def get(self): + return self._get() + + +class ReceivingBatchRowViews(APIBatchRowView): + + model_class = model.PurchaseBatchRow + default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' + route_prefix = 'receiving.rows' + permission_prefix = 'receiving' + collection_url_prefix = '/receiving-batch-rows' + object_url_prefix = '/receiving-batch-row' + supports_quick_entry = True + + def normalize(self, row): + batch = row.batch + data = super(ReceivingBatchRowViews, self).normalize(row) + + data['item_id'] = row.item_id + data['upc'] = six.text_type(row.upc) + data['upc_pretty'] = row.upc.pretty() if row.upc else None + data['description'] = row.description + data['full_description'] = row.product.full_description if row.product else row.description + return data + + def collection_get(self): + return self._collection_get() + + def get(self): + return self._get() + + +def includeme(config): + ReceivingBatchViews.defaults(config) + ReceivingBatchRowViews.defaults(config) diff --git a/tailbone/api/core.py b/tailbone/api/core.py index c8855161..f129ec82 100644 --- a/tailbone/api/core.py +++ b/tailbone/api/core.py @@ -63,3 +63,8 @@ class APIView(View): """ Base class for all API views. """ + + def pretty_datetime(self, dt): + if not dt: + return "" + return dt.strftime('%Y-%m-%d @ %I:%M %p') diff --git a/tailbone/api/master.py b/tailbone/api/master.py index cec37b77..78bc9262 100644 --- a/tailbone/api/master.py +++ b/tailbone/api/master.py @@ -27,6 +27,7 @@ Tailbone Web API - Master View from __future__ import unicode_literals, absolute_import import json +import six from rattail.config import parse_bool @@ -364,3 +365,55 @@ class APIMasterView(APIView): # that's all we can do here, subclass must override if more needed return obj + + ############################## + # autocomplete + ############################## + + def autocomplete(self): + term = self.request.params.get('term', '').strip() + term = self.prepare_autocomplete_term(term) + if not term: + return [] + + results = self.get_autocomplete_data(term) + return [{'label': self.autocomplete_display(x), + 'value': self.autocomplete_value(x)} + for x in results] + + @property + def autocomplete_fieldname(self): + raise NotImplementedError("You must define `autocomplete_fieldname` " + "attribute for API view class: {}".format( + self.__class__)) + + def autocomplete_display(self, obj): + return getattr(obj, self.autocomplete_fieldname) + + def autocomplete_value(self, obj): + return obj.uuid + + def get_autocomplete_data(self, term): + query = self.make_autocomplete_query(term) + return query.all() + + def make_autocomplete_query(self, term): + model_class = self.get_model_class() + query = self.Session.query(model_class) + query = self.filter_autocomplete_query(query) + + field = getattr(model_class, self.autocomplete_fieldname) + query = query.filter(field.ilike('%%%s%%' % term))\ + .order_by(field) + + return query + + def filter_autocomplete_query(self, query): + return query + + def prepare_autocomplete_term(self, term): + """ + If necessary, massage the incoming search term for use with the + autocomplete query. + """ + return term diff --git a/tailbone/api/vendors.py b/tailbone/api/vendors.py new file mode 100644 index 00000000..533d7094 --- /dev/null +++ b/tailbone/api/vendors.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2019 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Tailbone Web API - Vendor Views +""" + +from __future__ import unicode_literals, absolute_import + +import six + +from rattail.db import model + +from cornice import Service +from cornice.resource import resource, view + +from tailbone.api import APIMasterView + + +@resource(collection_path='/vendors', path='/vendor/{uuid}') +class VendorView(APIMasterView): + + model_class = model.Vendor + autocomplete_fieldname = 'name' + + def normalize(self, vendor): + return { + 'uuid': vendor.uuid, + '_str': six.text_type(vendor), + 'id': vendor.id, + 'name': vendor.name, + } + + @view(permission='vendors.list') + def collection_get(self): + return self._collection_get() + + @view(permission='vendors.create') + def collection_post(self): + return self._collection_post() + + @view(permission='vendors.view') + def get(self): + return self._get() + + @view(permission='vendors.edit') + def post(self): + return self._post() + + +def includeme(config): + config.scan(__name__) + + autocomplete = Service(name='vendors.autocomplete', path='/vendors/autocomplete') + autocomplete.add_view('GET', 'autocomplete', klass=VendorView) + config.add_cornice_service(autocomplete)