Skip to content

Instantly share code, notes, and snippets.

@miohtama
Last active August 29, 2015 14:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save miohtama/d787e7674e63eceb80a3 to your computer and use it in GitHub Desktop.
Save miohtama/d787e7674e63eceb80a3 to your computer and use it in GitHub Desktop.
Mapping objects to Pyramid traversing ids and vice vrsa
"""CRUD based on SQLAlchemy and Deform."""
from abc import abstractmethod, abstractproperty
from websauna.system.core import traverse
from . import mapper
class CRUD(traverse.Resource):
"""Define create-read-update-delete interface for an model.
We use Pyramid traversing to get automatic ACL permission support for operations. As long given CRUD resource parts define __acl__ attribute, permissions are respected automatically.
URLs are the following:
List: $base/listing
Add: $base/add
Show $base/$id/show
Edit: $base/$id/edit
Delete: $base/$id/delete
"""
# How the model is referred in templates. e.g. "User"
title = "xx"
#: Helper noun used in the default placeholder texts
singular_name = "item"
#: Helper noun used in the default placeholder texts
plural_name = "items"
#: Mapper defines how objects are mapped to URL space
mapper = mapper.IdMapper()
def make_resource(self, obj):
"""Take raw model instance and wrap it to Resource for traversing.
:param obj: SQLALchemy object or similar model object.
:return: :py:class:`websauna.core.traverse.Resource`
"""
# Use internal Resource class to wrap the object
if hasattr(self, "Resource"):
return self.Resource(obj)
raise NotImplementedError("Does not know how to wrap to resource: {}".format(obj))
def wrap_to_resource(self, obj):
# Wrap object to a traversable part
instance = self.make_resource(obj)
path = self.mapper.get_path_from_object(obj)
assert type(path) == str, "Object {} did not map to URL path correctly, got path {}".format(obj, path)
instance.make_lineage(self, instance, path)
return instance
def traverse_to_object(self, path):
"""Wraps object to a traversable URL.
Loads raw database object with id and puts it inside ``Instance`` object,
with ``__parent__`` and ``__name__`` pointers.
"""
# First try if we get an view for the current instance with the name
id = self.mapper.get_id_from_path(path)
obj = self.fetch_object(id)
return self.wrap_to_resource(obj)
@abstractmethod
def fetch_object(self, id):
"""Load object from the database for CRUD path for view/edit/delete."""
raise NotImplementedError("Please use concrete subclass like websauna.syste.crud.sqlalchemy")
def get_object_url(self, request, obj, view_name=None):
"""Get URL for view for an object inside this CRUD.
;param request: HTTP request instance
:param obj: Raw object, e.g. SQLAlchemy instance, which can be wrapped with ``wrap_to_resource``.
:param view_name: Traverse view name for the resource. E.g. ``show``, ``edit``.
"""
res = self.wrap_to_resource(obj)
if view_name:
return request.resource_url(res, view_name)
else:
return request.resource_url(res)
def __getitem__(self, path):
if self.mapper.is_id(path):
return self.traverse_to_object(path)
else:
# Signal that this id is not part of the CRUD database and may be a view
raise KeyError
class Resource(traverse.Resource):
"""One object in CRUD traversing.
Maps the raw database object under CRUD view/edit/delete control to traverse path.
Presents an underlying model instance mapped to an URL path. ``__parent__`` attribute points to a CRUD instance.
"""
def __init__(self, obj):
self.obj = obj
def get_object(self):
"""Return the wrapped database object."""
return self.obj
def get_path(self):
"""Extract the traverse path name from the database object."""
assert hasattr(self, "__parent__"), "get_path() can be called only for objects whose lineage is set by make_lineage()"
crud = self.__parent__
path = crud.mapper.get_path_from_object(self.obj)
return path
def get_model(self):
return self.__parent__.get_model()
def get_title(self):
"""Title used on view, edit, delete, pages.
By default use the capitalized URL path path.
"""
return self.get_path()
"""Map URL traversing ids to database ids and vice versa."""
import abc
from websauna.utils import slug
class Mapper(abc.ABC):
"""Define mapping interface used by CRUD subsystem."""
@abc.abstractmethod
def get_path_from_object(self, obj):
"""Map database object to an travesable URL path."""
raise NotImplementedError()
@abc.abstractmethod
def get_id_from_path(self, path):
"""Map traversable resource name to an database object id."""
raise NotImplementedError()
class IdMapper(Mapper):
"""Use object/column attribute id to map functions.
By default this is set to use integer ids, but you can override properties.
"""
#: What is the object attibute name defining the its id
mapping_attribute = "id"
#: Function to translate obj attribute -> URL path str
transform_to_path = str
#: Function to translate URL path str -> object id
transform_to_id = int
#: is_id(path) function checks whether the given URL path should be mapped to object and is a valid object id. Alternatively, if the path doesn't look like an object id, it could be a view name. Because Pyramid traversing checks objects prior views, we need to let bad object ids to fall through through KeyError, so that view matching mechanism kicks in. By default we check for number integer id.
#: Some object paths cannot be reliable disquished from view names, like UUID strings. In this case ``is_id`` is None, the lookup always first goes to the database. The database item is not with view name a KeyError is triggerred and thus Pyramid continues to view name resolution. This behavior is suboptimal and may change in the future versions
is_id = staticmethod(lambda value: value.isdigit())
def __init__(self, mapping_attribute=None, transform_to_path=None, transform_to_id=None, is_id=None):
if mapping_attribute:
self.mapping_attribute = mapping_attribute
if transform_to_path:
self.transform_to_path = transform_to_path
if transform_to_id:
self.transform_to_id = transform_to_id
if is_id:
self.is_id = is_id
def get_path_from_object(self, obj):
return self.transform_to_path(getattr(obj, self.mapping_attribute))
def get_id_from_path(self, path):
return self.transform_to_id(path)
class Base64UUIDMapper(IdMapper):
"""Map objects to URLs using their UUID property."""
#: Override this if you want to change the column name containing uuid
mapping_attribute = "uuid"
#: Use utils.slug package to produce B64 strings from UUIDs
transform_to_id = staticmethod(slug.slug_to_uuid)
#: Use utils.slug package to produce B64 strings from UUIDs
transform_to_path = staticmethod(slug.uuid_to_slug)
@staticmethod
def is_id(val):
"""Try guess if the value is valid base64 UUID slug or not.
Note that some view names can be valid UUID slugs, thus we might hit database in any case for the view lookup.
"""
try:
slug.slug_to_uuid(val)
return True
except ValueError:
# bytes is not 16-char string
return False
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment