diff --git a/tailbone/api/__init__.py b/tailbone/api/__init__.py
index 6c310fa7..d09c669a 100644
--- a/tailbone/api/__init__.py
+++ b/tailbone/api/__init__.py
@@ -27,7 +27,10 @@ Tailbone Web API
from __future__ import unicode_literals, absolute_import
from .core import APIView, api
+from .master import APIMasterView
def includeme(config):
config.include('tailbone.api.auth')
+ config.include('tailbone.api.customers')
+ config.include('tailbone.api.users')
diff --git a/tailbone/api/customers.py b/tailbone/api/customers.py
new file mode 100644
index 00000000..75f1b438
--- /dev/null
+++ b/tailbone/api/customers.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2018 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 - Customer Views
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+from rattail.db import model
+
+from tailbone.api import APIMasterView
+
+
+class CustomerView(APIMasterView):
+
+ model_class = model.Customer
+
+ def normalize(self, customer):
+ return {
+ 'id': customer.id,
+ 'name': customer.name,
+ }
+
+
+def includeme(config):
+ CustomerView.defaults(config)
diff --git a/tailbone/api/master.py b/tailbone/api/master.py
new file mode 100644
index 00000000..ff3261c9
--- /dev/null
+++ b/tailbone/api/master.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2018 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 - Master View
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+from tailbone.api import APIView, api
+from tailbone.db import Session
+
+
+class APIMasterView(APIView):
+
+ @classmethod
+ def get_model_class(cls):
+ if hasattr(cls, 'model_class'):
+ return cls.model_class
+ raise NotImplementedError("must set `model_class` for {}".format(cls.__name__))
+
+ @classmethod
+ def get_model_key(cls):
+ if hasattr(cls, 'model_key'):
+ return cls.model_name
+ return cls.get_model_class().__name__.lower()
+
+ @classmethod
+ def get_model_key_plural(cls):
+ if hasattr(cls, 'model_key_plural'):
+ return cls.model_key_plural
+ return '{}s'.format(cls.get_model_key())
+
+ @classmethod
+ def get_route_prefix(cls):
+ if hasattr(cls, 'route_prefix'):
+ return cls.route_prefix
+ return 'api.{}'.format(cls.get_model_key_plural())
+
+ @classmethod
+ def get_permission_prefix(cls):
+ if hasattr(cls, 'permission_prefix'):
+ return cls.permission_prefix
+ return cls.get_model_key_plural()
+
+ @classmethod
+ def get_url_prefix(cls):
+ if hasattr(cls, 'url_prefix'):
+ return cls.url_prefix
+ return '/api/{}'.format(cls.get_model_key_plural())
+
+ @property
+ def Session(self):
+ return Session
+
+ @api
+ def index(self):
+ objects = self.Session.query(self.model_class)
+
+ sort = self.request.params.get('sort')
+ if sort:
+ # TODO: this is fragile, but what to do if bad params?
+ sortkey, sortdir = sort.split('|')
+ sortkey = getattr(self.model_class, sortkey)
+ objects = objects.order_by(getattr(sortkey, sortdir)())
+
+ data = [self.normalize(obj) for obj in objects]
+ return data
+
+ def normalize(self, obj):
+ raise NotImplementedError("must implement `normalize()` method for: {}".format(self.__class__.__name__))
+
+ @classmethod
+ def defaults(cls, config):
+ route_prefix = cls.get_route_prefix()
+ url_prefix = cls.get_url_prefix()
+ permission_prefix = cls.get_permission_prefix()
+
+ # index
+ config.add_route(route_prefix, '{}/'.format(url_prefix), request_method='GET')
+ config.add_view(cls, attr='index', route_name=route_prefix,
+ renderer='json', permission='{}.list'.format(permission_prefix))
diff --git a/tailbone/api/users.py b/tailbone/api/users.py
new file mode 100644
index 00000000..56cf17fd
--- /dev/null
+++ b/tailbone/api/users.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2018 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 - User Views
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+import six
+
+from rattail.db import model
+
+from tailbone.api import APIMasterView
+
+
+class UserView(APIMasterView):
+
+ model_class = model.User
+
+ def normalize(self, user):
+ return {
+ 'username': user.username,
+ 'person': six.text_type(user.person or ''),
+ }
+
+
+def includeme(config):
+ UserView.defaults(config)