=========================== Configuring Folder Contents =========================== The folder contents, as mentioned previously in :ref:`sdi-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: .. code-block:: python 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: .. code-block:: python 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: .. image:: images/catalog_contents.png The ``Reindex`` button illustrates a useful facility for performing many custom operations at once. The :py:mod:`substanced.catalog` module's ``@content`` directive has a ``buttons`` argument: .. code-block:: python @content( 'Catalog', icon='glyphicon glyphicon-search', service_name='catalog', buttons=catalog_buttons, ) This points at a callable: .. code-block:: python 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 styling - ``btn-sdi-act`` makes the button always enabled - ``btn-sdi-sel`` disables the button until one or more items are selected - ``btn-sdi-one`` disables the button until exactly one item is selected - ``btn-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. :py:mod:`substanced.catalog.views.catalog` shows this: .. code-block:: python @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: .. code-block:: python 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: Filtering What Can Be Added =========================== Not all kinds of resources make sense to be added inside a certain kind of container. For example, :py:class:`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 :term:`pyramid: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 :func:`~substanced.content.add_content_type`. For example, if you pass ``add_view='foo'`` to :func:`~substanced.content.add_content_type`, the ``meta`` of the content type will be ``{'add_view':'foo'}``. ``content_type`` The content type value passed to :func:`~substanced.content.add_content_type`. ``factory_type`` The ``factory_type`` value passed to :func:`~substanced.content.add_content_type`. ``original_factory`` The original content factory (without any wrapping) passed to :func:`~substanced.content.add_content_type`. ``factory`` The potentially wrapped content factory derived from the original factory in :func:`~substanced.content.add_content_type`. See :ref:`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: .. code-block:: python @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: .. code-block:: python 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 :py:func:`substanced.catalog.catalog_buttons` callable adds a new ``Reindex`` button in front of the standard set of buttons: .. code-block:: python 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: .. code-block:: python class MySuperVeryCoolResource(Persistent): pass MyCoolResource = MySuperVeryCoolResource # bw compat alias