Substance D¶
Overview¶
Substance D is an application server built using the Pyramid web framework. It can be used as a base to build a general-purpose web application like a blog, a shopping cart application, a scheduling application, or any other web app that requires both an administration and a retail interface.
Substance D owes much of its spirit to the Zope application server.
It requires Python 2.7, 3.4, or 3.5.
Installation¶
See Installation.
Demonstration Application¶
See the application running at http://demo.substanced.net for a demonstration of the Substance D management interface.
Narrative Documentation¶
Installation¶
Install using pip, e.g., (within a virtual environment).
$ pip install substanced
Warning
During Substance D’s alpha period, it may be necessary to use a checkout of Substance D as well as checkouts of the most recent versions of the libraries upon which Substance D depends.
Demonstration application¶
See the application running at http://demo.substanced.net for a demonstration of the Substance D management interface.
You can deploy the demonstration application locally by performing the following steps.
Create a new directory somewhere and
cd
to it:$ virtualenv -p python2.7 hack-on-substanced $ cd hack-on-substanced
Install Substance D either from PyPI or from a git checkout:
$ bin/pip install substanced
OR:
$ bin/pip install -e git+https://github.com/Pylons/substanced#egg=substanced
Alternatively create a writeable fork on GitHub and check that out.
Check that the python-magic library has been installed:
$ bin/python -c "from substanced.file import magic; assert magic is not None, 'python-magic not installed'"
If you then see “python-magic not installed” then you will need to take additional steps to install the python-magic library. See Installing python-magic.
Move back to the parent directory:
$ cd ..
Now you should be able to create new Substance D projects by using
pcreate
. The followingpcreate
command uses the scaffoldsubstanced
to create a new project namedmyproj
:$ hack-on-substanced/bin/pcreate -s substanced myproj
Now you can make a virtual environment for your project and move into it:
$ virtualenv -p python2.7 myproj $ cd myproj
Install that project using
pip install -e
into the virtual environment:$ bin/pip install -e .
Run the resulting project via
bin/pserve development.ini
. The development server listens to requests sent to http://0.0.0.0:6543 by default. Open this URL in a web browser.The initial Administrator password is randomly generated automatically. Use the following command to find the login information:
$ grep initial_password *.ini development.ini:substanced.initial_password = hNyrGI5nnl production.ini:substanced.initial_password = hNyrGI5nnl
Create a project from a scaffold in Substance D¶
After creating a development checkout, you can create a new project from the
default substanced scaffold by using pcreate
.
$ cd ../env
$ bin/pcreate -s substanced myproj
Then install that project using pip install -e .
into the virtual
environment.
$ cd myproj
$ ../bin/pip install -e .
Run the resulting project.
$ ../bin/pserve development.ini
Then start hacking on your new project.
Hacking on Substance D¶
See Hacking on Substance D, or look in
your checked out local git repository for HACKING.txt
, for information and
guidelines to develop your application, including testing and
internationalization.
Introduction¶
Substance D is an application server. It provides the following features:
- Facilities that allow developers to define “content” (e.g., “a blog entry”, “a product”, or “a news item”).
- A management (a.k.a., “admin”) web UI which allows non-expert but privileged users to create, edit, update, and delete developer-defined content, as well as managing other aspects of the system such as users, groups, security, and so on.
- “Undo” capability for actions taken via the management UI.
- A way to make highly granular hierarchical security declarations for content objects (e.g., “Bob can edit this post” or “Bob can edit all posts in this collection” as opposed to just “Bob can edit posts”).
- Built-in users and groups management.
- Built-in content workflow.
- Indexing and searching of content (field, keyword, facet, and full-text). Searches can be easily filtered by the security settings of individual pieces of content, by path in the content hierarchy, or by a combination of the two.
- A facility for relating content objects to each other (with optional referential integrity).
- An “evolve” mechanism for evolving content over time as it changes.
- A mechanism to dump your site’s content to the filesystem in a mostly human-readable format, and a mechanism to reload a dump into the system.
Substance D is built upon the following technologies:
The Substance D Management Interface¶
Substance D’s prime directive is to help developers quickly build custom content management applications with a custom user experience. For the Substance D parts, though, a polished, stable, and supported management UI is provided.
The Substance D management interface (aka SDI) is a set of view registrations that are imposed upon the resource tree of your application. The SDI allows you to add, delete, change, and otherwise manage resources and services.

Benefits and features¶
- Create, Read, Update, Delete (CRUD) operations on content resources
- Extensible actions for each content type via management views
- Built-in support for hierarchies with security
- Already-done UIs for all supported features (e.g., references, principals)
- Undo facility to back out of the last transaction
- Copy and paste
Background and motivation¶
In prehistoric times there was a Python-based application server, derived from a commercial predecessor released in 1996. Zope and its predecessor had a unique “through-the-web” (TTW) UI for interacting with the system. This UI, called the “Zope Management Interface” (ZMI), had a number of capabilities for a number of audiences. Plone, built on Zope, extended this idea. Other systems, such as Django, have found that providing an out-of-the-box (OOTB) starting point with attractive pixels on the screen can be a key selling point.
Substance D taps into this. In particular, lessons learned from our long experience in this area are applied to the SDI:
- Attractive, official, supported OOTB management UI
- Be successful by being very clear what the SDI isn’t
What is and isn’t the SDI¶
The SDI is for:
- Developers to use while building their application
- Administrators to use after deployment, to manage certain Substance D or application settings provided by the developer
- Certain power users to use as a behind-the-scenes UI
The SDI is not for:
- The retail UI for your actual application. Unlike Plone, we don’t expect developers to squeeze their UX expectations into an existing UX
- Overridable, customizable, replaceable, frameworky new expectations
The SDI does have a short list of clearly-defined places for developers to plug in parts of their application. As a prime example, you can define a Management View that gets added as a new tab on a resource.
The SDI is extensible and allows you to plug your own views into it, so you can present nontechnical users with a way to manage arbitrary kinds of custom content.
Once again, for clarity: the SDI is not a framework, it is an application. It is not your retail UI.
Layout¶
The SDI has a mostly-familiar layout:
- A header that shows the username as a dropdown menu containing a link to the principal screen as well as a logout link
- Breadcrumbs with a path from the root
- A series of tabs for the management views of the current resource
- Optionally, a flash message showing results of the previous operation, a warning, or some other notice
- A footer
Folder contents¶
Folders show a listing of items they contain using a powerful data grid based on SlickGrid:

This dynamic grid features:
- Only loading the items needed for display, to speed up operations on large folders
- “Infinite scrolling” via the scrollbar to go directly to a batch at any point in the folder
- Column resizing and re-ordering
- Sorting on sort-supported columns
- Filtering based on search string
- Selection of one or more items and performing an operation by clicking on a button
- Styling integrated with Twitter Bootstrap
- Detection and re-layout on responsive design operations
The Configuring Folder Contents chapter covers how Substance D developers can plug their custom content types into folder contents.
Undo¶
In Substance D, many transactions can be undone and redone after a commit. This “Undo” ability is one of the key features that people notice immediately and it has real, deep value to a developer’s customers.
Many of the built-in operations display an undo button. For example, if we delete an item from a folder, we get a “flash” message telling us the deletion was performed, but with a button allowing us to undo it if that was a mistake:

Clicking “undo” restores the deleted item, with a flash message offering to redo the undo:

Undo button support is enabled by the developer in their management views that commit data. It isn’t available on every kind of change. Instead developers need to wrap their commit with certain information used by the SDI’s undo features.
All actions that change data (even ones without undo button support) can be
undone. These screenshots show an Undo
tab on the site’s root folder. This
provides a global way to see recent transactions and perform an undo:

Sometimes a particular transaction cannot be undone without undoing an earlier transaction. For example, if you make three changes to a resource, the first two can’t be undone without first undoing the last, as the resource will have been changed by a later transaction.
Catalog¶
With cataloging developers have a powerful indexing and searching facility that can be added to their application. Like other first-class parts of Substance D’s machinery, it includes an SDI UI for interacting with the catalog:

Catalogues are content, meaning they show up as folder items in the SDI. You can visit a catalog and update its indexes, or see some statistics for those indexes. You can also use the SDI to reindex the contents of an index, if you suspect it has gotten out of sync with the content.
The catalog also registers a management view on content resources, which
gain an Indexing
tab:

This shows some statistics and allows an SDI user to reindex an individual resource.
Principals¶
Managing users and groups, a.k.a., principals, is more interesting in a system like Substance D with rich hierarchies. You can add a folder of principals to any folder or other kind of container that allows adding principals:

A principals folder allows you to manage (e.g., add, edit, delete, or rename) users and groups via the SDI, as well as password resets. Since users and groups are content, you gain some of the other SDI tabs for managing them (e.g., Security, References):

Users and groups can also grow extra attributes and behavior because they’re just content, so you can customize your user model out of the box.
Workflows¶
The workflows service provides a powerful system for managing states and transitions. This service shows up in the SDI as a tab on content types that have workflows registered for them:

This provides a way, via the SDI, to transition the workflow state of a resource.
References¶
With the built-in support for references, Substance D helps manage relationships between resources. The SDI provides a UI into the reference service.
If the resource you are viewing has any references, a References
tab will appear:

In this example, mydoc1
is a target of an ACL reference from the
admin1
user.
An integrity error can occur if you try to delete a source or target of a reference that claims to be “integral”. The SDI will then show this with an explanation:

Manage Database¶
The object database inside Substance D has some management knobs that can be adjusted via the SDI:

This tab appears on the root object of the site and lets you:
- Pack the old revisions of objects in the database.
- Inspect and run evolution steps.
- Flush the object cache.
- See details and statistics about the database, the connection, and activity.
Implementation Notes¶
While it doesn’t matter for developers of Substance D applications, some notes are below, regarding how the SDI is implemented:
- We use a high-performance, modern, responsive UI based on Twitter Bootstrap
- We use the upstream LESS variables from Bootstrap in a LESS file for parts of the SDI.
- Our grid is based on SlickGrid.
Defining Content¶
Resource is the term that Substance D uses to describe an object placed in the resource tree.
Ideally, all resources in your resource tree will be content. “Content” is the term that Substance D uses to describe resource objects that are particularly well-behaved when they appear in the SDI management interface. The Substance D management interface (aka SDI) is a set of views imposed upon the resource tree that allow you to add, delete, change and otherwise manage resources.
You can convince the management interface that your particular resources are content. To define a resource as content, you need to associate a resource with a content type.
Registering Content¶
In order to add new content to the system, you need to associate a resource factory with a content type. A resource factory that generates content must have these properties:
- It must be a class, or a factory function that returns an instance of a resource class.
- Instances of the resource class must be persistent (it must derive from
the
persistent.Persistent
class or a class that derives from Persistent such assubstanced.folder.Folder
). - The resource class or factory must be decorated with the
@content
decorator, or must be added at configuration time viaconfig.add_content_type
. - It must have a type. A type acts as a globally unique categorization marker, and allows the content to be constructed, enumerated, and introspected by various Substance D UI elements such as “add forms”, and queries by the management interface for the icon class of a resource. A type can be any hashable Python object, but it’s most often a string.
Here’s an example which defines a content resource factory as a class:
# in a module named blog.resources
from persistent import Persistent
from substanced.content import content
@content('Blog Entry')
class BlogEntry(Persistent):
def __init__(self, title='', body=''):
self.title = title
self.body = body
Here’s an example of defining a content resource factory using a function instead:
# in a module named blog.resources
from persistent import Persistent
from substanced.content import content
class BlogEntry(Persistent):
def __init__(self, title, body):
self.title = title
self.body = body
@content('Blog Entry')
def make_blog_entry(title='', body=''):
return BlogEntry(title, body)
When a resource factory is not a class, Substance D will wrap the resource
factory in something that changes the resource object returned from the
factory. In the above case, the BlogEntry instance returned from
make_blog_entry
will be changed; its __factory_type__
attribute will be
mutated.
Notice that when we decorate a resource factory class with @content
, and
the class’ __init__
function takes arguments, we provide those arguments
with default values. This is mandatory if you’d like your content objects to
participate in a “dump”. Dumping a resource requires that the resource be
creatable without any mandatory arguments. It’s a similar story if our factory
is a function; the function decorated by the @content
decorator should
provide defaults to any argument. In general, a resource factory can take
arguments, but each parameter of the factory’s callable should be given a
default value. This also means that all arguments to a resource factory
should be keyword arguments, and not positional arguments.
In order to activate a @content
decorator, it must be scanned using the
Pyramid config.scan()
machinery:
# in a module named blog.__init__
from pyramid.config import Configurator
def main(global_config, **settings):
config = Configurator()
config.include('substanced')
config.scan('blog.resources')
# .. and so on ...
Instead of using the @content
decorator, you can alternately add a
content resource imperatively at configuration time using the
add_content_type
method of the Configurator:
# in a module named blog.__init__
from pyramid.config import Configurator
from .resources import BlogEntry
def main(global_config, **settings):
config = Configurator()
config.include('substanced')
config.add_content_type('Blog Entry', BlogEntry)
This does the same thing as using the @content
decorator, but you don’t
need to scan()
your resources if you use add_content_type
instead of
the @content
decorator.
Once a content type has been defined (and scanned, if it’s been defined using a decorator), an instance of the resource can be constructed from within a view that lives in your application:
# in a module named blog.views
from pyramid.httpexceptions import HTTPFound
from pyramid.view import (
view_config,
view_defaults,
)
@view_config(name='add_blog_entry', request_method='POST')
def add_blogentry(context, request):
title = request.POST['title']
body = request.POST['body']
entry = request.registry.content.create('Blog Entry', title, body)
context[title] = entry
return HTTPFound(request.resource_url(entry))
The arguments passed to request.registry.content.create
must start with
the content type, and must be followed with whatever arguments are required
by the resource factory.
Creating an instance of content this way isn’t particularly more useful than
creating an instance of the resource object by calling its class __init__
directly unless you’re building a highly abstract system. But even if you’re
not building a very abstract system, types can be very useful. For instance,
types can be enumerated:
# in a module named blog.views
@view_config(name='show_types', renderer='show_types.pt')
def show_types(request):
all_types = request.registry.content.all()
return {'all_types':all_types}
request.registry.content.all()
will return all the types you’ve defined
and scanned.
Metadata¶
A content’s type can be associated with metadata about that type, including the
content type’s name, its icon in the SDI management interface, an add view
name, and other things. Pass arbitrary keyword arguments to the @content
decorator or config.add_content_type
to specify metadata.
Names¶
You can associate a content type registration with a name that shows up when
someone attempts to add such a piece of content using the SDI management
interface “Add” tab by passing a name
keyword argument to @content
or config.add_content_type
.
# in a module named blog.resources
from persistent import Persistent
from substanced.content import content
@content('Blog Entry', name='Cool Blog Entry')
class BlogEntry(Persistent):
def __init__(self, title='', body=''):
self.title = title
self.body = body
Once you’ve done this, the “Add” tab in the SDI management interface will show your content as addable using this name instead of the type name.
Icons¶
You can associate a content type registration with a management view icon class
by passing an icon
keyword argument to @content
or add_content_type
.
# in a module named blog.resources
from persistent import Persistent
from substanced.content import content
@content('Blog Entry', icon='glyphicon glyphicon-file')
class BlogEntry(Persistent):
def __init__(self, title='', body=''):
self.title = title
self.body = body
Once you’ve done this, content you add to a folder in the sytem will display
the icon next to it in the contents view of the management interface and in
the breadcrumb list. The available icon class names are listed at
http://getbootstrap.com/components/#glyphicons . For glyphicon icons, you’ll
need to use two classnames: glyphicon
and glyphicon-foo
, separated by
a space.
You can also pass a callback as an icon
argument:
from persistent import Persistent
from substanced.content import content
def blogentry_icon(context, request):
if context.body:
return 'glyphicon glyphicon-file'
else:
return 'glyphicon glyphicon-gift'
@content('Blog Entry', icon=blogentry_icon)
class BlogEntry(Persistent):
def __init__(self, title='', body=''):
self.title = title
self.body = body
A callable used as icon
must accept two arguments: context
and
request
. context
will be an instance of the type and request
will
be the current request; your callback will be called at the time the folder
view is drawn. The callable should return either an icon class name or
None
. For example, the above blogentry_icon
callable tells the SDI to
use an icon representing a file if the blogentry has a body, otherwise show an
icon representing gift.
Add Views¶
You can associate a content type with a view that will allow the type to be
added by passing the name of the add view as a keyword argument to
@content
or add_content_type
.
# in a module named blog.resources
from persistent import Persistent
from substanced.content import content
@content('Blog Entry', add_view='add_blog_entry')
class BlogEntry(Persistent):
def __init__(self, title='', body=''):
self.title = title
self.body = body
Once you’ve done this, if the button is clicked in the “Add” tab for this content type, the related view will be presented to the user.
You can also pass a callback as an add_view
argument:
from persistent import Persistent
from substanced.content import content
from substanced.folder import Folder
def add_blog_entry(context, request):
if request.registry.content.istype(context, 'Blog'):
return 'add_blog_entry'
@content('Blog')
class Blog(Folder):
pass
@content('Blog Entry', add_view=add_blog_entry)
class BlogEntry(Persistent):
def __init__(self, title='', body=''):
self.title = title
self.body = body
A callable used as add_view
must accept two arguments: context
and
request
. context
will be the potential parent object of the content
(when the SDI folder view is drawn), and request
will be the current
request at the time the folder view is drawn. The callable should return
either a view name or None
if the content should not be addable in this
circumstance. For example, the above add_blog_entry
callable asserts that
Blog Entry content should only be addable if the context we’re adding to is of
type Blog; it returns None otherwise, signifying that the content is not
addable in this circumstance.
Obtaining Metadata About a Content Object’s Type¶
Return the icon class name for the blogentry’s content type or
None
if it does not exist:
request.registry.content.metadata(blogentry, 'icon')
Return the icon for the blogentry’s content type or
glyphicon glyphicon-file
if it does not exist:
request.registry.content.metadata(blogentry, 'icon',
'glyphicon glyphicon-file')
Affecting Content Creation¶
In some cases you might want your resource to perform some actions that
can only take place after it has been seated in its container, but
before the creation events have fired. The @content
decorator and
add_content_type
method both support an after_create
argument,
pointed at a callable.
For example:
@content(
'Document',
icon='glyphicon glyphicon-align-left',
add_view='add_document',
propertysheets = (
('Basic', DocumentPropertySheet),
),
after_create='after_creation'
)
class Document(Persistent):
name = renamer()
def __init__(self, title, body):
self.title = title
self.body = body
def after_creation(self, inst, registry):
pass
If the value provided for after_create
is a string, it’s assumed to
be a method of the created object. If it’s a sequence, each value
should be a string or a callable, which will be called in turn. The
callable(s) are passed the instance being created and the registry.
Afterwards, substanced.event.ContentCreatedEvent
is emitted.
Construction of the root folder in Substance D is a special case. Most Substance D applications will start with:
from substanced.db import root_factory
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings, root_factory=root_factory)
The substanced.db.root_factory()
callable contains the following
line:
app_root = registry.content.create('Root')
In many cases you want to perform some extra work on the Root
. For
example, you might want to create a catalog with indexes. Substance D
emits an event when the root is created, so you can subscribe to that
event and perform some actions:
from substanced.root import Root
from substanced.event import subscribe_created
from substanced.catalog import Catalog
@subscribe_created(Root)
def root_created(event):
root = event.object
catalog = Catalog()
catalogs = root['catalogs']
catalogs.add_service('catalog', catalog)
catalog.update_indexes('system', reindex=True)
catalog.update_indexes('sdidemo', reindex=True)
Names and Renaming¶
A resource’s “name” (__name__
) is important to the system in
Substance D. For example, traversal uses the value in URLs and paths to
walk through hierarchy. Containers need to know when a resource’s
__name__
changes.
To help support this, Substance D provides
substanced.util.renamer()
. You use it as a class attribute
wrapper on resources that want “managed” names. These resources then
gain a name
attribute with a getter/setter from renamer
.
Getting the name
returns the __name__
. Setting name
grabs
the container and calls the rename
method on the folder.
For example:
class Document(Persistent):
name = renamer()
Special Colander Support¶
Forms and schemas for resources become pretty easy in Substance D. To make it easier for forms to interact with the Substance D machinery, it includes some special Colander schema nodes you can use on your forms.
NameSchemaNode
¶
If you want your form to affect the __name__
of a resource,
certain constraints become applicable. These constraints might be
different, so you might want to know if you are on an add form versus
an edit form. substanced.schema.NameSchemaNode
provides a
schema node and default widget that bundles up the common rules for this.
For example:
class BlogEntrySchema(Schema):
name = NameSchemaNode()
The above provides the basics of support for editing a name property,
especially when combined with the renamer()
utility mentioned above.
By default the name is limited to 100 characters. NameSchemaNode
accepts an argument that can set a different limit:
class BlogEntrySchema(Schema):
name = NameSchemaNode(max_len=20)
You can also provide an editing
argument, either as a boolean or a
callable which returns a boolean, which determines whether the form is
rendered in “editing” mode. For example:
class BlogEntrySchema(Schema):
name = NameSchemaNode(
editing=lambda c, r: r.registry.content.istype(c, 'BlogEntry')
)
PermissionSchemaNode
¶
A form might want to allow selection of zero or more permissions from
the site’s defined list of permissions.
PermissionSchemaNode
collects the possible
state from the system, the currently-assigned values, and presents a
widget that manages the values.
MultireferenceIdSchemaNode
¶
References are a very powerful facility in Substance D. Naturally
you’ll want your application’s forms to assign references.
MultireferenceIdSchemaNode
gives a schema node and widget
that allows multiple selections of possible values in the system for
references, including the current assignments.
As an example, the built-in substanced.principal.UserSchema
uses this schema node:
class UserSchema(Schema):
""" The property schema for :class:`substanced.principal.User`
objects."""
groupids = MultireferenceIdSchemaNode(
choices_getter=groups_choices,
title='Groups',
)
Overriding Existing Content Types¶
Perhaps you would like to slightly adjust an existing content type,
such as Folder
, without re-implementing it. For exampler,
perhaps you would like to override just the add_view
and provide
your own view, such as:
@mgmt_view(
context=IFolder,
name='my_add_folder',
tab_condition=False,
permission='sdi.add-content',
renderer='substanced.sdi:templates/form.pt'
)
class MyAddFolderView(AddFolderView):
def before(self, form):
# Perform some custom work before validation
pass
With this you can override any of the view predicates (such as
permission
) and override any part of the form handling (such as
adding a before
that performs some custom processing.)
To make this happen, you can re-register, so to speak, the content type during startup:
from substanced.folder import Folder
from .views import MyAddFolderView
config.add_content_type('Folder', Folder,
add_view='my_add_folder',
icon='glyphicon glyphicon-folder-close')
This, however, keeps the same content type class. You can also go further by overriding the content type definition itself:
@content(
'Folder',
icon='glyphicon glyphicon-folder-close',
add_view='my_add_folder',
)
@implementer(IFolder)
class MyFolder(Folder):
def send_email(self):
pass
The class for the Folder
content type has now been replaced. Instead
of substanced.folder.Folder
it is MyFolder
.
Note
Overriding a content type is a pain-free way to make a custom
Root
object. You could supply your own root_factory
to
the Configurator
but that means replicating all its rather
complicated goings-on. Instead, provide your own content type
factory, as above, for Root
.
Adding Automatic Naming for Content¶
On some sites you don’t want to set the name for every piece of content you create. Substance D provides support for this with a special kind of folder. You can configure your site to use the autonaming folder by overriding the standard folder:
from substanced.folder import SequentialAutoNamingFolder
from substanced.interaces import IFolder
from zope.interface import implementer
@content(
'Folder',
icon='glyphicon glyphicon-folder-close',
add_view='add_folder',
)
@implementer(IFolder)
class MyFolder(SequentialAutoNamingFolder):
""" Override Folder content type """
The add view
for Documents can then be edited to no longer require a name:
def add_success(self, appstruct):
registry = self.request.registry
document = registry.content.create('Document', **appstruct)
self.context.add_next(document)
return HTTPFound(
self.request.sdiapi.mgmt_path(self.context, '@@contents')
)
Note
This does not apply to the root object.
Affecting the Tab Order for Management Views¶
The tab_order
parameter overrides the mgmt_view tab settings
for a content type. Its value should be a sequence of view names, each
corresponding to a tab that will appear in the management interface. Any
registered view names that are omitted from this sequence will be placed
after the other tabs.
Handling Content Events¶
Adding and modifying data related to content is, thanks to the framework, easy to do. Sometimes, though, you want to intervene and, for example, perform some extra work when content resources are added. Substance D has several framework events you can subscribe to using Pyramid events.
The substanced.events
module imports these events as interfaces
from substanced.interfaces
and then provides decorator
subscribers as convenience for each:
substanced.interfaces.IObjectAdded
as subscriber@subscriber_added
substanced.interfaces.IObjectWillBeAdded
as subscriber@subscriber_will_be_added
substanced.interfaces.IObjectRemoved
as subscriber@subscriber_removed
substanced.interfaces.IObjectWillBeRemoved
as subscriber@subscriber_will_be_removed
substanced.interfaces.IObjectModified
as subscriber@subscriber_modified
substanced.interfaces.IACLModified
as subscriber@subscriber_acl_modified
substanced.interfaces.IContentCreated
as subscriber@subscriber_created
As an example, the
substanced.principal.subscribers.user_added()
function is a
subscriber to the IObjectAdded
event:
@subscribe_added(IUser)
def user_added(event):
""" Give each user permission to change their own password."""
if event.loading: # fbo dump/load
return
user = event.object
registry = event.registry
set_acl(
user,
[(Allow, get_oid(user), ('sdi.view', 'sdi.change-password'))],
registry=registry,
)
As with the rest of Pyramid, you can do imperative configuration if you
don’t like decorator-based configuration, using
config.add_content_subscriber
Both the declarative and imperative
forms result in substanced.event.add_content_subscriber()
.
Note
While the event subscriber is de-coupled logically from the action that triggers the event, both the action and the subscriber run in the same transaction.
The IACLModified
event (and @subscriber_acl_modified
subscriber)
is used internally by Substance D to re-index information in the system
catalog’s ACL index. Substance D also uses this event to maintain
references between resources and principals. Substance D applications
can use this in different ways, for example recording a security audit
trail on security changes.
Sometimes when you perform operations on objects you don’t want to perform the standard events. For example, in folder contents you can select a number of resources and move them to another folder. Normally this would fire content change events that re-index the files. This is fairly pointless: the content of the file hasn’t changed.
If you looked at the interface for one of the content events,
you would see some extra information supported. For example, in
substanced.interfaces.IObjectWillBeAdded
:
class IObjectWillBeAdded(IObjectEvent):
""" An event type sent when an before an object is added """
object = Attribute('The object being added')
parent = Attribute('The folder to which the object is being added')
name = Attribute('The name which the object is being added to the folder '
'with')
moving = Attribute('None or the folder from which the object being added '
'was moved')
loading = Attribute('Boolean indicating that this add is part of a load '
'(during a dump load process)')
duplicating = Attribute('The object being duplicated or ``None``')
moving
, loading
, and duplicating
are flags that can be set
on the event when certain actions are triggered. These help in cases
such as the one above: certain subscribers might want “flavors” of
standard events and, in some cases, handle the event in a different
way. This helps avoid lots of special-case events or the need for a
hierarchy of events.
Thus in the case above, the catalog subscriber can see that the changes
triggered by the event where in the special case of “moving”. This can
be seen in substanced.catalog.subscribers.object_added
.
Management Views¶
A management view is a view configuration that applies only
when the URL is prepended with the manage prefix. The manage prefix
is usually /manage
, unless you’ve changed it from its default by setting
a custom substanced.manage_prefix
in your application’s .ini
file.
This means that views declared as management views will never show up in your application’s “retail” interface (the interface that normal unprivileged users see). They’ll only show up when a user is using the SDI to manage content.
There are two ways to define management views:
- Using the
substanced.sdi.mgmt_view
decorator on a function, method, or class. - Using the
substanced.sdi.add_mgmt_view()
Configurator (aka.config.add_mgmt_view
) API.
The former is most convenient, but they are functionally equivalent.
mgmt_view
just calls into add_mgmt_view
when found via a
scan.
Declaring a management view is much the same as declaring a “normal” Pyramid
view using pyramid.view.view_config
with a route_name
of
substanced_manage
. For example, each of the following view declarations
will register a view that will show up when the /manage/foobar
URL is
visited:
1 2 3 4 5 6 7 8 9 10 | from pyramid.view import view_config
@view_config(
renderer='string',
route_name='substanced_manage',
name='foobar',
permission='sdi.view',
)
def foobar(request):
return 'Foobar!'
|
The above is largely functionally the same as this:
1 2 3 4 5 | from substanced.sdi import mgmt_view
@mgmt_view(renderer='string', name='foobar')
def foobar(request):
return 'Foobar!'
|
Management views, in other words, are really just plain-old Pyramid views with a slightly shorter syntax for definition. Declaring a view a management view, however, does do some extra things that make it advisable to use rather than a plain Pyramid view registration:
- It registers introspectable objects that the SDI interface uses to try to find management interface tabs (the row of actions at the top of every management view rendering).
- It allows you to associate a tab title, a tab condition, and cross-site request forgery attributes with the view.
- It uses the default permission
sdi.view
.
So if you want things to work right when developing management views, you’ll
use @mgmt_view
instead of @view_config
, and config.add_mgmt_view
instead of config.add_view
.
As you use management views in the SDI, you might notice that the URL
includes @@
as “goggles”. For example,
http://0.0.0.0:6541/manage/@@contents
is the URL for seeing the
folder contents. The @@
is a way to ensure that you point at the
URL for a view and not get some resource with the __name__
of
contents
. You can still get to the folder contents management view
using http://0.0.0.0:6541/manage/contents
…until that folder
contains something named contents
.
mgmt_view
View Predicates¶
Since mgmt_view
is an extension of Pyramid’s view_config
,
it re-uses the same concept of view predicates as well as some of the
same actual predicates:
request_type
,request_method
,request_param
,containment
,attr
,renderer
,wrapper
,xhr
,accept
,header
,path_info
,context
,name
,custom_predicates
,decorator
,mapper
, andhttp_cache
are supported and behave the same.permission
is the same but defaults tosdi.view
.
The following are new view predicates introduced for mgmt_view
:
tab_title
takes a string for the label placed on the tab.tab_condition
takes a callable that returnsTrue
orFalse
, orTrue
orFalse
. If you state a callable, this callable is passedcontext
andrequest
. The boolean determines whether the tab is listed in a certain situation.tab_before
takes the view name of amgmt_view
that thismgmt_view
should appear after (covered in detail in the next section.)tab_after
takes the view name of amgmt_view
that thismgmt_view
should appear after. Also covered below.tab_near
takes a “sentinel” fromsubstanced.sdi
(orNone
) that makes a best effort at placement independent of another particularmgmt_view
. Also covered below. The possible sentinel values are:substanced.sdi.LEFT substanced.sdi.MIDDLE substanced.sdi.RIGHT
Tab Ordering¶
If you register a management view, a tab will be added in the list of tabs. If
no mgmt view specifies otherwise via its tab data, the tab order will use a
default sorting: alphabetical order by the tab_title
parameter of each tab
(or the view name if no tab_title
is provided.) The first tab in this tab
listing acts as the “default” that is open when you visit a resource. Substance
D does, though, give you some options to control tab ordering in larger systems
with different software registering management views.
Perhaps a developer wants to ensure that one of her tabs appears first in the
list and another appears last, no matter what other management views have been
registered by Substance D or any add-on packages. @mgmt_view
(or the
imperative call) allow a keyword of tab_before
or tab_after
. Each take
the string tab name
of the management view to place before or after. If
you don’t care (or don’t know) which view name to use as a tab_before
or
tab_after
value, use tab_near
, which can be any of the sentinel values
MIDDLE
, LEFT
, or
RIGHT
, each of which specifies a target “zone” in the
tab order. Substance D will make a best effort to do something sane with
tab_near
.
As in many cases, an illustration is helpful:
from substanced.sdi import LEFT, RIGHT
@mgmt_view(
name='tab_1',
tab_title='Tab 1',
renderer='templates/tab.pt'
)
def tab_1(context, request):
return {}
@mgmt_view(
name='tab_2',
tab_title='Tab 2',
renderer='templates/tab.pt',
tab_before='tab_1'
)
def tab_2(context, request):
return {}
@mgmt_view(
name='tab_3',
tab_title='Tab 3',
renderer='templates/tab.pt',
tab_near=RIGHT
)
def tab_3(context, request):
return {}
@mgmt_view(
name='tab_4',
tab_title='Tab 4',
renderer='templates/tab.pt',
tab_near=LEFT
)
def tab_4(context, request):
return {}
@mgmt_view(
name='tab_5',
tab_title='Tab 5',
renderer='templates/tab.pt',
tab_near=LEFT
)
def tab_5(context, request):
return {}
This set of management views (combined with the built-in Substance D
management views for Contents
and Security
) results in:
Tab 4 | Tab 5 | Contents | Security | Tab 2 | Tab 1 | Tab 3
These management view arguments apply to any content type that the view
is registered for. What if you want to allow a content type to
influence the tab ordering? As mentioned in the
content type docs, the tab_order
parameter
overrides the mgmt_view tab settings, for a content type, with a
sequence of view names that should be ordered (and everything
not in the sequence, after.)
Filling Slots¶
Each management view that you write plugs into various parts of the SDI
UI. This is done using normal ZPT fill-slot
semantics:
page-title
is the<title>
in the<head>
head-more
is a place to inject CSS and JS in the<head>
after all the SDI elementstail-more
does the same, just before the</body>
main
is the main content area
SDI API¶
All templates in the SDI share a common “layout”. This layout needs information from the environment to render markup that is common to every screen, as well as the template used as the “main template.”
This “template API” is known as the SDI API
. It is an instance of
the sdiapi
class in substanced.sdi.__init__.py
and is made
available as request.sdiapi
.
The template for your management view should start with a call to
requests.sdiapi
:
<div metal:use-macro="request.sdiapi.main_template">
The request.sdiapi
object has other convenience features as well.
See the Substance D interfaces documentation for more information.
Flash Messages¶
Often you perform an action on one view that needs a message displayed by another view on the next request. For example, if you delete a resource, the next request might confirm to the user “Deleted 1 resource.” Pyramid supports this with “flash messages.”
In Substance D, your applications can make a call to the sdiapi
such as:
request.sdiapi.flash('ACE moved up')
…and the next request will process this flash message:
- The message will be removed from the stack of messages
- It will then be displayed in the appropriate styling based on the “queue”
The sdiapi
provides another helper:
request.sdiapi.flash_with_undo(‘ACE moved up’)
This displays a flash message as before, but also provides an Undo
button to remove the previous transaction.
- title, content, flash messages, head, tail
Forms¶
When writing a Substance D application, you are free to use any library you would like for forms and schemas. This applies both for your retail views and for the management views that you plug into the SDI.
For the built-in content types and management views,
you will see that Substance D has standardized on Colander and
Deform for schemas and forms.
Additionally, Substance D defines a substanced.form.FormView
class, discussed below.
FormView
¶
Form handling is ground that is frequently covered, usually in different ways. Substance D provides a class to help implement common patterns in form handling.
Imagine this example:
@mgmt_view(
context=IFolder,
name='add_document',
tab_title='Add Document',
permission='sdi.add-content',
renderer='substanced.sdi:templates/form.pt',
tab_condition=False,
)
class AddDocumentView(FormView):
title = 'Add Document'
schema = DocumentSchema()
buttons = ('add',)
def add_success(self, appstruct):
registry = self.request.registry
name = appstruct.pop('name')
document = registry.content.create('Document', **appstruct)
self.context[name] = document
return HTTPFound(
self.request.mgmt_path(self.context, '@@contents'))
This mgmt_view
adds a view add_document
to resources with the
IFolder
interface. The form gets a title
, a Colander schema,
and asks for just one button.
Since the mgmt_view
is associated with a renderer,
we have an SDI template form.pt
which does the basics of laying out
the rendering before handing the work over to Deform.
The @action
of the form is the mgmt_view
itself,
making it a self-posting form. The button that was clicked causes the
FormView
to, upon validation success, route processing to a handler
for that button. By convention, FormView
looks for a method
starting with the name of the button (e.g. add
) and finishing with
_success
(e.g. add_success
.) The class also supports a similar
protocol for _failure
.
FormView
also supports the following methods that can be overridden:
before(self, form)
is called before validation and processing of any_success
or_failure
methodsfailure(self, e)
is called with the exception, if the there is no button-specific_failure
methodshow(self, form)
returns{'form':form.render()}
and thus can be a place to affect form rendering
Services¶
A service is a name for a content object that provides a service to application code. It looks just like any other content object, but services that are added to a site can be found by name using various Substance D APIs.
Services expose APIs that exist for the benefit of application developers. For
instance, the catalogs
service provides an API that allows a developer to
index and query for content objects using a structured query API. The
principals
service allows a developer to add and enumerate users and
groups.
A service is added to a folder via the
substanced.folder.Folder.add_service()
API.
An existing service can be looked up in one of two ways: using the
substanced.util.find_service()
API or the
substanced.folder.Folder.find_service()
API. They are functionally
equivalent. The latter exists only as a convenience so you don’t need to
import a function if you know you’re dealing with a folder.
Either variant of find_service
will look down the resource hierarchy
towards the root until it finds a parent folder that has had add_service
called on it. If the name passed in matches the service name, the object
will be returned, otherwise the search will continue down the tree.
Note that a content object may exist in the folder with the same name as
you’re looking for via find_service
, but if that object was not added via
add_service
(instead it’s just a “normal” content object), it won’t be
found by find_service
.
Here’s how to use substanced.util.find_service()
:
from substanced.util import find_service
principals = find_service(somecontext, 'principals')
somecontext
above is any resource in the resource tree.
For example, somecontext
could be a “document” object you’ve added to a
folder.
Here’s how to use substanced.folder.Folder.find_service()
:
principals = somefolder.find_service('principals')
somefolder
above is any substanced.folder.Folder
object (or any
object which inherits from that class) present in the resource tree.
There is also the find-multiple-services variants
substanced.util.find_services()
and
substanced.folder.Folder.find_services()
.
Cataloging¶
Substance D provides application content indexing and querying via a catalog.
A catalog is an object named catalog
which lives in a service named
catalogs
within your application’s resource tree. A catalog has a number
of indexes, each of which keeps a certain kind of information about your
content.
The Default Catalog¶
A default catalog named system
is installed into the root folder’s
catalogs
subfolder when you start Substance D. This system
catalog
contains a default set of indexes:
path (a
path
index)Represents the path of the content object.
name (a
field
index), usescontent.__name__
exclusivelyRepresents the local name of the content object.
interfaces (a
keyword
index)Represents the set of interfaces possessed by the content object.
content_type (a
field
index)Represents the Substance D content-type of an object.
allowed (an
allowed
index)An index which can be used to filter resultsets using principals and permissions.
text (a
text
index)Represents the text searched for when you use the filter box within the folder contents view of the SDI.
Adding a Catalog¶
The system
catalog won’t have enough information to form all the queries you
need. You’ll have to add a catalog via code related to your application. The first
step is adding a catalog factory.
A catalog factory is a collection of index descriptions. Creating a catalog factory doesn’t actually add a catalog to your database, but it makes it possible to add one later.
Here’s an example catalog factory named mycatalog
:
from substanced.catalog import (
catalog_factory,
Text,
Field,
)
@catalog_factory('mycatalog')
class MyCatalogFactory(object):
freaky = Text()
funky = Field()
In order to activate a @catalog_factory
decorator, it must be scanned using the
Pyramid config.scan()
machinery. This will allow you to use
substanced.catalog.CatalogsService.add_catalog()
to add a catalog with that
factory’s name:
# in a module named blog.__init__
from pyramid.config import Configurator
def main(global_config, **settings):
config = Configurator()
config.include('substanced')
config.scan('blog.catalogs')
# .. and so on ...
Once you’ve done this, you can then add the catalog to the database in any bit of code that has access to the database. For example, in an event handler when the root object is created for the first time.
from substanced.root import Root
from substanced.event import subscribe_created
@subscribe_created(Root)
def created(event):
root = event.object
service = root['catalogs']
service.add_catalog('mycatalog', update_indexes=True)
Object Indexing¶
Once a new catalog has been added to the database, each time a new
catalogable object is added to the site, its attributes will be indexed by
each catalog in its lineage that “cares about” the object. The object will
always be indexed in the “system” catalog. To make sure it’s cataloged in
custom catalogs, you’ll need to do some work. To index the object in a custom
application index, you will need to create an index view for your content
using substanced.catalog.indexview
, and scan the resulting index
view using pyramid.config.Configurator.scan()
:
For example:
from substanced.catalog import indexview
class MyCatalogViews(object):
def __init__(self, resource):
self.resource = resource
@indexview(catalog_name='mycatalog')
def freaky(self, default):
return getattr(self.resource, 'freaky', default)
An index view class should be a class that accepts a single argument,
(conventionally named resource
), in its constructor, and which has one or
more methods named after potential index names. When it comes time for the
system to index your content, Substance D will create an instance of your
indexview class, and it will then call one or more of its methods; it will call
methods on the indexview object matching the attr
passed in to
add_indexview
. The default
value passed in should be returned if the
method is unable to compute a value for the content object.
Once this is done, whenever an object is added to the system, a value (the
result of the freaky(default)
method of the catalog view) will be indexed in the
freaky
field index.
You can attach multiple index views to the same index view class:
from substanced.catalog import indexview
class MyCatalogViews(object):
def __init__(self, resource):
self.resource = resource
@indexview(catalog_name='mycatalog')
def freaky(self, default):
return getattr(self.resource, 'freaky', default)
@indexview(catalog_name='mycatalog')
def funky(self, default):
return getattr(self.resource, 'funky', default)
You can use the “index_name” parameter to indexview
to tell the system that
the index name is not the same as the method name in the index view:
from substanced.catalog import indexview
class MyCatalogViews(object):
def __init__(self, resource):
self.resource = resource
@indexview(catalog_name='mycatalog')
def freaky(self, default):
return getattr(self.resource, 'freaky', default)
@indexview(catalog_name='mycatalog', index_name='funky')
def notfunky(self, default):
return getattr(self.resource, 'funky', default)
You can use the context
parameter to indexview
to tell the system that
this particular index view should only be executed when the class of the
resource (or any of its interfaces) matches the value of the context:
from substanced.catalog import indexview
class MyCatalogViews(object):
def __init__(self, resource):
self.resource = resource
@indexview(catalog_name='mycatalog', context=FreakyContent)
def freaky(self, default):
return getattr(self.resource, 'freaky', default)
@indexview(catalog_name='mycatalog', index_name='funky')
def notfunky(self, default):
return getattr(self.resource, 'funky', default)
You can use the indexview_defaults
class decorator to save typing in each
indexview
declaration. Keyword argument names supplied to
indexview_defaults
will be used if the indexview
does not supply the
same keyword:
from substanced.catalog import (
indexview,
indexview_defaults,
)
@indexview_defaults(catalog_name='mycatalog')
class MyCatalogViews(object):
def __init__(self, resource):
self.resource = resource
@indexview()
def freaky(self, default):
return getattr(self.resource, 'freaky', default)
@indexview()
def notfunky(self, default):
return getattr(self.resource, 'funky', default)
The above configuration is the same as:
from substanced.catalog import indexview
class MyCatalogViews(object):
def __init__(self, resource):
self.resource = resource
@indexview(catalog_name='mycatalog')
def freaky(self, default):
return getattr(self.resource, 'freaky', default)
@indexview(catalog_name='mycatalog')
def notfunky(self, default):
return getattr(self.resource, 'funky', default)
You can also use the substanced.catalog.add_indexview()
directive to add
index views imperatively, instead of using the @indexview
decorator.
Querying the Catalog¶
You execute a catalog query using APIs of the catalog’s indexes.
from substanced.util import find_catalog
catalog = find_catalog(resource, 'system')
name = catalog['name']
path = catalog['path']
# find me all the objects that exist under /somepath with the name 'somename'
q = name.eq('somename') & path.eq('/somepath')
resultset = q.execute()
for contentob in resultset:
print contentob
The calls to name.eq()
and path.eq()
above each return a query object.
Those two queries are ANDed together into a single query via the
&
operator between them (there’s also the |
character to OR the
queries together, but we don’t use it above). Parentheses can be used to
group query expressions together for the purpose of priority.
Different indexes have different query methods, but most support the eq
method. Other methods that are often supported by indexes: noteq
, ge
,
le
, gt
, any
, notany
, all
, notall
, inrange
,
notinrange
. The AllowedIndex
supports
an additional allows()
method.
Query objects support an execute
method. This method returns a
hypatia.util.ResultSet
. A hypatia.util.ResultSet
can be iterated over; each iteration returns a content object.
hypatia.util.ResultSet
also has methods like one
and first
, which
return a single content object instead of a set of content objects. A
hypatia.util.ResultSet
also has a sort
method which accepts an index
object (the sort index) and returns another (sorted) hypatia.util.ResultSet
.
catalog = find_catalog(resource, 'system')
name = catalog['name']
path = catalog['path']
# find me all the objects that exist under /somepath with the name 'somename'
q = name.eq('somename') & path.eq('/somepath')
resultset = q.execute()
newresultset = resultset.sort(name)
Note
If you don’t call sort
on the hypatia.util.ResultSet
you get back,
the results will not be sorted in any particular order.
Querying Across Catalogs¶
In many cases, you might only have one custom attribute that you need
indexed, while the system
catalog has everything else you need. You
thus need an efficient way to combine results from two catalogs,
before executing the query:
system_catalog = find_catalog(resource, 'system')
my_catalog = find_catalog(resource, 'mycatalog')
path = system_catalog['path']
funky = my_catalog['funky']
# find me all funky objects that exist under /somepath
q = funky.eq(True) & path.eq('/somepath')
resultset = q.execute()
newresultset = resultset.sort(system_catalog['name'])
Filtering Catalog Results Using the Allowed Index¶
The Substance D system catalog at
substanced.catalog.system.SystemCatalogFactory
contains a number of
default indexes, including an allowed
index. Its job is to index security
information to allow security-aware results in queries. This index allows us
to filter queries to the system catalog based on whether the principal issuing
the request has a permission on the matching resource.
For example, the below query will find:
- all of the subresources inside a folder
- which is of content type
News Item
- which the current user also possesses the
view
permission against
system_catalog = find_catalog(resource, 'system')
path = system_catalog['path']
content_type = system_catalog['content_type']
allowed = system_catalog['allowed']
q = ( path.eq(resource, depth=1, include_origin=False) &
content_type.eq('News Item') &
allowed.allows(request, 'view')
)
return q
Filtering Catalog Results Using The Objectmap¶
It is possible to postfilter catalog results using the
substanced.objectmap.ObjectMap.allowed()
API. For example:
def get_allowed_to_view(context, request):
catalog = find_catalog(context, 'system')
q = catalog['content_type'].eq('News Item')
resultset = q.execute()
objectmap = find_objectmap(context)
return objectmap.allowed(
resultset.oids, request.effective_principals, 'view')
The result of allowed()
is a generator
which returns oids, so the result must be listified if you intend to index into
it, or slice it, or what-have-you.
Setting ACLs¶
The objectmap keeps track of ACLs in a cache to make catalog security
functionality work. Note that for the object map’s cached version of ACLs to
be correct, you will need to set ACLs in a way that helps keep track of all the
contracts. For this, the helper function substanced.util.set_acl()
can
be used. For example, the site root at substanced.root.Root
finishes
with:
set_acl(
self,
[(Allow, get_oid(admins), ALL_PERMISSIONS)],
registry=registry,
)
Using set_acl
this way will generate an event that will keep the
objectmap’s cache updated. This will allow the allowed
index to work and
the substanced.objectmap.ObjectMap.allowed()
method to work.
Deferred Indexing and Mode Parameters¶
As a lesson learned from previous cataloging experience, Substance D natively supports deferred indexing. As an example, in many systems the text indexing can be done after the change to the object is committed in the web request’s transaction. Doing so has a number of performance benefits: the user’s request processes more quickly, the work to extract text from a Word file can be performed later, less chance to have a conflict error, etc.
As such, the
substanced.catalog.system.SystemCatalogFactory
, by default,
has indexes that aren’t updated immediately when a resource is
changed. For example:
# name is MODE_ATCOMMIT for next-request folder contents consistency
name = Field()
text = Text(action_mode=MODE_DEFERRED)
content_type = Field()
The Field
indexes use the default of MODE_ATCOMMIT. The Text
overrides the default and set action_mode
to MODE_DEFERRED.
There are three such catalog “modes” for indexing:
substanced.interfaces.MODE_IMMEDIATE
means indexing action should take place as immediately as possible.substanced.interfaces.MODE_ATCOMMIT
means indexing action should take place at the successful end of the current transaction.substanced.interfaces.MODE_DEFERRED
means indexing action should be performed by an external indexing processor (e.g.drain_catalog_indexing
) if one is active at the successful end of the current transaction. If an indexing processor is unavailable at the successful end of the current transaction, this mode will be taken to imply the same thing asMODE_ATCOMMIT
.
Running an Indexer Process¶
Great, we’ve now deferred indexing to a later time. What exactly do we do at that later time?
Indexer processes are easy to write and schedule with supervisor
.
Here is an example of a configuration for supervisor.conf
that will
run in indexer process every five seconds:
[program:indexer]
command = %(here)s/../bin/sd_drain_indexing %(here)s/production.ini
redirect_stderr = true
stdout_logfile = %(here)s/../var/indexing.log
autostart = true
startsecs = 5
This calls sd_drain_indexing
which is a console script that
Substance D automatically creates in your bin
directory. Indexing
messages are logged with standard Python logging to the file that you
name. You can view these messages with the supervisorctl
command
tail indexer
. For example, here is the output from
sd_drain_indexing
when changing a simple Document
content type:
2013-01-07 11:07:38,306 INFO [substanced.catalog.deferred][MainThread] no actions to execute
2013-01-07 11:08:38,329 INFO [substanced.catalog.deferred][MainThread] executing <substanced.catalog.deferred.IndexAction object oid 5886459017869105529 for index u'text' at 0x106e52910>
2013-01-07 11:08:38,332 INFO [substanced.catalog.deferred][MainThread] executing <substanced.catalog.deferred.IndexAction object oid 5886459017869105529 for index u'interfaces' at 0x106e52dd0>
2013-01-07 11:08:38,333 INFO [substanced.catalog.deferred][MainThread] executing <substanced.catalog.deferred.IndexAction object oid 5886459017869105529 for index u'content_type' at 0x1076e2ed0>
2013-01-07 11:08:38,334 INFO [substanced.catalog.deferred][MainThread] committing
2013-01-07 11:08:38,351 INFO [substanced.catalog.deferred][MainThread] committed
Overriding Default Modes Manually¶
Above we set the default mode used by an index when Substance D indexes a resource automatically. Perhaps in an evolve script, you’d like to override the default mode for that index and reindex immediately.
The index_resource
on an index can be passed an action_mode
flag that overrides the configured mode for that index, and instead,
does exactly what you want for only that call. It does not permanently
change the configured default for indexing mode. This applies also to
reindex_resource
and unindex_resource
. You can also grab the
catalog itself and reindex with a mode that overrides all default modes
on each index.
Autosync and Autoreindex¶
If you add substanced.catalogs.autosync = true
within your application’s
.ini
file, all catalog indexes will be resynchronized with their catalog
factory definitions at application startup time. Indices which were added to
the catalog factory since the last startup time will be added to each catalog
which uses the index factory. Likewise, indices which were removed will be
removed from each catalog, and indices which were modified will be modified
according to the catalog factory. Having this setting in your .ini
file is
like pressing the Update indexes
button on the Manage
tab of each of
your catalogs. The SUBSTANCED_CATALOGS_AUTOSYNC
environment variable can
also be used to turn this behavior on. For example export
SUBSTANCED_CATALOGS_AUTOSYNC=true
.
If you add substanced.catalogs.autoreindex = true
within your application’s
.ini
file, all catalogs that were changed as the result of an auto-sync
will automatically be reindexed. Having this setting in your .ini
file is
like pressing the Reindex catalog
button on the Manage
tab of each
catalog which was changed as the result of hitting Update indexes
. The
SUBSTANCED_CATALOGS_AUTOREINDEX
environment variable can also be used to
turn this behavior on. For example export
SUBSTANCED_CATALOGS_AUTOREINDEX=true
.
Forcing Deferral of Indexing¶
There may be times when you’d like to defer all catalog indexing operations,
such as during a bulk load of data from a script. Normally, only indexes
marked with MODE_DEFERRED
use deferred indexing, and actions associated
with those indexes are even then only actually deferred if an index processor
is active.
You can force Substance D to defer all catalog indexing using the
substanced.catalogs.force_deferred
flag in your application’s .ini
file. When this flag is used, all catalog indexing operations will be added to
the indexer’s queue, even those indexes marked as MODE_IMMEDIATE
or
MODE_ATCOMMIT
. Deferral will also happen whether or not the indexer is
running, unlike during normal operations.
When you use this flag, you can stop the indexer process, do your bulk load, and start the indexer again when it’s convenient to have all the content indexing done in the background.
The SUBSTANCED_CATALOGS_FORCE_DEFERRED
environment variable can also be
used to turn this behavior on. For example export
SUBSTANCED_CATALOGS_FORCE_DEFERRED=true
.
References¶
Objects that live in the Substance D resource tree can be related to one another using references.
The most user-visible facet of references is the SDI “References” tab, which is presented to SDI admin users when the object they’re looking at is involved in a reference relation. For example, you’ll notice that the built-in user and group implementations already have references to each other, and you can visit their References tabs to see them. Likewise, when you use the Security tab to change the ACL associated with an object, and include in the ACL a user or group that lives in the principals folder, a relation is formed between the ACL-bearing object and the principal. So, as you can see, references aren’t just for application developers; Substance D itself uses references under the hood to do its job too.
A reference has a type and a direction. A reference is formed using methods of the object map.
from substanced.interfaces import ReferenceType
from substanced.objectmap import find_objectmap
class ContextToRoot(ReferenceType):
pass
def connect_reference(context, request):
objectmap = find_objectmap(context)
root = request.root
objectmap.connect(context, root, ContextToRoot)
A reference type is a class (not an instance) that inherits from
substanced.interfaces.ReferenceType
. The reference’s name should
indicate its directionality.
Warning
One caveat: reference types are pickled, so if you move a reference type
from one location to another, you’ll have to leave behind a backwards
compatibility import in its original location “forever”, so choose its name
and location wisely. We recommend that you place it in an interfaces.py
file in your project.
A reference can be removed using the object map too:
from substanced.interfaces import ReferenceType
from substanced.objectmap import find_objectmap
class ContextToRoot(ReferenceType):
pass
def disconnect_reference(context, request):
objectmap = find_objectmap(context)
root = request.root
objectmap.disconnect(context, root, ContextToRoot)
The first two arguments to connect()
or
disconnect()
are source and target.
These can be either resource objects or oids. The third argument to these
functions is the reference type.
Once a reference is formed between two objects, you can see the reference within the “References” tab in the SDI. The References tab of either side of the reference (in the above example, either the root or the context) when visited in the SDI will display the reference to the other side.
Once a reference is made between two objects, the object map can be queried for objects which take part in the reference.
from substanced.interfaces import ReferenceType
from substanced.objectmap import find_objectmap
class ContextToRoot(ReferenceType):
pass
def query_reference_sources(context, request):
objectmap = find_objectmap(context)
return objectmap.sourceids(request.root, ContextToRoot)
def query_reference_targets(context, request):
objectmap = find_objectmap(context)
return objectmap.targetids(context, ContextToRoot)
The sourceids()
method returns the set of
objectids which are sources of the object and reference type it’s passed.
The targetids()
method returns the set of
objectids which are targets of the object and reference type it’s passed. If
no objects are involved in the relation, an empty set will be returned in
either case. sources()
and
targets()
methods also exist which are
analogous, but return the actual objects involved in the relation instead of the
objectids:
from substanced.interfaces import ReferenceType
from substanced.objectmap import find_objectmap
class ContextToRoot(ReferenceType):
pass
def query_reference_sources(context, request):
objectmap = find_objectmap(context)
return objectmap.sources(request.root, ContextToRoot)
def query_reference_targets(context, request):
objectmap = find_objectmap(context)
return objectmap.targets(context, ContextToRoot)
A reference type can claim that it is “integral”, which just means that the deletion of either the source or the target of a reference will be prevented. Here’s an example of a “source integral” reference type:
from substanced.interfaces import ReferenceType
class UserToGroup(ReferenceType):
source_integrity = True
This reference type will prevent any object on the “user” side of the
UserToGroup reference (as opposed to the group side) from being deleted. When
a user attempts to delete a user that’s related to a group using this reference
type, a substanced.objectmap.SourceIntegrityError
will be raised and
the deletion will be prevented. Only when the reference is removed or the
group is deleted will the user deletion be permitted.
The flip side of this is target integrity:
from substanced.interfaces import ReferenceType
class UserToGroup(ReferenceType):
target_integrity = True
This is the inverse. The reference will prevent any object on the “group” side
of the UserToGroup reference from being deleted unless the associated user is
first removed or the reference itself is no longer active. When a user
attempts to delete a user that’s related to a group using this reference type,
a substanced.objectmap.TargetIntegrityError
will be raised and the
deletion will be prevented.
substanced.objectmap.SourceIntegrityError
and
substanced.objectmap.TargetIntegrityError
both inherit from
substanced.objectmap.ReferentialIntegrityError
, so you can catch
either in your code.
There are convenience functions that you can add to your resource objects that
give them special behavior:
reference_sourceid_property()
,
reference_targetid_property()
,
reference_source_property()
,
reference_target_property()
,
multireference_sourceid_property()
,
multireference_targetid_property()
,
reference_source_property()
, and
reference_target_property()
.
Here’s use of a reference property:
1 2 3 4 5 6 7 8 9 | from persistent import Persistent
from substanced.objectmap import reference_sourceid_property
from substanced.interfaces import ReferenceType
class LineItemToOrder(ReferenceType):
pass
class LineItem(Persistent):
order = reference_target_property(LineItemToOrder)
|
Once you’ve seated a resource object in a folder, you can then begin to use its special properties:
1 2 3 4 5 | from mysystem import LineItem, Order
lineitem = LineItem()
folder['lineitem'] = lineitem
lineitem.order = Order()
|
This is just a nicer way to use the objectmap query API; you don’t have to
interact with it at all, just assign and ask for attributes of your object.
The multireference_*
variants are similar to the reference variants, but
they allow for more than one object on the “other side”.
ACLs and Principal References¶
When an ACL is modified on a resource, a statement is being made about a relationship between that resource and a principal or group of principals. Wouldn’t it be great if a reference was established, allowing you to then see such connections in the SDI?
This is indeed exactly how Substance D behaves: a source-integral PrincipalToACLBearing reference is set up between an ACL-bearing resource and the principals referred to within the ACL.
Workflows¶
A workflow is a collection of transitions that transition
between states. Specifically, substanced.workflow
implements
event-driven finite-state machine workflows.
Workflows are used to ease following tasks when content goes through the lifecycle:
- updating security (adding/removing permissions)
- sending emails
- …
States and transitions together with metadata are stored on the
Workflow
. Workflows are stored in
config.registry.workflows
. The only thing that content has from the
workflow machinery is content.__workflow_state__
attribute that stores a
dict of all workflow types and corresponding states assigned. When content is
added to the database (ObjectAdded
event is
emitted), all relevant registered workflows are initialized for it.
Features¶
- Site-wide workflows
- Multiple workflows per object
- Content type specific workflows
- Restrict transitions by permission
- Configurable callbacks when entering state
- Configurable callbacks when executing transition
- Reset workflow to initial state
Adding a workflow¶
Suppose we want to add a simple workflow:
/-----\ <-- to_draft ----- /---------\
|draft| |published|
\-----/ --- to_publish --> \---------/
Using add_workflow()
Pyramid configuration
directive:
>>> workflow = Workflow(initial_state="draft", type="article")
>>> workflow.add_state("draft")
>>> workflow.add_state("published")
>>> workflow.add_transition('to_publish', from_state='draft', to_state='published')
>>> workflow.add_transition('to_draft', from_state='published', to_state='draft')
...
>>> config.add_workflow(workflow, ('News',))
Interaction with the workflow¶
Retrieve a Workflow
instance using
the substanced.workflow.get_workflow()
:
>>> from substanced.workflow import get_workflow
>>> workflow = get_workflow(request, type='article', content_type='News')
Suppose there is a context object at hand, you can
reset()
its workflow to initial state:
>>> workflow.reset(context, request)
You could check it has_state()
and assert
state_of()
context is initial state name
of the workflow:
>>> assert workflow.has_state(context) == True
>>> assert workflow.state_of(context) == workflow.initial_state
List possible transitions from the current state of the workflow
with get_transitions()
:
>>> workflow.get_transitions(context, request)
[{'from_state': 'draft',
'callback': None,
'permission': None,
'name': 'to_publish',
'to_state': 'published'}]
Execute a transition()
:
>>> workflow.transition(context, request, 'to_publish')
List all states of the workflow with
get_states()
:
>>> workflow.get_states(context, request)
[{'name': 'draft',
'title': 'draft',
'initial': True,
'current': False,
'transitions': [{'from_state': 'draft',
'callback': None,
'permission': None,
'name': 'to_publish',
'to_state': 'published'}],
'data': {'callback': None}},
{'name': 'published',
'title': 'published',
'initial': False,
'current': True,
'transitions': [{'from_state': 'published',
'callback': None,
'permission': None,
'name': 'to_draft',
'to_state': 'draft'}],
'data': {'callback': None}}]
Execute a transition_to_state()
:
>>> workflow.transition_to_state(context, request, 'draft')
Using callbacks¶
Typically you will want to define custom actions when transition is executed or when content enters a specific state. Let’s define a transition with a callback:
>>> def cb(context, **kw):
... print "keywords: ", kw
>>> workflow.add_transition('to_publish_with_callback',
... from_state='draft',
... to_state='published',
... callback=cb)
When you execute the transition, callback is called:
>>> workflow.transition(context, request, 'to_publish_with_callback')
keywords: {'workflow': <Workflow ...>, 'transition': {'to_state': 'published', 'from_state': 'draft', ...}, request=<Request ...>}
To know more about callback parameters, read
add_transition()
signature.
Dumping Content to Disk¶
Substance D’s object database stores native Python representations of
resources. This is easy enough to work with: you can run
bin/pshell
to get an interactive prompt, write longer ad-hoc
console scripts, or just put code into your application.
However, production sites usually want exportable representations of important data stored in a long-term format. For this, Substance D provides a dump facility for content types to be serialized in a YAML representation on disk.
Note
You’ll note in the following the absence of docs on loading data. This is intentional. The process of loading data into a new, or semi-new, or newer-than-new site has many policy implications. Too many to fit into a single loading script. Substance D considers the particulars of loading data to be in the province of the application developer.
Dumping Resources Using sd_dump
¶
The sd_dump
console script loads your Substance D application,
connects to your object database, and writes serialized representations
of resources to disk in a directory hierarchy:
$ ../bin/sd_dump --help
Usage: sd_dump [options]
Dump an object (and its subobjects) to the filesystem: sd_dump [--source
=ZODB-PATH] [--dest=FILESYSTEM-PATH] config_uri Dumps the object at ZODB-
PATH and all of its subobjects to a filesystem path. Such a dump can be
loaded (programmatically) by using the substanced.dump.load function e.g.
sd_dump --source=/ --dest=/my/dump etc/development.ini
Options:
-h, --help show this help message and exit
-s ZODB-PATH, --source=ZODB-PATH
The ZODB source path to dump (e.g. /foo/bar or /)
-d FILESYSTEM-PATH, --dest=FILESYSTEM-PATH
The destination filesystem path to dump to.
For example:
$ ../bin/sd_dump ../etc/development.ini
2013-01-07 13:27:03,939 INFO [ZEO.ClientStorage][MainThread] ('localhost', 9963) ClientStorage (pid=93148) created RW/normal for storage: 'main'
2013-01-07 13:27:03,941 INFO [ZEO.cache][MainThread] created temporary cache file '<fdopen>'
2013-01-07 13:27:03,981 WARNI [ZEO.zrpc][Connect([(2, ('localhost', 9963))])] (93148) CW: error connecting to ('fe80::1%lo0', 9963): EHOSTUNREACH
2013-01-07 13:27:03,982 WARNI [ZEO.zrpc][Connect([(2, ('localhost', 9963))])] (93148) CW: error connecting to ('fe80::1%lo0', 9963): EHOSTUNREACH
2013-01-07 13:27:04,002 WARNI [ZEO.zrpc][Connect([(2, ('localhost', 9963))])] (93148) CW: error connecting to ('::1', 9963): EINVAL
2013-01-07 13:27:04,003 INFO [ZEO.ClientStorage][Connect([(2, ('localhost', 9963))])] ('localhost', 9963) Testing connection <ManagedClientConnection ('127.0.0.1', 9963)>
2013-01-07 13:27:04,004 INFO [ZEO.zrpc.Connection(C)][('localhost', 9963) zeo client networking thread] (127.0.0.1:9963) received handshake 'Z3101'
2013-01-07 13:27:04,105 INFO [ZEO.ClientStorage][Connect([(2, ('localhost', 9963))])] ('localhost', 9963) Server authentication protocol None
2013-01-07 13:27:04,106 INFO [ZEO.ClientStorage][Connect([(2, ('localhost', 9963))])] ('localhost', 9963) Connected to storage: ('localhost', 9963)
2013-01-07 13:27:04,108 INFO [ZEO.ClientStorage][Connect([(2, ('localhost', 9963))])] ('localhost', 9963) No verification necessary -- empty cache
2013-01-07 13:27:04,727 INFO [substanced.catalog][MainThread] system update_indexes: no indexes added or removed
2013-01-07 13:27:04,730 INFO [substanced.catalog][MainThread] sdidemo update_indexes: no indexes added or removed
2013-01-07 13:27:04,732 INFO [substanced.dump][MainThread] Dumping /
2013-01-07 13:27:04,749 INFO [substanced.dump][MainThread] Dumping /principals
2013-01-07 13:27:04,754 INFO [substanced.dump][MainThread] Dumping /principals/users
2013-01-07 13:27:04,760 INFO [substanced.dump][MainThread] Dumping /principals/users/admin
2013-01-07 13:27:04,779 INFO [substanced.dump][MainThread] Dumping /principals/resets
2013-01-07 13:27:04,783 INFO [substanced.dump][MainThread] Dumping /principals/groups
…with logging messages being emitted until all known content is
dumped. A dump
subdirectory in the current directory is created (if
no argument is provided) containing:
$ ls
acl.yaml propsheets references.yaml resource.yaml resources
Note
To correctly encode as much meaning as possible, the dump files contain some advanced and custom YAML constructs when needed.
acl.yaml
For Security Settings¶
This YAML file contains security settings for this resource. For example:
- !!python/tuple [Allow, 1644064392535565429, !all_permissions '']
references.yaml
for Reference Information¶
Data about references aren’t stored on the resources involved in the reference. Instead, they are stored in the objectmap. This file contains the reference information for the resource identified at the current dump directory. For example:
!interface 'substanced.interfaces.PrincipalToACLBearing':
sources: [1644064392535565429]
workflow.yaml
for Workflow Settings¶
The workflow engine can contain information about resource state. For example:
!!python/object:persistent.mapping.PersistentMapping
data: {document: draft}
propsheets
Directory for Property Sheet Data¶
Resources can have multiple system-defined or application-defined
property sheets on resources. These are serialized as subdirectories
under propsheets
, with a directory for each property sheet. For
example, a resources propsheets/Basic/properties.yaml
might contain:
{body: !!python/unicode 'The quick brown fox jumps over the lazy dog. The quick brown
fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the
lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps
over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown
fox jumps over the lazy dog. ', name: !!python/unicode 'document_0', title: !!python/unicode 'Document
0 Binder 0'}
resource.yaml
for Content Type Information¶
Each directory after the top corresponds to a resource in the database. As such, the resource likely has content type information. The dump script encodes this into a YAML file in the resource’s dump directory:
{content_type: Root, created: !!timestamp '2013-01-07 14:23:23.133436', is_service: false,
name: null, oid: 1644064392535565415}
resources
for Contained Resources in Containers¶
If the resource at a current dump directory is a Folder
or some
other kind of container, it will contain a resources
subdirectory.
This might contain more subfolders and thus subdirectories. It might
also contain individual resources, as a subdirectory named with the
resource name.
Custom Dumping with __dump__
¶
The built-in facilities allow automatic dumping of most information for your content, including information in your property sheets, the content type, security settings, references, workflows, etc.
If you do need extra information dumped to YAML about your content type,
Substance D has a Python protocol using an __dump__
on your
@content
class. As an example,
:py:meth:substanced.principal.User.dump
is a callable which returns
a mapping of simple Python objects. The dumper checks to see if a
resource has a __dump__
method. If so, it calls the method,
encodes the result to YAML, and writes it to an adhoc.yaml
file in
the dumped-resource’s directory.
The inverse is also true. If a content type has a __load__
method,
information from that method is added to the state that is loaded.
Adding New Dumpers¶
The adhoc.yaml
file that we just saw is an example of the
AdhocAttrDumper
. There are seven other dumpers built-in: acl,
workflow, references, sdiproperties, interfaces, order, and propsheets.
If you would like a custom dumper, you can register it with
config.add_dumper
. For example,
substanced.dump.includeme()
registers the existing dumpers and
their dumper factories:
def includeme(config):
DEFAULT_DUMPERS = [
('acl', ACLDumper),
('workflow', WorkflowDumper),
('references', ReferencesDumper),
('sdiproperties', SDIPropertiesDumper),
('interfaces', DirectlyProvidedInterfacesDumper),
('order', FolderOrderDumper),
('propsheets', PropertySheetDumper),
('adhoc', AdhocAttrDumper),
]
config.add_directive('add_dumper', add_dumper)
for dumper_name, dumper_factory in DEFAULT_DUMPERS:
config.add_dumper(dumper_name, dumper_factory)
Changing Resource Structure With Evolution¶
As you develop your software and make changes to structures, your existing content will be in an old state. Whether in production or during development, you need a facility to correct out-of-date data.
Evolution provides a rich facility for “evolving” your resources to match changes during development. Substance D’s evolution facility gives Substance D developers full control over the data updating process:
- Write scripts for each package that get called during an update
- Set revision markers in the data to indicate the revision level a database is at
- Console script and SDI GUI that can be run to “evolve” a database
Running an Evolution from the Command Line¶
Substance D applications generate a console script at
bin/sdi_evolve
. Running this without arguments displays some help:
$ bin/sd_evolve
Requires a config_uri as an argument
sd_evolve [--latest] [--dry-run] [--mark-finished=stepname] [--mark-unfinished=stepname] config_uri
Evolves new database with changes from scripts in evolve packages
- with no arguments, evolve displays finished and unfinished steps
- with the --latest argument, evolve runs scripts as necessary
- with the --dry-run argument, evolve runs scripts but does not issue any commits
- with the --mark-finished argument, marks the stepname as finished
- with the --mark-unfinished argument, marks the stepname as unfinished
e.g. sd_evolve --latest etc/development.ini
Running with your INI file, as explained in the help, shows information about the version numbers of various packages:
$ bin/sd_evolve etc/development.ini
Finished steps:
2013-06-14 13:01:28 substanced.evolution.legacy_to_new
Unfinished steps:
This shows that one evolution step has already been run and that there are no unfinished evolution steps.
Running an Evolution from the SDI¶
The Evolution section of the Database
tab of the Substance D root object
allows you to do what you might have otherwise done using the sd_evolve
console script described above.
In some circumstances when Substance D itself needs to be upgraded, you may
need to use the sd_evolve
script rather than the GUI. For example, if the
way that Substance D Folder
objects work is changed and folder objects need
to be evolved, it may be impossible to view the evolution GUI, and you may need
to use the console script.
Autoevolve¶
If you add substanced.autoevolve = true
within your application .ini file,
all pending evolution upgrade steps will be run when your application starts.
Alternately you can use the SUBSTANCED_AUTOEVOLVE
evnironment variable
(e.g. export SUBSTANCED_AUTOEVOLVE=true
) to do the same thing.
Adding Evolution Support To a Package¶
Let’s say we have been developing an sdidemo
package and,
with content already in the database, we want to add evolution support.
Our sdidemo
package is designed to be included into a site,
so we have the traditional Pyramid includeme
support. In there we
add the following:
import logging
logger = logging.getLogger('evolution')
def evolve_stuff(root, registry):
logger.info('Stuff evolved.')
def includeme(config):
config.add_evolution_step(evolve_stuff)
We’ve used the substanced.evolution.add_evolution_step()
API to add an
evolution step in this package’s includeme
function.
Running sd_evolve
without --latest
(meaning,
without performing an evolution) shows that Substance D’s evolution now
knows about our package:
$ bin/sd_evolve etc/development.ini
Finished steps:
2013-06-14 13:01:28 substanced.evolution.legacy_to_new
Unfinished steps:
sdidemo.evolve_stuff
Let’s now run sd_evolve
“for real”. This will cause the evolution step to
be executed and marked as finished.
$ bin/sd_evolve --latest etc/development.ini
2013-06-14 13:22:51,475 INFO [evolution][MainThread] Stuff evolved.
Evolution steps executed:
substanced.evolution.evolve_stuff
This examples shows a number of points:
- Each package can easily add evolution support via the
config.add_evolution_step()
directive. You can learn more about this directive by reading its API documentation atsubstanced.evolution.add_evolution_step()
. - Substance D’s evolution service looks at the database to see which steps haven’t been run, then runs all the needed evolve scripts, sequentially, to bring the database up to date.
- All changes within an evolve script are in the scope of a transaction. If all the evolve scripts run to completion without exception, the transaction is committed.
Manually Marking a Step As Evolved¶
In some cases you might have performed the work in an evolve step by hand and
you know there is no need to re-perform that work. You’d like to mark the step
as finished for one or more evolve scripts, so these steps don’t get run. The
--mark-step-finished
argument to sd_evolve
accomplishes this. The
“Mark finished” button in the SDI evolution GUI does the same.
Baselining¶
Evolution is baselined at first startup. When there’s no initial list of finished steps in the database. Substance D, in the root factory, says: “I know all the steps participating in evolution, so when I first create the root object, I will set all of those steps to finished.”
If you wish to perform something after Root
was
created, see Affecting Content Creation.
Configuring Folder Contents¶
The folder contents, as mentioned previously in Folder contents, the SDI’s folder contents uses a powerful datagrid to view and manage items in a folder. This chapter covers how your content types can plug into the folder contents view.
Adding Columns¶
Perhaps your system has content types with extra attributes that are
meaningful and you’d like your contents listings to show that column.
You can change the columns available on folder contents listings by
passing in a columns
argument to the @content
directive. The
value of this argument is a callable which returns a sequence of
mappings conforming to the datagrid’s contract. For example:
def binder_columns(folder, subobject, request, default_columnspec):
subobject_name = getattr(subobject, '__name__', str(subobject))
objectmap = find_objectmap(folder)
user_oid = getattr(subobject, 'creator', None)
created = getattr(subobject, 'created', None)
modified = getattr(subobject, 'modified', None)
if user_oid is not None:
user = objectmap.object_for(user_oid)
user_name = getattr(user, '__name__', 'anonymous')
else:
user_name = 'anonymous'
if created is not None:
created = created.isoformat()
if modified is not None:
modified = modified.isoformat()
return default_columnspec + [
{'name': 'Title',
'value': getattr(subobject, 'title', subobject_name),
},
{'name': 'Created',
'value': created,
'formatter': 'date',
},
{'name': 'Last edited',
'value': modified,
'formatter': 'date',
},
{'name': 'Creator',
'value': user_name,
}
]
@content(
'Binder',
icon='glyphicon glyphicon-book',
add_view='add_binder',
propertysheets = (
('Basic', BinderPropertySheet),
),
columns=binder_columns,
)
The callable is passed the folder, a subobject, the request
,
and a set of default column specifications. To display the datagrid
column headers, your callable is invoked on the first resource.
Later, this callable is used to get the value for the fields of each
column for each resource in a request’s batch.
The mappings returned can indicate whether a particular column should be
sorted. If you want your column to be sortable, you must provide a sorter
key in the mapping. If supplied, the sorter
value must either be None
if the column is not sortable, or a function which accepts a resource (the
folder), a “resultset”, a limit
keyword argument, and a reverse
keyword
argument and which must return a sorted result set. Here’s an example sorter:
from substanced.util import find_index
def sorter(folder, resultset, reverse=False, limit=None):
index = find_index(folder, 'mycatalog', 'date')
if index is not None:
resultset = resultset.sort(index, reverse=reverse, limit=limit)
return resultset
def my_columns(folder, subobject, request, default_columnspec):
return default_columnspec + [
{'name': 'Date',
'value': getattr(subobject, 'title', subobject_name),
'sorter': 'sorter',
},
Most often, sorting is done by passing a catalog index into the resultset.sort method as above (resultset.sort returns another resultset), but sorting can be performed manually, as long as the sorter returns a resultset.
Buttons¶
As we just showed, you can extend the folder contents with extra columns to display and possibly sort on. You can also add new buttons that will trigger operations on selected resources.
As with columns, we pass a new argument to the @content
directive.
For example, the folder contents view for the catalogs folder allows you
to reindex multiple indexes at once:

The Reindex
button illustrates a useful facility for performing
many custom operations at once.
The substanced.catalog
module’s @content
directive has a
buttons
argument:
@content(
'Catalog',
icon='glyphicon glyphicon-search',
service_name='catalog',
buttons=catalog_buttons,
)
This points at a callable:
def catalog_buttons(context, request, default_buttons):
""" Show a reindex button before default buttons in the folder contents
view of a catalog"""
buttons = [
{'type':'single',
'buttons':
[
{'id':'reindex',
'name':'form.reindex',
'class':'btn-primary btn-sdi-sel',
'value':'reindex',
'text':'Reindex'}
]
}
] + default_buttons
return buttons
In this case, the Reindex
button was inserted before the other
buttons, in the place where an add button would normally appear.
The class
on your buttons affect behavior in the datagrid:
btn-primary
gives this button the styling for the primary button of a form, using Twitter Bootstrap form stylingbtn-sdi-act
makes the button always enabledbtn-sdi-sel
disables the button until one or more items are selectedbtn-sdi-one
disables the button until exactly one item is selectedbtn-sdi-del
disables the button if any of the selected resources is marked as “non-deletable” (discussed below)
When clicked, this button will do a form POST
of the selected
docids to a view that you have implemented. Which view? The
'name': 'form.reindex'
item sets the parameter on the POST. You can
then register a view against this.
substanced.catalog.views.catalog
shows this:
@mgmt_view(
context=IFolder,
content_type='Catalog',
name='contents',
request_param='form.reindex',
request_method='POST',
renderer='substanced.folder:templates/contents.pt',
permission='sdi.manage-contents',
tab_condition=False,
)
def reindex_indexes(context, request):
toreindex = request.POST.getall('item-modify')
if toreindex:
context.reindex(indexes=toreindex, registry=request.registry)
request.sdiapi.flash(
'Reindex of selected indexes succeeded',
'success'
)
else:
request.sdiapi.flash(
'No indexes selected to reindex',
'danger'
)
return HTTPFound(request.sdiapi.mgmt_path(context, '@@contents'))
Selection and Button Enabling¶
As mentioned above, some buttons are driven by the selection. If nothing is selected, the button is disabled.
Buttons can also be disabled if any selected item is “non-deletable”.
How does that get signified? An item is ‘deletable’ if the user has
the sdi.manage-contents
permission on folder
and if the
subobject has a __sdi_deletable__
attribute which resolves to a
boolean True
value.
It is also possible to make button enabling and disabling depend on some
application-specific condition. To do this, assign a callable to the
enabled_for
key in the button spec. For example:
def catalog_buttons(context, request, default_buttons):
def is_indexable(folder, subobject, request):
""" only enable the button if subobject is indexable """
return subobject.is_indexable()
buttons = [
{'type':'single',
'buttons':
[
{'id':'reindex',
'name':'form.reindex',
'class':'btn-primary btn-sdi-sel',
'value':'reindex',
'enabled_for': is_indexable,
'text':'Reindex'}
]
}
] + default_buttons
return buttons
In the example above, we define a button similar to our previous reindex
button, except this time we have an enabled_for
key that is assigned
the is_indexable
function. When the buttons are rendered, each element
is passed to this function, along with the folder and request. If any one
of the folder subobjects returns False
for this call, the button will
not be enabled.
Filtering What Can Be Added¶
Not all kinds of resources make sense to be added inside a certain kind
of container. For example, substanced.catalog.Catalog
is a content type that can hold only indexes. That is,it isn’t meant to
hold any arbitrary kind of thing.
To tell the SDI what can be added inside a container content type, add a
__sdi_addable__
method to your content type. This method is passed the
folder object representing the place the object might be added, and a Substance
D introspectable for a content type. When Substance D tries to
figure out whether an object is addable to a particular folder, it will call
the __sdi_addable__
method of your folderish type once for each content
type.
The introspectable is a dictionary-like object which contains information about the content type. The introspectable contains the following keys:
meta
- A dictionary representing “meta” values passed to
add_content_type()
. For example, if you passadd_view='foo'
toadd_content_type()
, themeta
of the content type will be{'add_view':'foo'}
. content_type
- The content type value passed to
add_content_type()
. factory_type
- The
factory_type
value passed toadd_content_type()
. original_factory
- The original content factory (without any wrapping) passed to
add_content_type()
. factory
- The potentially wrapped content factory derived from the original factory in
add_content_type()
.
See Registering Content for more information about content type registration and what the above introspectable values mean.
Your __sdi_addable__
method can perform some logic using the values it is
passed, and then it must return a filtered sequence.
As an example, the __sdi_addable__
method on the Catalog
filters out the kinds of things that can be added in a catalog.
Extending Which Columns Are Displayed¶
The folder contents grid displays a number of columns by default. If
you are managing content with custom properties, in some cases you want
to list those properties in the columns the grid can display. You can
do so on custom folder content types by adding a columns
argument
to your @content
decorator.
As an example, imagine a Binder
kind of container. It has a content
type declaration:
@content(
'Binder',
icon='glyphicon glyphicon-book',
add_view='add_binder',
propertysheets = (
('Basic', BinderPropertySheet),
),
columns=binder_columns,
)
The binder_columns
points to a callable where we perform the work
to both add the column to the list of columns, but also specify how to
get the row data for that column:
def binder_columns(folder, subobject, request, default_columnspec):
subobject_name = getattr(subobject, '__name__', str(subobject))
objectmap = find_objectmap(folder)
user_oid = getattr(subobject, 'creator', None)
created = getattr(subobject, 'created', None)
modified = getattr(subobject, 'modified', None)
if user_oid is not None:
user = objectmap.object_for(user_oid)
user_name = getattr(user, '__name__', 'anonymous')
else:
user_name = 'anonymous'
if created is not None:
created = created.isoformat()
if modified is not None:
modified = modified.isoformat()
return default_columnspec + [
{'name': 'Title',
'value': getattr(subobject, 'title', subobject_name),
},
{'name': 'Created',
'value': created,
'formatter': 'date',
},
{'name': 'Last edited',
'value': modified,
'formatter': 'date',
},
{'name': 'Creator',
'value': user_name,
}
]
Here we add four columns to the standard set of grid columns,
whenever we are in a Binder
folder.
Adding New Folder Contents Buttons¶
The grid in folder contents makes it easy to select multiple resources then click a button to perform an action. Wouldn’t it be great, though, if we could add a new button to all or certain folders, to perform custom actions?
In the previous section we saw how to pass another argument to the
@content
decorator. We do the same for new buttons. A content type
can pass in buttons=callable
to modify the list of buttons on a
particular kind of folder.
For example, the substanced.catalog.catalog_buttons()
callable
adds a new Reindex
button in front of the standard set of buttons:
def catalog_buttons(context, request, default_buttons):
""" Show a reindex button before default buttons in the folder contents
view of a catalog"""
buttons = [
{'type':'single',
'buttons':
[
{'id':'reindex',
'name':'form.reindex',
'class':'btn-primary btn-sdi-sel',
'value':'reindex',
'text':'Reindex'}
]
}
] + default_buttons
return buttons
The button is disabled until one or more resources are selected which
have the correct permission (discussed above.) If our new button is
clicked, the form is posted with the form.reindex
value in post
data. You can then make a @mgmt_view
with
request_param='form.reindex'
in the declaration to handle the form
post when that button is clicked.
Broken Objects and Class Aliases¶
Let’s assume that there’s an object in your database that is an instance of the
class myapplication.resources.MyCoolResource
. If that class is
subsequently renamed to myapplication.resources.MySuperVeryCoolResource
,
the MyCoolResource
object that exists in the database will become broken.
This is because the ZODB database used by Substance D uses the Python
pickle
persistence format, and pickle
writes the literal class name
into the record associated with an object instance. Therefore, if a class is
renamed or moved, when you come along later and try to deserialize a pickle
with the old name, it will not work as it used to.
Persistent objects that exist in the database but which have a class that
cannot be resolved are called “broken objects”. If you ask a Substance D folder
(or the object map) for an object that turns out to be broken in this way, it
will hand you back an instance of the pyramid.util.BrokenWrapper
class.
This class tries to behave as much as possible like the original object for
data that exists in the original objects’ __dict__
(it defines a custom
__getattr__
that looks in the broken object’s state). However, you won’t
able to call methods of the original class against a broken object.
You can usually delete broken objects using the SDI folder contents view if necessary.
If you must rename or move a class, you can leave a class alias behind for backwards compatibility to avoid seeing broken objects in your database. For example:
class MySuperVeryCoolResource(Persistent):
pass
MyCoolResource = MySuperVeryCoolResource # bw compat alias
Using Auditing¶
Substance D keeps an audit log of all meaningful operations performed against content if you have an audit database configured. At the time of this writing, “meaningful” is defined as:
- When an ACL is changed.
- When a resource is added or removed.
- When a resource is modified.
The audit log is of a fixed size (currently 1,000 items). When the audit log fills up, the oldest audit event is thrown away. Currently we don’t have an archiving mechanism in place to keep around the items popped off the end of the log when it fills up; this is planned.
You can extend the auditing system by using the
substanced.audit.AuditLog
, writing your own events to the log.
Configuring the Audit Database¶
In order to enable auditing, you have to add an audit
database to your
Substance D configuration. This means adding a key to your application’s
section in the .ini
file associated with the app:
zodbconn.uri.audit = <some ZODB uri>
An example of “some ZODB URI” above might be (for a FileStorage database, if your application doesn’t use multiple processes):
zodbconn.uri.audit = file://%(here)s/auditlog.fs
Or if your application uses multiple processes, use a ZEO URL.
The database cannot be your main database. The reason that the audit database must live in a separate ZODB database is that we don’t want undo operations to undo the audit log data.
Note that if you do not configure an audit database, real-time SDI features such as your folder contents views updating without a manual refresh will not work.
Once you’ve configured the audit database, you need to add an audit log object to the new database. You can do this using pshell:
[chrism@thinko sdnet]$ bin/pshell etc/development.ini
Python 3.3.2 (default, Jun 1 2013, 04:46:52)
[GCC 4.6.3] on linux
Type "help" for more information.
Environment:
app The WSGI application.
registry Active Pyramid registry.
request Active request object.
root Root of the default resource tree.
root_factory Default root factory used to create `root`.
>>> from substanced.audit import set_auditlog
>>> set_auditlog(root)
>>> import transaction; transaction.commit()
Once you’ve done this, the “Auditing” tab of the root object in the SDI should no longer indicate that auditing is not configured.
Viewing the Audit Log¶
The root object will have a tab named “Auditing”. You can view the currently
active audit log entries from this page. Accessing this tab requires the
sdi.view-auditlog
permission.
Adding an Audit Log Entry¶
Here’s an example of adding an audit log entry of type NailsFiled
to the
audit log:
from substanced.util import get_oid, get_auditlog
def myview(context, request):
auditlog = get_auditlog(context)
auditlog.add('NailsFiled', get_oid(context), type='fingernails')
...
Warning
If you don’t have an audit database defined, the
get_auditlog()
API will return None
.
This will add a``NailsFiled`` event with the payload
{'type':'fingernails'}
to the audit log. The payload will also
automatically include a UNIX timestamp as the key time
. The first argument
is the audit log typename. Audit entries of the same kind should share the
same type name. It should be a string. The second argument is the oid of the
content object which this event is related to. It may be None
indicating
that the event is global, and unrelated to any particular piece of content.
You can pass any number of keyword arguments to
substanced.audit.AuditLog.add()
, each will be added to the payload.
Each value supplied as a keyword argument must be JSON-serializable. If one
is not, you will receive an error when you attempt to add the event.
Using The auditstream-sse
View¶
If you have auditing enabled, you can use a view named auditstream-sse
against any resource in your resource tree using JavaScript. It will return
an event stream suitable for driving an HTML5 EventSource
(an HTML 5
feature, see http://www.html5rocks.com/en/tutorials/eventsource/basics/ for more
information). The event stream will contain auditing events. This can be used
for progressive enhancement of your application’s UI. Substance D’s SDI uses
it for that purpose. For example, when an object’s ACL is changed, a user
looking at the “Security” tab of that object in the SDI will see the change
immediately, rather than upon the next page refresh.
Obtain events for the context of the view only:
var source = new EventSource(
"${request.sdiapi.mgmt_path(context, 'auditstream-sse')}");
Obtain events for a single OID unrelated to the context:
var source = new EventSource(
"${request.sdiapi.mgmt_path(context, 'auditstream-sse', query={'oid':'12345'})}");
Obtain events for a set of OIDs:
var source = new EventSource(
"${request.sdiapi.mgmt_path(context, 'auditstream-sse', query={'oid':['12345', '56789']})}");
Obtain all events for all oids:
var source = new EventSource(
"${request.sdiapi.mgmt_path(context, 'auditstream-sse', query={'all':'1'})}");
The executing user will need to possess the sdi.view-auditstream
permission
against the context on which the view is invoked. Each event payload will
contain detailed information about the audit event as a string which represents
a JSON dictionary.
See the acl.pt
template in the substanced/sdi/views/templates
directory
of Substance D to see a “real-world” usage of this feature.
Using Locking¶
Substance D allows you to lock content resources programmatically. When a resource is locked, its UI can change to indicate that it cannot be edited by someone other than the user holding the lock.
Locking a resource only locks the resource, not its children. The locking system is not recursive at this time.
Locking a Resource¶
To lock a resource:
from substanced.locking import lock_resource
from pyramid.security import has_permission
if has_permission('sdi.lock', someresource, request):
lock_resource(someresource, request.user, timeout=3600)
If the resource is already locked by the owner supplied as owner_or_ownerid
(the parameter filled by request.user
above), calling this function will
refresh the lock. If the resource is not already locked by another user,
calling this function will create a new lock. If the resource is already
locked by a different user, a substanced.locking.LockError
will be
raised.
Using the substanced.locking.lock_resource()
function has the side effect
of creating a “Lock Service” (named locks
) in the Substance D root if one
does not already exist.
Warning
Callers should assert that the owner has the sdi.lock
permission against
the resource before calling lock_resource()
to
ensure that a user can’t lock a resource he is not permitted to.
Unlocking a Resource¶
To unlock a resource:
from substanced.locking import unlock_resource
from pyramid.security import has_permission
if has_permission('sdi.lock', someresource, request):
unlock_resource(someresource, request.user)
If the resource is already locked by a user other than the owner supplied as
owner_or_ownerid
(the parameter filled by request.user
above) or the
resource isn’t already locked with this lock type, calling this function will
raise a substanced.locking.UnlockError
exception. Otherwise the lock
will be removed.
Using the substanced.locking.unlock_resource()
function has the side
effect of creating a “Lock Service” (named locks
) in the Substance D root
if one does not already exist.
Warning
Callers should assert that the owner has the sdi.lock
permission against
the resource before calling unlock_resource()
to
ensure that a user can’t lock a resource he is not permitted to.
To unlock a resource using an explicit lock token:
from substanced.locking import unlock_token
from pyramid.security import has_permission
if has_permission('sdi.lock', someresource, request):
unlock_token(someresource, token, request.user)
If the lock identified by token
belongs to a user other than the owner
supplied as owner_or_ownerid
(the parameter filled by request.user
above) or if no lock exists under token
, calling this function will
raise a substanced.locking.LockError
exception. Otherwise the lock
will be removed.
Using the substanced.locking.unlock_token()
function has the side
effect of creating a “Lock Service” (named locks
) in the Substance D root
if one does not already exist.
Warning
Callers should assert that the owner has the sdi.lock
permission against
the resource before calling unlock_token()
to
ensure that a user can’t lock a resource he is not permitted to.
Discovering Existing Locks¶
To discover any existing locks for a resource:
from substanced.locking import discover_resource_locks
locks = discover_resource_locks(someresource)
# "locks" will be a sequence
The substanced.locking.discover_resource_locks()
function will return a
sequence of substanced.locking.Lock
objects related to the resource
for the lock type provided to the function. By default, only valid locks are
returned. Invalid locks for the resource may exist, but they are not returned
unless the include_invalid
argument passed to
:discover_resource_locks()
is True
.
Under normal circumstances, the length of the sequence returned will be either
0 (if there are no locks) or 1 (if there is any lock). In some special
circumstances, however, when the substanced.locking.lock_resource()
API
is not used to create locks, there may be more than one lock related to a
resource of the same type.
By default, the discover_resource_locks
API returns locks for the
provided object, plus locks on any object in its lineage. To suppress
this default, pass include_lineage=False
, e.g.:
locks = discover_resource_locks(someresource)
# "locks" will be only those set on 'someresource'
In some applications, the important thing is to ensure that a particular
user could lock a resource before updating it (e.g., from a browser view
on a property sheet). The :could_lock_resource()
API is designed for these cases: if the supplied userid could not lock the
resource, it raises a substanced.locking.LockError
exception:
from substanced.locking import could_lock_resource, LockError
try:
could_lock_resource(someresource, request.user)
except LockError as e:
raise FormError('locked by "%s"' % e.lock.owner.__name__)
Viewing The Lock Service¶
Once some locks have been created, a lock service will have been created.
The lock service is an object named locks
in the Substance D root.
You can use the SDI UI of this locks service to delete and edit existing locks. It’s a good idea to periodically use the “Delete Expired” button in this UI to clear out any existing expired locks that were orphaned by buggy or interrupted clients.
Configuration¶
While writing a Substance D application is very similar to writing a Pyramid application, there are a few extra considerations to keep in mind.
Scan and Include¶
When writing Pyramid applications, the Configurator supports
config.include
and config.scan
Because of ordering
effects, do all your config.include
calls before any of your
config.scan
calls.
Using RelStorage¶
Content in Substance D is stored in a Python object database called the ZODB. The ZODB has deep integration with Pyramid. When developing Python applications that use ZODB, you have a number of storage options:
FileStorage
is the simplest and is used in the development scaffolds for Substance D. That is,development.ini
is configured useFileStorage
. Just a file on disk, no long-running server process.ZEO
keeps a file on disk but runs a server process that manages transactions over a socket. This allows multiple app servers on multiple boxes, or background processes such as deferred indexing, to access the database.- RelStorage stores and retrieves the Python objects from a relational database. This is the preferred deployment option for applications that need trusted reliability and scalability.
Switching between storages is mostly a matter of editing your configuration file and choosing a different storage.
Note
While RelStorage uses an RDBMS for transactions, storage, retrieval, failover, and other features, it does not use SQL or decompose your Python objects into columns and joined tables.
Although RelStorage supports a number of RDBMS packages, we’ll focus on PostgreSQL in these docs.
RelStorage + PostgreSQL Configuration¶
First, read the RelStorage docs, focusing on the PostgreSQL section and the command line needed for database setup. In particular, make sure that you:
Have a system user account named
database
:$ sudo su - postgres $ createuser --pwprompt zodbuser $ createdb -O zodbuser zodb
The user that you created (e.g.
zodbuser
) can make local connections
Next, we’ll make some changes to some of the configuration files. In
your setup.py
, indicate that you need the RelStorage
package
as well as the psycopg2
Python binding for PostgreSQL. This
presumes that the binaries for the PostgreSQL client are available on
your path.
In your configuration file (e.g. production.ini
), the
[app:main]
section should have:
zodbconn.uri = zconfig://%(here)s/relstorage.conf
We thus need a relstorage.conf
file:
%import relstorage
<zodb main>
<relstorage>
blob-dir ../var/blobs
<postgresql>
dsn dbname='zodb' user='zodbuser' host='localhost' password='zodbuser'
</postgresql>
</relstorage>
cache-size 100000
</zodb>
Resetting Your Substance D Database¶
During development you frequently need to blow away all your data and start over. You can do this via evolution, but usually it isn’t worth the work.
This is very easy with FileStorage
: just rm var/Data.fs*
and
restart your app server. It is also easy with ZEO: shut down the
supervisor service, remove the data as above, restart it,
and restart the app server.
With RelStorage, you get a rich set of existing tools such as
pgadmin
to browse and modify table data. You can, though,
do it the quickie way via bin/pshell
and just delete the root
object, then commit the transaction.
If you need to remove evolve data as well, open up pshell and do
root._p_jar.root()
. You’ll see the ZODB root
(not the app root). Inside of it is the app root and the evolve data.
Gathering Runtime Statistics¶
Problems can come up in production. When they do, you usually want forensics that show aspects of the system under load, over a period of time.
Of course, you don’t want the collection of such data to affect performance. What’s needed is a mechanism to log data all the time, in a lightweight way, that can later be analyzed in productive ways. This system needs both built-in hooks at the Substance D framework level as well as extension points to analyze function points in the application you are writing.
Three components are involved in the process of collecting statistics:
substanced.stats
exposes Python API to collect data and sends it to to a StatsD <https://github.com/etsy/statsd> agent- The StatsD agent aggregates data and sends it to backend service
- A backend service displays graphs based on stored data. The service can be self-hosted such as Graphite or it can be a SaaS solution such as DataDog.
Setting Up¶
To enable statistics gathering in your site, edit your .ini
configuration
file and add the following lines to your [app:main]
section:
substanced.statsd.enabled = true
substanced.statsd.host = localhost
substanced.statsd.port = 8125
substanced.statsd.prefix = substanced
Using DataDog with SubstanceD statistics¶
Substance D supports DataDog, a Software-as-a-Service (SaaS) provider for monitoring and visualizing performance data. DataDog installs an dogstatsd agent for sending custom metrics on your local system. The agent is based on StatsD.
Using DataDog is an an easy way to get started with Substance D statistics. Sign up for an account with DataDog. This will provide you with the instructions for downloading and running the local agent. You’ll need to get the agent installed before proceeding.
Once you’ve got the agent installed, and the proper settings in your Substance
D ini file, you will be able to see statistics in the DataDog user interface.
Once you log into your DataDog dashboard, click on Infrastructure
and
you’ll see any hosts configured as part of your account:

The substanced
entry in Apps
table column is from the
substanced.statsd.prefix
configured in Settings up section. Clicking on
that brings up Substance D specific monitoring in DataDog:

Clicking settings symbol on a graph will lead you to graph editor, where you can change how DataDog interprets and renders your graphs. A good resource how the editor works is Graphing Primer.
DataDog also supports Metric Alerts allowing you to send alerts when your statistics reach certain state.
Logging Custom Statistics¶
Over time, Substance D itself will include more framework points where statistics are collected. Most likely, though, you’ll want some statistics that are very meaningful to your application’s specific functionality.
If you look at the docs for the Python statsd module you will see three main types:
- Counters for simply incrementing a value,
- Timers for logging elapsed time in a code block, and
- Gauges for tracking a constant at a particular point in time
Each of these map to methods in substanced.stats.StatsdHelper
. This
class is available as an instance available via import:
from substanced.stats import statsd_gauge
Your application code can then make calls to these stats-gathering
methods. For example, substanced.principal.User
does the
following to note that check password was used:
statsd_gauge('check_password', 1)
Here is an example in substanced.catalog.Catalog.index_resource()
that
measures elapsed indexing time inside a Python with
block:
with statsd_timer('catalog.index_resource'):
if oid is None:
oid = oid_from_resource(resource)
for index in self.values():
index.index_resource(resource, oid=oid, action_mode=action_mode)
self.objectids.insert(oid)
Virtual Rooting¶
You can present a folder other than the physical Substance D root object as the “SDI root” to people. For example, if you have the following structure from your physical Substance D root:
root--
\-- folder1
|
|-- folder2
You can present either folder1
or folder2
to the user as a virtual
root when people log in to the SDI.
To do so, you have to pass an X-Vhm-Root
header to SubstanceD in each
request. It’s easiest to do this with Apache or another frontend web server.
Here’s a sample configuration which assumes you are telling Apache to proxy to
a Substance D application that runs on localhost on port 6543:
<VirtualHost *:80>
ServerAdmin webmaster@agendaless.com
ServerName example.com
ErrorLog /var/log/apache2/example.com-error.log
CustomLog /var/log/apache2/example.com-access.log combined
RewriteEngine On
RewriteRule ^(.*) http://127.0.0.1:6543/$1 [L,P]
ProxyPreserveHost On
RequestHeader add X-Vhm-Root /folder1
</VirtualHost>
In the above configuration, when users log in on http://example.com/manage
,
the root they see in the SDI will be /folder1
instead of the real root.
They will not be able to access the real root.
Note that retail requests (requests without /manage
) to the same hostname
will also be rooted at folder1
.
This feature requires Pyramid version 1.4.4 or better.
Building a Retail Application¶
It’s not the intent that normal unprivileged users of an application you build using Substance D ever see the SDI management interface. That interface is reserved for privileged users, like you and your staff.
To build a “retail” application, you just use normal Pyramid view configuration to associate objects with view logic based on the content types provided to you by Substance D and the content types you’ve defined.
For example, here’s a view that will respond on the root Substance D object and return its SDI title:
1 2 3 4 5 6 7 | from pyramid.view import view_config
@view_config(content_type='Root')
def hello(request):
html = u'<html><head></head><body>Hello from %s!</body></html>'
request.response.text = html % request.context.sdi_title
return request.response
|
Note that we did not use the substanced.sdi.mgmt_view
decorator.
Instead we used the pyramid.view.view_config
decorator, which will
expose the view to normal site visitors, not just those visiting the
resource via the SDI.
To see that code working, create a retail
package within the myproj
package (that is the inner myproj
folder that contains the
__init__.py
, resources.py
and views.py
files). The package will
have two files: an empty __init__.py
and a views.py
with the code
snippet above. If you now visit http://localhost:6543/
you will see the
“Hello from…” message.
To display actual content stored in the database, Substance D exposes a resource tree that you can hang views from to build your application. You’ll want to read up on traversal to understand how to associate view configuration with resource objects.
Substance D Command-Line Utilities¶
Substance D installs a number of helper scripts for performing admin-related
tasks. To get full command-line syntax for any script, run it with the
option --help
.
sd_adduser¶
Add a new user, making them part of the ‘admins’ group. Useful when recovering from a forgotten password for the default ‘admin’ user. E.g.:
$ /path/to/virtualenv/bin/sd_adduser /path/to/virtualenv/etc/production.ini phred password
sd_drain_indexing¶
Process deferred indexing actions. E.g., run this from a cron job to drain the queue every two minutes:
0-59/2 * * * * /path/to/virtualenv/bin/sd_drain_indexing /path/to/virtualenv/etc/production.ini
sd_dump¶
Dump an object (and its subobjects) to the filesystem:
sd_dump [--source=ZODB-PATH] [--dest=FILESYSTEM-PATH] config_uri
Dumps the object at ZODB-PATH and all of its subobjects to a
filesystem path. Such a dump can be loaded (programmatically)
by using the substanced.dump.load function
E.g.:
$ /path/to/virtualenv/bin/sd_dump --source=/ --dest=/tmp/dump /path/to/virtualenv/etc/development.ini
sd_evolve¶
Query for pending evolution steps, or run them to get the database up-to-date. See Running an Evolution from the Command Line.
sd_reindex¶
Reindex the catalog. E.g.:
$ /path/to/virtualenv/bin/sd_reindex /path/to/virtualenv/etc/development.ini
Installing python-magic
¶
Use of the substanced.file.USE_MAGIC
constant for guessing file
types from stream content requires the python-magic
library, which works
without extra help on most systems, but may require special dependency
installations on Mac OS and Windows systems. You’ll need to follow these
steps on those platforms to use this feature:
Mac OS X
Windows
“Installation on Win32” in https://github.com/ahupp/python-magic
API Documentation¶
substanced
API¶
-
substanced.
includeme
(config)¶ Do the work of
substanced.include()
, thensubstanced.scan()
. Makesconfig.include(substanced)
work.
-
substanced.
include
(config)¶ Perform all
config.include
tasks required for Substance D and the default aspects of the SDI to work.
-
substanced.
scan
(config)¶ Perform all
config.scan
tasks required for Substance D and the default aspects of the SDI to work.
substanced.audit
API¶
-
class
substanced.audit.
AuditLog
(max_layers=10, layer_size=100, entries=None)¶ -
add
(_name, _oid, **kw)¶ Add a record the audit log.
_name
should be the event name,_oid
should be an object oid orNone
, andkw
should be a json-serializable dictionary
-
latest_id
()¶ Return the generation and the index id as a tuple, representing the latest audit log entry
-
newer
(generation, index_id, oids=None)¶ Return the events newer than the combination of
generation
andoid
. Filter usingoids
if supplied.
-
substanced.catalog
API¶
-
class
substanced.catalog.
Text
(**kw)¶
-
class
substanced.catalog.
Field
(**kw)¶
-
class
substanced.catalog.
Keyword
(**kw)¶
-
class
substanced.catalog.
Facet
(**kw)¶
-
class
substanced.catalog.
Allowed
(**kw)¶
-
class
substanced.catalog.
Path
(**kw)¶
-
class
substanced.catalog.
Catalog
(family=None)¶ -
__setitem__
(name, other)¶ Set object
other
into this folder under the namename
.name
must be a Unicode object or a bytestring object.If
name
is a bytestring object, it must be decodable using the system default encoding.name
cannot be the empty string.When
other
is seated into this folder, it will also be decorated with a__parent__
attribute (a reference to the folder into which it is being seated) and__name__
attribute (the name passed in to this function. It must not already have a__parent__
attribute before being seated into the folder, or an exception will be raised.If a value already exists in the foldr under the name
name
, raiseKeyError
.When this method is called, the object will be added to the objectmap, an
substanced.event.ObjectWillBeAdded
event will be emitted before the object obtains a__name__
or__parent__
value, then asubstanced.event.ObjectAdded
will be emitted after the object obtains a__name__
and__parent__
value.
-
__getitem__
(name)¶ Return the object named
name
added to this folder or raiseKeyError
if no such object exists.name
must be a Unicode object or directly decodeable to Unicode using the system default encoding.
Retrieve an index.
-
get
(name, default=None)¶ Return the object named by
name
or the default.name
must be a Unicode object or a bytestring object.If
name
is a bytestring object, it must be decodable using the system default encoding.
Retrieve an index or return failobj.
-
flush
(all=True)¶ Flush pending indexing actions for all indexes in this catalog.
If
all
isTrue
, all pending indexing actions will be immediately executed regardless of the action’s mode.If
all
isFalse
, pending indexing actions which areMODE_ATCOMMIT
will be executed but actions which areMODE_DEFERRED
will not be executed.
-
index_resource
(resource, oid=None, action_mode=None)¶ Register the resource in indexes of this catalog using
oid
as the indexing identifier. Ifoid
is not supplied, the__oid__
attribute of theresource
will be used as the indexing identifier.action_mode
, if supplied, should be one ofNone
,MODE_IMMEDIATE
,MODE_ATCOMMIT
orMODE_DEFERRED
, indicating when the updates should take effect. Theaction_mode
value will overrule any action mode a member index has been configured with exceptNone
which explicitly indicates that you’d like to use the index’s action_mode value.
-
reindex
(dry_run=False, commit_interval=3000, indexes=None, path_re=None, output=None, registry=None)¶ Reindex all objects in the catalog using the existing set of indexes immediately.
If
dry_run
isTrue
, do no actual work but send what would be changed to the logger.commit_interval
controls the number of objects indexed between each call totransaction.commit()
(to control memory consumption).indexes
, if notNone
, should be a list of index names that should be reindexed. Ifindexes
isNone
, all indexes are reindexed.path_re
, if it is notNone
should be a regular expression object that will be matched against each object’s path. If the regular expression matches, the object will be reindexed, if it does not, it won’t.output
, if passed should be one ofNone
,False
or a function. If it is a function, the function should accept a single message argument that will be used to record the actions taken during the reindex. IfFalse
is passed, no output is done. IfNone
is passed (the default), the output will wind up in thesubstanced.catalog
Python logger output atinfo
level.registry
, if passed, should be a Pyramid registry. If one is not passed, theget_current_registry()
function will be used to look up the current registry. This function needs the registry in order to access content catalog views.
-
reindex_resource
(resource, oid=None, action_mode=None)¶ Register the resource in indexes of this catalog using
oid
as the indexing identifier. Ifoid
is not supplied, the__oid__
attribute ofresource
will be used as the indexing identifier.action_mode
, if supplied, should be one ofNone
,MODE_IMMEDIATE
,MODE_ATCOMMIT
orMODE_DEFERRED
indicating when the updates should take effect. Theaction_mode
value will overrule any action mode a member index has been configured with exceptNone
which explicitly indicates that you’d like to use the index’s action_mode value.The result of calling this method is logically the same as calling
unindex_resource
, thenindex_resource
with the same resource, but calling those two methods in succession is often more expensive than calling this single method, as member indexes can choose to do smarter things during a reindex than what they would do during an unindex followed by a successive index.
-
reset
()¶ Reset all indexes in this catalog and clear self.objectids.
-
transaction
= <module 'transaction' from '/home/docs/checkouts/readthedocs.org/user_builds/substanced/envs/latest/local/lib/python2.7/site-packages/transaction/__init__.pyc'>¶
-
unindex_resource
(resource_or_oid, action_mode=None)¶ Deregister the resource in indexes of this catalog using the indexing identifier
resource_or_oid
. Ifresource_or_oid
is an integer, it will be used as the indexing identifier; ifresource_or_oid
is a resource, its__oid__
attribute will be used as the indexing identifier.action_mode
, if supplied, should be one ofNone
,MODE_IMMEDIATE
,MODE_ATCOMMIT
orMODE_DEFERRED
indicating when the updates should take effect. Theaction_mode
value will overrule any action mode a member index has been configured with exceptNone
which explicitly indicates that you’d like to use the index’s action_mode value.
-
update_indexes
(registry=None, dry_run=False, output=None, replace=False, reindex=False, **reindex_kw)¶ Use the candidate indexes registered via
config.add_catalog_factory
to populate this catalog. Any indexes which are present in the candidate indexes, but not present in the catalog will be created. Any indexes which are present in the catalog but not present in the candidate indexes will be deleted.registry
, if passed, should be a Pyramid registry. If one is not passed, theget_current_registry()
function will be used to look up the current registry. This function needs the registry in order to access content catalog views.If
dry_run
isTrue
, don’t commit the changes made when this function is called, just send what would have been done to the logger.output
, if passed should be one ofNone
,False
or a function. If it is a function, the function should accept a single message argument that will be used to record the actions taken during the reindex. IfFalse
is passed, no output is done. IfNone
is passed (the default), the output will wind up in thesubstanced.catalog
Python logger output atinfo
level.This function does not reindex new indexes added to the catalog unless
reindex=True
is passed.Arguments to this method captured as
kw
are passed tosubstanced.catalog.Catalog.reindex()
ifreindex
is True, otherwisekw
is ignored.If
replace
isTrue
, an existing catalog index that is not in thecategory
supplied but which has the same name as a candidate index will be replaced. Ifreplace
isFalse
, existing indexes will never be replaced.
-
-
class
substanced.catalog.
CatalogsService
(data=None, family=None)¶ -
class
Catalog
(family=None)¶ -
flush
(all=True)¶ Flush pending indexing actions for all indexes in this catalog.
If
all
isTrue
, all pending indexing actions will be immediately executed regardless of the action’s mode.If
all
isFalse
, pending indexing actions which areMODE_ATCOMMIT
will be executed but actions which areMODE_DEFERRED
will not be executed.
-
index_resource
(resource, oid=None, action_mode=None)¶ Register the resource in indexes of this catalog using
oid
as the indexing identifier. Ifoid
is not supplied, the__oid__
attribute of theresource
will be used as the indexing identifier.action_mode
, if supplied, should be one ofNone
,MODE_IMMEDIATE
,MODE_ATCOMMIT
orMODE_DEFERRED
, indicating when the updates should take effect. Theaction_mode
value will overrule any action mode a member index has been configured with exceptNone
which explicitly indicates that you’d like to use the index’s action_mode value.
-
reindex
(dry_run=False, commit_interval=3000, indexes=None, path_re=None, output=None, registry=None)¶ Reindex all objects in the catalog using the existing set of indexes immediately.
If
dry_run
isTrue
, do no actual work but send what would be changed to the logger.commit_interval
controls the number of objects indexed between each call totransaction.commit()
(to control memory consumption).indexes
, if notNone
, should be a list of index names that should be reindexed. Ifindexes
isNone
, all indexes are reindexed.path_re
, if it is notNone
should be a regular expression object that will be matched against each object’s path. If the regular expression matches, the object will be reindexed, if it does not, it won’t.output
, if passed should be one ofNone
,False
or a function. If it is a function, the function should accept a single message argument that will be used to record the actions taken during the reindex. IfFalse
is passed, no output is done. IfNone
is passed (the default), the output will wind up in thesubstanced.catalog
Python logger output atinfo
level.registry
, if passed, should be a Pyramid registry. If one is not passed, theget_current_registry()
function will be used to look up the current registry. This function needs the registry in order to access content catalog views.
-
reindex_resource
(resource, oid=None, action_mode=None)¶ Register the resource in indexes of this catalog using
oid
as the indexing identifier. Ifoid
is not supplied, the__oid__
attribute ofresource
will be used as the indexing identifier.action_mode
, if supplied, should be one ofNone
,MODE_IMMEDIATE
,MODE_ATCOMMIT
orMODE_DEFERRED
indicating when the updates should take effect. Theaction_mode
value will overrule any action mode a member index has been configured with exceptNone
which explicitly indicates that you’d like to use the index’s action_mode value.The result of calling this method is logically the same as calling
unindex_resource
, thenindex_resource
with the same resource, but calling those two methods in succession is often more expensive than calling this single method, as member indexes can choose to do smarter things during a reindex than what they would do during an unindex followed by a successive index.
-
reset
()¶ Reset all indexes in this catalog and clear self.objectids.
-
transaction
= <module 'transaction' from '/home/docs/checkouts/readthedocs.org/user_builds/substanced/envs/latest/local/lib/python2.7/site-packages/transaction/__init__.pyc'>¶
-
unindex_resource
(resource_or_oid, action_mode=None)¶ Deregister the resource in indexes of this catalog using the indexing identifier
resource_or_oid
. Ifresource_or_oid
is an integer, it will be used as the indexing identifier; ifresource_or_oid
is a resource, its__oid__
attribute will be used as the indexing identifier.action_mode
, if supplied, should be one ofNone
,MODE_IMMEDIATE
,MODE_ATCOMMIT
orMODE_DEFERRED
indicating when the updates should take effect. Theaction_mode
value will overrule any action mode a member index has been configured with exceptNone
which explicitly indicates that you’d like to use the index’s action_mode value.
-
update_indexes
(registry=None, dry_run=False, output=None, replace=False, reindex=False, **reindex_kw)¶ Use the candidate indexes registered via
config.add_catalog_factory
to populate this catalog. Any indexes which are present in the candidate indexes, but not present in the catalog will be created. Any indexes which are present in the catalog but not present in the candidate indexes will be deleted.registry
, if passed, should be a Pyramid registry. If one is not passed, theget_current_registry()
function will be used to look up the current registry. This function needs the registry in order to access content catalog views.If
dry_run
isTrue
, don’t commit the changes made when this function is called, just send what would have been done to the logger.output
, if passed should be one ofNone
,False
or a function. If it is a function, the function should accept a single message argument that will be used to record the actions taken during the reindex. IfFalse
is passed, no output is done. IfNone
is passed (the default), the output will wind up in thesubstanced.catalog
Python logger output atinfo
level.This function does not reindex new indexes added to the catalog unless
reindex=True
is passed.Arguments to this method captured as
kw
are passed tosubstanced.catalog.Catalog.reindex()
ifreindex
is True, otherwisekw
is ignored.If
replace
isTrue
, an existing catalog index that is not in thecategory
supplied but which has the same name as a candidate index will be replaced. Ifreplace
isFalse
, existing indexes will never be replaced.
-
-
add_catalog
(name, update_indexes=True)¶ Create and add a catalog named
name
to this catalogs service. Return the newly created catalog object. If a catalog namedname
already exists in this catalogs service, an exception will be raised.Example usage in a root created subscriber:
@subscribe_created(content_type='Root') def created(event): root = event.object service = root['catalogs'] catalog = service.add_catalog('app1', update_indexes=True)
If
update_indexes
is True, indexes in the named catalog factory will be added to the newly created catalog.
-
class
-
substanced.catalog.
is_catalogable
(resource, registry=None)¶
-
substanced.catalog.
catalog_factory
(name)¶ Decorator for a class which acts as a template for index creation.:
from substanced.catalog import Text @catalog_factory('myapp') class MyAppIndexes(object): text = Text() title = Field()
When scanned, this catalog factory will be added to the registry as if
substanced.catalog.add_catalog_factory()
were called like:config.add_catalog_factory('myapp', MyAppIndexes)
-
substanced.catalog.
includeme
(config)¶
-
substanced.catalog.
add_catalog_factory
(config, name, cls)¶ Directive which adds a named catalog factory to the configuration state. The
cls
argument should be a class that has named index factory instances as attributes. Thename
argument should be a string.
-
substanced.catalog.
add_indexview
(self, *arg, **kw)¶ Directive which adds an index view to the configuration state state. The
view
argument should be function that is an indeview function, or or a class with a__call__
method that acts as an indexview method. For example:def title(resource, default): return getattr(resource, 'title', default) config.add_indexview(title, catalog_name='myapp', index_name='title')
Or, a class:
class IndexViews(object): def __init__(self, resource): self.resource = resource def __call__(self, default): return getattr(self.resource, 'title', default) config.add_indexview( IndexViews, catalog_name='myapp', index_name='title' )
If an
attr
arg is supplied toadd_indexview
, you can use a different attribute of the class instad of__call__
:class IndexViews(object): def __init__(self, resource): self.resource = resource def title(self, default): return getattr(self.resource, 'title', default) def name(self, default): return getattr(self.resource, 'name', default) config.add_indexview( IndexViews, catalog_name='myapp', index_name='title', attr='title' ) config.add_indexview( IndexViews, catalog_name='myapp', index_name='name', attr='name' )
In this way you can use the same class to represent a bunch of different index views. An index view will be looked up by the cataloging machinery when it wants to insert value into a particular catalog type’s index. The
catalog_name
you use specify which catalog name this indeview is good for; it should match the string passed toadd_catalog_factory
as aname
. Theindex_name
argument should match an index name used within such a catalog.Index view lookups work a bit like Pyramid view lookups: you can use the
context
argument to pass an interface or class which should be used to register the index view; such an index view will only be used when the resource being indexed has that class or interface. Eventually we’ll provide a way to add predicates other thancontext
too.The
substanced.catalog.indexview
decorator provides a declarative analogue to using this configuration directive.
-
class
substanced.catalog.
indexview
(**settings)¶ A class decorator which, when applied to an index view class method, will mark the method as an index view. This decorator accepts all the arguments accepted by
substanced.catalog.add_indexview()
, and each has the same meaning.
-
class
substanced.catalog.
indexview_defaults
(**settings)¶ A class decorator which, when applied to a class, will provide defaults for all index view configurations defined in the class. This decorator accepts all the arguments accepted by
substanced.catalog.indexview()
, and each has the same meaning.
substanced.catalog.indexes
API¶
-
class
substanced.catalog.indexes.
FieldIndex
(discriminator=None, family=None, action_mode=None)¶
-
class
substanced.catalog.indexes.
KeywordIndex
(discriminator=None, family=None, action_mode=None)¶
-
class
substanced.catalog.indexes.
TextIndex
(discriminator=None, lexicon=None, index=None, family=None, action_mode=None)¶
-
class
substanced.catalog.indexes.
FacetIndex
(discriminator=None, facets=None, family=None, action_mode=None)¶
-
class
substanced.catalog.indexes.
PathIndex
(discriminator=None, family=None)¶ Uses the
substanced.objectmap.ObjectMap.pathlookup()
to apply a query to retrieve object identifiers at or under a path.path can be passed to methods as:
- resource object
- tuple of strings (usually returned value of
pyramid.traverse.resource_path_tuple()
) - a string path (e.g. /foo/bar)
Query methods accept following parameters:
- include_origin (by default True), see
substanced.objectmap.ObjectMap.pathlookup()
for explanation. - depth (by default None) see
substanced.objectmap.ObjectMap.pathlookup()
for explanation.
Query types supported:
- Eq
- NotEq
-
class
substanced.catalog.indexes.
AllowedIndex
(discriminator, family=None)¶ An index which defers to
objectmap.allowed
as part of a query intersection.-
allows
(principals, permission)¶ principals
may either be 1) a sequence of principal indentifiers, 2) a single principal identifier, or 3) a Pyramid request, which indicates that all the effective principals implied by the request are used.permission
must be a permission name.
-
hypatia.query
API¶
Comparators¶
-
class
hypatia.query.
Contains
(index, value)¶ Contains query.
CQE equivalent: ‘foo’ in index
-
class
hypatia.query.
Eq
(index, value)¶ Equals query.
CQE equivalent: index == ‘foo’
-
class
hypatia.query.
NotEq
(index, value)¶ Not equal query.
CQE eqivalent: index != ‘foo’
-
class
hypatia.query.
Gt
(index, value)¶ Greater than query.
CQE equivalent: index > ‘foo’
-
class
hypatia.query.
Lt
(index, value)¶ Less than query.
CQE equivalent: index < ‘foo’
-
class
hypatia.query.
Ge
(index, value)¶ Greater (or equal) query.
CQE equivalent: index >= ‘foo’
-
class
hypatia.query.
Le
(index, value)¶ Less (or equal) query.
CQE equivalent: index <= ‘foo
-
class
hypatia.query.
Contains
(index, value) Contains query.
CQE equivalent: ‘foo’ in index
-
class
hypatia.query.
NotContains
(index, value)¶ CQE equivalent: ‘foo’ not in index
-
class
hypatia.query.
Any
(index, value)¶ Any of query.
CQE equivalent: index in any([‘foo’, ‘bar’])
-
class
hypatia.query.
NotAny
(index, value)¶ Not any of query (ie, None of query)
CQE equivalent: index not in any([‘foo’, ‘bar’])
-
class
hypatia.query.
All
(index, value)¶ All query.
CQE equivalent: index in all([‘foo’, ‘bar’])
-
class
hypatia.query.
NotAll
(index, value)¶ NotAll query.
CQE equivalent: index not in all([‘foo’, ‘bar’])
-
class
hypatia.query.
InRange
(index, start, end, start_exclusive=False, end_exclusive=False)¶ Index value falls within a range.
- CQE eqivalent: lower < index < upper
- lower <= index <= upper
-
class
hypatia.query.
NotInRange
(index, start, end, start_exclusive=False, end_exclusive=False)¶ Index value falls outside a range.
- CQE eqivalent: not(lower < index < upper)
- not(lower <= index <= upper)
Boolean Operators¶
-
class
hypatia.query.
Or
(*queries)¶ Boolean Or of multiple queries.
-
class
hypatia.query.
And
(*queries)¶ Boolean And of multiple queries.
-
class
hypatia.query.
Not
(query)¶ Negation of a query.
Other Helpers¶
-
class
hypatia.query.
Name
(name)¶ A variable name in an expression, evaluated at query time. Can be used to defer evaluation of variables used inside of expressions until query time.
Example:
from hypatia.query import Eq from hypatia.query import Name # Define query at module scope find_cats = Eq('color', Name('color')) & Eq('sex', Name('sex')) # Use query in a search function, evaluating color and sex at the # time of the query def search_cats(catalog, resolver, color='tabby', sex='female'): # Let resolver be some function which can retrieve a cat object # from your application given a docid. params = dict(color=color, sex=sex) count, docids = catalog.query(find_cats, params) for docid in docids: yield resolver(docid)
-
hypatia.query.
parse_query
(expr, catalog, optimize_query=True)¶ Parses the given expression string and returns a query object. Requires Python >= 2.6.
hypatia.util
API¶
-
class
hypatia.util.
ResultSet
(ids, numids, resolver, sort_type=None)¶ Implements
hypatia.interfaces.IResultSet
-
intersect
(docids)¶ Intersect this resultset with a sequence of docids or another resultset. Returns a new ResultSet.
-
-
interface
hypatia.interfaces.
IResultSet
¶ Iterable sequence of documents or document identifiers.
-
sort
(index, reverse=False, limit=None, sort_type=None, raise_unsortable=True)¶ Return another IResultSet sorted using the
index
(an IIndexSort) passed to it after performing the sort using the index and thelimit
,reverse
, andsort_type
parameters.If
sort_type
is notNone
, it should be the valuehypatia.interfaces.STABLE
to specify that the sort should be stable orhypatia.interfaces.OPTIMAL
to specify that the sort algorithm chosen should be optimal (but not necessarily stable). It’s usually unnecessary to pass this value, even if you’re resorting an already-sorted set of docids, because the implementation of IResultSet will internally ensure that subsequent sorts of the returned result set of an initial sort will be stable; if you don’t want this behavior, explicitly passhypatia.interfaces.OPTIMAL
on the second and subsequent sorts of a set of docids.If
raise_unsortable
isTrue
(the default), if the index cannot resolve any of the docids in the set of docids in this result set, ahypatia.exc.Unsortable
exception will be raised during iteration over the sorted docids.
-
all
(resolve=True)¶ Return a sequence representing all elements in the resultset. If ``resolve` is True, and the result set has a valid resolver, return an iterable of the resolved documents, otherwise return an iterable containing the document id of each document.
-
ids
¶ An iterable sequence of document identifiers
-
one
(resolve=True)¶ Return the element in the resultset, asserting that there is only one result. If the resultset has more than one element, raise an
hypatia.exc.MultipleResults
exception. If the resultset has no elements, raise anhypatia.exc.NoResults
exception. If ``resolve` is True, and the result set has a valid resolver, return the resolved document, otherwise return the document id of the document.
-
__iter__
()¶ Return an iterator over the results of
self.all()
-
resolver
¶ A callable which accepts a document id and which returns a document. May be
None
, in which case, resolution performed by result set methods is not performed, and document identifiers are returned unresolved.
-
__len__
()¶ Return the length of the result set
-
first
(resolve=True)¶ Return the first element in the sequence. If
resolve
is True, and the result set has a valid resolver, return the resolved document, otherwise return the document id of the first document.
-
substanced.content
API¶
-
class
substanced.content.
content
(content_type, factory_type=None, **meta)¶ Use as a decorator for a content factory (usually a class). Accepts a content type, a factory type (optionally), and a set of meta keywords. These values mean the same thing as they mean for
substanced.content.add_content_type()
. This decorator attaches information to the object it decorates which is used to calladd_content_type()
during a scan.
-
class
substanced.content.
service
(content_type, factory_type=None, **meta)¶ This class is meant to be used as a decorator for a content factory that creates a service object (aka a service factory). A service object is an instance of a content type that can be looked up by name and which provides a service to application code. Services have well-known names within a folder. For example, the
principals
service within a folder is ‘the principals service’, thecatalog
object within a folder is ‘the catalog service’ and so on.This decorator accepts a content type, a factory type (optionally), and a set of meta keywords. These values mean the same thing as they mean for the
substanced.content.content
decorator andsubstanced.content.add_content_type()
. The decorator attaches information to the object it decorates which is used to calladd_content_type()
during a scan.There is only one difference between using the
substanced.content.content
decorator and thesubstanced.service.service
decorator. Theservice
decorator honors aservice_name
keyword argument. If this argument is passed, and a service already exists in the folder by this name, the service will not be shown as addable in the add-content dropdown in the SDI UI.
-
substanced.content.
add_content_type
(config, content_type, factory, factory_type=None, **meta)¶ Configurator directive method which register a content type factory with the Substance D type system. Call via
config.add_content_type
during Pyramid configuration phase.content_type
is a hashable object (usually a string) representing the content type.factory
is a class or function which produces a content instance. It must be a global object (e.g. it cannot be a callable which is a method of a class or a callable instance). Iffactory
is a function rather than a class, a factory wrapper is used (see below).**meta
is an arbitrary set of keywords associated with the content type in the content registry.Some of the keywords in
**meta
have special meaning:- If
meta
contains the keywordpropertysheets
, the content type will obtain a tab in the SDI that allows users to manage its properties. - If
meta
contains the keywordicon
, this value will be used as the icon for the content type that shows up next to the content in a folder content view.
Other keywords in
meta
will just be stored, and have no special meaning.factory_type
is an optional argument that can be used if the same factory must be used for two different content types; it is used during content type lookup (e.g.substanced.util.get_content_type()
) to figure out which content type a constructed object is an instance of; it only needs to be used when the same factory is used for two different content types. Note that two content types cannot have the same factory type, unless it isNone
.If
factory_type
is passed, the supplied factory will be wrapped in a factory wrapper which adds a__factory_type__
attribute to the constructed instance. The value of this attribute will be used to determine the content type of objects created by the factory.If the factory is a function rather than a class, a factory wrapper is used unconditionally.
The upshot wrt to
factory_type
: if your factory is a class and you pass afactory_type
or if your factory is a function, you won’t be able to successfully use the ‘bare’ factory callable to construct an instance of this content in your code, because the resulting instance will not have a__factory_type__
attribute. Instead, you’ll be required to usesubstanced.content.Content.create()
to create an instance of the object with a proper__factory_type__
attribute. But if your factory is a class, and you don’t passfactory_type
(the ‘garden path’), you’ll be able to use the class’ constructor directly in your code to create instances of your content objects, which is more convenient and easier to unit test.- If
-
substanced.content.
add_service_type
(config, content_type, factory, factory_type=None, **meta)¶ Configurator directive method which registers a service factory. Call via
config.add_service_type
during Pyramid configuration phase. All arguments mean the same thing as they mean for thesubstanced.content.add_content_type
.A service factory is a special kind of content factory. A service factory creates a service object. A service object is an instance of a content type that can be looked up by name and which provides a service to application code. Services often have well-known names within the services folder. For example, the
principals
object within a services folder is ‘the principals service’, thecatalog
object within a services folder is ‘the catalog service’ and so on.There is only one difference between using the
substanced.content.add_content_type
function and thesubstanced.service.add_service_type
decorator. Theadd_service_type
function honors aservice_name
keyword argument in its**meta
. If this argument is passed, and a service already exists in a folder by this name, the service will not be shown as addable in the add-content dropdown in the SDI UI of the folder.
-
class
substanced.content.
ContentRegistry
(registry)¶ An object accessible as
registry.content
(akarequest.registry.content
, akaconfig.registry.content
) that contains information about Substance D content types.-
add
(content_type, factory_type, factory, **meta)¶ Add a content type to this registry
-
all
()¶ Return all content types known my this registry as a sequence.
-
create
(content_type, *arg, **kw)¶ Create an instance of
content_type
by calling its factory with*arg
and**kw
. If the meta of the content type has anafter_create
value, call it (if it’s a string, it’s assumed to be a method of the created object, and if it’s a sequence, each value should be a string or a callable, which will be called in turn); then send asubstanced.event.ContentCreatedEvent
. Return the created object.If the key
__oid
is in thekw
arguments, it will be used as the created object’s oid.
-
exists
(content_type)¶ Return
True
ifcontent_type
has been registered within this content registry,False
otherwise.
-
factory_type_for_content_type
(content_type)¶ Return the factory_type value for the content_type requested
-
find
(resource, content_type)¶ Return the first object in the lineage of the
resource
that supplies thecontent_type
orNone
if no such object can be found.See also
pyramid.traversal.find_interface()
to find object by an interface or a class.
-
istype
(resource, content_type)¶ Return
True
ifresource
is of content typecontent_type
,False
otherwise.
-
metadata
(resource, name, default=None)¶ Return a metadata value for the content type of
resource
based on the**meta
value passed toadd()
. If a value in that content type’s metadata was passed usingname
as its name, the value will be returned, otherwisedefault
will be returned.
-
typeof
(resource)¶ Return the content type of
resource
-
-
substanced.content.
includeme
(config)¶
substanced.db
API¶
-
substanced.db.
root_factory
(request, t=<module 'transaction' from '/home/docs/checkouts/readthedocs.org/user_builds/substanced/envs/latest/local/lib/python2.7/site-packages/transaction/__init__.pyc'>, g=<function get_connection>, mark_unfinished_as_finished=<function mark_unfinished_as_finished>)¶ A function which can be used as a Pyramid
root_factory
. It accepts a request and returns an instance of theRoot
content type.
substanced.editable
API¶
-
interface
substanced.editable.
IEditable
¶ Adapter interface for editing content as a file.
-
put
(fileish)¶ Update context based on the contents of
fileish
.fileish
is a file-type object: itsread
method should return the (new) file representation of the context.
-
get
()¶ Return
(body_iter, mimetype)
representing the context.body_iter
is an iterable, whose chunks are bytes represenating the context as an editable file.mimetype
is the MIMEType corresponding tobody_iter
.
-
-
class
substanced.editable.
FileEditable
(context, request)¶ IEditable adapter for stock SubstanceD ‘File’ objects.
-
substanced.editable.
register_editable_adapter
(config, adapter, iface)¶ Configuration directive: register
IEditable
adapter foriface
.adapter
is the adapter factory (a class or other callable taking(context, request)
).iface
is the interface / class for which the adapter is registered.
-
substanced.editable.
get_editable_adapter
(context, request)¶ Return an editable adapter for the context
Return
None
if no editable adapter is registered.
substanced.event
API¶
-
class
substanced.event.
ObjectAdded
(object, parent, name, duplicating=False, moving=False, loading=False)¶ An event sent just after an object has been added to a folder.
-
class
substanced.event.
ObjectWillBeAdded
(object, parent, name, duplicating=False, moving=False, loading=False)¶ An event sent just before an object has been added to a folder.
-
class
substanced.event.
ObjectRemoved
(object, parent, name, removed_oids, moving=False, loading=False)¶ An event sent just after an object has been removed from a folder.
-
class
substanced.event.
ObjectWillBeRemoved
(object, parent, name, moving=False, loading=False)¶ An event sent just before an object has been removed from a folder.
-
removed_oids
¶ Helper property that caches oids that will be removed as the result of this event. Will return an empty sequence if objectmap cannot be found on self.parent.
-
-
class
substanced.event.
ObjectModified
(object)¶ An event sent when an object has been modified.
-
class
substanced.event.
ACLModified
(object, old_acl, new_acl)¶
-
class
substanced.event.
LoggedIn
(login, user, context, request)¶
-
class
substanced.event.
RootAdded
(object)¶
-
class
substanced.event.
AfterTransition
(object, old_state, new_state, transition)¶ Event sent after any workflow transition happens
-
class
substanced.event.
subscribe_added
(obj=None, container=None, **predicates)¶ Decorator for registering an object added event subscriber (a subscriber for ObjectAdded).
-
event
= <InterfaceClass substanced.interfaces.IObjectAdded>¶
-
-
class
substanced.event.
subscribe_removed
(obj=None, container=None, **predicates)¶ Decorator for registering an object removed event subscriber (a subscriber for ObjectRemoved).
-
event
= <InterfaceClass substanced.interfaces.IObjectRemoved>¶
-
-
class
substanced.event.
subscribe_will_be_added
(obj=None, container=None, **predicates)¶ Decorator for registering an object will-be-added event subscriber (a subscriber for ObjectWillBeAdded).
-
event
= <InterfaceClass substanced.interfaces.IObjectWillBeAdded>¶
-
-
class
substanced.event.
subscribe_will_be_removed
(obj=None, container=None, **predicates)¶ Decorator for registering an object will-be-removed event subscriber (a subscriber for ObjectWillBeRemoved).
-
event
= <InterfaceClass substanced.interfaces.IObjectWillBeRemoved>¶
-
-
class
substanced.event.
subscribe_modified
(obj=None, **predicates)¶ Decorator for registering an object modified event subscriber (a subscriber for ObjectModified).
-
event
= <InterfaceClass substanced.interfaces.IObjectModified>¶
-
-
class
substanced.event.
subscribe_acl_modified
(obj=None, **predicates)¶ Decorator for registering an acl modified event subscriber (a subscriber for ObjectModified).
-
event
= <InterfaceClass substanced.interfaces.IACLModified>¶
-
-
class
substanced.event.
subscribe_logged_in
(**predicates)¶ Decorator for registering an event listener for when a user is logged in
-
event
= <InterfaceClass substanced.interfaces.ILoggedIn>¶
-
substanced.evolution
API¶
-
substanced.evolution.
add_evolution_step
(config, func, before=None, after=None, name=None)¶ A configurator directive which adds an evolution step. An evolution step can be used to perform upgrades or migrations of data structures in existing databases to meet expectations of new code.
func
should be a function that performs the evolution logic. It should accept two arguments (conventionally-named)root
andregistry. ``root
will be the root of the ZODB used to serve your Substance D site, andregistry
will be the Pyramid application registry.before
should either beNone
, another evolution step function, or the dotted name to such a function. By default, it isNone
, which means execute in the order defined by the calling order ofadd_evolution_step
.after
should either beNone
, another evolution step function, or the dotted name to such a function. By default, it isNone
.name
is the name of the evolution step. It must be unique between all registered evolution steps. If it is not provided, the dotted name of the function used asfunc
will be used as the evolution step name.
-
substanced.evolution.
mark_unfinished_as_finished
(app_root, registry, t=None)¶ Given the root object of a Substance D site as
app_root
and a Pyramid registry, mark all pending evolution steps as completed without actually executing them.
-
substanced.evolution.
includeme
(config)¶
substanced.file
API¶
-
substanced.file.
USE_MAGIC
¶ A constant value used as an argument to various methods of the
substanced.file.File
class.
-
class
substanced.file.
File
(stream=None, mimetype=None, title=u'')¶ -
__init__
(stream=None, mimetype=None, title=u'')¶ The constructor of a File object.
stream
should be a filelike object (an object with aread
method that takes a size argument) orNone
. If stream isNone
, the blob attached to this file object is created empty.title
must be a string or Unicode object.mimetype
may be any of the following:None
, meaning set this file object’s mimetype toapplication/octet-stream
(the default).A mimetype string (e.g.
image/gif
)The constant
substanced.file.USE_MAGIC
, which will derive the mimetype from the stream content (ifstream
is also supplied) using thepython-magic
library.Warning
On non-Linux systems, successful use of
substanced.file.USE_MAGIC
requires the installation of additional dependencies. See Installing python-magic.
-
blob
¶ The ZODB blob object associated with this file.
-
mimetype
¶ The mimetype of this file object (a string).
-
get_etag
()¶ Return a token identifying the “version” of the file.
-
get_response
(**kw)¶ Return a WebOb-compatible response object which uses the blob content as the stream data and the mimetype of the file as the content type. The
**kw
arguments will be passed to thepyramid.response.FileResponse
constructor as its keyword arguments.
-
get_size
()¶ Return the size in bytes of the data in the blob associated with the file
-
upload
(stream, mimetype_hint=None)¶ Replace the current contents of this file’s blob with the contents of
stream
.stream
must be a filelike object (it must have aread
method that takes a size argument).mimetype_hint
can be any of the following:None
, meaning don’t reset the current mimetype. This is the default. If you already know the file’s mimetype, and you don’t want it divined from a filename or stream content, useNone
as themimetype_hint
value, and set themimetype
attribute of the file object directly before or after calling this method.A string containing a filename that has an extension; the mimetype will be derived from the extension in the filename using the Python
mimetypes
module, and the result will be set as the mimetype attribute of this object.The constant
substanced.file.USE_MAGIC
, which will derive the mimetype using thepython-magic
library based on the stream’s actual content. The result will be set as the mimetype attribute of this object.Warning
On non-Linux systems, successful use of
substanced.file.USE_MAGIC
requires the installation of additional dependencies. See Installing python-magic.
-
substanced.folder
API¶
-
class
substanced.folder.
FolderKeyError
¶
-
class
substanced.folder.
Folder
(data=None, family=None)¶ A folder implementation which acts much like a Python dictionary.
Keys must be Unicode strings; values must be arbitrary Python objects.
-
__init__
(data=None, family=None)¶ Constructor. Data may be an initial dictionary mapping object name to object.
-
order
¶ A tuple of name values. If set, controls the order in which names should be returned from
__iter__()
,keys()
,values()
, anditems()
. If not set, use an effectively random order.
-
add
(name, other, send_events=True, reserved_names=(), duplicating=None, moving=None, loading=False, registry=None)¶ Same as
__setitem__
.If
send_events
is False, suppress the sending of folder events. Don’t allow names in thereserved_names
sequence to be added.If
duplicating
notNone
, it must be the object which is being duplicated; a result of a non-None
duplicating means that oids will be replaced in objectmap. Ifmoving
is notNone
, it must be the folder from which the object is moving; this will be themoving
attribute of events sent by this function too. Ifloading
isTrue
, theloading
attribute of events sent as a result of calling this method will beTrue
too.This method returns the name used to place the subobject in the folder (a derivation of
name
, usually the result ofself.check_name(name)
).
-
add_service
(name, obj, registry=None, **kw)¶ Add a service to this folder named
name
.
-
check_name
(name, reserved_names=())¶ Perform all the validation checks implied by
validate_name()
against thename
supplied but also fail with aFolderKeyError
if an object with the namename
already exists in the folder.
-
clear
(registry=None)¶ Clear all items from the folder. This is the equivalent of calling
.remove
with each key that exists in the folder.
-
copy
(name, other, newname=None, registry=None)¶ Copy a subobject named
name
from this folder to the folder represented byother
. Ifnewname
is not none, it is used as the target object name; otherwise the existing subobject name is used.
-
find_service
(service_name)¶ Return a service named by
service_name
in this folder or any parent service folder orNone
if no such service exists. A shortcut forsubstanced.service.find_service()
.
-
find_services
(service_name)¶ Returns a sequence of service objects named by
service_name
in this folder’s lineage or an empty sequence if no such service exists. A shortcut forsubstanced.service.find_services()
-
get
(name, default=None)¶ Return the object named by
name
or the default.name
must be a Unicode object or a bytestring object.If
name
is a bytestring object, it must be decodable using the system default encoding.
-
is_ordered
()¶ Return true if the folder has a manually set ordering, false otherwise.
-
is_reorderable
()¶ Return true if the folder can be reordered, false otherwise.
-
items
()¶ Return an iterable sequence of (name, value) pairs in the folder.
Respect
order
, if set.
-
keys
()¶ Return an iterable sequence of object names present in the folder.
Respect order, if set.
-
load
(name, newobject, registry=None)¶ A replace method used by the code that loads an existing dump. Events sent during this replace will have a true
loading
flag.
-
move
(name, other, newname=None, registry=None)¶ Move a subobject named
name
from this folder to the folder represented byother
. Ifnewname
is not none, it is used as the target object name; otherwise the existing subobject name is used.This operation is done in terms of a remove and an add. The Removed and WillBeRemoved events as well as the Added and WillBeAdded events sent will indicate that the object is moving.
-
order
Return an iterable sequence of object names present in the folder.
Respect order, if set.
-
pop
(name, default=<object object>, registry=None)¶ Remove the item stored in the under
name
and return it.If
name
doesn’t exist in the folder, anddefault
is not passed, raise aKeyError
.If
name
doesn’t exist in the folder, anddefault
is passed, returndefault
.When the object stored under
name
is removed from this folder, remove its__parent__
and__name__
values.When this method is called, emit an
substanced.event.ObjectWillBeRemoved
event before the object loses its__name__
or__parent__
values. Emit ansubstanced.event.ObjectRemoved
after the object loses its__name__
and__parent__
value,
-
remove
(name, send_events=True, moving=None, loading=False, registry=None)¶ Same thing as
__delitem__
.If
send_events
is false, suppress the sending of folder events.If
moving
is notNone
, themoving
argument must be the folder to which the named object will be moving. This value will be passed along as themoving
attribute of the events sent as the result of this action. Ifloading
isTrue
, theloading
attribute of events sent as a result of calling this method will beTrue
too.
-
rename
(oldname, newname, registry=None)¶ Rename a subobject from oldname to newname.
This operation is done in terms of a remove and an add. The Removed and WillBeRemoved events sent will indicate that the object is moving.
-
reorder
(names, before)¶ Move one or more items from a folder into new positions inside that folder.
names
is a list of ids of existing folder subobject names, which will be inserted in order before the item namedbefore
. All other items are left in the original order. Ifbefore
isNone
, the items will be appended after the last item in the current order. If this method is called on a folder which does not have an order set, or which is not reorderable, aValueError
will be raised. AKeyError
is raised, ifbefore
does not correspond to any item, and is notNone
.
-
replace
(name, newobject, send_events=True, registry=None)¶ Replace an existing object named
name
in this folder with a new objectnewobject
. If there isn’t an object namedname
in this folder, an exception will not be raised; instead, the new object will just be added.This operation is done in terms of a remove and an add. The Removed and WillBeRemoved events will be sent for the old object, and the WillBeAdded and Added events will be sent for the new object.
-
set_order
(names, reorderable=None)¶ Sets the folder order.
names
is a list of names for existing folder items, in the desired order. All names that currently exist in the folder must be mentioned innames
, or aValueError
will be raised.If
reorderable
is passed, value, it must beNone
,True
orFalse
. If it isNone
, the reorderable flag will not be reset from its current value. If it is anything exceptNone
, it will be treated as a boolean and the reorderable flag will be set to that value. Thereorderable
value of a folder will be returned by that folder’sis_reorderable()
method. Theis_reorderable()
method is used by the SDI folder contents view to indicate that the folder can or cannot be reordered via the web UI.If
reorderable
is set toTrue
, thereorder()
method will work properly, otherwise it will raise aValueError
when called.
-
unset_order
()¶ Remove set order from a folder, making it unordered, and non-reorderable.
-
validate_name
(name, reserved_names=())¶ Validate the
name
passed to ensure that it’s addable to the folder. Returns the name decoded to Unicode if it passes all addable checks. It’s not addable if:- the name is not decodeable to Unicode.
- the name starts with
@@
(conflicts with explicit view names). - the name has slashes in it (WSGI limitation).
- the name is empty.
If any of these conditions are untrue, raise a
ValueError
. If the name passed is in the list ofreserved_names
, raise aValueError
.
-
values
()¶ Return an iterable sequence of the values present in the folder.
Respect
order
, if set.
-
-
class
substanced.folder.
SequentialAutoNamingFolder
(data=None, family=None, autoname_length=None, autoname_start=None)¶ An auto-naming folder which autonames a subobject by sequentially incrementing the maximum key of the folder.
Example names:
0000001
, then0000002
, and so on.This class implements the
substanced.interfaces.IAutoNamingFolder
interface and inherits fromsubstanced.folder.Folder
.This class is typically used as a base class for a custom content type.
-
__init__
(data=None, family=None, autoname_length=None, autoname_start=None)¶ Constructor. Data may be an initial dictionary mapping object name to object. Autoname length may be supplied. If it is not, it will default to 7. Autoname start may be supplied. If it is not, it will default to -1.
-
add_next
(subobject, send_events=True, duplicating=None, moving=None, registry=None)¶ Add a subobject, naming it automatically, giving it the name returned by this folder’s
next_name
method. It has the same effect as callingsubstanced.folder.Folder.add()
, but you needn’t provide a name argument.This method returns the name of the subobject.
-
next_name
(subobject)¶ Return a name string based on:
- intifying the maximum key of this folder and adding one.
- zero-filling the left hand side of the result with as many zeroes
as are in the value of this folder’s
autoname_length
constructor value.
If the folder has no items in it, the initial value used as a name will be the value of this folder’s
autoname_start
constructor value.
-
add
(name, other, send_events=True, reserved_names=(), duplicating=None, moving=None, loading=False, registry=None)¶ The
add
method of a SequentialAutoNamingFolder will raise aValueError
if thename
it is passed is not intifiable, as itsnext_name
method relies on controlling the types of names that are added to it (they must be intifiable). It will also zero-fill the name passed based on this folder’sautoname_length
constructor value. It otherwise just calls its superclass’add
method and returns the result.
-
-
class
substanced.folder.
RandomAutoNamingFolder
(data=None, family=None, autoname_length=None)¶ An auto-naming folder which autonames a subobject using a random string.
Example names:
MXF937A
,FLTP2F9
.This class implements the
substanced.interfaces.IAutoNamingFolder
interface and inherits fromsubstanced.folder.Folder
.This class is typically used as a base class for a custom content type.
-
__init__
(data=None, family=None, autoname_length=None)¶ Constructor. Data may be an initial dictionary mapping object name to object. Autoname length may be supplied. If it is not, it will default to 7.
-
add_next
(subobject, send_events=True, duplicating=None, moving=None, registry=None)¶ Add a subobject, naming it automatically, giving it the name returned by this folder’s
next_name
method. It has the same effect as callingsubstanced.folder.Folder.add()
, but you needn’t provide a name argument.This method returns the name of the subobject.
-
next_name
(subobject)¶ Return a name string based on generating a random string composed of digits and uppercase letters of a length determined by this folder’s
autoname_length
constructor value. It tries generatoing values continuously until one that is unused is found.
-
substanced.folder.views
API¶
substanced.form
API¶
-
class
substanced.form.
Form
(schema, action='', method='POST', buttons=(), formid='deform', use_ajax=False, ajax_options='{}', autocomplete=None, **kw)¶ Subclass of
deform.form.Form
which uses a custom resource registry designed for Substance D. XXX point at deform docs.
-
class
substanced.form.
FormView
(context, request)¶ A class which can be used as a view which introspects a schema to present the form. XXX describe better using
pyramid_deform
documentation.
-
class
substanced.form.
FileUploadTempStore
(request)¶ A Deform
FileUploadTempStore
implementation that stores file upload data in the Pyramid session and on disk. The request passed to its constructor must be a fully-initialized Pyramid request (it have aregistry
attribute, which must have asettings
attribute, which must be a dictionary). Thesubstanced.uploads_tempdir
variable in thesettings
dictionary must be set to the path of an existing directory on disk. This directory will temporarily store file upload data on behalf of Deform and Substance D when a form containing a file upload widget fails validation.See the Deform documentation for more information about
FileUploadTempStore
objects.
substanced.locking
API¶
Advisory exclusive DAV-style locks for content objects.
When a resource is locked, it is presumed that its SDI UI will display a warning to users who do not hold the lock. The locking service can also be used by add-ons such as DAV implementations.
-
class
substanced.locking.
Lock
(infinite=False, timeout=3600, comment=None, last_refresh=None)¶ A persistent object representing a lock.
-
ownerid
¶ The owner oid for this lock.
-
owner
¶ The owner object of this lock (a User).
-
resourceid
¶ The oid of the resource related to this lock.
-
resource
¶ The resource object related to this lock.
-
commit_suicide
()¶ Remove this lock from the lock service.
-
expires
()¶ Return the future datetime at which this lock will expire.
For invalid locks, the returned value indicates the point in the past at which the lock expired.
-
is_valid
(when=None)¶ Return True if the lock has not expired and its resource exists.
-
refresh
(timeout=None, when=None)¶ Refresh the lock.
If the timeout is not None, set the timeout for this lock too.
-
-
class
substanced.locking.
LockError
(lock)¶ Raised when a lock cannot be created due to a conflicting lock.
Instances of this class have a
lock
attribute which is asubstanced.locking.Lock
object, representing the conflicting lock.
-
class
substanced.locking.
UnlockError
(lock)¶ Raised when a lock cannot be removed
This may be because the owner suplied in the unlock request does not match the owner of the lock, or becaues the lock no longer exists.
Instances of this class have a
lock
attribute which is asubstanced.locking.Lock
object, representing the conflicting lock, orNone
if there was no lock to unlock.
-
substanced.locking.
lock_resource
(resource, owner_or_ownerid, timeout=None, comment=None, locktype=<ReferenceClass substanced.interfaces.WriteLock>, infinite=False)¶ Lock a resource using the lock service.
If the resource is already locked by the owner supplied as owner_or_ownerid, refresh the lock using
timeout
.If the resource is not already locked by another user, create a new lock with the given values.
If the resource is already locked by a different user, raise a
substanced.locking.LockError
If a Lock Service does not already exist in the lineage, a
ValueError
will be raised.Warning
Callers should assert that the owner has the
sdi.lock
permission against the resource before calling this function to ensure that a user can’t lock a resource he is not permitted to.
-
substanced.locking.
unlock_resource
(resource, owner_or_ownerid, locktype=<ReferenceClass substanced.interfaces.WriteLock>)¶ Unlock a resource using the lock service.
If the resource is already locked by a user other than the owner supplied as
owner_or_ownerid
or the resource isn’t already locked with this lock type, raise asubstanced.locking.UnlockError
exception.Otherwise, remove the lock.
If a Lock Service does not already exist in the lineage, a
ValueError
will be raised.Warning
Callers should assert that the owner has the
sdi.lock
permission against the resource before calling this function to ensure that a user can’t lock a resource he is not permitted to.
-
substanced.locking.
discover_resource_locks
(resource, include_invalid=False, include_lineage=True, locktype=<ReferenceClass substanced.interfaces.WriteLock>)¶ Return locks related to
resource
for the givenlocktype
.Return a sequence of
substanced.locking.Lock
objects.By default, only valid locks are returned.
Invalid locks for the resource may exist, but they are not returned unless
include_invalid
isTrue
.Under normal circumstances, the length of the sequence returned will be either 0 (if there are no locks) or 1 (if there is any lock).
In some special circumstances, however, when the
substanced.locking.lock_resource
API is not used to create locks, there may be more than one lock of the same type related to a resource.
substanced.objectmap
API¶
-
class
substanced.objectmap.
ObjectMap
(root, family=None)¶ -
add
(obj, path_tuple, duplicating=False, moving=False)¶ Add a new object to the object map at the location specified by
path_tuple
(must be the path of the object in the object graph as a tuple, as returned by Pyramid’sresource_path_tuple
function).If
duplicating
isTrue
, replace the oid of the added object even if it already has one and adjust extents involving the new oid.If
moving
isTrue
, don’t add any extents.It is an error to pass a true value for both
duplicating
andmoving
.
-
allowed
(oids, principals, permission)¶ For the set of oids present in
oids
, return a sequence of oids that are permittedpermission
against each oid if the implied user is a member of the set of principals implied byprincipals
. This method uses the data collected via theset_acl
method of this class.
-
connect
(source, target, reftype)¶ Connect a source object or objectid to a target object or objectid using reference type
reftype
-
disconnect
(source, target, reftype)¶ Disconnect a source object or objectid from a target object or objectid using reference type
reftype
-
get_extent
(name, default=())¶ Return the extent for
name
(typically a factory name, e.g. the dotted name of the content class). It will be a TreeSet composed entirely of oids. If no extent exist by this name, this will return the value ofdefault
.
-
get_reftypes
()¶ Return a sequence of reference types known by this objectmap.
-
has_references
(obj, reftype=None)¶ Return true if the object participates in any reference as a source or a target.
obj
may be an object or an oid.
-
new_objectid
()¶ Obtain an unused integer object identifier
-
object_for
(objectid_or_path_tuple, context=None)¶ Returns an object or
None
given an object id or a path tuple
-
objectid_for
(obj_or_path_tuple)¶ Returns an objectid or
None
, given an object or a path tuple
-
order_sources
(targetid, reftype, order=<object object>)¶ Set the ordering of the source ids of a reference relative to the
targetid
.order
should be a tuple or list of oids or objects in the order that they should be kept in the reference map. If the reftyp+targetid combination has existing reference values, the values inorder
must mention all of their oids, or aValueError
will be raised. You can unset an order for this targetid+reftype combination by passingNone
as the order.
-
order_targets
(sourceid, reftype, order=<object object>)¶ Set the ordering of the target ids of a reference type.
order
should be a tuple (or list) of oids or objects in the order that they should be kept in the reference map. If the reference type has existing reference values, the values inorder
must mention all of their oids, or aValueError
will be raised. You can unset an ordering by passingNone
as the order.
-
path_for
(objectid)¶ Returns an path or
None
given an object id
-
pathcount
(obj_or_path_tuple, depth=None, include_origin=True)¶ Return the total number of objectids under a given path given an object or a path tuple. If
depth
is None, count all object ids under the path. Ifdepth
is an integer, use that depth instead. Ifinclude_origin
isTrue
, count the object identifier of the object that was passed, otherwise omit it.
-
pathlookup
(obj_or_path_tuple, depth=None, include_origin=True)¶ Return a set of objectids under a given path given an object or a path tuple. If
depth
is None, return all object ids under the path. Ifdepth
is an integer, use that depth instead. Ifinclude_origin
isTrue
, include the object identifier of the object that was passed, otherwise omit it from the returned set.
-
remove
(obj_objectid_or_path_tuple, moving=False)¶ Remove an object from the object map give an object, an object id or a path tuple. If
moving
isFalse
, also remove any references added viaconnect
and any extents related to the removed objects.Return a set of removed oids (including the oid related to the object passed).
-
set_acl
(obj_objectid_or_path_tuple, acl)¶ For the resource implied by
obj_objectid_or_path_tuple
, set the cached version of its ACL (for later used byallowed
) to the ACL passed asacl
-
sourceids
(obj, reftype)¶ Return a set of object identifiers of the objects connected to
obj
a source using reference typereftype
-
sources
(obj, reftype)¶ Return a generator which will return the objects connected to
obj
as a source using reference typereftype
-
targetids
(obj, reftype)¶ Return a set of object identifiers of the objects connected to
obj
a target using reference typereftype
-
targets
(obj, reftype)¶ Return a generator which will return the objects connected to
obj
as a target using reference typereftype
-
-
class
substanced.objectmap.
Multireference
(context, objectmap, reftype, ignore_missing, resolve, orientation, ordered=False)¶ An iterable of objects (if
resolve
is true) or oids (ifresolve
is false). Also supports the Python sequence protocol.Additionally supports
connect
,disconnect
, andclear
methods for mutating the relationships implied by the reference.-
clear
()¶ Clear all references in this relationship.
-
connect
(objects, ignore_missing=None)¶ Connect
objects
to this reference’s relationship.objects
should be a sequence of content objects or object identifiers.
-
disconnect
(objects, ignore_missing=None)¶ Disconnect
objects
from this reference’s relationship.objects
should be a sequence of content objects or object identifiers.
-
-
substanced.objectmap.
reference_sourceid_property
(reftype)¶ Returns a property which, when set, establishes an object map reference between the property’s instance (the source) and another object in the objectmap (the target) based on the reference type
reftype
. It is comparable to a Python ‘weakref’ between the persistent object instance which the property is attached to and the persistent target object id; when the target object or the object upon which the property is defined is removed from the system, the reference is destroyed.The
reftype
argument is a reference type, a hashable object that describes the type of the relation. Seesubstanced.objectmap.ObjectMap.connect()
for more information about reference types.You can set, get, and delete the value. When you set the value, a relation is formed between the object which houses the property and the target object id. When you get the value, the related value (or
None
if no relation exists) is returned, when you delete the value, the relation is destroyed and the value will revert toNone
.For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# definition from substanced.content import content from substanced.objectmap import reference_sourceid_property @content('Profile') class Profile(Persistent): user_id = reference_sourceid_property('profile-to-userid') # subsequent usage of the property in a view... profile = registry.content.create('Profile') somefolder['profile'] = profile profile.user_id = get_oid(request.user) print profile.user_id # will print the oid of the user # if the user is later deleted by unrelated code... print profile.user_id # will print None # or if you delete the value explicitly... del profile.user_id print profile.user_id # will print None
-
substanced.objectmap.
reference_source_property
(reftype)¶ Exactly like
substanced.objectmap.reference_sourceid_property()
, except its getter returns the instance related to the target instead of the target object id. Likewise, its setter will accept another persistent object instance that has an object id.For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# definition from substanced.content import content from substanced.objectmap import reference_source_property @content('Profile') class Profile(Persistent): user = reference_source_property('profile-to-user') # subsequent usage of the property in a view... profile = registry.content.create('Profile') somefolder['profile'] = profile profile.user = request.user print profile.user # will print the user object # if the user is later deleted by unrelated code... print profile.user # will print None # or if you delete the value explicitly... del profile.user print profile.user # will print None
-
substanced.objectmap.
reference_targetid_property
(reftype)¶ Same as
substanced.objectmap.reference_sourceid_property()
, except the object upon which the property is defined is the target of the reference and any object assigned to the property is the source.
-
substanced.objectmap.
reference_target_property
(reftype)¶ Same as
substanced.objectmap.reference_source_property()
, except the object upon which the property is defined is the target of the reference and any object assigned to the property is the source.
-
substanced.objectmap.
multireference_sourceid_property
(reftype, ignore_missing=False, ordered=None)¶ Like
substanced.objectmap.reference_sourceid_property()
, but maintains asubstanced.objectmap.Multireference
rather than an object id. Ifignore_missing
isTrue
, attempts to connect or disconnect unresolveable object identifiers will not cause an exception. Ifordered
isTrue
, the relative ordering of references in a sequence will be maintained when you assign that sequence to the property and when you use the.connect
method of the property. Ifordered
isNone
, defers to the appropriate attribute on thereftype
.
-
substanced.objectmap.
multireference_source_property
(reftype, ignore_missing=False, ordered=None)¶ Like
substanced.objectmap.reference_source_property()
, but maintains asubstanced.objectmap.Multireference
rather than a single object reference. Ifignore_missing
isTrue
, attempts to connect or disconnect unresolveable object identifiers will not cause an exception. Ifordered
isTrue
, the relative ordering of references in a sequence will be maintained when you assign that sequence to the property and when you use the.connect
method of the property. Ifordered
isNone
, defers to the appropriate attribute on thereftype
.
-
substanced.objectmap.
multireference_targetid_property
(reftype, ignore_missing=False, ordered=None)¶ Like
substanced.objectmap.reference_targetid_property()
, but maintains asubstanced.objectmap.Multireference
rather than an object id. Ifignore_missing
isTrue
, attempts to connect or disconnect unresolveable object identifiers will not cause an exception. Ifordered
isTrue
, the relative ordering of references in a sequence will be maintained when you assign that sequence to the property and when you use the.connect
method of the property. Ifordered
isNone
, defers to the appropriate attribute on thereftype
.
-
substanced.objectmap.
multireference_target_property
(reftype, ignore_missing=False, ordered=None)¶ Like
substanced.objectmap.reference_target_property()
, but maintains asubstanced.objectmap.Multireference
rather than a single object reference. Ifignore_missing
isTrue
, attempts to connect or disconnect unresolveable object identifiers will not cause an exception. Ifordered
isTrue
, the relative ordering of references in a sequence will be maintained when you assign that sequence to the property and when you use the.connect
method of the property. Ifordered
isNone
, defers to the appropriate attribute on thereftype
.
-
class
substanced.objectmap.
ReferentialIntegrityError
(obj, reftype, oids)¶ Exception raised when a referential integrity constraint is violated. Raised before an object involved in a relation with an integrity constraint is removed from a folder.
Attributes:
obj: the object which would have been removed were its removal not prevented by the raising of this exception reftype: the reference type (usually a class) oids: the oids that reference the to-be-removed object.
-
get_objects
()¶ Return the objects which hold a reference to the object inovlved in the integrity error.
-
-
class
substanced.objectmap.
SourceIntegrityError
(obj, reftype, oids)¶
-
class
substanced.objectmap.
TargetIntegrityError
(obj, reftype, oids)¶
substanced.principal
API¶
substanced.property
API¶
-
class
substanced.property.
PropertySheet
(context, request)¶ Bases:
object
Convenience base class for concrete property sheet implementations
-
before_render
(form)¶ Hook: allow subclasses to scribble on form.
Called by
substanced.property.views.PropertySheetsView.before
, after building the form but before rendering it.
-
-
substanced.property.
add_propertysheet
(self, *arg, **kw)¶ Add a propertysheet for the content types implied by
iface
andpredicates
.The
propsheet
argument represents a propertysheet class (or a dotted Python name which identifies such a class); it will be called with two objects:context
andrequest
whenever Substance D determines that the propertysheet is necessary to display. Theiface
may be an interface or a class or a dotted Python name to a global object representing an interface or a class.Using the default
iface
value,None
will cause the propertysheet to be registered for all content types.Any number of predicate keyword arguments may be passed in
**predicates
. Each predicate named will narrow the set of circumstances in which the propertysheet will be invoked. Each named predicate must have been registered viapyramid.config.Configurator.add_propertysheet_predicate()
before it can be used.
-
substanced.property.
add_propertysheet_predicate
(self, *arg, **kw)¶ Adds a property sheet predicate factory. The associated property sheet predicate can later be named as a keyword argument to
pyramid.config.Configurator.add_propertysheet()
in the**predicates
anonymous keyword argument dictionary.name
should be the name of the predicate. It must be a valid Python identifier (it will be used as a**predicates
keyword argument toadd_propertysheet()
).factory
should be a predicate factory or dotted Python name which refers to a predicate factory.
-
class
substanced.property.
PropertySheet
(context, request) Convenience base class for concrete property sheet implementations
-
before_render
(form) Hook: allow subclasses to scribble on form.
Called by
substanced.property.views.PropertySheetsView.before
, after building the form but before rendering it.
-
substanced.schema
API¶
-
class
substanced.schema.
Schema
(*arg, **kw)¶ A
colander.Schema
subclass which generates and validates a CSRF token automatically. You must use it like so:from substanced.schema import Schema as CSRFSchema import colander class MySchema(CSRFSchema): my_value = colander.SchemaNode(colander.String())
And in your application code, bind the schema, passing the request as a keyword argument:
def aview(request): schema = MySchema().bind(request=request)
In order for the CRSFSchema to work, you must configure a session factory in your Pyramid application. This is usually done by Substance D itself, but may not be done for you in extremely custom configurations.
-
schema_type
¶ alias of
RemoveCSRFMapping
-
-
class
substanced.schema.
NameSchemaNode
(*arg, **kw)¶ Convenience Colander schemanode used to represent the name (aka
__name__
) of an object in a propertysheet or add form which allows for customizing the detection of whether editing or adding is being done, and setting a max length for the name.By default it uses the context’s
check_name
API to ensure that the name provided is valid, and limits filename length to a default of 100 characters. Some usage examples follow.This sets up the name_node to assume that it’s in ‘add’ mode with the default 100 character max limit.:
name_node = NameSchemaNode()
This sets up the name_node to assume that it’s in ‘add’ mode, and that the maximum length of the name provided is 20 characters:
name_node = NameSchemaNode(max_len=20)
This sets up the name_node to assume that it’s in ‘edit’ mode (
check_name
will be called on the parent of the bind context, not on the context itself):name_node = NameSchemaNode(editing=True)
This sets up the name_node to condition whether it’s in edit mode on the result of a function:
def i_am_editing(context, request): return request.registry.content.istype(context, 'Document') name_node = NameSchemaNode(editing=i_am_editing)
-
class
substanced.schema.
PermissionsSchemaNode
(*arg, **kw)¶ A SchemaNode which represents a set of permissions; uses a widget which collects all permissions from the introspection system. Deserializes to a set.
substanced.root
API¶
-
class
substanced.root.
Root
(data=None, family=None)¶ An object representing the root of a Substance D application (the object represented in the root of the SDI). It is a subclass of
substanced.folder.Folder
.When created as the result of
registry.content.create
, an instance of a Root will contain aprincipals
service. The principals service will contain a user whose name is specified via thesubstanced.initial_login
deployment setting with a password taken from thesubstanced.initial_password
setting. This user will also be a member of anadmins
group. Theadmins
group will be granted theALL_PERMISSIONS
special permission in the root.If this class is created by hand, its
after_create
method must be called manually to create its objectmap, the services, the user, and the group.
substanced.stats
API¶
-
substanced.stats.
statsd_timer
()¶ Return a context manager that can be used for statsd timing, e.g.:
with statsd_timer('addlotsofstuff'): # add lots of stuff
name
is the statsd stat name,rate
is the sample rate (a float between 0 and 1), andregistry
can be passed to speed up lookups (it should be the Pyramid registry).
-
substanced.stats.
statsd_gauge
()¶ Register a statsd gauge value. For example:
statsd_gauge('connections', numconnections)
name
is the statsd stat name,rate
is the sample rate (a float between 0 and 1), andregistry
can be passed to speed up lookups (it should be the Pyramid registry).
-
substanced.stats.
statsd_incr
()¶ Incremement or decrement a statsd counter value. For example:
statsd_incr('hits', 1) To decrement:: statsd_incr('numusers', -1)
name
is the statsd stat name,rate
is the sample rate (a float between 0 and 1), andregistry
can be passed to speed up lookups (it should be the Pyramid registry).
substanced.util
API¶
-
substanced.util.
acquire
(resource, name, default=<object object>)¶
-
substanced.util.
get_oid
(resource, default=<object object>)¶ Return the object identifer of
resource
. Ifresource
has no object identifier, raise an AttributeError exception unlessdefault
was passed a value; ifdefault
was passed a value, return the default in that case.
-
substanced.util.
set_oid
(resource, oid)¶ Set the object id of the resource to oid.
-
substanced.util.
get_acl
(resource, default=<object object>)¶ Return the ACL of the object or the default if the object has no ACL. If no default is passed, an
AttributeError
will be raised if the object doesn’t have an ACL.
-
substanced.util.
set_acl
(resource, new_acl, registry=None)¶ Change the ACL on resource to
new_acl
, which may be a valid ACL orNone
. Ifnew_acl
isNone
, any existing non-None
__acl__
attribute of the resource will be removed (viadel resource.__acl__
). Otherwise, if the resource’s__acl__
and thenew_acl
differ, set the resource’s__acl__
tonew_acl
via setattr.If the new ACL and the object’s original ACL differ, send a
substanced.event.ACLModified
event with the new ACL and the original ACL (the__acl__
attribute of the resource, orNone
if it doesn’t have one) as arguments to the event.This function will return
True
if a mutation to the resource’s __acl__ was performed, andFalse
otherwise.If
registry
is passed, it should be a Pyramid registry; if it is not passed, this function will use the current threadlocal registry to send the event.
-
substanced.util.
get_interfaces
(obj, classes=True)¶ Return the set of interfaces provided by
obj
. Include its __class__ if classes is True.
-
substanced.util.
get_content_type
(resource, registry=None)¶ Return the content type of a resource or
None
if the object has no content type. Ifregistry
is not supplied, the current Pyramid registry will be looked up as a thread local in order to find the Substance D content registry.
-
substanced.util.
find_content
(resource, content_type, registry=None)¶ Return the first object in the lineage of the resource that supplies the
content_type
. Ifregistry
is not supplied, the current Pyramid registry will be looked up as a thread local in order to find the Substance D content registry.
-
substanced.util.
find_service
(resource, name, *subnames)¶ Find the first service named
name
in the lineage ofresource
or returnNone
if no such-named service could be found.If
subnames
is supplied, when a service namedname
is found in the lineage, it will attempt to traverse the service as a folder, finding a content object inside the service, and it will return it instead of the service object itself. For example,find_service(resource, 'principals', 'users')
would find and return theusers
subobject in theprincipals
service.find_service(resource, 'principals', 'users', 'fred')
would find and return the fred subobject of the users subobject of the principals service, and so forth. Ifsubnames
are supplied, and the named object cannot be found, the lineage search continues.
-
substanced.util.
find_services
(resource, name, *subnames)¶ Finds all services named
name
in the lineage ofresource
and returns a sequence containing those service objects. The sequence will begin with the most deepest nested service and will end with the least deeply nested service. Returns an empty sequence if no such-named service could be found.If
subnames
is supplied, when a service namedname
is found in the lineage, it will attempt to traverse the service as a folder, finding a content object inside the service, and this API will append this object rather than the service itself to the list of things returned. For example,find_services(resource, 'principals', 'users')
would find theusers
subobject in theprincipals
service.find_services(resource, 'principals', 'users', 'fred')
would find the fred subobject of the users subobject of the principals service, and so forth. Ifsubnames
are supplied, whether or not the named object can be found, the lineage search continues.
-
substanced.util.
find_objectmap
(context)¶ Returns the object map for the root object in the lineage of the
context
orNone
if no objectmap can be found.
-
substanced.util.
find_catalogs
(resource, name=None)¶ Return all catalogs in the lineage. If
name
is supplied, return only catalogs that have this name in the lineage, otherwise return all catalogs in the lineage.
-
substanced.util.
find_catalog
(resource, name)¶ Return the first catalog named
name
in the lineage of the resource
-
substanced.util.
find_index
(resource, catalog_name, index_name)¶ Find the first catalog named
catalog_name
in the lineage of the resource, and ask it for itsindex_name
index; return the resulting index. If either a catalog of the provided name or an index of the provided name does not exist, this function will returnNone
.
-
substanced.util.
get_principal_repr
(principal_or_id)¶ Given as
principal_or_id
a resource object that has a__principal_repr__
method, return the result of calling that method (without arguments); it must be a string that uniquely identifies the principal amongst all principals in the system.Given as
principal_or_id
a resource object that does not have a__principal_repr__
method, return the result of the stringification of the__oid__
attribute of the resource object.Given an integer as
principal_or_id
, return a stringification of the integer.Given any other string value, return it.
-
substanced.util.
is_folder
(resource)¶ Return
True
if the object is a folder,False
if not.
-
substanced.util.
is_service
(resource)¶ Returns
True
if the resource is a service,False
if not.
-
substanced.util.
get_factory_type
(resource)¶ If the resource has a __factory_type__ attribute, return it. Otherwise return the full Python dotted name of the resource’s class.
-
substanced.util.
coarse_datetime_repr
(date)¶ Convert a datetime to an integer with 100 second granularity.
The granularity reduces the number of index entries in a fieldindex when it’s used in an indexview to convert a datetime value to an integer.
-
substanced.util.
postorder
(startnode)¶ Walks over nodes in a folder recursively. Yields deepest nodes first.
-
substanced.util.
merge_url_qs
(url, **kw)¶ Merge the query string elements of a URL with the ones in
kw
. If any query string element exists inurl
that also exists inkw
, replace it.
-
substanced.util.
chunks
(stream, chunk_size=10000)¶ Return a generator that will iterate over a stream (a filelike object)
chunk_size
bytes at a time.
-
substanced.util.
renamer
()¶ Returns a property. The getter of the property returns the
__name__
attribute of the instance on which it’s defined. The setter of the property callsrename()
on the__parent__
of the instance on which it’s defined if the new value doesn’t match the existing__name__
of the instance (this will cause__name__
to be reset if the parent is a normal Substance D folder ). Sample usage:class SomeContentType(Persistent): name = renamer()
-
substanced.util.
get_dotted_name
(g)¶ Return the dotted name of a global object.
-
substanced.util.
get_icon_name
(resource, request)¶ Returns the content registry icon name of the resource or
None
if the resource type has no icon in the content registry.
-
substanced.util.
get_auditlog
(context)¶ Returns the current
substanced.audit.AuditLog
object orNone
if no audit database is configured
-
class
substanced.util.
Batch
(seq, request, url=None, default_size=10, toggle_size=40, seqlen=None)¶ Given a sequence named
seq
, and a Pyramid request, return an object with the following attributes:items
A list representing a slice of
seq
. It will contain the number of elements inrequest.params['batch_size']
or thedefault_size
number if such a key does not exist in request.params or the key is invalid. The slice will begin atrequest.params['batch_num']
or zero if such a key does not exist inrequest.params
or thebatch_num
key could not successfully be converted to a positive integer.This value can be iterated over via the
__iter__
of the batch object.size
The value obtained fromrequest.params['batch_size']
ordefault_size
if nobatch_size
parameter exists inrequest.params
or thebatch_size
parameter could not successfully be converted to a positive interger.num
The value obtained fromrequest.params['batch_num']
or0
if nobatch_num
parameter exists inrequest.params
or if thebatch_num
parameter could not successfully be converted to a positive integer. Batch numbers are indexed from zero, so batch0
is the first batch, batch1
the second, and so forth.length
This is length of the current batch. It is usually equal tosize
but may be different in the very last batch. For example, if theseq
is[1,2,3,4]
and the batch size is3
, the first batch’slength
will be3
because the batch content will be[1,2,3]
; but the second and final batch’slength
will be1
because the batch content will be[4]
.last
The batch number computed from the sequence length of the last batch (indexed from zero).first_url
The URL of the first batch. This will be a URL withbatch_num
andbatch_size
in its query string. The base URL will be taken from theurl
value passed to this function. If aurl
value is not passed to this function, the URL will be taken fromrequest.url
. This value will beNone
if the currentbatch_num
is 0.prev_url
The URL of the previous batch. This will be a URL withbatch_num
andbatch_size
in its query string. The base URL will be taken from theurl
value passed to this function. If aurl
value is not passed to this function, the URL will be taken fromrequest.url
. This value will beNone
if there is no previous batch.next_url
The URL of the next batch. This will be a URL withbatch_num
andbatch_size
in its query string. The base URL will be taken from theurl
value passed to this function. If aurl
value is not passed to this function, the URL will be taken fromrequest.url
. This value will beNone
if there is no next batch.last_url
The URL of the next batch. This will be a URL withbatch_num
andbatch_size
in its query string. The base URL will be taken from theurl
value passed to this function. If aurl
value is not passed to this function, the URL will be taken fromrequest.url
. This value will beNone
if there is no next batch.required
True
if eithernext_url
orprev_url
areTrue
(meaning batching is required).multicolumn
True
if the current view should be rendered in multiple columns.toggle_url
The URL to be used for the multicolumn/single column toggle button. Thebatch_size
,batch_num
, andmulticolumn
parameters are converted to their multicolumn or single column equivalents. If a user is viewing items 40-80 in multiple columns, the toggle will switch to items 40-50 in a single column. If a user is viewing items 50-60 in a single column, the toggle will switch to items 40-80 in multiple columns.toggle_text
The text to display on the multi-column/single column toggle.make_columns
A method to splititems
into a nested list representing columns.seqlen
This is total length of the sequence (across all batches).startitem
The item number that starts this batch (indexed from zero).enditem
The item number that ends this batch (indexed from zero).-
make_columns
(column_size=10, num_columns=4)¶ Break
self.items
into a nested list representing columns.
-
substanced.workflow
API¶
-
class
substanced.workflow.
ACLState
(acl=None, **kw)¶ Bases:
dict
-
class
substanced.workflow.
ACLWorkflow
(initial_state, type, name='', description='')¶ Bases:
substanced.workflow.Workflow
-
class
substanced.workflow.
Workflow
(initial_state, type, name='', description='')¶ Bases:
object
Finite state machine.
Implements substanced.interfaces.IWorkflow.
Parameters: - initial_state (string) – Initial state of the workflow assigned to the content
- type (string) – Identifier to separate multiple workflows on same content.
- name (string) – Display name.
- description (string) – Not used internally, provided as help text to describe what workflow does.
-
add_state
(state_name, callback=None, **kw)¶ Add a new workflow state.
Parameters: - state_name – Unique name of the state for this workflow.
- callback (callable) – Will be called when content enters this state.
Meaning
Workflow.reset()
,Workflow.initialize()
,Workflow.transition()
andWorkflow.transition_to_state()
will trigger callback if entering this state. - **kw – Metadata assigned to this state.
Raises: WorkflowError
if state already exists.Callback is called with content as a single positional argument and the keyword arguments workflow, transition, and request. Be aware that methods as
Workflow.initialize()
pass transition as an empty dictionary.Note
**kw
must not contain the keycallback
. This name is reserved for internal use.
-
add_transition
(transition_name, from_state, to_state, callback=None, permission=None, **kw)¶ Add a new workflow transition.
Parameters: - transition_name – Unique name of transition for this workflow.
- callback (callable) – Will be called when transition is executed.
Meaning
Workflow.transition()
andWorkflow.transition_to_state()
will trigger callback if this transition is executed. - **kw – Metadata assigned to this transition.
Raises: WorkflowError
if transition already exists.Raises: WorkflowError
if from_state or to_state don’t exist.Callback is called with content as a single positional argument and the keyword arguments workflow, transition, and request.
Note
**kw
must not contain any of the keysfrom_state
,name
,to_state
, orcallback
; these are reserved for internal use.
-
check
()¶ Check the consistency of the workflow state machine.
Raises: WorkflowError
if workflow is inconsistent.
-
get_states
(content, request, from_state=None)¶ Return all states for the workflow.
Parameters: - content – Object to be operated on
- request – pyramid.request.Request instance
- from_state – State of the content. If None,
Workflow.state_of()
will be used on content.
Return type: list of dicts
Returns: Where dictionary contains information about the transition, such as title, initial, current, transitions and data. transitions is return value of
Workflow.get_transitions()
call for current state. data is a dictionary containing at least callback.Note
States that fail has_permission check for their transition are left out.
-
get_transitions
(content, request, from_state=None)¶ Get all transitions from the content state.
Parameters: - content – Object to be operated on.
- request – pyramid.request.Request instance
- from_state – Name of the state to retrieve transitions. If None,
Workflow.state_of()
will be used on content.
Return type: list of dicts
Returns: Where dictionary contains information about the transition, such as from_state, to_state, callback, permission and name.
Note
Transitions that fail has_permission check are left out.
-
has_state
(content)¶ Return True if the content has state for this workflow, False if not.
-
initialize
(content, request=None)¶ Initialize the content object to the initial state of this workflow.
Parameters: - content – Object to be operated on
- request – pyramid.request.Request instance
Returns: (initial_state, msg)
msg is a string returned by the state callback.
-
reset
(content, request=None)¶ Reset the content workflow by calling the callback of it’s current state and setting its state attr.
If content has no current state, it will be initialized for this workflow (see initialize).
msg is a string returned by the state callback.
Parameters: - content – Object to be operated on
- request – pyramid.request.Request instance
Returns: (state, msg)
-
state_of
(content)¶ Return the current state of the content object or None if the content object does not have this workflow.
-
transition
(content, request, transition_name)¶ Execute a transition using a transition_name on content.
Parameters: - content – Object to be operated on.
- request – pyramid.request.Request instance
- transition_name – Name of transition to execute.
Raises: WorkflowError
if no transition is foundRaises: WorkflowError
if transition doesn’t pass has_permission check
-
transition_to_state
(content, request, to_state, skip_same=True)¶ Execute a transition to another state using a state name (to_state). All possible transitions towards to_state will be tried until one if found that passes without exception.
Parameters: - content – Object to be operated on.
- request – pyramid.request.Request instance
- to_state – State to transition to.
- skip_same – If True and the to_state is the same as the content state, no transition is issued.
Raises: WorkflowError
if no transition is found
-
exception
substanced.workflow.
WorkflowError
¶ Bases:
exceptions.Exception
Exception raised for anything related to
substanced.workflow
.
-
substanced.workflow.
add_workflow
(config, workflow, content_types=(None, ))¶ Configurator method for adding a workflow.
If no content_types is given, workflow is registered globally.
Parameters: - config – Pyramid configurator
- workflow –
Workflow
instance - content_types (iterable) – Register workflow for given content_types
Raises: ConfigurationError
ifWorkflow.check()
failsRaises: ConfigurationError
if content_type does not existRaises: DoesNotImplement
if workflow does not implement IWorkflow
-
substanced.workflow.
get_workflow
(request, type, content_type=None)¶ Return a workflow based on a content_type and the workflow type.
Parameters: - request – pyramid.request.Request instance
- type – Workflow type
- content_type – Substanced content type or None for default workflow.
substanced.interfaces
¶
These represent interfaces implemented by various Substance D objects.
-
interface
substanced.interfaces.
IACLModified
¶ Extends:
zope.interface.interfaces.IObjectEvent
May be sent when an object’s ACL is modified
-
old_acl
¶ The object ACL before the modification
-
object
¶ The object being modified
-
new_acl
¶ The object ACL after the modification
-
-
interface
substanced.interfaces.
IAfterTransition
¶ An event type sent after a transition has been done
-
transition
¶ The transition name
-
new_state
¶ The new state of the object
-
object
¶ The object on which the transition has been done
-
initial_state
¶ The initial state of the object
-
-
interface
substanced.interfaces.
ICatalog
¶ A collection of indices.
-
reset
()¶ Clear all indexes in this catalog and clear self.objectids.
-
__getitem__
(name)¶ Return the index named
name
-
update_indexes
(registry=None, dry_run=False, output=None, replace=False, reindex=False, **kw)¶ Use the candidate indexes registered via
config.add_catalog_factory
to populate this catalog.
-
reindex_resource
(resource, oid=None, action_mode=None)¶ Register the resource in indexes of this catalog using objectid
oid
. Ifoid
is not supplied, the__oid__
of theresource
will be used.action_mode
, if supplied, should be one ofNone
,MODE_IMMEDIATE
,MODE_ATCOMMIT
orMODE_DEFERRED
indicating when the updates should take effect. Theaction_mode
value will overrule any action mode that a member index has been configured with.The result of calling this method is logically the same as calling
unindex_resource
, thenindex_resource
for the same resource/oid combination, but calling those two methods in succession is often more expensive than calling this single method, as member indexes can choose to do smarter things during a reindex than what they would do during an unindex then an index.
-
reindex
(dry_run=False, commit_interval=200, indexes=None, path_re=None, output=None)¶ Reindex all objects in this collection of indexes.
If
dry_run
isTrue
, do no actual work but send what would be changed to the logger.commit_interval
controls the number of objects indexed between each call totransaction.commit()
(to control memory consumption).indexes
, if notNone
, should be a list of index names that should be reindexed. Ifindexes
isNone
, all indexes are reindexed.path_re
, if it is notNone
should be a regular expression object that will be matched against each object’s path. If the regular expression matches, the object will be reindexed, if it does not, it won’t.output
, if passed should be one ofNone
,False
or a function. If it is a function, the function should accept a single message argument that will be used to record the actions taken during the reindex. IfFalse
is passed, no output is done. IfNone
is passed (the default), the output will wind up in thesubstanced.catalog
Python logger output atinfo
level.
-
unindex_resource
(resource_or_oid, action_mode=None)¶ Deregister the resource in indexes of this catalog using objectid or resource
resource_or_oid
. Ifresource_or_oid
is an integer, it will be used as the oid; ifresource_or_oid
is a resource, its__oid__
attribute will be used as the oid.action_mode
, if supplied, should be one ofNone
,MODE_IMMEDIATE
,MODE_ATCOMMIT
orMODE_DEFERRED
.
-
index_resource
(resource, oid=None, action_mode=None)¶ Register the resource in indexes of this catalog using objectid
oid
. Ifoid
is not supplied, the__oid__
of theresource
will be used.action_mode
, if supplied, should be one ofNone
,MODE_IMMEDIATE
,MODE_ATCOMMIT
orMODE_DEFERRED
.
-
objectids
¶ a sequence of objectids that are cataloged in this catalog
-
flush
(immediate=True)¶ Flush any pending indexing actions for all indexes in this catalog. If
immediate
isTrue
, all actions will be immediately executed. Ifimmediate
isFalse
,MODE_DEFERRED
actions will be sent to the actions processor if one is active, and all other actions will be executed immediately.
-
-
interface
substanced.interfaces.
IContentCreated
¶ An event type sent when a Substance D content object is created via
registry.content.create
-
object
¶ The freshly created content object. It will not yet have been seated into any folder.
-
meta
¶ The metainformation about the content type in the content registry
-
content_type
¶ The content type of the object that was created
-
-
interface
substanced.interfaces.
IDefaultWorkflow
¶ Marker interface used internally for workflows that aren’t associated with a particular content type
-
interface
substanced.interfaces.
IEditable
¶ Adapter interface for editing content as a file.
-
put
(fileish)¶ Update context based on the contents of
fileish
.fileish
is a file-type object: itsread
method should return the (new) file representation of the context.
-
get
()¶ Return
(body_iter, mimetype)
representing the context.body_iter
is an iterable, whose chunks are bytes represenating the context as an editable file.mimetype
is the MIMEType corresponding tobody_iter
.
-
-
interface
substanced.interfaces.
IEvolutionSteps
¶ Utility for obtaining evolution step data
-
interface
substanced.interfaces.
IFile
¶ An object representing file content
-
mimetype
¶ The mimetype of the file content
-
upload
(stream, mimetype_hint=False)¶ Replace the current contents of this file’s blob with the contents of
stream
.mimetype_hint
can be any of the folliwing:None
, meaning don’t reset the current mimetype. This is the default.- A string containing a filename with an extension; the mimetype will be derived from the extension in the filename.
- The constant
substanced.file.USE_MAGIC
, which will derive the content type using thepython-magic
library based on the stream’s actual content.
-
get_size
()¶ Return the size in bytes of the data in the blob associated with the file
-
blob
¶ The ZODB blob object holding the file content
-
get_response
(**kw)¶ Return a WebOb-compatible response object which uses the blob content as the stream data and the mimetype of the file as the content type. The
**kw
arguments will be passed to thepyramid.response.FileResponse
constructor as its keyword arguments.
-
-
interface
substanced.interfaces.
IFolder
¶ A Folder which stores objects using Unicode keys.
All methods which accept a
name
argument expect the name to either be Unicode or a byte string decodable using the default system encoding or the UTF-8 encoding.-
rename
(oldname, newname)¶ Rename a subobject from oldname to newname.
This operation is done in terms of a remove and an add. The Removed and WillBeRemoved events sent will indicate that the object is moving.
-
load
(name, newobject)¶ Same as
substanced.interfaces.IFolder.replace()
except it causes theloading
flag of added and removed events sent during the add and remove events implied by the replacement to beTrue
.
-
move
(name, other, newname=None)¶ Move a subobject named
name
from this folder to the folder represented byother
. Ifnewname
is not none, it is used as the target object name; otherwise the existing subobject name is used.This operation is done in terms of a remove and an add. The Removed and WillBeRemoved events sent will indicate that the object is moving.
-
set_order
(value, reorderable=None)¶ Makes the folder orderable and sets its order to the list of names provided in value. Names should be existing names for objects contained in the folder at the time order is set.
If
reorderable
is passed, value, it must beNone
,True
orFalse
. If it isNone
, the reorderable flag will not be reset from its current value. If it is anything exceptNone
, it will be treated as a boolean and the reorderable flag will be set to that value. Thereorderable
value of a folder will be returned by that folder’sis_reorderable()
method.The
is_reorderable()
method is used by the SDI folder contents view to indicate that the folder can or cannot be reordered via the web UI.If
reorderable
is set toTrue
, thereorder()
method will work properly, otherwise it will raise aValueError
when called.
-
pop
(name, default=None)¶ Remove the item stored in the under
name
and return it.If
name
doesn’t exist in the folder, anddefault
is not passed, raise aKeyError
.If
name
doesn’t exist in the folder, anddefault
is passed, returndefault
.When the object stored under
name
is removed from this folder, remove its__parent__
and__name__
values.When this method is called, emit an
IObjectWillBeRemoved
event before the object loses its__name__
or__parent__
values. Emit anObjectRemoved
after the object loses its__name__
and__parent__
value,
-
replace
(name, newobject)¶ Replace an existing object named
name
in this folder with a new objectnewobject
. If there isn’t an object namedname
in this folder, an exception will not be raised; instead, the new object will just be added.This operation is done in terms of a remove and an add. The Removed and WillBeRemoved events will be sent for the old object, and the WillBeAdded and Add events will be sent for the new object.
-
__contains__
(name)¶ Does the container contains an object named by name?
name
must be a Unicode object or a bytestring object.If
name
is a bytestring object, it must be decodable using the system default encoding or the UTF-8 encoding.
-
is_reorderable
()¶ Return true if the folder can be reordered, false otherwise.
-
keys
()¶ Return an iterable sequence of object names present in the folder.
Respect
order
, if set.
-
add
(name, other, send_events=True, reserved_names=(), duplicating=None, moving=None, loading=False, registry=None)¶ Same as
__setitem__
.If
send_events
is false, suppress the sending of folder events. Disallow the addition of the name provided is in thereserved_names
list. Ifduplicating
is not None, it must be the object being duplicated; when non-None, the ObjectWillBeAdded and ObjectAdded events sent will be marked as ‘duplicating’, which typically has the effect that the subobject’s object id will be overwritten instead of reused. Ifregistry
is passed, it should be a Pyramid registry object; otherwise thepyramid.threadlocal.get_current_registry()
function is used to look up the current registry.This method returns the name used to place the subobject in the folder (a derivation of
name
, usually the result ofself.check_name(name)
).
-
__len__
()¶ Return the number of subobjects in this folder.
-
sort
(oids, reverse=False, limit=None)¶ Return the intersection of the oids of the folder’s order with the oids passed in. If
reverse
is True, reverse the result set. Iflimit
is an integer, return only that number of items (after reversing, if reverse is True).
-
__getitem__
(name)¶ Return the object represented by
name
in this folder or raise a KeyError if no such object exists.
-
get
(name, default=None)¶ Return the object named by
name
or the default.name
must be a Unicode object or a bytestring object.If
name
is a bytestring object, it must be decodable using the system default encoding or the UTF-8 encoding.
-
is_ordered
()¶ Return
True
if the folder has a manual ordering (e.g. itsorder
attribute has been set),False
otherwise.
-
unset_order
()¶ Removes the folder internal ordering, making it an unordered folder.
-
validate_name
(name, reserved_names=())¶ Checks the name passed for validity. If the name is valid and is not present in
reserved_names
returns a validated name. Otherwise aValueError
will be raised.
-
__iter__
()¶ An alias for
keys
.Respect
order
, if set.
-
check_name
(name, reserved_names=())¶ Performs all checks associated with
validate_name
but also raises asubstanced.folder.FolderKeyError
if an object with the namename
already exists in the folder. Returns the name (with any modifications) returned byvalidate_name
.
-
__delitem__
(name)¶ Remove the object from this folder stored under
name
.name
must be a Unicode object or a bytestring object.If
name
is a bytestring object, it must be decodable using the system default encoding or the UTF-8 encoding.If no object is stored in the folder under
name
, raise aKeyError
.When the object stored under
name
is removed from this folder, remove its__parent__
and__name__
values.When this method is called, emit an
IObjectWillBeRemoved
event before the object loses its__name__
or__parent__
values. Emit anIObjectRemoved
after the object loses its__name__
and__parent__
value,
-
__nonzero__
()¶ Always return True
-
items
()¶ Return an iterable sequence of (name, value) pairs in the folder.
Respect
order
, if set.
-
clear
()¶ Clear all objects from the folder. Calling this is equivalent to calling
.remove
for each key in the folder.
-
__setitem__
(name, other)¶ Set object
other
into this folder under the namename
.name
must be a Unicode object or a bytestring object.If
name
is a bytestring object, it must be decodable using the system default encoding or the UTF-8 encoding.name
cannot be the empty string.When
other
is seated into this folder, it will also be decorated with a__parent__
attribute (a reference to the folder into which it is being seated) and__name__
attribute (the name passed in to this function.If a value already exists in the foldr under the name
name
, raiseKeyError
.When this method is called, emit an
IObjectWillBeAdded
event before the object obtains a__name__
or__parent__
value. Emit anIObjectAdded
event after the object obtains a__name__
and__parent__
value.
-
remove
(name, send_events=True, moving=None, loading=False)¶ Same thing as
__delitem__
.If
send_events
is false, suppress the sending of folder events. Ifmoving
is notNone
, it should be the folder object from which the object is being moved; the events sent will indicate that a move is in process.
-
values
()¶ Return an iterable sequence of the values present in the folder.
Respect
order
, if set.
-
reorder
(items, before)¶ Move one or more items from a folder into new positions inside that folder.
items
is a list of ids of existing folder items, which will be inserted in order before the item namedbefore
. All other items are left in the original order. If this method is called on a folder which does not have an order set, or which is not reorderable, aValueError
will be raised.
-
-
interface
substanced.interfaces.
IGroup
¶ Extends:
substanced.interfaces.IPrincipal
Marker interface representing a group
-
interface
substanced.interfaces.
IGroups
¶ Marker interface representing a collection of groups
-
interface
substanced.interfaces.
IIndexFactory
¶ A factory for an index
-
interface
substanced.interfaces.
IIndexingActionProcessor
¶ Processor of deferred indexing/unindexing actions of catalogs in the system
-
interface
substanced.interfaces.
ILock
¶ Represents a lock to be applied by the lock service
-
interface
substanced.interfaces.
ILoggedIn
¶ An event type sent when a user supplies a valid username and password to a login view. Note that this event is not sent on every request that the user initiates, just ones which result in an interactive login.
-
request
¶ The request which resulted in the login
-
user
¶ The user object computed by Substance D
-
context
¶ The context resource that was active during login
-
login
¶ The login name used by the user
-
-
interface
substanced.interfaces.
IObjectAdded
¶ Extends:
zope.interface.interfaces.IObjectEvent
An event type sent when an object is added
-
loading
¶ Boolean indicating that this add is part of a load (during a dump load process)
-
name
¶ The name of the object within the folder
-
parent
¶ The folder to which the object is being added
-
object
¶ The object being added
-
duplicating
¶ The object being duplicated or
None
-
moving
¶ None or the folder from which the object being added was moved
-
-
interface
substanced.interfaces.
IObjectMap
¶ A map of objects to paths and a reference engine
-
pathlookup
(obj_or_path_tuple, depth=None, include_origin=True)¶ Returns an iterator of document ids within obj_or_path_tuple (a traversable object or a path tuple). If depth is specified, returns only objects at that depth. If
include_origin
isTrue
, returns the docid of the object passed asobj_or_path_tuple
in the returned set, otherwise it omits it.
-
disconnect
(src, target, reftype)¶ Disonnect
src_object
fromtarget_object
using the reference typereftype
.src
andtarget
may be objects or object identifiers
-
path_for
(objectid)¶ Return the path tuple for objectid
-
objectid_for
(obj_or_path_tuple)¶ Return the object id for obj_or_path_tuple
-
remove
(obj_objectid_or_path_tuple)¶ Removes an object from the object map using the object itself, an object id, or a path tuple. Returns a set of objectids (children, inclusive) removed as the result of removing this object from the object map.
-
targets
(obj, reftype)¶ Return a generator consisting of objects which have
obj
as a relationship target usingreftype
.obj
can be an object or an object id.
-
sources
(obj, reftype)¶ Return a generator consisting of objects which have
obj
as a relationship source usingreftype
.obj
can be an object or an object id.
-
add
(obj)¶ Add a new object to the object map. Assigns a new objectid to obj.__oid__ to the object if it doesn’t already have one. The object’s path or objectid must not already exist in the map. Returns the object id.
-
connect
(src, target, reftype)¶ Connect
src_object
totarget_object
using the reference typereftype
.src
andtarget
may be objects or object identifiers.
-
targetids
(obj, reftype)¶ Return a set of objectids which have
obj
as a relationship target usingreftype
.obj
can be an object or an object id.
-
object_for
(objectid)¶ Return the object associated with
objectid
orNone
if the object cannot be found.
-
sourceids
(obj, reftype)¶ Return a set of objectids which have
obj
as a relationship source usingreftype
.obj
can be an object or an object id.
-
-
interface
substanced.interfaces.
IObjectModified
¶ Extends:
zope.interface.interfaces.IObjectEvent
May be sent when an object is modified
-
object
¶ The object being modified
-
-
interface
substanced.interfaces.
IObjectRemoved
¶ Extends:
zope.interface.interfaces.IObjectEvent
An event type sent when an object is removed
-
loading
¶ Boolean indicating that this remove is part of a load (during a dump load process)
-
name
¶ The name of the object within the folder
-
parent
¶ The folder from which the object is being removed
-
object
¶ The object being removed
-
moving
¶ None or the folder to which the object being removed will be moved
-
removed_oids
¶ The set of oids removed as the result of this object being removed (including the oid of the object itself). This may be any number of oids if the object was folderish
-
-
interface
substanced.interfaces.
IObjectWillBeAdded
¶ Extends:
zope.interface.interfaces.IObjectEvent
An event type sent when an before an object is added
-
loading
¶ Boolean indicating that this add is part of a load (during a dump load process)
-
name
¶ The name which the object is being added to the folder with
-
parent
¶ The folder to which the object is being added
-
object
¶ The object being added
-
duplicating
¶ The object being duplicated or
None
-
moving
¶ None or the folder from which the object being added was moved
-
-
interface
substanced.interfaces.
IObjectWillBeRemoved
¶ Extends:
zope.interface.interfaces.IObjectEvent
An event type sent before an object is removed
-
loading
¶ Boolean indicating that this remove is part of a load (during a dump load process)
-
name
¶ The name of the object within the folder
-
parent
¶ The folder from which the object is being removed
-
object
¶ The object being removed
-
moving
¶ None or the folder to which the object being removed will be moved
-
-
interface
substanced.interfaces.
IPasswordReset
¶ Marker interface represent a password reset request
-
interface
substanced.interfaces.
IPasswordResets
¶ Marker interface representing a collection of password reset requests
-
interface
substanced.interfaces.
IPrincipal
¶ Marker interface representing a user or group
-
interface
substanced.interfaces.
IPrincipals
¶ Marker interface representing a container of users and groups
-
interface
substanced.interfaces.
IPropertySheet
¶ Interface for objects with a set of properties defined by a Colander schema. The class
substanced.property.PropertySheet
(which is meant to be subclassed for specialization) implements this interface.-
set
(struct, omit=())¶ Accept
struct
(a dictionary representing the property state) and persist it to the context, refraining from persisting the keys in the struct that are named inomit
(a sequence of strings or a string). The data structure will have already been validated against the propertysheet schema.You can return a value from this method. It will be passed as
changed
into theafter_set
method. It should beFalse
if yourset
implementation did not change any persistent data. Any other return value will be conventionally interpreted as the implementation having changed persistent data.
-
get
()¶ Return a dictionary representing the current property state compatible with the schema for serialization
-
request
¶ The current request
-
context
¶ The context of the property sheet (a resource)
-
after_set
(changed)¶ Perform operations after a successful set.
changed
is the value returned from theset
method.The default propertysheet implementation sends an ObjectModified event if the
changed
value is notFalse.
-
schema
¶ The Colander schema instance which defines the fields related to this property sheet
-
-
interface
substanced.interfaces.
IRootAdded
¶ An event type sent when the Substance D root object has a connection to the database as its
_p_jar
attribute.-
object
¶ The root object
-
-
interface
substanced.interfaces.
ISDIAPI
¶ Easy access to common templating operations on all views. This object is available as
request.sdiapi
.-
mgmt_path
(obj, *arg, **kw)¶ Return the route_path inside the SDI for an object
-
sdi_title
()¶ The
sdi_title
of the virtual root or “Substance D” if not defined
-
mgmt_views
(context)¶ The list of management views on a resource
Return a sequence of dicts for the breadcrumb information. Each dict contains:
url
: Therequest.mgmt_path
to that resourcename
: The resource__name__
or ‘Home’ for the rootactive
: Boolean representing whether the resource is in the breadcrumb is the current contexticon
: The full path to the icon for that resource type
-
get_macro
(asset_spec, name=None)¶ Return a Chameleon template macro based on the asset spec (e.g.
somepackage:templates/foo.pt
) and the name. If the name is None, the bare template implementation is returned, otherwise the named macro from within the template is returned.
-
mgmt_url
(obj, *arg, **kw)¶ Return the route_url inside the SDI for an object
-
main_template
¶ The loaded
master.pt
which can be used in view templates withmetal:use-macro="request.sdiapi.main_template"
.
-
flash_with_undo
(msg, queue='', allow_duplicate=True)¶ Display a Pyramid
flash message
to the appropriate queue with a button to allow an undo of the commit.
-
-
interface
substanced.interfaces.
IService
¶ Marker for items which are showin in the “Services” tab.
-
interface
substanced.interfaces.
IUser
¶ Extends:
substanced.interfaces.IPrincipal
Marker interface representing a user
-
interface
substanced.interfaces.
IUserLocator
¶ Adapter responsible for returning a user by his login name and/or userid as well as group objects of a user by his userid.
-
get_groupids
(userid)¶ Return all the group-related principal identifiers for a user with the user principal identifier
userid
as a sequence. If no user exists underuserid
, returnNone
.
-
get_user_by_email
(email)¶ Return an IUser object or
None
if no such user exists. Theemail
argument is the email address of the user.
-
get_user_by_userid
(userid)¶ Return an IUser object or
None
if no such user exists. Theuserid
argument is the user id of the user (usually an oid).
-
get_user_by_login
(login)¶ Return an IUser object or
None
if no such user exists. Thelogin
argument is the login name of the user, not an oid.
-
-
interface
substanced.interfaces.
IUsers
¶ Marker interface representing a collection of users
-
interface
substanced.interfaces.
IWorkflow
¶ -
reset
(content, request=None)¶
-
has_state
(content)¶
-
get_transitions
(content, request, from_state=None)¶
-
add_state
(name, callback=None, **kw)¶
-
transition
(content, request, transition_name)¶
-
state_of
(content)¶
-
check
()¶
-
get_states
(content, request, from_state=None)¶
-
initialize
(content, request=None)¶
-
transition_to_state
(content, request, to_state, skip_same=True)¶
-
add_transition
(name, from_state, to_state, callback=None, **kw)¶
-
-
interface
substanced.interfaces.
MODE_ATCOMMIT
¶ Sentinel indicating that an indexing action should take place at the successful end of the current transaction.
-
interface
substanced.interfaces.
MODE_DEFERRED
¶ Sentinel indicating that an indexing action should be performed by an external indexing processor (e.g.
drain_catalog_indexing
) if one is active at the successful end of the current transaction. If an indexing processor is unavailable at the successful end of the current transaction, this mode will be taken to imply the same thing asMODE_ATCOMMIT
.
-
interface
substanced.interfaces.
MODE_IMMEDIATE
¶ Sentinel indicating that an indexing action should take place as immediately as possible.
-
class
substanced.interfaces.
ReferenceClass
(*arg, **kw)¶ Bases:
zope.interface.interface.InterfaceClass
-
interface
substanced.interfaces.
UserToLock
¶ Extends:
substanced.interfaces.ReferenceType
A reference type which represents the relationship from a user to his set of locks
-
interface
substanced.interfaces.
WriteLock
¶ Extends:
substanced.interfaces.ReferenceType
Represents a DAV-style writelock. It’s a Substance D reference type from resource object to lock object
Substance D SDI Permission Names¶
sdi.add-content
Protects views which allow users to add content to a folder.
sdi.add-group
Protects views which add groups to a groups collection within a principals service.
sdi.add-services
Protects views which add built-in Substance D services.
sdi.add-user
Protects views which add users to a users collection within a principals service.
sdi.change-acls
Protects arbitrary locations, allowing certain people to execute views the under that location which change ACLs associated with a resource.
sdi.change-password
Protects views of a user which allow for the changing of passwords.
sdi.lock
Protects views which allow users to lock or unlock a resource.
sdi.manage-catalog
Protects views which allow users to manage catalog data and indexes within a catalog service.
sdi.manage-contents
Protects views which allow users to add, remove, and rename items within folders.
sdi.manage-database
Protects the “manage database” view at the root.
sdi.manage-references
Protects views which allow users to manage the references associated with a resource.
sdi.manage-user-groups
Protects views which allow admin users to update groups for users.
sdi.manage-workflow
Protects the views associated with managing the workflows of an object.
sdi.undo
Protects the capability of users to execute views which undo transactions.
sdi.view
Protects whether a user can view the SDI management pages associated with a resource.
sdi.view-services
Protects whether a user can view the “Services” tab in a folder.
sdi.edit-properties
Allows for the editing of the properties of a property sheet for an object.
sdi.view-auditlog
Allows the user to view the audit log event stream (auditstream-sse
) view.
Support / Reporting Bugs / Development Versions¶
Visit http://github.com/Pylons/substanced to download development or tagged versions.
Visit http://github.com/Pylons/substanced/issues to report bugs.
The mailing list exists at https://groups.google.com/group/substanced-users
The IRC channel is at irc://freenode.net/#substanced
Copyright, Trademarks, and Attributions¶
Copyright, Trademarks, and Attributions¶
Substance D
by Chris McDonough
Copyright © 2011-2013, Agendaless Consulting.
All rights reserved. This documentation is offered under a (BSD-like) license .
All terms mentioned in this documentation that are known to be trademarks or service marks have been appropriately capitalized. However, use of a term in this book should not be regarded as affecting the validity of any trademark or service mark.
Every effort has been made to make this documentation as complete and as accurate as possible, but no warranty of fitness is implied. The information provided is on as “as-is” basis. The author and the publisher shall have neither liability nor responsibility to any person or entity with respect to any loss or damages arising from the information contained in this book. No patent liability is assumed with respect to the use of the information contained herein.
Attributions¶
- Editor:
- TBD
- Contributors:
- Steve Piercy, Eric Rasmussen, Domen Kožar, Paul Everitt, Carlos de la Guardia, Balazs Ree, Douglas Cerna, and a number of people with only pseudonyms on GitHub.
- SubstanceD.net Website Theme:
- The assets in the directory assets are not open source; they are copyrighted by Tamerlan Soziev and released under a proprietary license. To purchase a license for these assets, visit https://wrapbootstrap.com/theme/venera-responsive-multipurpose-template- WB059C895
- Documentation Template:
- Steve Piercy based on the Venera theme. Used by permission.
Some Substance D Interface images copyright Rokey, in particular http://www.iconarchive.com/show/smooth-icons-by-rokey/capsule-icon.html
Contacting The Publisher¶
Please send documentation licensing inquiries, translation inquiries, and other business communications to Agendaless Consulting. For software and other technical queries see Support / Reporting Bugs / Development Versions.
HTML Version and Source Code¶
The source code for the examples used in this documentation are available within the Substance D software distribution, available via https://github.com/Pylons/substanced
Indices and tables¶
Glossary¶
- Colander
- A schema library which can be used used to describe arbitrary data structures. See http://docs.pylonsproject.org/projects/colander/en/latest/ for more information.
- Content
- A resource which is particularly well-behaved when viewed via the Substance D management interface.
- Content type
- An interface associated with a particular kind of content object. A content type also has metadata like an icon, an add view name, and other things.
- DataDog
- A Software-as-a-Service (SaaS) provider for monitoring and visualizing
performance data that is compatible with the
statsd
statistics output channel used by Substance D. See http://www.datadoghq.com - Deform
- A form library that draws and validates forms based on Colander schemas. See http://docs.pylonsproject.org/projects/deform/en/latest/ for more information.
- Factory Wrapper
- A function that wraps a content factory when the content factory is not
a class or when a
factory_name
is used within the content type declaration. - Folder
- A resource object which contains other resource objects. See
substanced.folder.Folder
. - Global Object
- A Python object that can be obtained via an
import
statement. - Manage prefix
- The prepended portion of the URL (usually
/manage
) which signifies that view lookup should be done only amongst the set of views registered as management view types. This can be changed by setting thesubstanced.manage_prefix
key in yourdevelopment.ini
orproduction.ini
configuration files. - Management view
- A view configuration that is only invoked when a user visits a URL prepended with the manage prefix.
- Object Map
- A Substance D service which maps the object IDs of persistent objects to paths and object IDs to other object IDs in the system.
- Object Map Reference
- A relationship kept in the object map between two persistent objects. It is composed of a source, some number of targets, and a reference type.
- Pyramid
- A web framework.
- Reference Type
- A hashable object describing the type of relationship between two objects in the object map. It’s usually a string.
- Resource
- An object representing a node in the resource tree of your Substance D application. A resource becomes the context of a view when someone visits a URL in your application.
- Resource factory
- An object which creates a resource when it’s called. It’s often just a class that implements the resource itself.
- Resource tree
- A nested set of folder objects and other kinds of content objects, each of which is a resource. Your content objects are laid out hierarchically in the resource tree as they’re added.
- SDI
- An acronym for the “Substance D (Management) Interface”. What you see
when you visit
/manage
. - Service
- A persistent object in the resource tree that exposes an API to
application developers. For example, the
principals
service. - Service
- A Substance D content object which provides a service to application code (such as a catalog or a principals service).
- State
- States
- TODO
- Transition
- Transitions
- TODO
- Workflow
- Workflows
- TODO
- Zope
- An application server from which much of the spirit of Substance D is derived. See http://zope.org.