Consolidate master API view logic
also let all API views use new config defaults convention
This commit is contained in:
parent
f2c73acd3b
commit
bc51a868ce
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2020 Lance Edgar
|
# Copyright © 2010-2022 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
|
||||||
|
# TODO: remove this
|
||||||
from .master2 import APIMasterView2
|
from .master2 import APIMasterView2
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -219,5 +219,12 @@ class AuthenticationView(APIView):
|
||||||
config.add_cornice_service(change_password)
|
config.add_cornice_service(change_password)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
AuthenticationView = kwargs.get('AuthenticationView', base['AuthenticationView'])
|
||||||
AuthenticationView.defaults(config)
|
AuthenticationView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -30,12 +30,9 @@ import logging
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from rattail.time import localtime
|
from cornice import Service
|
||||||
from rattail.util import load_object
|
|
||||||
|
|
||||||
from cornice import resource, Service
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
from tailbone.api import APIMasterView2 as APIMasterView
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -70,10 +67,11 @@ class APIBatchMixin(object):
|
||||||
table name, although technically it is whatever value returns from the
|
table name, although technically it is whatever value returns from the
|
||||||
``batch_key`` attribute of the main batch model class.
|
``batch_key`` attribute of the main batch model class.
|
||||||
"""
|
"""
|
||||||
|
app = self.get_rattail_app()
|
||||||
key = self.get_batch_class().batch_key
|
key = self.get_batch_class().batch_key
|
||||||
spec = self.rattail_config.get('rattail.batch', '{}.handler'.format(key),
|
spec = self.rattail_config.get('rattail.batch', '{}.handler'.format(key),
|
||||||
default=self.default_handler_spec)
|
default=self.default_handler_spec)
|
||||||
return load_object(spec)(self.rattail_config)
|
return app.load_object(spec)(self.rattail_config)
|
||||||
|
|
||||||
|
|
||||||
class APIBatchView(APIBatchMixin, APIMasterView):
|
class APIBatchView(APIBatchMixin, APIMasterView):
|
||||||
|
@ -89,12 +87,12 @@ class APIBatchView(APIBatchMixin, APIMasterView):
|
||||||
self.handler = self.get_handler()
|
self.handler = self.get_handler()
|
||||||
|
|
||||||
def normalize(self, batch):
|
def normalize(self, batch):
|
||||||
|
app = self.get_rattail_app()
|
||||||
created = localtime(self.rattail_config, batch.created, from_utc=True)
|
created = app.localtime(batch.created, from_utc=True)
|
||||||
|
|
||||||
executed = None
|
executed = None
|
||||||
if batch.executed:
|
if batch.executed:
|
||||||
executed = localtime(self.rattail_config, batch.executed, from_utc=True)
|
executed = app.localtime(batch.executed, from_utc=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'uuid': batch.uuid,
|
'uuid': batch.uuid,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -178,6 +178,15 @@ class InventoryBatchRowViews(APIBatchRowView):
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
InventoryBatchViews = kwargs.get('InventoryBatchViews', base['InventoryBatchViews'])
|
||||||
InventoryBatchViews.defaults(config)
|
InventoryBatchViews.defaults(config)
|
||||||
|
|
||||||
|
InventoryBatchRowViews = kwargs.get('InventoryBatchRowViews', base['InventoryBatchRowViews'])
|
||||||
InventoryBatchRowViews.defaults(config)
|
InventoryBatchRowViews.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -68,6 +68,15 @@ class LabelBatchRowViews(APIBatchRowView):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
LabelBatchViews = kwargs.get('LabelBatchViews', base['LabelBatchViews'])
|
||||||
LabelBatchViews.defaults(config)
|
LabelBatchViews.defaults(config)
|
||||||
|
|
||||||
|
LabelBatchRowViews = kwargs.get('LabelBatchRowViews', base['LabelBatchRowViews'])
|
||||||
LabelBatchRowViews.defaults(config)
|
LabelBatchRowViews.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -31,7 +31,6 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from rattail.core import Object
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
from rattail.util import pretty_quantity
|
from rattail.util import pretty_quantity
|
||||||
|
|
||||||
|
@ -274,6 +273,15 @@ class OrderingBatchRowViews(APIBatchRowView):
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
OrderingBatchViews = kwargs.get('OrderingBatchViews', base['OrderingBatchViews'])
|
||||||
OrderingBatchViews.defaults(config)
|
OrderingBatchViews.defaults(config)
|
||||||
|
|
||||||
|
OrderingBatchRowViews = kwargs.get('OrderingBatchRowViews', base['OrderingBatchRowViews'])
|
||||||
OrderingBatchRowViews.defaults(config)
|
OrderingBatchRowViews.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -32,7 +32,6 @@ import six
|
||||||
import humanize
|
import humanize
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
from rattail.time import make_utc
|
|
||||||
from rattail.util import pretty_quantity
|
from rattail.util import pretty_quantity
|
||||||
|
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
|
@ -392,7 +391,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
data['received_alert'] = None
|
data['received_alert'] = None
|
||||||
if self.handler.get_units_confirmed(row):
|
if self.handler.get_units_confirmed(row):
|
||||||
msg = "You have already received some of this product; last update was {}.".format(
|
msg = "You have already received some of this product; last update was {}.".format(
|
||||||
humanize.naturaltime(make_utc() - row.modified))
|
humanize.naturaltime(app.make_utc() - row.modified))
|
||||||
data['received_alert'] = msg
|
data['received_alert'] = msg
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -444,6 +443,15 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
renderer='json')
|
renderer='json')
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
ReceivingBatchViews = kwargs.get('ReceivingBatchViews', base['ReceivingBatchViews'])
|
||||||
ReceivingBatchViews.defaults(config)
|
ReceivingBatchViews.defaults(config)
|
||||||
|
|
||||||
|
ReceivingBatchRowViews = kwargs.get('ReceivingBatchRowViews', base['ReceivingBatchRowViews'])
|
||||||
ReceivingBatchRowViews.defaults(config)
|
ReceivingBatchRowViews.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -129,5 +129,12 @@ class CommonView(APIView):
|
||||||
config.add_cornice_service(feedback)
|
config.add_cornice_service(feedback)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
CommonView = kwargs.get('CommonView', base['CommonView'])
|
||||||
CommonView.defaults(config)
|
CommonView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2020 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -26,8 +26,6 @@ Tailbone Web API - Core Views
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from rattail.util import load_object
|
|
||||||
|
|
||||||
from tailbone.views import View
|
from tailbone.views import View
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,6 +100,8 @@ class APIView(View):
|
||||||
info.pop('short_name', None)
|
info.pop('short_name', None)
|
||||||
return info
|
return info
|
||||||
"""
|
"""
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
|
||||||
# basic / default info
|
# basic / default info
|
||||||
is_admin = user.is_admin()
|
is_admin = user.is_admin()
|
||||||
employee = user.employee
|
employee = user.employee
|
||||||
|
@ -119,7 +119,7 @@ class APIView(View):
|
||||||
extra = self.rattail_config.get('tailbone.api', 'extra_user_info',
|
extra = self.rattail_config.get('tailbone.api', 'extra_user_info',
|
||||||
usedb=False)
|
usedb=False)
|
||||||
if extra:
|
if extra:
|
||||||
extra = load_object(extra)
|
extra = app.load_object(extra)
|
||||||
info = extra(self.request, user, **info)
|
info = extra(self.request, user, **info)
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
|
@ -30,7 +30,7 @@ import six
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView2 as APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
|
|
||||||
class CustomerView(APIMasterView):
|
class CustomerView(APIMasterView):
|
||||||
|
@ -53,5 +53,12 @@ class CustomerView(APIMasterView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
CustomerView = kwargs.get('CustomerView', base['CustomerView'])
|
||||||
CustomerView.defaults(config)
|
CustomerView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -27,10 +27,11 @@ Tailbone Web API - Master View
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import six
|
|
||||||
|
|
||||||
from rattail.config import parse_bool
|
from rattail.config import parse_bool
|
||||||
|
|
||||||
|
from cornice import resource, Service
|
||||||
|
|
||||||
from tailbone.api import APIView, api
|
from tailbone.api import APIView, api
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
|
|
||||||
|
@ -46,6 +47,14 @@ class APIMasterView(APIView):
|
||||||
"""
|
"""
|
||||||
Base class for data model REST API views.
|
Base class for data model REST API views.
|
||||||
"""
|
"""
|
||||||
|
listable = True
|
||||||
|
creatable = True
|
||||||
|
viewable = True
|
||||||
|
editable = True
|
||||||
|
deletable = True
|
||||||
|
supports_autocomplete = False
|
||||||
|
supports_download = False
|
||||||
|
supports_rawbytes = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Session(self):
|
def Session(self):
|
||||||
|
@ -120,6 +129,34 @@ class APIMasterView(APIView):
|
||||||
return cls.collection_key
|
return cls.collection_key
|
||||||
return '{}s'.format(cls.get_object_key())
|
return '{}s'.format(cls.get_object_key())
|
||||||
|
|
||||||
|
@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 make_filter_spec(self):
|
def make_filter_spec(self):
|
||||||
if not self.request.GET.has_key('filters'):
|
if not self.request.GET.has_key('filters'):
|
||||||
return []
|
return []
|
||||||
|
@ -371,6 +408,67 @@ class APIMasterView(APIView):
|
||||||
# that's all we can do here, subclass must override if more needed
|
# that's all we can do here, subclass must override if more needed
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# delete
|
||||||
|
##############################
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# download
|
||||||
|
##############################
|
||||||
|
|
||||||
|
def download(self):
|
||||||
|
"""
|
||||||
|
GET view allowing for download of a single file, which is attached to a
|
||||||
|
given record.
|
||||||
|
"""
|
||||||
|
obj = self.get_object()
|
||||||
|
|
||||||
|
filename = self.request.GET.get('filename', None)
|
||||||
|
if not filename:
|
||||||
|
raise self.notfound()
|
||||||
|
path = self.download_path(obj, filename)
|
||||||
|
|
||||||
|
response = self.file_response(path)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def download_path(self, obj, filename):
|
||||||
|
"""
|
||||||
|
Should return absolute path on disk, for the given object and filename.
|
||||||
|
Result will be used to return a file response to client.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def rawbytes(self):
|
||||||
|
"""
|
||||||
|
GET view allowing for direct access to the raw bytes of a file, which
|
||||||
|
is attached to a given record. Basically the same as 'download' except
|
||||||
|
this does not come as an attachment.
|
||||||
|
"""
|
||||||
|
obj = self.get_object()
|
||||||
|
|
||||||
|
filename = self.request.GET.get('filename', None)
|
||||||
|
if not filename:
|
||||||
|
raise self.notfound()
|
||||||
|
path = self.download_path(obj, filename)
|
||||||
|
|
||||||
|
response = self.file_response(path, attachment=False)
|
||||||
|
return response
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# autocomplete
|
# autocomplete
|
||||||
##############################
|
##############################
|
||||||
|
@ -426,3 +524,81 @@ class APIMasterView(APIView):
|
||||||
autocomplete query.
|
autocomplete query.
|
||||||
"""
|
"""
|
||||||
return term
|
return term
|
||||||
|
|
||||||
|
@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')
|
||||||
|
if hasattr(cls, 'permission_to_create'):
|
||||||
|
permission = cls.permission_to_create
|
||||||
|
else:
|
||||||
|
permission = '{}.create'.format(permission_prefix)
|
||||||
|
resource.add_view(cls.collection_post, permission=permission)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
permission='{}.list'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(autocomplete)
|
||||||
|
|
||||||
|
# download
|
||||||
|
if cls.supports_download:
|
||||||
|
download = Service(name='{}.download'.format(route_prefix),
|
||||||
|
# TODO: probably should allow for other (composite?) key fields
|
||||||
|
path='{}/{{uuid}}/download'.format(object_url_prefix))
|
||||||
|
download.add_view('GET', 'download', klass=cls,
|
||||||
|
permission='{}.download'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(download)
|
||||||
|
|
||||||
|
# rawbytes
|
||||||
|
if cls.supports_rawbytes:
|
||||||
|
rawbytes = Service(name='{}.rawbytes'.format(route_prefix),
|
||||||
|
# TODO: probably should allow for other (composite?) key fields
|
||||||
|
path='{}/{{uuid}}/rawbytes'.format(object_url_prefix))
|
||||||
|
rawbytes.add_view('GET', 'rawbytes', klass=cls,
|
||||||
|
permission='{}.download'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(rawbytes)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -26,8 +26,7 @@ Tailbone Web API - Master View (v2)
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from pyramid.response import FileResponse
|
import warnings
|
||||||
from cornice import resource, Service
|
|
||||||
|
|
||||||
from tailbone.api import APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
|
@ -36,174 +35,9 @@ class APIMasterView2(APIMasterView):
|
||||||
"""
|
"""
|
||||||
Base class for data model REST API views.
|
Base class for data model REST API views.
|
||||||
"""
|
"""
|
||||||
listable = True
|
|
||||||
creatable = True
|
|
||||||
viewable = True
|
|
||||||
editable = True
|
|
||||||
deletable = True
|
|
||||||
supports_autocomplete = False
|
|
||||||
supports_download = False
|
|
||||||
supports_rawbytes = False
|
|
||||||
|
|
||||||
@classmethod
|
def __init__(self, request, context=None):
|
||||||
def establish_method(cls, method_name):
|
warnings.warn("APIMasterView2 class is deprecated; please use "
|
||||||
"""
|
"APIMasterView instead",
|
||||||
Establish the given HTTP method for this Cornice Resource.
|
DeprecationWarning, stacklevel=2)
|
||||||
|
super(APIMasterView2, self).__init__(request, context=context)
|
||||||
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()
|
|
||||||
|
|
||||||
##############################
|
|
||||||
# download
|
|
||||||
##############################
|
|
||||||
|
|
||||||
def download(self):
|
|
||||||
"""
|
|
||||||
GET view allowing for download of a single file, which is attached to a
|
|
||||||
given record.
|
|
||||||
"""
|
|
||||||
obj = self.get_object()
|
|
||||||
|
|
||||||
filename = self.request.GET.get('filename', None)
|
|
||||||
if not filename:
|
|
||||||
raise self.notfound()
|
|
||||||
path = self.download_path(obj, filename)
|
|
||||||
|
|
||||||
response = self.file_response(path)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def download_path(self, obj, filename):
|
|
||||||
"""
|
|
||||||
Should return absolute path on disk, for the given object and filename.
|
|
||||||
Result will be used to return a file response to client.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def rawbytes(self):
|
|
||||||
"""
|
|
||||||
GET view allowing for direct access to the raw bytes of a file, which
|
|
||||||
is attached to a given record. Basically the same as 'download' except
|
|
||||||
this does not come as an attachment.
|
|
||||||
"""
|
|
||||||
obj = self.get_object()
|
|
||||||
|
|
||||||
filename = self.request.GET.get('filename', None)
|
|
||||||
if not filename:
|
|
||||||
raise self.notfound()
|
|
||||||
path = self.download_path(obj, filename)
|
|
||||||
|
|
||||||
response = self.file_response(path, attachment=False)
|
|
||||||
return response
|
|
||||||
|
|
||||||
@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')
|
|
||||||
if hasattr(cls, 'permission_to_create'):
|
|
||||||
permission = cls.permission_to_create
|
|
||||||
else:
|
|
||||||
permission = '{}.create'.format(permission_prefix)
|
|
||||||
resource.add_view(cls.collection_post, permission=permission)
|
|
||||||
|
|
||||||
# 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,
|
|
||||||
permission='{}.list'.format(permission_prefix))
|
|
||||||
config.add_cornice_service(autocomplete)
|
|
||||||
|
|
||||||
# download
|
|
||||||
if cls.supports_download:
|
|
||||||
download = Service(name='{}.download'.format(route_prefix),
|
|
||||||
# TODO: probably should allow for other (composite?) key fields
|
|
||||||
path='{}/{{uuid}}/download'.format(object_url_prefix))
|
|
||||||
download.add_view('GET', 'download', klass=cls,
|
|
||||||
permission='{}.download'.format(permission_prefix))
|
|
||||||
config.add_cornice_service(download)
|
|
||||||
|
|
||||||
# rawbytes
|
|
||||||
if cls.supports_rawbytes:
|
|
||||||
rawbytes = Service(name='{}.rawbytes'.format(route_prefix),
|
|
||||||
# TODO: probably should allow for other (composite?) key fields
|
|
||||||
path='{}/{{uuid}}/rawbytes'.format(object_url_prefix))
|
|
||||||
rawbytes.add_view('GET', 'rawbytes', klass=cls,
|
|
||||||
permission='{}.download'.format(permission_prefix))
|
|
||||||
config.add_cornice_service(rawbytes)
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -30,7 +30,7 @@ import six
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView2 as APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
|
|
||||||
class PersonView(APIMasterView):
|
class PersonView(APIMasterView):
|
||||||
|
@ -52,5 +52,12 @@ class PersonView(APIMasterView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
PersonView = kwargs.get('PersonView', base['PersonView'])
|
||||||
PersonView.defaults(config)
|
PersonView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2020 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -32,7 +32,7 @@ from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView2 as APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
|
|
||||||
class ProductView(APIMasterView):
|
class ProductView(APIMasterView):
|
||||||
|
@ -78,5 +78,12 @@ class ProductView(APIMasterView):
|
||||||
return product.full_description
|
return product.full_description
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
ProductView = kwargs.get('ProductView', base['ProductView'])
|
||||||
ProductView.defaults(config)
|
ProductView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2020 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -30,7 +30,7 @@ import six
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView2 as APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
|
|
||||||
class UpgradeView(APIMasterView):
|
class UpgradeView(APIMasterView):
|
||||||
|
@ -57,5 +57,12 @@ class UpgradeView(APIMasterView):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
UpgradeView = kwargs.get('UpgradeView', base['UpgradeView'])
|
||||||
UpgradeView.defaults(config)
|
UpgradeView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2020 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -26,11 +26,9 @@ Tailbone Web API - User Views
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView2 as APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
|
|
||||||
class UserView(APIMasterView):
|
class UserView(APIMasterView):
|
||||||
|
@ -60,5 +58,12 @@ class UserView(APIMasterView):
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
UserView = kwargs.get('UserView', base['UserView'])
|
||||||
UserView.defaults(config)
|
UserView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2020 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -30,7 +30,7 @@ import six
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView2 as APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
|
|
||||||
class VendorView(APIMasterView):
|
class VendorView(APIMasterView):
|
||||||
|
@ -50,5 +50,12 @@ class VendorView(APIMasterView):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
VendorView = kwargs.get('VendorView', base['VendorView'])
|
||||||
VendorView.defaults(config)
|
VendorView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -31,12 +31,10 @@ import datetime
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from rattail.db.model import WorkOrder
|
from rattail.db.model import WorkOrder
|
||||||
from rattail.time import localtime
|
|
||||||
from rattail.util import OrderedDict
|
|
||||||
|
|
||||||
from cornice import Service
|
from cornice import Service
|
||||||
|
|
||||||
from tailbone.api import APIMasterView2 as APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
|
||||||
|
|
||||||
class WorkOrderView(APIMasterView):
|
class WorkOrderView(APIMasterView):
|
||||||
|
|
Loading…
Reference in a new issue