You are not logged in Log in Join
You are here: Home » Members » camil7 » Silva Documentation and HowTo's » Customizing Silva's Content Publish

Log in
Name

Password

 

Customizing Silva's Content Publish

Intro

This HowTo describes the steps in which the CMS Silva renders its contents. There are several hooks in the publication flow where You are able to customize it.

As "customizing" is a rather general term, this HowTo is not organized as a recipe telling You to do X to get Y. Instead this is more a WhatsGoingOn than a HowTo. At some points I try to tell You by example how to change special item. I hope You can generalize the examples; in this case You should be able to customize the output of Silva to Your needs.

I do not think someone can understand whats going on here without simultaneously looking at the source code. Thus I kindly ask You to open another browser window and point it to the ZMI of Your Zope, where You have installed Silva, and read the code along with the text. (Even better, do it the other way round: read the code and come back to this text from time to time, or when You get lost.)

If You rather want to go straight ahead, You may better read the shorter Layout HowTo of Benno Luthiger.

Additionally there is a more structured Reference at infrae.

Of course any comments or suggestions You may have are highly appreciated.

"Valid for" reclaimer

This Howto is based on Silva-0.8.x and assumes You have created a Silva root as /silva .

If You have installed Silva-0.9, most of the things should work at least very similar.
However, I have not really updated this how-to for Silva-0.9 yet; please stay tuned.

Most important differences are
  • the views in the service_view_registry for Silva0.8.x can now be found in the service_views/Silva subfolder as a "Directory View" or in the Products/Silva/views directory on the file system. Editing via the ZMI does not work for them; use a normal text editor on the file system and run Zope in debug mode instead for testing.
  • Similar the service_widgets are actually places in the file system at Products/Silva/widgets.

Overview

Silva published its content by an involved process, where You can customize it at three levels (at least):

  1. Page Design :
    First Silva renders the content independent part of the page via a page template, leaving a slot to be filled by the content.
  2. Content Layout :
    All Silva content is rendered via a layout dispatcher, which dispatches the rendering of contents to a view which depends on their type.

    If the content contains XML, it may take one more step.

  3. Tag Layout :
    Every XML-tag in a content may be rendered depending on its name.

That describes the rendering of contents to the public visitor. A similar process is used to render the contents to the authors to show the edit-forms; however this is much more involved, as one has to process the user input, etc. I did not take look at it yet.

Beside of the "official" view there is an analogous "preview", which works very similarly, except it does not display the public available version of the content, but the last recent one. This is useful to preview the currently edited version before publishing it. As it is very similar to display the public version, it is not discussed here in detail.

Page Design

If a Silva content object is accessed by its id through the web, it first acquires the /silva/index_html (, if You have not added another index_html in the acquisition path, which I consider to be a nasty thing for Silva.)

The index_html is a script dispatching to the page template /silva/content.html, which in turn uses the page macro defined in the page template /silva/layout_macro.html. This page macro defines the following slots, which are filled by the content.html as follows:

title

the html <title> tag; the content.html inserts the Silva title of the content here.

styles

add a reference to cascading style sheets here; the content.html adds the frontend.css here.

script

may be used to add an extra reference to a javascript file used on this page. The content.html delivered with silva actually does not use this slot.

page_header

adds a page footer directly after opening the <body> tag It is not used in content.html.

navigation

add a navigation bar. The content.html fills this slot by "breadcrumb" links as delivered by the get_breadcrumbs() method of the content.

table_of_contents

show a table of contents; this is not used in content.html

main

render the content itself. The content.html fills this slot by the output of the contents here.view() method.

(It actually calls here[view_method](), which is resolved properly, as view_method="view" here; in case of the preview the view_method="preview", and the here.preview() is called. You could even dispatch to different methods e.g. by calling this page template with view_method="get_xml" ; however I have found no good application for this.

If You do not understand the last paragraph, maybe You should take a look at the source ;-).)

page_footer

as You may have guessed, this adds a page footer. It is not used in content.html.

Additionally it defines two slots for the whole <head> and <body> tag, thus one could possibly override the whole layout template in layout_macro.html.

(This list of slots is incomplete; the statements about the slot usage in content.html are outdated by Silva-0.8.3. I hope You can figure out the missing parts. An up to date list is available at infrae's documentation )

If You want to customize the overall look of the pages, as long as it is independent of the actual content, You can edit layout_macro.html. You do not even have to define the slots with the same names as in the delivered layout_macro.html ; of course You will have to adapt the content.html to reflect the changes of of the layout macros.

Now as You have adapted the overall design, You may want to have some pages to look different (mostly the root page, e.g. /silva usually will look a little different than the subpages). I can guess of two possibilites here:

  1. Define our own content.html and/or layout_macro.html in the current Silva content via the ZMI.

    If You apply this to a Silva Folder, the new layout will spead to all contents within this folder via acquisition. Thus this approach seems to be useful for a "subsite layout change".

    If You apply this to a Silva Content, only the layout of this content will be different. I can think of no good use for this. (... well, wait: maybe You want the index content of a folder displayed differently, e.g. adding a list of all items in this folder in a way not covered by Silva's "table of contents". hmmm ...)

  2. Define an override.html in the Silva object You want to change, and define the new layout of the page there.

    If You look at the /silva/index_html, You can see this script preferably dispatches to an override.html, if this exist. As this is looked up directly bypassing acquisition, this change in layout does not inherit to subcontent, if You apply this to a Folder. (For Silva-0.8 additionally You have to fix the spelling in index_html from overide.html to override.html ; Silva-0.8.3 has fixed this.)

    This approach is the "right one" in the scenario, where one wants to change the root page, but not the subpages.

Additionally You could get rid of the whole dispatching by defining an own index_html ; however You get rid of the "single request entry point", where You can do something which should be done for all request. You should know what You are doing when You try this.

If You have installed Silva-0.8.3 from scratch You can find examples for the two appoaches in the /silva/demo/layout_demo folder.

Content Layout

The content.html displays the content by sending it a view(). To understand the flow of control, You have to leave the ZMI shortly and look into the Products/Silva folder in Your file system. No matter what kind of content is view ed, the called method can be found in SilvaObject.py (, as long as You have not defined Your own content types and overridden view there).

This method does the following:

      def view(self):
         """Render this with the public view. If there is no viewable,
         should return something indicating this.
         """
         return self.service_view_registry.render_view('public', self)

Thus by default it dispatches the rendering to a service_view_registry. This is an instance of the ViewRegistry defined in the ViewRegistry.py. However You may now return to the ZMI, because this attribute is not defined in the python class of the contents, but obtained via acquisition from /silva/service_view_registry. (You can check this object is really the referenced attribute by renaming the object via the ZMI; if You do so, Silva will not publish contents any longer, but raise TALESError's.)

Silva-0.8 only: What looks like a Folder from the ZMI is really a special thing (You may add a different icon to ViewRegistry in the Products/Silva/__init__.py, to visually distinguish it from a normal folder.) Silva-0.8.3 has defined an own icon for that.

Please change to this folder and take a look at the register_everything script.

Silva-0.8.3: The script is now named register_silva_core. You can expect changes to this script to be overwritten by the next update; if You have specific changes to Your Silva installation, You maybe should put it into a separate script instead of changing this one. (after all this is only a plain Python script.)

This script is used to register a mapping (view_type, content_type) to a view rendering contents matching the mapping. view_type is a string, which for now may have the values edit and public. The first one is used for authoring silva contents (not treated here), the second one for publishing content. (You may note the first argument of the method call in the view method above is public, i.e. that is the view type!).

Defining new view types is possible in principle but I assume it is advanced hacking (yet) ... I have tried to do it, but it is a very little involved. Another HowTo will cover that; maybe coming up next month.

The content_type is a string naming the content type as shown by the ZMI (or as defined by the meta_type attribute of the corresponding python class). Defining new content types is not in the scope of this HowTo.

But back to the register_everything. It contains lines of the form:


     context.register(view_type, content_type, acquisition_path)

for example:

     context.register('public', 'Silva Folder', ['public', 'Folder'])

This means the public view type of Silva Folders is registered as public/Folder, as acquired from the current position; the view for a folder thus can be found in /silva/service_view_registry/public/Folder. If You look there, this is indeed a folder containing two scripts render_view and render_preview ; the render_view is the one the service_view_registry dispatches to. (If You care why it does, read the render_view method in the Silva/ViewRegistry.py in the file system and compare to the render_preview method there.)

If You look at the render_view for Silva Folders:

      <div tal:omit-tag="" tal:define="model request/model">
      <p tal:condition="model/get_default | nothing" 
         tal:replace="structure python:model.get_default().view()">View</p>
      </div>

it renders the content returned by get_default, if there is one. (The get_default returns the object with id index of this folder, if it exist; look at Product/Silva/Folder.py.)

Note: The request attribute model is the same object as here in the content.html. It is put into the request attribute model by the service_view_registry.render_view() method.

Example 1: Customizing the "no public version" error message

If there is no public version for a Silva content, Silva instead displays a string There is no public version.

Maybe You think thats too terse, or it should be Spanish instead of English. Where is the string?:

       context.register('public', 'Silva Document', ['public', 'Document'])

Ok, it should be in the 'public/Document/render_view':

        model = context.REQUEST.model
        version = model.get_viewable()
        if version is None:
           return "There is no public version"
        node = version.documentElement
        context.service_editor.setViewer('service_doc_viewer')
        return context.service_editor.getViewer().getWidget(node).render()

Voilá! If version is None You can modify the returned string to whatever You like (e.g. forward to a detailed error page).

This approach has one drawback: You will have to repeat it for every content type, e.g. currently for public/Course/render_view, the only other predefined content type. If You do not want to loop over all content types for every change, loop once to return container.no_public_version() in every case and define a public/no_public_version which renders the error message. (untested)

Example 2: Customizing the view for the Silva root

As a second example I show how to customize the display of the contents of the silva root (as opposed to the layout independent of the content). The change is quite silly, as You could obtain the same by editing the layout as discussed above; it is only for the sake of demonstration.

If You look at the register_everything script for the public Silva Root view You can find they are displayed like Folders:

         context.register('public', 'Silva Root', ['public', 'Folder'])

You can change this by these steps:

  1. change to the public subfolder and make a copy of Folder (and name this copy e.g. Root).
  2. Edit the Root/render_view by inserting some lines, e.g.:
          <div tal:omit-tag="" tal:define="model request/model">
          <h2>This is a Silva Root folder</h2>
          <p tal:condition="model/get_default | nothing" 
             tal:replace="structure python:model.get_default().view()">View</p>
          </div>
          
  3. Edit the register_everything and change the Silva Root registration:
             context.register('public', 'Silva Root', ['public', 'Root'])
    
  4. To take things into effect, save the script and execute it by clicking its Test tab.

Now Your Silva Root folder /silva should state loudly it is indeed a root folder.

Silva-0.8.3: You can have a look on the currently active mappings by selecting the "Assiciations" tab of /zlb_silva/service_view_registry.

Tag Layout

To render contents containing XML one could get the current valid version of the content, and then traversing the XML Tree and extracting the content. This is quite cumbersome, if the XML structure is a fixed one, and one knows where to find which tags. In case of the Document content type, which may be an arbitrarily nested assembly of lists, tables, etc. this does not look even feasible.

Instead Silva uses the XMLWidgets product. This allows to map tags to methods rendering them by their name. E.g. when traversing a XML tree each time a tag &lt;list&gt; is found, this tag is processed by some script registered at a XMLWidgets registry to listen for 'list'-tags.

I have not understand this stuff throughout, thus my explanations are possibly more complicated then necessary. (Things look very involved and unnecessary compicated if only the public view is taken into account. However the effort seems to pay off for the edit view.)

Hm, I try to explain in by an example. Please look at the XML of some Document instance, e.g. by querying the URL /silva/index/get_xml of Your Zope and view The Source:

   <silva_document id="index">
     <title>Silva Top Publication</title>
     <doc>
       <p type="normal">My first page has a little bit of content.</p>
       <p>A bunch of paragraphs, perhaps..</p>
       <list type="disc" title="">
         <li>And a</li><li>list</li><li>of</li><li>items</li>
       </list>
     </doc>
   </silva_document>

(I confess I faked the layout a little ... originally its all on one line ...).

Actually the XML content starts with the <doc> target; the outer tags are generated at runtime. (Please look at the get_xml in SilvaObject.py and to_xml method of 'Document.py', if You do not trust me.)

If You look up the view for the Document /silva/service_view_registry/public/Document/render_view, it dispatches in a two stage process:

    context.service_editor.setViewer('service_doc_viewer')
    return context.service_editor.getViewer().getWidget(node).render()

The first line ensures the primary dispatcher service_editor keeps service_doc_viewer as the secondary dispatcher for the rest of the request. You can find both objects in /silva/service_editor and /silva/service_doc_viewer in the ZMI, though the ZMI does not help a lot discovering their functionality; it only teaches You that one is a "XMLWidgets Editor Service" and the other "XMLWidgets Registry". Similar to the /silva/service_view_registry these objects are configured by scripts.

Unlike the /silva/service_view_registry there is not one script to register all views, but one script for each XMLWidgets Registry. For each /silva/service_<em>foo</em>_viewer there is one /silva/service_setup/service_<em>foo</em>_viewer_setup script.

If You look at the /silva/service_setup/service_doc_viewer_setup :

    wr = context.service_doc_viewer
    wr.clearWidgets()
    wr.addWidget('doc', ('service_widgets', 'top', 'doc', 'mode_view'))
    for name in ['p', 'list', 'heading', 'toc', 'image', 'nlist', 'table']:
      wr.addWidget(name, ('service_widgets', 'element', 'doc_elements', name, 'mode_view'))
    return "Done"

this first associates the doc tag with the view at /silva/service_widgets/top/doc/mode_view ; and then the tag p with /silva/service_widgets/element/doc_elements/p/mode_view, and so on.

When rendering, the render method of the view is called, and the node to be rendered is passed as the request attribute node. If You look at /silva/service_widgets/top/doc/mode_view/render, which gets the <doc> tag to render, this does little else than forwarding this to a helper method of the first dispatcher service_editor.renderElementsView (defined in Products/XMLWidgets/EditorService.py). This helper methods simply causes all proper child nodes to be rendered, e.g. first the &lt;p type="normal"&gt;My first page has a little bit of content.&lt;/p&gt;.

The service_editor remembers that the service_doc_viewer is responsible to render these nodes. This object looks up the view for the &lt;p&gt; tags and comes out with /silva/service_widgets/element/doc_elements/p/mode_view , to which it sends a render message. If You look there (klick, klick, klick ... the ZMI is getting a little cumbersome here ...), You get into a page template:

    <div tal:omit-tag="" tal:define="type here/get_type">
      <p tal:condition="python:type == 'normal'"
         tal:content="structure here/content">p</p>
      <p class="lead" 
         tal:condition="python:type == 'lead'"
         tal:content="structure here/content">p</p>
    </div>

The first line calls here/get_type aka /silva/service_widgets/element/doc_elements/p/get_type which checks the type attribute of the tag, which is normal in our case.

Thus the condition of the second line is true, and the page template simply renders:

   <p>My first page has a little bit of content.</p>

(and some newlines ...)

(If You didn't get it: here/content is again in /silva/service_widgets/element/doc_elements/p/content via acquisition; and this script does little else than call node.render_text_as_html(node). The render_text_as_html is defined in Products/Silva/EditorSupport.py and obtained by a combination of Acquisition and Inheritance: first acquisition climbs up through all XML Nodes to the doc node, and than even one level higher to the Document. There the attribute render_text_as_html is found, as Document inherits from EditorSupport.)

Hm, how can one customize that?

Let's try to make all lists within the document ordered, to have a simple objective. (Better do this in a version, for You may easily discard the changes.)

Look again at /silva/service_setup/service_doc_setup. As lists in the document are contained in the &lt;list&gt; tag, their view can be found in /silva/service_widgets/element/doc_elements/mode_view/render :

    <init tal:omit-tag="" tal:define="global node request/node" />
    <h4 tal:condition="python:node.getAttribute('title')"
        tal:content="structure python:node.output_convert_html(node.getAttribute('title'))">
        title</h4>
    <content tal:replace="structure here/content" />

If You want, You could change the rendering of the title of the list here, but where are the list items?

here/content is acquired to be /silva/service_widgets/element/doc_elements/list/content. This basically calls: util.render_list(type, list_elements), where util resolves to: /silva/service_widgets/util, and the contained render_list allows the following modifications:

  • either to change the surrounding tag to &lt;ol&gt; for quite all lists by editing /silva/service_widgets/util/render_list (or maybe even put them in a table?)
  • or call this script with type="1" in /silva/service_widgets/element/doc_elements/list/content to have only lists within documents numbered.

If You try one of these changes, the lists should get numbered for Silva rendered contents.

Acknowledgements

Many thanks to Martijn Faassen who corrected some misunderstandings and fixed a lot of typos.

All bugs and typos still present are mine, of course.

To the index.