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

Log in
Name

Password

 

Define Custom Content Types

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 ...

Specify the content type

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.)

Implement the class.

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:

  1. 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.
  2. 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.

  3. 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.
  4. 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.
  5. 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:

    • initialize it in __init__ as:
              self.name = ParsedXML('name', valid_xml)
              
      where name it the attribute name (pass as String, i.e. in quotes,) to the XML constructor. The valid_xml should be a string containing valid XML; there are good reasons to use "<doc></doc>" here (explained in the view part below.)
    • do little else here: the setter/getter thing is handled elsewhere.
  6. 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 ?).

  7. adapt the boilerplate text in the python class:
    • come back to News.py
    • change InitializeClass(DemoObject)
    • change InitializeClass(DemoObjectVersion)
    • change
      manage_addDemoObjectForm = PageTemplateFile("www/demoObjectAdd", globals(),
                                              __name__='manage_addDemoObjectForm')
            

    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.

Register the new content type class to Silva

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;
    1. add an import Custom near the head below the other import a statement.
    2. 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.

  1. 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.)

  2. 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.

  3. 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.

  4. If that succeeds, the first step is complete. Now you are ready to add the views you need to access the content via Silva.

Register a public view

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 &lt;image&gt; 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.)

  • Testing: Open the URL to the test news in your browser you have added before, and append /preview_html to it (as there is no public version yet.) E.g. if you have created the content in the test folder names folder of the default Silva installation and named it new_news, type in:
         http://localhost:8080/silva/folder/new_news/preview_html
    
      Now you can debug and fine tune your public view.
    

Register an add and edit view for it.

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.
    1. Copy add, add_submit and macro_index from /silva/service_view_registry/add to /silva/service_custom_view_registry/add
    2. 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.

  1. may contain an extra linked image leading to another form (for the content); mostly empty
  2. the short title for the field
  3. the mark for mandatory fields
  4. the input tag itself
  5. 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">&nbsp;</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">&nbsp;</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.

Miscellanea

Coming back the next day, I found there are a lot of things to polish and add:

  • The export does not work; it displays something like
       <unknown id="test_news">My Silva News</unknown>
      

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':

  1. in the <init> block use global title python:editable.get_title() instead of global title python:editable.get_title_editable()
  2. 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.
  3. in the demoObject_submit instead of: info = result["info"] use info = model.input_convert(result["info"])

Additionally there are some open questions, e.g.:

  • add more possible attribute types (e.g. lists)
  • full XML solution: all content within XML.

    This has been investigated by Benno Luthiger here

  • investigate XML widgets in more detail? I hope I understand the stuff one day, maybe ...

Acknowledgements

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