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