Modify an Existing Custom Content Type
Created by .
Last modified on 2003/08/01.
Upgrading a content type - Add markup to the text.
This HowTo proposes a way to modify an existing
custom content type and simultaneously update all
existing contents of this content type. The
proposed process has been tested in practice on a
development installation; it may be applicable on
a live server, too, though I would prefer instead
to copy
the Data.fs
to a separate test installation and
do most of the update there.
It is not stated that the proposed way is the
best possible; sure there are alternatives, which
I did not explore in detail. If you have comments
on this HowTo, please use the feedback link on the
bottom.
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.
This HowTo assumes you have read the HowTo
create new custom content
types.
The example My Silva News
described there is
reused in this HowTo.
Important note: the currently proposed
Folder structure will not work well when
upgrading silva. See also the remark in the
other HowTo. This will
improve in future ...
The Goal: add markup to the text
Let us shortly review the custom News
content
type. This content type has four fields:
- a
title
, which is a heading
- an
image
referencing a Silva Image
- an
image_title
containing one line of plain text
- a
text
containing multiline text
While the title
and the image_title
is
properly HTML-encoded (simply by leaving out
the structure
keyword in the public
view)
avoiding the possibility
that the editors may smash the page layout, this
is not true for the text
. Here HTML tags are
passed through unescaped.
Instead of allowing all HTML tags we want to
restrict the text
to the markup supported by
the Silva Document paragraphs, i.e. something
like
**bold text**
for bold text,
and ((contact|http://my.company.com/contact))
for embedding links.
This will be realized by replacing the current
text
attribute by a ParsedXML instance
containing one paragraph of text. Because
maybe already gazillions of News
content
objects are out there, we need to upgrade these
objects automatically.
This will be done in the following steps:
- implement a new attribute to replace the old
one
- disable changing
the current contents for a
transitional phase.
- implement an update method
for the News and an
updates script calling this method.
- migrate the public view
to the new attribute
- migrate the edit view
to the new attribute
- clean up
First we have to change the class definition of
the News content type on the file system level in
the folder Silva/Custom/News.py
.
The new attribute will be a ParsedXML instance
containing a single paragraph. For newly created
News objects it will be stored in the text_xml
attribute.
Thus the new constructor of the NewsVersion
looks like:
def __init__(self, id, title):
"""Set id and initialize the ParsedXML tree.
"""
self.id = id
self._imageTitle = 'the image title'
self._text = ""
self.text_xml = ParsedXML('text', '<p/>')
self._image = ParsedXML('image', "<image/>");
The new attribute is made public accessible
automatically as it does not begin with an
underscore _
. If you do not like that, you may
define the name with an underscore and add a
separate getter to access the attribute, like it
has been done for the image when creating the
content type.
To avoid having trouble with concurrent
modifications you may make the text
attribute
read only. The most simple way is to modify the
set_news_data
method:
def set_news_data(self, imageTitle, text=None):
"""Set the plain fields for this version.
"""
self._imageTitle = imageTitle
# commented out: deprecated, read only
#self._text = text
Giving the text
argument a default value allows
to drop the parameter from the list in the
script calling this method later.
Additionally you should notify all the Authors
about disabling the text
field, i.e. by editing
the
/silva/service_custom_view_registry/edit/VersionedContent/News/normal_edit
putting a line in somewhere.
If you are your only one author, you can skip
this section, of course.
So much about defining the new attribute; but
what is about the already existing contents? We
want to move their text
attribute values into
the new text_xml
attribute. This is achieved by
two parts: an upgrade
method in the News
class and a ZMI level script upgrading all
existing content objects.
First the upgrade
method: the following version
is added to the News
class, not the
NewsVersion
class. (You could do different, of
course ...)
security.declareProtected(SilvaPermissions.ChangeSilvaViewRegistry,
'upgrade')
def upgrade(self, upgrade_key):
""" upgrades to text with some markup """
import types
if upgrade_key != 'News text to xml': return None
for obj in self.objectValues():
if obj.meta_type != NewsVersion.meta_type:
# this should not happen (normally)
continue
if hasattr(obj,'text_xml'):
# already upgraded
continue
text = obj.text()
new_text = ParsedXML("text", "<p />")
self.replace_text(new_text.documentElement, text)
obj.text_xml = new_text
return "ok"
In detail, the code does the following:
- the security clause guarantees only Manager
roles can perform the update
- the upgrade key is a simple string I propose to
define to avoid problems when one has several
updates within a small time frame. It prevents
the calling script using the
wrong method accidentally. This does not make
lots of sense if there is only one thing to
upgrade; but I will add a second
upgrade later on already for this single change.
- if the key matches, the script loops over all
contained values in this object (remember Silva
Versioned Content is folderish by it's nature,
keeping the versions as contained
objects.) Usually all objects here will be of
type
NewsVersion
, but just in case someone has
put an override.html
page template or something
similar here, we check the type to bypass all
other kind of objects.
- as a final check we test if the content is
already upgraded; this allows us to run the
upgrade script several times without harm.
This may be useful if the script crashed in the
middle of an update due to some silly error, or
accidentially did not update all content
objects.
- the real upgrade is delegated to a helper
method in the
EditorService
class:
replace_text
puts the content of the string
into the XML (XXX: here it parses for the
usual Silva structured text markup, but not the
HTML-tags allowed previously; these will be
escaped and need to be fixed manually. Maybe
look for a nicer way to do it.)
- after the upgrade the transformed content of
the
text
is put in the text_xml
attribute.
To trigger the method for all News object we
need a script in the ZMI. I simply borrow one
helper script from one of Silva version
upgrading scripts and adapt it:
updated = []
def find_recursive(start_point):
for id, obj in start_point.objectItems():
if obj.meta_type in ['Silva Folder', 'Silva Publication']:
find_recursive(obj)
elif obj.meta_type in ['My Silva News']:
if obj.upgrade('News text to xml'):
updated.append(obj.absolute_url(1))
find_recursive(container)
if len(updated) < 25:
print "Done updating my news:\r "+"".join(updated,"\r ")
else:
print "Done updating my news. (updated %d news)" % len(updated)
return printed
The script must be placed in the Silva Root
Publication which should be upgraded.
Before we trigger the script we need to restart
Zope to take the file system level changes into
effect. It may be a good idea to backup the
Data.fs
before, just in case our update
script is not perfect.
After the restart, check that the Silva product
is not broken. If it is, the upgrade method has a
serious problem (usually it does not
compile). We should fix this first.
Afterwards the script may be triggered, most
simply by visiting it's test
tab in the ZMI.
(You can type in the URL to this script in the
browser instead, if you want.)
If it raises exceptions, it is better to recover
the Data.fs
on a production system before
retry.
Now all the content is migrated from the text
to the text_xml
, we can migrate the public
view to show the new content.
For this edit the
/silva/service_custom_view_registry/add/News/render_helper
.
The smallest and thus most simple change is to
change the definition of the global text
variable in this script. Instead of using:
global text options/version/text;
use
text_xml options/version/text_xml;
global text python:model.render_text_as_html(text_xml.documentElement);
in the <init> block. (Cautious people do this
in a Zope Version.)
Actually that is all for the public view.
The text
field appears both in the add
and
edit
view; let's first fix the add
view.
The fix is in the
'/silva/service_custom_view_registry/add/News/add_submit_helper';
instead of putting the form data plainly
in the model (where it will be ignored now):
new_text = news.input_convert(result['text'])
silly_default_image_title = ''
editable.set_news_data(image_title=silly_default_image_title, text=new_text)
we take the text separately:
new_text = result['text']
silly_default_image_title = ''
editable.set_news_data(image_title=silly_default_image_title)
news.replace_text(editable.text_xml.documentElement, new_text)
Note: we drop the model.input_convert
here,
as the news.replace_text
does this for us, and
the encoding conversion should not be done twice.
The edit
view is actually some kind of
model/view/controller thing, where the content
object id the model, the input form the view and
the
/silva/service_custom_view_registry/edit/VersionedContent/News/news_submit
script handling the form data,
as send with the
form submit, as the controller. The controller
will be updated first, in analogy to the add
controller:
Instead of passing the plain text string into
the model, as done previously:
model.get_editable().set_news_data(image_title=default_image_title, text=text)
we now use an EditorService
method do the work:
editable = model.get_editable()
editable.set_news_data(imageTitle=image_title)
model.replace_text(editable.text_xml.documentElement, text)
Skip the model.input_convert
when getting the
text
from the form; again the encoding is
handled by the replace_text
method.
If you try it out, you get an empty Text
input field - because the view still presents
the wrong attribute.
This can be fixed in the
/silva/service_custom_view_registry/edit/VersionedContent/News/normal_edit
.
The input field:
<input tal:replace="structure python:view.form.text.render(model.output_convert_editable(editable.text()))" />
is changed to
<input tal:replace="structure python:view.form.text.render(
model.render_text_as_editable(editable.text_xml.documentElement))" />
That is all. (Maybe you should delete the "read
only" note yet to tell your authors the work is done.)
If you have implemented the to_xml
method in
the News
class, you should update this
method, too. Otherwise the export will not
work. This should be easy after all the
other changes.
As we are actually back on the file system level,
let us clean up the __init__
method.
It should no longer create a
self._text
attribute.
The text()
method may be removed, if you are
sure it is no longer needed. Optionally you could
return the new ParsedXML after rendering,
or a dummy string (for testing - my prefered
solution).
It should no longer reference the _text
attribute, as this is error prone.
To get rid of the _text
attribute, you can add
code to the upgrade
method, like:
security.declareProtected(SilvaPermissions.ChangeSilvaViewRegistry,
'upgrade')
def upgrade(self, upgrade_key):
""" upgrades to text with some markup """
import types
if upgrade_key == 'Delete _text':
done=""
for obj in self.objectValues():
if obj.meta_type != NewsVersion.meta_type:
continue
if hasattr(obj,'_text'):
done="ok"
del obj._text
return done
# previous upgrade:
if upgrade_key != 'News text to xml': return None
[ ... see above ... ]
Now to something completely different.
You may want to add the markup help to the text
input field in the edit view.
Instead of trying to include
the paragraph rendering widget, it seems simpler
to copy the description text (though this has to
be repeated after every Silva upgrade
changing the text, which will not be that often.)
The text actually can be found in:
/silva/service_widgets/element/doc_elements/p/mode_edit/render
Maybe you should put it in a separate page
template (e.g. in
/silva/service_widgets/markup_doc
and include it in the edit form, as it
can be shared between edit forms of different
custom content types this way.
(I sure have forgotten something ...)
back to the index