Pre-release
Edit me on GitHub

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 as substanced.folder.Folder).
  • The resource class or factory must be decorated with the @content decorator, or must be added at configuration time via config.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.

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:

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.