Last active
August 29, 2015 14:23
-
-
Save miohtama/d787e7674e63eceb80a3 to your computer and use it in GitHub Desktop.
Mapping objects to Pyramid traversing ids and vice vrsa
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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