Customizing Silva's Content Publish
Created by .
Last modified on 2003/08/01.
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):
- 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.
- 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.
- 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.
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:
- 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 ...)
- 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.
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:
- change to the
public
subfolder and make a copy of Folder
(and name this copy e.g. Root
).
- 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>
- Edit the
register_everything
and change the Silva Root
registration:
context.register('public', 'Silva Root', ['public', 'Root'])
- 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
.
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 <list>
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
<p type="normal">My first page has a little bit of content.</p>
.
The service_editor
remembers that the
service_doc_viewer
is
responsible to render these nodes. This object
looks up the view for
the <p>
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 <list>
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
<ol>
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.