Define Custom Content Types
Created by .
Last modified on 2003/08/01.
Define new content types for Silva
This HowTo explains how to create a new simple
custom content type. The type will versioned
and contain some fixed fields. It is mainly
obtained by taking the DemoObject
delivered
with Silva-0.8.3. Maybe some special features
as non-versioned content types will be added
later (the text is long enough anyway ...)
Note: Since Silva0.8.6 there has been a major rework,
especially an export of all system specific views, widgets, etc. to the file
system.
This makes it a lot easier to update and customize things, but unfortunately
it renders most of my HowTo's as outdated.
I did not really understand all details of the
XML editing part. There is a hole in the
explanation how the things are put
together. Maybe this will be filled later, too.
Note: for some reason the indentation
is somewhat rotten, suddenly items in the
structured test open a new list instead of
keeping in the old one. This happens especially
after code snippets. I get bored of trying to
fix it. Hope you can still read it ...
Important note: The currently proposed
Folder structure will not work well when
upgrading Silva.
Instead of using custom folders for the Zope
stuff, I recommend to put everything in
subfolders of the service_view_registry
/
service_widgets
. Additionally I recommend
to put the file system stuff not in a
subdirectory of the Silva
Product
directory, but create a new Product for it
(i.e. create a separate directory in the
Products
directory).
(if You do not know what I am talking about
here, read on ;)
)
To define new content types one has to go through the following steps:
Remark: most of this HowTo has been written
aside while I was
trying to find out how it works. Thus most often
I did not exactly know what to write
in the next paragraph, as I simply did not know
for sure how it
works. Thus this HowTo has a somewhat odd style
of writing and less
structure; I simply wrote down what I did this
day. This makes it a
little hard to read,
but its more fun to write.
Well, I did not wrote it all of it
in one day; I just checked out the main
part, and filled some holes later. And, yes, I have tried to eliminate
spelling errors ...
Before You define a new content type, You should
first try to
define what the content type looks
like. Basically the content type
itself has a name and a sequence of attributes,
which in turn have a
name and a type.
The names should be valid python identifiers,
and not start with an
underscore.
The type of an attribute may be one of the following:
- plain text (i.e. a paragraph)
- heading text (only a few markup allowed)
- special format text, e.g. numbers, dates, etc.
- Document XML text.
This means the item looks like a small
document inside
the content type; you can add there everything
like in a Document.
It is possible to restrict the set of
available elements here, e.g. disallow tables,
One has to define a new service_foo_editor
for this, like the service_field_editor
.
This is not treated in this HowTo.
- a special single node from the Document XML
This is done by faking the XML Widgets; in this HowTo it is used to
include an image. You can do similar for
tables, lists, ...
- a list of items (maybe?) (not discussed yet)
- other things may be possible, e.g. tables
with a fixed number of columns; I have to
check this
out, however.
Attributes can be mandatory and/or limited in
format or length, (or
whatever restrictions the Formulator
allows
you to define or what
you implement.)
A content type must always contain a title,
which is plain
text. Oh, and it must have an id
, as every
Zope object.
It's maybe best you write first down what your
new content type should
look like.
As an example I will define a simple
News
item:
Attributes of News
content type:
- Title
- one line heading, no markup, mandatory
- Image
- an image
- Image Title
- plain one line text
- Text
- plain text (multi line), with simple
markup (in the first try we allow full HTML
markup; see another
howto if you want
to change this.)
Go to the place in the file system, where the Silva product is
installed (i.e. most probably ZOPEHOME/lib/python/Products/Silva
).
Create a new file there, which will contain two classes; the first one
is the "main" class (which is mainly a container for the versions), and
the second class will represent "versions" of the
content types instances.
The first class should contain the name of the
content type
in the meta_type
attribute, and normally
little else; the real
contents are in the version class.
The version class should define one variable of
the proper type
and one method for read access for each attribute.
Additionally it should have one set_data
method allowing to
set all simple attributes at once. (This is due
to the
fact the contents are
managed via a web interface, where the form
always contains the
information about all attributes, thus they
were (potentially)
modified all at once. If you plan to have have
several edit forms
allowing to edit just a part of the attributes,
having a set
method
for each part of attributes edited together in
one form would make
sense.)
The Document-type attributes or otherwise
XML-based contents are handled differently,
however.
Finally you need some manage_addXXX
functions
for both classes
outside the class definitions. These functions
will be needed by
Zope to put newly created instances of your
content in the proper place.
For the example, this includes the following steps:
- Create a new file
News.py
.
This is best done by copying the DemoObject.py
.
To keep it away from the Silva core, I argue
to create
a subdirectory Custom
and place the file there.
- Open the file with a text editor; first edit the class
DemoObject
by changing the class name to News
.
Note that you have to adjust the
class name in the __init__
method, too. The base classes actually do the rest for
you. VersionedContent
does the management of the versions, and
EditorSupport
is a
mix-in class helping you to display the
XML instance attributes in the
public
and the edit
view.
- You should change the
meta_type
attribute
of this
class, too. The string here is displayed in
the drop-down menu's
near the "Add" buttons in the ZMI and SMI,
where you choose the
type of the new object to add. Thus it
should be a human readable,
but maybe not to long string.
I choose My Silva News
here.
- Change the
DemoObjectVersion
to NewsVersion
. As this inherits
from SimpleItem.SimpleItem
, it supports some of the Zope
persistency magic, and little else. Change also the meta_type
properly.
- Define (i.e. change) the attributes. Hm, we
maybe first have a look
on the attributes of the 'DemoObject':
-
title
- plain one line text, mandatory
-
information
- plain multi line text
-
content
- Document XML text
-
number
- a number
-
date
- a date
You can get this information simply by creating an instance of the
demo object; the mandatory elements are filled in in the add form,
and you see all of them in the edit tab.
Where are they in the code?
First, the special formatting
information is not present in the object;
this is handled by the
public
and edit
view. You neither see if
it is multi line or single line
text, nor if it is a number or a date here
(you can check this via
pythons type
function, of course). However
you see if something
is a Document XML.
Second, the title is stored in the
DemoObject
(or News
)
class, not in the versions. You can see the title passed through
the __init__
of the 'DemoObject';
accessing the title is handled
in the base class.
Thus the title must be the same for all
versions. Indeed you can
check this via the SMI: if you change the
title of a not approved
version, the public visible title changes,
too.
Basically, we do not have to care about the
title on this level,
except we want to do something extraordinary
with it. I do not
want it (here).
The next attribute Information
is stored in the instance
attribute self._info
. This is initialized in the __init__
method to be an empty string, and passed in the set_demo_data
as an argument. There is a getter method info
here.
This repeats for Number
and Date
... thus we now know how to
add plain text objects. In my example, there are two items.
Thus wee need:
- have a
self._text="test text"
in the __init__
I did not use an empty string, as I would
like to have here something for
debugging. When its ready for production
code, an empty string will be used.
- basically rename the
info
method to text
(Do not forget to change this in the security.declareProtected
method call above; otherwise text
will not be accessible. The
method tells Zope that this method is available to everyone
having the SilvaPermissions.AccessContentsInformation
. Looking
at SilvaPermissions.py
you can guess it is the same as
the standard Zope permission Access contents information
, thus
is usually accessible to everyone.)
- pass an argument
text
to the set_demo_data
and use it to
change the attribute _text
. (As we are at it, we maybe
should rename the method to set_news_data
.)
Again this method has a permission guard; in this case
consulting SilvaPermissions.py
should tell you that you need
the Author
role or higher to access this methods.
Makes sense. (to me.)
- Same repeats for the image title, for
which I use a property
image_title
. The set_news_method
now looks like:
def set_news_data(self, image_title, text):
"""Set the plain fields for this version.
"""
self._image_title = image_title
self._text = text
Next thing I need it the Image
. Silva content does not contain
images, it references them. Thus I need a reference to an image.
Silva Document
handles this by a special XML-Tag "<image>"
I fake this by a special ParsedXML
thing:
- in
__init__
, add a line:
self._image = ParsedXML("image", "<image/>");
- add an
image
method returning the image:
security.declareProtected(SilvaPermissions.AccessContentsInformation,
'image')
def image(self):
"""Get the image XML object
"""
return self._image
Alternatively you can simply put the image in the self.image
attribute, in analogy to the content
in the DemoObject
.
The NewsVersion
calls now looks like:
class NewsVersion(SimpleItem.SimpleItem):
"""Silva Custom News Version.
"""
security = ClassSecurityInfo()
meta_type = "My Silva News Version"
def __init__(self, id, title):
"""Set id and initialize all attributes
"""
self.id = id
self._text = 'test text'
self._image_title = 'test image title'
self._image = ParsedXML('image', "<image/>");
# MANIPULATORS
security.declareProtected(SilvaPermissions.ChangeSilvaContent,
'set_news_data')
def set_news_data(self, image_title, text):
"""Set the plain fields for this version.
"""
self._image_title = image_title
self._text = text
# ACCESS
security.declareProtected(SilvaPermissions.AccessContentsInformation,
'image_title')
def image_title(self):
"""Get the info for this version.
"""
return self._image_title
security.declareProtected(SilvaPermissions.AccessContentsInformation,
'text')
def text(self):
"""Get the text for this version.
"""
return self._text
security.declareProtected(SilvaPermissions.AccessContentsInformation,
'image')
def image(self):
"""Get the image XML object
"""
return self._image
I did not need it here, but you can guess how to handle a
Document XML attribute:
- To be able to manage your objects via Zope, (well, at least to
add them,) you need a add
form/action pair. I propose to:
- create a
www
directory in the Custom
directory
- copy the page templates from the demo
object, i.e. in Unix notation:
cp www/demoObjectAdd.zpt Custom/www/newsAdd.zpt
cp www/demoObjectVersionAdd.zpt Custom/www/newsVersionAdd.zpt
- adapt the boilerplate text in the copied templates;
at least change the
action
target of the
<form>, e.g.
<form action="manage_addNews" method="post">
for addNews.zpt
. (Maybe you want to change the occurrence of
DemoObject
and the like, as you are already here.)
You do not have to add new input fields for the other mandatory
elements (XXX but one may be able to do so; add this later ?).
- adapt the boilerplate text in the python class:
to
manage_addNewsForm = PageTemplateFile("www/newsAdd", globals(),
__name__='manage_addNewsForm')
and similar for the version.
- change the
manage_addDemoObject
to
'manage_addNews';
when adapting the body of the function,
fix the constructor of
the instance itself, and of the initial
NewsVersion
(with id 0
)
which is added the new instance.
- similar but simpler: adapt
manage_addDemoObjectVersion
Now you are ready to register the new classes to Silva/Zope.
You have to tell Silva/Zope there is a new
content type. This is really boilerplate text;
however to keep the code change to
the Silva core small, I recommend to do this in
two steps:
- edit
__init__.py
in the Silva
directory;
- add an
import Custom
near the head below the
other import a statement.
- at the end of the
initialize
method
add the line: Custom.initialize(context)
- create a new file
__init__.py
in the
Custom
folder, and put the
following contents there:
import News;
def initialize(context):
context.registerClass(
News.News,
constructors = (News.manage_addNewsForm,
News.manage_addNews),
# icon="www/news.gif"
)
context.registerClass(
News.NewsVersion,
constructors = (News.manage_addNewsVersionForm,
News.manage_addNewsVersion),
# icon="www/newsVersion.gif"
)
If you want, you can create 16x16 images to
show up in the ZMI,
move them to the given file name, and
uncomment the icon
clause.
That's cosmetics, I think. Instead let's
test, if we can add News
via the ZMI.
- Restart Zope
Check, if the server logs any exceptions at
startup.
It it does, your changes smashed Silva.
Go back and fix it. (Most probably you still
have a reference to
DemoObject
in your new files.
I get used to search
for this string before
I test and
only proceed if it is not found any more.)
- Enter the ZMI and check the drop-down box of
all addable types.
If there is no
My Silva News
, go to
/Control_Panel/Products
and check out why the news are broken; the
Silva
product
should contain a traceback. Go and fix it. Alternatively there could be a problem with
the permissions of Your account.
- Otherwise move via the ZMI to some test
Silva folder and try to add a
My Silva News
object there. If it fails,
it should tell you
what's wrong. Please add the test content somewhere
inside the Silva Root folder. The ZMI
allows you to add Silva content objects
elsewhere in theory
(You have them in your drop down
box in the management screen), but they
will be broken and Zope will throw
unfriendly exceptions on You.
I assume in the sequel you add
an object with id new_news
in a
subfolder named folder
of the silva root.
- If that succeeds, the first step is
complete. Now you are ready
to add the views you need to access the
content via Silva.
To display instances of your new content type
to the public, you have to tell Silva how to
render it to the public. I can guess
of two possibilities:
The hard boiled way
If you want it quick and dirty; override the
view
and preview
methods the News
content inherits from
SilvaObject
. E.g. in the News
class
add something like:
def view(self):
""" render public view """
public_version = self.get_viewable()
if not public_version:
return '<p class="error">No public version available</p>'
return public_version.render(self)
def preview(self):
return self.get_previewable().render(self)
and in the NewsVersion
class something like:
def render(self, news):
""" render the content of this version; no html-encoding for the text :( """
args = { "title" : news.get_title_html(),
"text" : self.text() }
return '<h1>%(title)s</h1><p class="text">News text: %(text)s</p>' % args
Of course this is not very flexible, and I recommend it only
if all the stuff should have been ready
last week. (Rendering of the image is
missing here; can be done via cut &
paste from the public
view below.)
The recommended way
Define and register a public
view via the
Silva registry.
This is the recommended way, and I will
discuss it in the rest of
this section.
(I do not explain en detail how the content to
be displayed happen
to arrive there. You may consult the Template
Manager documentation;
additionally there is another HowTo
sketching
what is happening
behind the scenes.)
The creation of a public
view is done by the following steps:
- enter the ZMI and go to
/silva/service_view_registry
There are the subfolders add
, edit
and
public
. We need
public
yet. Inside the public there is a
folder DemoVersion
.
Create a copy of it and name it News
.
(Or
better: create another folder
/silva/service_custom_view_registry/
with
'add', edit
and public
subfolders.
Move to the
public
subfolder and place the copy
of the DemoVersion
there unter the name 'News';
that keeps Your views away from update
scripts and is still systematic.)
If you enter the folder you find the
following contents:
- get_content
- helper script, explained soon
- render_view
- will be called in case of a view
- render_preview
- will be called in case of preview.
both scripts pass the proper version as a parameter to
render_helper
. This is a page template doing the main rendering.
- render_helper
- displays the content object
via the steps explained in the following.
- first define some global variables; the
model
is the object to
be viewed, the view
is the template
used for viewing (not
really, it is actually the folder, where
the current template
is located). Finally every attribute of
the content is
separately retrieved into an own
variable, except for the
Document XML content.
(XXX: there is even the title
retrieved from the version. It's the usual
acquisition trick, as the version is
contained within the content.)
- The title is HTML-encoded and displayed
within an '<h2>'-tag.
- The Document XML is displayed next after
a hard boiled
'<h4>'-String
Content
. The
Document XML is displayed by
dispatching the output to a XML Widget,
which is obtained by
the get_content
helper script. The helper script simply accesses the
root tag of the XML
stored in the content
attribute, and
passes this to the
service_field_viewer
registered at the
service editor.
TODO: it needs a separate
HowTo to explain adaption of this widget,
I guess.
- Afterwards the fixed content are displayed straight away
without major tricks. Well, with minor tricks:
info
may contain plain html-tags:
<span tal:replace="structure info"></span>
date
is a date object, and thus formatted via an helper (which can
be found in /silva/service_utils/frontend_datetime_to_str
) As we got here to adapt it to the News
content,
we can replace
most of the stuff by our own. The head of the
template looks
afterwards like:
<tal:init define="global model request/model; global view here;
global title options/version/get_title_html;
global image_title options/version/image_title;
global text options/version/text" />
The body looks somewhat like:
<!-- News Title and Items -->
<table border="0" cellpadding="0" cellspacing="0">
<tr><td colspan="2" valign="top" valign="top">
<b class="headline" tal:content="title">Le Title</b>
</td></tr>
<tr><td align="left" valign="top">
<table border="0" cellpadding="0" cellspacing="0" align="left" valign="top">
<tr vspace="0" hspace="0">
<td valign="top" border="0">
<img src="images/testimage.jpg"
tal:replace="structure python:view.get_image(options['version'])"
/>
</td></tr>
<tr vspace="0" hspace="0">
<td valign="top" border="0" align="left">
<span class="imagedesc" tal:replace="python:model.output_convert_html(image_title)">image description</span>
</td></tr>
</table>
<td>
<p tal:content="structure python:model.output_convert_editable(text)">The text of the content</p>
</td></tr>
</table>
(I admit this is a little involved. I grabbed it from a
real design hanging around at my current project. I already
simplified the design as much as easyly possible ...)
The here/get_image
is a renamed copy of the get_content
script:
context.service_editor.setViewer('service_field_viewer')
return context.service_editor.getViewer().getWidget(version.image().documentElement).render()
That works, as the widget knows how to render <image>
tags.
It behaves like it would meeting it inside of some Document XML.
(*The service_field_viewer
is actually
designed to render XML elements within a table
cell of a complex list item. For images this is
basically the same as the
service_doc_viewer
. See e.g. the last part of
my Modify Silva's Content
Publish to find out about the
difference between the two of them.*)
- As the next step you have to register this new view to Silva;
otherwise Silva will not find it. For this, change to the
/silva/service_custom_view_registry
folder and create a Python script
there, say register_custom_types
. The script should contain the
following lines:
context.service_view_registry.register('public', 'My Silva News',
['service_custom_view_registry', 'public', 'News'])
context.service_view_registry.register('add', 'My Silva News',
['add', 'service_custom_view_registry', 'add', 'News'])
context.service_view_registry.register('edit', 'My Silva News',
['service_custom_view_registry', 'edit', 'VersionedContent', 'News'])
return "done"
Hm, looks strange? Compare with to the
/silva/service_view_registry/register_silva_core
we have to add one more level of attribute
lookup first when accessing the registry, second
when defining the acquisition path to get the
view. (Looking up a view
for a content happens inside the
service_view_registry
, which therefore needs
to explicit acquire the service_custom_view_registry
.)
Both times the service-folders
acquired via the silva root.
XXX: For the add
view I even have to
acquire from /silva/service_view_registry/add
for some very obscure reasons. *(Directly
acquiring from /silva/service_view_registry
does not cause the __getitem__
method been
called, which sets the request attribute model
as a side effect.)*
Additionally it may look strange that I
recommend to register things here that
do not yet exist. No problem, we will add it in
the next step.
For now, execute the script by selecting the test
tab. Now you
can check if the public view works.
Note:
each time you have to run the default
registry
/silva/service_view_registry/register_silva_core
,
you loose the registration you have done in the
last step, as
register_silva_core
first clears the registry. This may happen
implicitly in
Silva version upgrades. Run
register_custom_types
again and you
should be fine. (Simply patch the upgrade script
to make the call for You for an smooth upgrade.)
The major part comes as the last one: You will
want to edit the
contents of your new type via the Silva
Management Interface, I
guess. Thus you have to add two views, one to
add a new content and
one to edit existing content. Things are a
little more involved here,
as we are going to implement forms, and need a
controller receiving the
submitted form and put the data in the model
(i.e. the content). Don't
panic, we use the fine Formulator framework to
do most of the work,
and for the rest we steal from the
'DemoObject'.
First recall: the add form should show all
mandatory contents. I do
not like to have the image mandatory. Neither
the ImageTitle is. Hm, the
Text is mandatory, at least. Then there is the
Title and the id. Makes three things.
Now furnish the add
form. Take a look how DemoObject
does it:
/silva/service_view_registry/add/DemoObject
.
Well, the folder contains as much as two things:
a script to put the
submitted form data into the object, and a
form
.
Copy it to a new Folder /silva/service_custom_view_registry/add/News
and let us see what we have to adapt.
- better change first the title of the folder to avoid confusion (and
because it is a simple start).
- enter the
add_submit_helper
. As the name
suggests, this is not
called directly form the web, but from another
script obtained via
acquisition (XXX: add_submit
, should be
explained). After hacking it suitably, it
looks like:
# the model is the container within we create the new content
# id and title are mandatory meta data
# result is a mapping containing the rest of the form
# create a new News object
model.manage_addProduct['Silva'].manage_addNews(id, title)
# get the new object
news = getattr(model, id)
# retrieve the proper version
editable = news.get_editable()
# get the mandatory text element from the form
new_text = result['text']
default_image_title = 'No image title'
# and put it into the version, beside some default values
editable.set_news_data(image_title=default_image_title, text=new_text)
# return the new initialized object
return news
- Now adapt the
form
(do this via the ZMI, a
text editor-FTP hook is
of less use here.) If you care, you may first
check out the
documentation (There is even online help
within Zope! Whew!)
For each field in the form, there is a
widget. DemoObject has
pretty three of them: info
. object_id
and
object_title
. Note
that the title of the widgets is pretty what's
displayed to the user.
To make things more interesting, we keep the
info
widget around
and add a new one for the text (the Info
field will show up in the
form, but will be ignored by the processing
script; this it does not
harm and can be cleaned up later.)
Choose "TextAreaField" for multi line input
field; Id is text
(the key of the mapping
above!), Title is Text
.
Now you can adapt "Height" and "Width", add a
"Description", and decide whether to use the
requires
check box.
Other interesting possibilities in the current
context would be:
- StringField
- for one line input.
- IntegerField
- integer numbers
- DateTimeField
- dates
- RawTextField
- FIXME: a different kind of
text field; didn't look at it yet.
and there is more; check it out by yourself.
- before You can test it You need to copy
a few objects the above copies from
add/DemoObject
rely on.
- Copy
add
, add_submit
and macro_index
from /silva/service_view_registry/add
to
/silva/service_custom_view_registry/add
- Copy
get_tab_info
, has_focus_box
,
logout
, macro_tabs
and macro_index
from /silva/service_view_registry
to
/silva/service_custom_view_registry
- To test, go to the SMI and try to create a
News content (Do not
choose
Add and Edit
as the edit form does
not exist yet for this
content type). Can you add it? Can you preview
it? Well, after some tracebacks; you will ;-)
Finally the edit
view; when that works, we can
go home.
We need the following fields:
- Title: one line text
- Image: a form to reference one of the available
images.
- Image Title: again one line text
- Text: multi line text
Take a look how the DemoContent is implemented
and then adapt it.
First copy the
/silva/service_view_registry/edit/VersionedContent/DemoObject
to
/silva/service_custom_view_registry/edit/VersionedContent/News
.
(Again I take over the naming conventions from the
edit views, which differ a little from the
conventions o the other view. They usually
mimics the
interface inheritance hierarchy a little,
e.g. Publication
and
Folder
are
subfolders of Container
as the python class
Publication
and
Folder
implements the interface 'Container';
thus News
is in a subfolder of VersionedContent
.)
There is a little more stuff here. First the
edit
script is a
little involved, as there are two views; one for
the plain form, and
one for the Document XML content
. We actually
do not need the
latter, but for now leave it as it is.
demoObject_submit
gets renamed to
'news_submit'; here is where the
form data is put into the model. The first part
contains some definitions
of local variables; the next is the form
validation or the
Formulator. Leave it as it is, it looks fine.
The next lines get the form data from the
validated mapping and put it
into the model. Fix this:
# Add the results to the object
title = model.input_convert(result['object_title'])
image_title = model.input_convert(result['imagetitle'])
text = model.input_convert(result['text'])
model.set_title(title)
model.get_editable().set_news_data(imageTitle=image_title, text=text)
Note about encoding (i.e. skip at first
reading ;-) : the input_convert
method is a helper method defined in
SilvaObject
to convert the input to
unicode. To far we have not discussed it, but if
You cut and paste text into the input fields
containing non-ASCII characters, you have to
take care of the encoding. In the add
view we
did not take care of it, because this happened
behind the scenes for the title. Hm looking back
it should be useful to apply the helper to the
text
input ...
The encoding has been handled in the public
view silently; the title
has been defined as
get_title_html
instead of the plain
'get_title'; this also handles HTML escaping.
The image caption has been inserted with an explicit
python:model.output_convert_html(image_title)
which uses another helper method to handle encoding.
Same is done for the 'text'; the
output_convert_editable
actually differs only
in the missing HTML escaping, despite the name
of the method suggests it has something to to
with editing.
Now to the image: we borrow this from the 'Document's
image-edit.
We could try to make this a separate view; in
this case it is handled
like the documents tab; it is only a link
plus an extra
view, and the
real edit gets dispatched elsewhere.
Well, thats too much effort for only adding an
image; instead we assume the id of the selected
image comes shipped with the image_id
parameter in the request. For reasons explained below
later its not verified by the
Formulator. Instead we hand-check it a little:
image_id = getattr(context.REQUEST,'image_id', None)
if image_id:
model.get_editable().image().documentElement.setAttribute("image_id", image_id)
Finally the last author is updated and a message
is passed to the
view that everything is fine. Maybe change
DemoObject
to News
here.
model.sec_update_last_author_info()
return view.tab_edit(message_type="feedback", message="News data changed")
(If you want to have a different message_type
than
feedback
or error
, you will have to fix
/silva/service_custom_view_registry/macro_index
,
where the messages are displayed.)
For reference I give the complete script (after
some cleanup)
from Products.Formulator.Errors import ValidationError, FormValidationError
request = context.REQUEST
model = request.model
view = context
editable = model.get_editable()
# Validate the form
try:
result = view.form.validate_all(request)
except FormValidationError, e:
return view.tab_edit(message_type="error", message=view.render_form_errors(e))
# Add the results to the object
title = result['object_title']
image_title = model.input_convert(result['image_title'])
text = model.input_convert(result['text'])
image_id = getattr(request,'image_id', None)
model.set_title(model.input_convert(title))
editable.set_news_data(image_title=image_title, text=text)
if image_id:
model.get_editable().get_image().documentElement.setAttribute("image_id", image_id)
model.sec_update_last_author_info()
return view.tab_edit(message_type="feedback", message="News data changed")
That about converting the result data, but what
about constructing the
form? We have to fix two things; the Formulator
form
get a text
and
image_title
widget instead of a date
and a
number
one.
For the image
we need a custom field.
This is
done by the form
rendering template (oh, yes, this does not
happen automatically; we
just did not take notice of it until yet.)
Leave the form
and go to the
normal_edit
page template. Replace Demo
by
News
for cosmetics,
and to start slowly exploring the template. Hey,
there is the action;
it should not read demoSubmit_form
but
newsSubmit_form
, as we have
changed that.
The other input fields are pretty rendered by hand in
the five columns table.
- may contain an extra linked image leading to another form (for the
content
); mostly empty
- the short title for the field
- the mark for mandatory fields
- the input tag itself
- more elaborate description of the input field.
Let's go the rows top down:
- Title: looks OK to me.
- Information: we do not have it. We want a one line input field
below the image; cut and paste it below the
'<content-editor>'-tag.
Fix it now: replace
Information
by Image
Subtitle
and
Some information
by Text displayed below the image (40 chars max.)
Fix the widget the input-tag rendering is
delegated to; instead of
view.form.info.render(editable.info())
try
view.form.imagetitle.render(editable.image_title())
- Rendering the text goes along; you can paste the last item again or
take a look at the
number
and date
input fields. They look
pretty the same. Note that the mandatory
star is added by hand here. (as I came back later: can you write it in a
loop instead of cut and paste?
Well, I can, but I need a little helper script
the create the loop sequence ...)
- finally the reference to the image: we replace the
content-edit
tag by something appropriate (but take a look how it works, before we
skip it.) Hm, This has an arrow linking forwarding to the
content_mode
script ... TODO: explain more To display the image selection we actually
borrow the whole image selection view from
there. However we define an own stand alone
XML Service Registry for it. Thus we dive one
step deeper in the Silva view widget stuff.
I am going to get lost in details
here; if you don't care about that, skip down
to the next code snippets.
The table itself is painted via the
/silva/service_widgets/macro_edit_view
which has a slot 'node_content'.
Layout is in:
/silva/service_widgets/element/doc_elements/image/mode_edit/render
.
The slot node_content
is filled there with
the actual form elements;
if displays radio buttons with name
image_id
(hence the request getattr
call above!)
Thus we will want to include this page
template into our edit form.
There is a problem with the submit button:
the macro template: /silva/service_widgets/macro_edit_view/
uses three macros in
'/silva/service_widgets/macro_helpers':
edit_bottom_start
, edit_bottom_controls
and
edit_bottom_end
. edit_bottom_controls
defines the submit buttons
"Save" and "Save and Exit" we want to have here. (They redefine the
action
of the form via the usual Zope forms name mangling hack by
changing the name of the submit button to a
'script:method';
the submitted form will be send to <em>script</em>
. The script is
here defined as ${rel_url_start}/save
(or save_and_exit
).
rel_url_start
is defined in the outer macro as
'${request/xml_rel_url}/${node_path}/edit'.
Within the Silva Document edit this resolves to
something interesting like
/silva/test_folder/my_document/0/widget,e0,e1/edit
where /silva/test_folder/my_document/0
is representing the current
version object (which is an instance of
ParsedXML
), and widget,e0,e1
a notation to
get the proper path (deep involved stuff in
the ParsedXML
), and edit
is acquired to be
something about editing this node ... I did
not found it, however.
Here I got lost. Indeed
/silva/service_widgets/save(_and_exit)
seems to be used, which is called in the
context of
/silva/service_widgets/element/doc_elements/image/mode_edit/
While looking this up,
it also manages to put
the current node in
the request.
( XXX guess I found out what happens inbetween. however I did not have the time to write it down :(
)
Actually I did not want that all together in
the current place. Fortunately we can just put
silly stubs for the action in it; if Zope does
not find the
script described by the button name, it uses
the default action; this is what we want here.
Define and configure the XML Widget for
editing images
First add an XML Widget Registry to the
Silva Root folder and name it
service_image_editor
.
You need to configure this via a Python script
(which is placed by convention in
/silva/service_setup/service_image_editor_setup
).
The script may contain the following code
wr_edit = context.service_image_editor
wr_edit.clearWidgets()
wr_edit.addWidget('image', ('service_custom_widgets', 'top', 'image', 'mode_edit'))
wr_edit.setDisplayName('image', 'Image')
I actually dispatch the display of the
outermost image
tag to an object, which is
located at:
/silva/service_custom_widgets/top/image/mode_edit/render
.
(the render
is added implicitely.)
You may use another place,
e.g. /silva/service_widgets/top/image/mode_edit
looks more natural. However this may lead to
some problems when upgrading Silva, as this
usally cleans up the whole
/silva/service_widgets
directory.
As the current XML <image> tag has no
inner tags, that is all to be registered.
Execute the script by e.g. selecting the
Test tab in the ZMI. Now we can delegate
rendering the XML <image>-tag to the
widget via the service_editor
with code
like:
context.service_editor.setViewer('service_image_editor')
return service_editor.getViewer().render(model.get_editable().image().documentElement)
... except we should maybe first define the
script at
/silva/service_custom_widgets/top/image/mode_edit/render
before testing if ;)
I choose to copy the
/silva/service_widgets/element/doc_elements/image/mode_edit/render
to
/silva/service_custom_widgets/top/image/mode_edit/render
.
As this uses the macros in
/silva/service_widgets/macro_*
I simply copy
them over, too; You may instead want to change
the render
script to use the original templates.
(This may avoid the copy orgy while testing; I
actually have to copy over at least the
get_display_name
script, too ... actually I
get tired and simply copied all over. Clearly
this approach could be improved.)
Now that we have the widget, let us look how
to use it;
looking at the globals defined in the render
script and the macro_edit_view
, we have
to do the following to
use this template:
- put the item to be displayed into
request['node']
- put the widget name
service_image_editor
into request['wr_name']
- put fake entries in request['xml_rel_url']
and request['xml_url']
- dispatch to the template.
I propose to do this in a separate script,
(I named it get_image_selection
):
view = context
request = container.REQUEST
model = request['model']
editable = model.get_editable()
node = editable.image().documentElement
# XXX: broken, this could contain "foo/bar" as well :(
request.set('xml_url', '.')
request.set('xml_rel_url', '.')
request.set('wr_name', 'service_image_editor')
service_editor = context.service_editor
service_editor.setViewer( 'service_image_editor')
viewer = service_editor.getViewer()
# debugging: bypass cache:
#return viewer.getWidget(node).render()
return viewer.render(node)
This script is called within the normal_edit
view:
<image-editor tal:omit-tag="">
<tr comment="News Image" class="focus" valign="top">
<td width="16"> </td>
<td class="rowhead" align="right">Choose Image</td>
<td align="right"> </td>
<td colspan="2">
<image tal:replace="structure here/get_image_selection" />
</td>
<td align="right" valign="top"> </td>
</tr>
</image-editor>
Now that is looking a little odd, if you try
it out via the edit tab on this content, as
the usual
control elements to add new Document XML parts
and delete or move the image are present. Even
worse the HTML source tells us the image
selection is in its own form. Fortunately
this can be helped with; this is actually why
I needed the new
service_image_editor
widget; otherwise we
could get away by using e.g. the
service_doc_editor
widget, which knows how to
render <image> tags, too.
If you look into
/silva/service_widgets/macro_edit_view
you
find in the init
block something like:
<init tal:omit-tag=""
tal:define="global silva_root here/silva_root;
....
global is_embedded is_embedded | nothing;
global content_cell_class content_cell_class | nothing;
global disable_edit disable_edit | nothing;
global disable_insert disable_insert | nothing;
global disable_delete disable_delete | nothing;
global disable_move_up disable_move_up | nothing;
global disable_move_down disable_move_down | nothing;
...
<form name="focusbox" action="." method="POST" tal:omit-tag="is_embedded">
...
If you can define is_embedded
before using
the macro, the extra form tag will vanish. For
this edit the
/silva/service_custom_widgets/top/image/mode_edit/render
and add the initialization at the top:
<tal:block tal:define="
global is_embedded python:1;
global disable_insert python:1;
global disable_delete python:1;
global disable_move_up python:1;
global disable_move_down python:1;
" />
This additionally removes the control images
for deleting and moving, which make no sense here.
As I were actually at it, I hacked the
'/silva/service_custom_widgets/macro_helpers'; in
case of is_embedded
I expect there is not
Save and Exit
button useful, thus I added a
tal:condition
there:
<input class="button" type="submit"
tal:condition="not:is_embedded"
value=" Save and exit "
tal:attributes="name string:${rel_url_start}/save_and_exit:method" />
(Even if You did not copy the macro over, You
can do this in the original macro, as it did
not break something - at least not in my Silva-0.8.3)
- Finally I have to recommend to copy over all
objects from the
/silva/service_view_registry/edit
folder
(except of the subfolders,) to the
/silva/service_custom_view_registry/edit
folder. There are some important items needed to
be acquired. This may lead to problems when
upgrading; unfortunately I see no good way to
avoid this here.
(You can check out by yourself what is actually
needed, and possibly fix the pathes to use the
original version, if wanted;
I guess you will learn a lot about how the SMI
is put together ;)
Ok, now figure out, if our News contents may be
edited. I expect there will be several errors
due to objects either You forgot to copy and
adapt or I forgot to tell You ...
Please tell me especially if You run into
problems when testing the last step.
Finally after some debugging You should be able
to view the edit form, be able to include some
changes and view the changes in the preview form.
If you can do this, congratulations, the new
content type works.
Coming back the next day, I found there are a lot
of things to polish and add:
This output is actually generated by the
to_xml(self, file)
method in the SilvaObject
class on the file system level. This method must
be overridden for each custom content type
separately to get a more interesting export
(until someone invents something very cool to
avoid it.)
For the News
content type this could like:
def to_xml(self, f):
version_id = self.get_public_version()
if version_id is None:
return
version = getattr(self, version_id)
f.write('<news id="%s">' % self.id)
f.write('<title>')
f.write(self.get_title_html())
f.write('</title>')
version.image().documentElement.writeStream(f)
for attr in ['image_title', 'text' ]:
f.write("<%s>" % attr)
f.write( translateCdata(getattr(version,attr)()) )
f.write("</%s>" % attr)
f.write('</news>')
As the semantic of the export XML is not fixed
for custom content types you can implement
differently, of course.
- there is no meta data tab. XXX: missing yet.
Silva-0.8.3 issue:
When just copying the views of the
DemoObject
, You will run into some encoding
problems. In this HowTo they have been fixed
explicitely for the public
view (which has
been replaced in larger parts by a different
layout) but not everywhere in the
edit
view. If You got problems with
encodings, please check the 'normal_edit':
- in the <init> block use
global title python:editable.get_title()
instead of
global title python:editable.get_title_editable()
- in the input field for the
info
fix:
python:view.form.info.render(editable.info())
to
python:view.form.info.render(model.output_convert_editable(editable.info()))
before copying it around.
- in the
demoObject_submit
instead of:
info = result["info"]
use
info = model.input_convert(result["info"])
Additionally there are some open questions, e.g.:
Many thanks to Samuel Schluep, who pointed out
some inconsistencies in a previous
version. (Samuel: unfortunately I have not been
able to reply to comments, as the reply-header
of the mail seemed to be broken, or Your mailer
has been down.)
Additionaly thanks to Jean-Paul Ladage for
pointing
out some more inconsistencies and gotchas,
which slipped
in after the last update.
All bugs and typos still present are mine, of
course. (They may be a lot of new ones as I have
migrated the views to a new folder recently.)
back to the index