You are not logged in Log in Join
You are here: Home » Members » camil7 » Silva Documentation and HowTo's » archive for ancient content » Modify an Existing Custom Content Type

Log in
Name

Password

 

Modify an Existing Custom Content Type

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:

  1. implement a new attribute to replace the old one
  2. disable changing the current contents for a transitional phase.
  3. implement an update method for the News and an updates script calling this method.
  4. migrate the public view to the new attribute
  5. migrate the edit view to the new attribute
  6. clean up

Implement a new attribute to replace the text node

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.

Make the content objects read only

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.

Update existing content objects

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.

Migrate the public view

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.

Migrate the edit 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.)

Final cleanup

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