Add new "master" API view class; refactor products and batches to use it
This commit is contained in:
parent
df00dd600a
commit
113c0af49d
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2018 Lance Edgar
|
# Copyright © 2010-2020 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -28,6 +28,7 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from .core import APIView, api
|
from .core import APIView, api
|
||||||
from .master import APIMasterView, SortColumn
|
from .master import APIMasterView, SortColumn
|
||||||
|
from .master2 import APIMasterView2
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2019 Lance Edgar
|
# Copyright © 2010-2020 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -33,7 +33,7 @@ from rattail.util import load_object
|
||||||
|
|
||||||
from cornice import resource
|
from cornice import resource
|
||||||
|
|
||||||
from tailbone.api import APIMasterView
|
from tailbone.api import APIMasterView2 as APIMasterView
|
||||||
|
|
||||||
|
|
||||||
class APIBatchMixin(object):
|
class APIBatchMixin(object):
|
||||||
|
@ -197,6 +197,7 @@ class APIBatchView(APIBatchMixin, APIMasterView):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
|
cls._defaults(config)
|
||||||
cls._batch_defaults(config)
|
cls._batch_defaults(config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -206,14 +207,6 @@ class APIBatchView(APIBatchMixin, APIMasterView):
|
||||||
collection_url_prefix = cls.get_collection_url_prefix()
|
collection_url_prefix = cls.get_collection_url_prefix()
|
||||||
object_url_prefix = cls.get_object_url_prefix()
|
object_url_prefix = cls.get_object_url_prefix()
|
||||||
|
|
||||||
# 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:
|
if cls.supports_toggle_complete:
|
||||||
|
|
||||||
# mark complete
|
# mark complete
|
||||||
|
@ -299,6 +292,7 @@ class APIBatchRowView(APIBatchMixin, APIMasterView):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
|
cls._defaults(config)
|
||||||
cls._batch_row_defaults(config)
|
cls._batch_row_defaults(config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -308,14 +302,6 @@ class APIBatchRowView(APIBatchMixin, APIMasterView):
|
||||||
collection_url_prefix = cls.get_collection_url_prefix()
|
collection_url_prefix = cls.get_collection_url_prefix()
|
||||||
object_url_prefix = cls.get_object_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))
|
|
||||||
if cls.editable:
|
|
||||||
resource.add_view(cls.post, permission='{}.edit'.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:
|
if cls.supports_quick_entry:
|
||||||
|
|
||||||
# quick entry
|
# quick entry
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2019 Lance Edgar
|
# Copyright © 2010-2020 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -43,15 +43,6 @@ class LabelBatchViews(APIBatchView):
|
||||||
object_url_prefix = '/label-batch'
|
object_url_prefix = '/label-batch'
|
||||||
supports_toggle_complete = True
|
supports_toggle_complete = True
|
||||||
|
|
||||||
def collection_get(self):
|
|
||||||
return self._collection_get()
|
|
||||||
|
|
||||||
def collection_post(self):
|
|
||||||
return self._collection_post()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return self._get()
|
|
||||||
|
|
||||||
|
|
||||||
class LabelBatchRowViews(APIBatchRowView):
|
class LabelBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
|
@ -74,12 +65,6 @@ class LabelBatchRowViews(APIBatchRowView):
|
||||||
data['full_description'] = row.product.full_description if row.product else row.description
|
data['full_description'] = row.product.full_description if row.product else row.description
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def collection_get(self):
|
|
||||||
return self._collection_get()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return self._get()
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
LabelBatchViews.defaults(config)
|
LabelBatchViews.defaults(config)
|
||||||
|
|
|
@ -84,15 +84,6 @@ class OrderingBatchViews(APIBatchView):
|
||||||
batch = super(OrderingBatchViews, self).create_object(data)
|
batch = super(OrderingBatchViews, self).create_object(data)
|
||||||
return batch
|
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 OrderingBatchRowViews(APIBatchRowView):
|
class OrderingBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
|
@ -103,6 +94,7 @@ class OrderingBatchRowViews(APIBatchRowView):
|
||||||
collection_url_prefix = '/ordering-batch-rows'
|
collection_url_prefix = '/ordering-batch-rows'
|
||||||
object_url_prefix = '/ordering-batch-row'
|
object_url_prefix = '/ordering-batch-row'
|
||||||
supports_quick_entry = True
|
supports_quick_entry = True
|
||||||
|
editable = True
|
||||||
|
|
||||||
def normalize(self, row):
|
def normalize(self, row):
|
||||||
batch = row.batch
|
batch = row.batch
|
||||||
|
@ -138,15 +130,6 @@ class OrderingBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def collection_get(self):
|
|
||||||
return self._collection_get()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return self._get()
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
return self._post()
|
|
||||||
|
|
||||||
def update_object(self, row, data):
|
def update_object(self, row, data):
|
||||||
"""
|
"""
|
||||||
Overrides the default logic as follows:
|
Overrides the default logic as follows:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2019 Lance Edgar
|
# Copyright © 2010-2020 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -86,15 +86,6 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
batch = super(ReceivingBatchViews, self).create_object(data)
|
batch = super(ReceivingBatchViews, self).create_object(data)
|
||||||
return batch
|
return batch
|
||||||
|
|
||||||
def collection_get(self):
|
|
||||||
return self._collection_get()
|
|
||||||
|
|
||||||
def collection_post(self):
|
|
||||||
return self._collection_post()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return self._get()
|
|
||||||
|
|
||||||
def mark_receiving_complete(self):
|
def mark_receiving_complete(self):
|
||||||
"""
|
"""
|
||||||
Mark the given batch as "receiving complete".
|
Mark the given batch as "receiving complete".
|
||||||
|
@ -148,6 +139,7 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
|
cls._defaults(config)
|
||||||
cls._batch_defaults(config)
|
cls._batch_defaults(config)
|
||||||
cls._receiving_batch_defaults(config)
|
cls._receiving_batch_defaults(config)
|
||||||
|
|
||||||
|
@ -340,12 +332,6 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def collection_get(self):
|
|
||||||
return self._collection_get()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return self._get()
|
|
||||||
|
|
||||||
def receive(self):
|
def receive(self):
|
||||||
"""
|
"""
|
||||||
View which handles "receiving" against a particular batch row.
|
View which handles "receiving" against a particular batch row.
|
||||||
|
@ -375,6 +361,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
|
cls._defaults(config)
|
||||||
cls._batch_row_defaults(config)
|
cls._batch_row_defaults(config)
|
||||||
cls._receiving_batch_row_defaults(config)
|
cls._receiving_batch_row_defaults(config)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2019 Lance Edgar
|
# Copyright © 2010-2020 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -81,7 +81,7 @@ class APIMasterView(APIView):
|
||||||
Returns a prefix which (by default) applies to all permissions
|
Returns a prefix which (by default) applies to all permissions
|
||||||
leveraged by this view class.
|
leveraged by this view class.
|
||||||
"""
|
"""
|
||||||
prefix = getattr(cls, 'permission_prefix')
|
prefix = getattr(cls, 'permission_prefix', None)
|
||||||
if prefix:
|
if prefix:
|
||||||
return prefix
|
return prefix
|
||||||
return cls.get_route_prefix()
|
return cls.get_route_prefix()
|
||||||
|
@ -371,6 +371,10 @@ class APIMasterView(APIView):
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
def autocomplete(self):
|
def autocomplete(self):
|
||||||
|
"""
|
||||||
|
View which accepts a single ``term`` param, and returns a list of
|
||||||
|
autocomplete results to match.
|
||||||
|
"""
|
||||||
term = self.request.params.get('term', '').strip()
|
term = self.request.params.get('term', '').strip()
|
||||||
term = self.prepare_autocomplete_term(term)
|
term = self.prepare_autocomplete_term(term)
|
||||||
if not term:
|
if not term:
|
||||||
|
|
141
tailbone/api/master2.py
Normal file
141
tailbone/api/master2.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2020 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Tailbone Web API - Master View (v2)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
from cornice import resource, Service
|
||||||
|
|
||||||
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
|
|
||||||
|
class APIMasterView2(APIMasterView):
|
||||||
|
"""
|
||||||
|
Base class for data model REST API views.
|
||||||
|
"""
|
||||||
|
listable = True
|
||||||
|
creatable = True
|
||||||
|
viewable = True
|
||||||
|
editable = True
|
||||||
|
deletable = True
|
||||||
|
supports_autocomplete = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def establish_method(cls, method_name):
|
||||||
|
"""
|
||||||
|
Establish the given HTTP method for this Cornice Resource.
|
||||||
|
|
||||||
|
Cornice will auto-register any class methods for a resource, if they
|
||||||
|
are named according to what it expects (i.e. 'get', 'collection_get'
|
||||||
|
etc.). Tailbone API tries to make things automagical for the sake of
|
||||||
|
e.g. Poser logic, but in this case if we predefine all of these methods
|
||||||
|
and then some subclass view wants to *not* allow one, it's not clear
|
||||||
|
how to "undefine" it per se. Or at least, the more straightforward
|
||||||
|
thing (I think) is to not define such a method in the first place, if
|
||||||
|
it was not wanted.
|
||||||
|
|
||||||
|
Enter ``establish_method()``, which is what finally "defines" each
|
||||||
|
resource method according to what the subclass has declared via its
|
||||||
|
various attributes (:attr:`creatable`, :attr:`deletable` etc.).
|
||||||
|
|
||||||
|
Note that you will not likely have any need to use this
|
||||||
|
``establish_method()`` yourself! But we describe its purpose here, for
|
||||||
|
clarity.
|
||||||
|
"""
|
||||||
|
def method(self):
|
||||||
|
internal_method = getattr(self, '_{}'.format(method_name))
|
||||||
|
return internal_method()
|
||||||
|
|
||||||
|
setattr(cls, method_name, method)
|
||||||
|
|
||||||
|
def _delete(self):
|
||||||
|
"""
|
||||||
|
View to handle DELETE action for an existing record/object.
|
||||||
|
"""
|
||||||
|
obj = self.get_object()
|
||||||
|
self.delete_object(obj)
|
||||||
|
|
||||||
|
def delete_object(self, obj):
|
||||||
|
"""
|
||||||
|
Delete the object, or mark it as deleted, or whatever you need to do.
|
||||||
|
"""
|
||||||
|
# flush immediately to force any pending integrity errors etc.
|
||||||
|
self.Session.delete(obj)
|
||||||
|
self.Session.flush()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
cls._defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _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()
|
||||||
|
|
||||||
|
# first, the primary resource API
|
||||||
|
|
||||||
|
# list/search
|
||||||
|
if cls.listable:
|
||||||
|
cls.establish_method('collection_get')
|
||||||
|
resource.add_view(cls.collection_get, permission='{}.list'.format(permission_prefix))
|
||||||
|
|
||||||
|
# create
|
||||||
|
if cls.creatable:
|
||||||
|
cls.establish_method('collection_post')
|
||||||
|
resource.add_view(cls.collection_post, permission='{}.create'.format(permission_prefix))
|
||||||
|
|
||||||
|
# view
|
||||||
|
if cls.viewable:
|
||||||
|
cls.establish_method('get')
|
||||||
|
resource.add_view(cls.get, permission='{}.view'.format(permission_prefix))
|
||||||
|
|
||||||
|
# edit
|
||||||
|
if cls.editable:
|
||||||
|
cls.establish_method('post')
|
||||||
|
resource.add_view(cls.post, permission='{}.edit'.format(permission_prefix))
|
||||||
|
|
||||||
|
# delete
|
||||||
|
if cls.deletable:
|
||||||
|
cls.establish_method('delete')
|
||||||
|
resource.add_view(cls.delete, permission='{}.delete'.format(permission_prefix))
|
||||||
|
|
||||||
|
# register primary resource API via cornice
|
||||||
|
object_resource = resource.add_resource(
|
||||||
|
cls,
|
||||||
|
collection_path=collection_url_prefix,
|
||||||
|
# TODO: probably should allow for other (composite?) key fields
|
||||||
|
path='{}/{{uuid}}'.format(object_url_prefix))
|
||||||
|
config.add_cornice_resource(object_resource)
|
||||||
|
|
||||||
|
# now for some more "custom" things, which are still somewhat generic
|
||||||
|
|
||||||
|
# autocomplete
|
||||||
|
if cls.supports_autocomplete:
|
||||||
|
autocomplete = Service(name='{}.autocomplete'.format(route_prefix),
|
||||||
|
path='{}/autocomplete'.format(collection_url_prefix))
|
||||||
|
autocomplete.add_view('GET', 'autocomplete', klass=cls)
|
||||||
|
config.add_cornice_service(autocomplete)
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2019 Lance Edgar
|
# Copyright © 2010-2020 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -27,18 +27,22 @@ Tailbone Web API - Product Views
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from cornice.resource import resource, view
|
from tailbone.api import APIMasterView2 as APIMasterView
|
||||||
|
|
||||||
from tailbone.api import APIMasterView
|
|
||||||
|
|
||||||
|
|
||||||
@resource(collection_path='/products', path='/product/{uuid}')
|
|
||||||
class ProductView(APIMasterView):
|
class ProductView(APIMasterView):
|
||||||
|
"""
|
||||||
|
API views for Product data
|
||||||
|
"""
|
||||||
model_class = model.Product
|
model_class = model.Product
|
||||||
|
collection_url_prefix = '/products'
|
||||||
|
object_url_prefix = '/product'
|
||||||
|
supports_autocomplete = True
|
||||||
|
|
||||||
def normalize(self, product):
|
def normalize(self, product):
|
||||||
cost = product.cost
|
cost = product.cost
|
||||||
|
@ -55,22 +59,24 @@ class ProductView(APIMasterView):
|
||||||
'default_unit_cost_display': "${:0.2f}".format(cost.unit_cost) if cost and cost.unit_cost is not None else None,
|
'default_unit_cost_display': "${:0.2f}".format(cost.unit_cost) if cost and cost.unit_cost is not None else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@view(permission='products.list')
|
def make_autocomplete_query(self, term):
|
||||||
def collection_get(self):
|
query = self.Session.query(model.Product)\
|
||||||
return self._collection_get()
|
.outerjoin(model.Brand)\
|
||||||
|
.filter(sa.or_(
|
||||||
|
model.Brand.name.ilike('%{}%'.format(term)),
|
||||||
|
model.Product.description.ilike('%{}%'.format(term))))
|
||||||
|
|
||||||
@view(permission='products.create')
|
if not self.request.has_perm('products.view_deleted'):
|
||||||
def collection_post(self):
|
query = query.filter(model.Product.deleted == False)
|
||||||
return self._collection_post()
|
|
||||||
|
|
||||||
@view(permission='products.view')
|
query = query.order_by(model.Brand.name,
|
||||||
def get(self):
|
model.Product.description)\
|
||||||
return self._get()
|
.options(orm.joinedload(model.Product.brand))
|
||||||
|
return query
|
||||||
|
|
||||||
@view(permission='products.edit')
|
def autocomplete_display(self, product):
|
||||||
def post(self):
|
return product.full_description
|
||||||
return self._post()
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
config.scan(__name__)
|
ProductView.defaults(config)
|
||||||
|
|
Loading…
Reference in a new issue