If an article comprises HTML text, then adding things like graphics or external content is not something which can be merged with the HTML document and stored as a unit in the database.  It becomes essential to be able to store and reference resources like that seperate from the article itself, and to facilitate linking back from the article to the stored resource.

We do want to store graphics and linked text in the ZoDB rather than as file system based resources, as this makes distribution of wiki content possible as a single integrated unit.

Defining attachments

We define an attachment as some content, either raw bytes or editable text, having a specific type or format, a name and a description.  We envisage an article being able to contain any number of attachments, and refer to these attachments from within the article itself.  Text attachments shall be rendered directly inline within a div element and not as an iframe, as the overheads associated with iframes is in this case unnecessary.  Images shall be referenced from <img /> tags.

Looking at attachments then, the following zope.schema interface defines an attachment:

class IAttachment(Interface):
    """ Schema for resources which we intend to store alongside articles
    """
    name  = DottedName(title=u'Resource Name:', 
                       description=u'The name of the resource being defined')
    description = TextLine(title=u'Description', 
                           description=u'A description for the resource')
    fmt   = Choice(title=u'Format:', description=u'The format of the source',
                 vocabulary='gfn.resourceTypes', required=True, default='python')
    fdata = Bytes(title=u'Upload file:', description=u'Upload source', required=False)
    text  = Text(title=u'or type Text:', description=u'Source Text', required=False)

    @invariant
    def file_or_text(self):
        if self.fdata or self.text: return True
        raise Invalid('You must either upload the source or type it in')

First, take a look at the IAttachment.fmt attribute in the above.  This is defined as a schema.Choice instance, having a vocabulary of 'gfn.resourceTypes'.  A vocabulary is a global utility registered with the ZCA and implementing an IVocabularyFactory with the associated name.  While there are a few ways to populate Choice fields, including specifying a list of options directly, vocabularies are well worth learning about.  They are immensely powerful, and provide the means to implementing dynamic options with the greatest of ease.

Our definition for 'gfn.resourceTypes' looks like this:

class ResourceTypes(grok.GlobalUtility):
    grok.implements(IVocabularyFactory)
    grok.name('gfn.resourceTypes')

    formats = [
        ('image',      u'A png, gif or jpeg image'),
        ('html',       u'HTML Source'),
        ('javascript', u'Javascript Code'),
        ('python',     u'Python Sources'),
        ('css',        u'CSS Source')]

    def __call__(self, context):
        self.context = context
        terms = []
        for value, title in self.formats:
            token  = str(value)
            terms.append(SimpleVocabulary.createTerm(value,token,title))
        return SimpleVocabulary(terms)

An important side note:  For a grok.GlobalUtility to work as described above, your setup.py for the site must include the grok.app.schema package. This is a known outstanding issue.

You will notice that the factory __call__() dunder method takes a context as argument.  This will be the view context for the form at the time the view is rendered, and the context may be used to decide which vocabulary items to return.  A vocabulary Term consists of a value, a token and a title.  The value is what we get back as a field value when a user chooses the associated choice in the form, and may be any Python object.  The title is what the user sees in the list of choices.  The token is what is used as an <option /> tag name.  There is thus no limit to the complexity of the object identified and returned by a schema.Choice field.

Our IAttachment also defines a fdata = Bytes(title=u'Upload file:'...). The default widget generated by the formlib library for an object of type schema,Bytes, is a <file /> upload entry box.  We can simply treat this attribute as a raw byte string.

The schema.TextLine attributes are reasonably self evident, and are ordinary Python unicode strings.

Although both the text and fdata attributes are defined as optional fields, we have defined an invariant on these two fields to enforce that either one or the other attribute is present during validation of the schema.  An invariant is a rule which returns True if the rule succeeds, or raises an exception otherwise.

A container for attachments

Now, having defined our attachment, we also need to define a bin which holds attachments.  This bin can then be associated with articles so that we can retrieve our attachments on demand.

class IAttachments(Interface):
    """  A bin holding attachments
    """

We can then define a model which implements IAttachments as a normal grok.Container:

class Attachments(grok.Container):
    ''' Holds things which implement IAttachment
    '''
    grok.implements(IAttachments, ILayout)
    title = u'Managing Attachments'

As you can see, our Attachments class also implements ILayout, so we know that if we traverse to an instance of an Attachments class, we will get the site index rendered for free; this implies that the Content viewlet manager will be called correctly to fill in that part of the site view.

class ListAttachments(grok.Viewlet):
    ''' Displays a list of attachments and allows edit/delete on each item
    '''
    grok.context(IAttachments)
    grok.viewletmanager(Content)

    def update(self):
        popups.need()

Here, the popups resource refers to some javascript defined in resource.py as:

popups     = Resource(library, 'popups.js', depends=[jqueryui])

It is a simple wrapper for displaying forms within a JQuery-UI dialog box and handling the response properly to ensure correct form submission.

$(document).ready(function(){
    $('div.popup').on('click', function(e){
        e.preventDefault();
        var href = $('a', this).attr('href');
        var title = $(this).attr('title');
        $('<div />').load(href, function(r, a , b) {
            $(this).dialog({
                title: title,
                modal: true,
                width: 'auto',
                resizable: false,
            });
        });
    });
});

An attachment manager UI

The Content viewlet for an Attachment context renders the following page template:

<table class="attachmentsTable">
    <thead>
        <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Type</th>
            <th />
            <th />
        </tr>
    </thead>
    <tbody>
        <tal:loop tal:repeat="v context/values">
            <tr tal:define="path python:viewlet.view.url(v)">
                <td tal:content="v/name" />
                <td tal:content="v/description" />
                <td tal:content="v/fmt" />
                <td class='buttons'>
                    <div tal:attributes="title string:Editing: ${v/description}" 
                         class="popup buttons">
                        <a  tal:attributes="href string:${path}/edit">Edit</a>
                    </div>
                </td>
                <td class='buttons'>
                    <div tal:attributes="title string:Deleting: ${v/description}" 
                        class="popup buttons">
                        <a  tal:attributes=" href string:${path}/delete">Delete</a>
                    </div>
                </td>
            </tr>
        </tal:loop>
    </tbody>
</table>

This renders a simple table listing the defined attachments for the page, together with links to change or delete an attachment. For example:

NameDescriptionType  
Attachments Attachments container python
Attachments.content Content viewlet for Attachments python
attachment.schema An attachment schema python
gfn.resourceTypes Our vocabulary python
iattachments IAttachments bin python
listattachments.pt ListAttachments Template python
popups.js Popups javascript javascript

 When an edit or delete link is clicked, the popups.js traps the click and instead of following the link, pops up the dialog with the edit form or delete confirmation.

For example,

 

Navigation menus for the attachment manager

Of course, normal article navigation does not apply when editing resources for an article, and we provide appropriate buttons to be rendered in the Navigation provider area:

class BackButton(MenuItem):
    grok.context(IAttachments)
    grok.order(0)
    title = u'Back to article'
    mclass = 'buttons'
    @property
    def link(self):
        return self.context.__parent__

 and add an attachment manager link into the menu for an IArticle, conditional on their being attachments to manage of course:

class ManageAttachments(MenuItem):
    grok.context(IArticle)
    grok.order(-1)
    title = u'Manage Attachments'
    link = u'attachments'
    mclass = 'buttons'

    def condition(self):
        a = self.context.attachments
        return a is not None and len(a)

 Kinds of attachment

We define two types of attachment that we want to add to articles; these are: Images and Source Text.  We know that they are attachments because they implement the IAttachment interface:

class Image(grok.Model):
    '''  An attachment being an image
    '''
    grok.implements(IAttachment)

    def __init__(self, name, description, fmt, fdata, text):
        self.name = name
        self.description = description
        self.fmt  = fmt
        self.fdata = fdata
        self.text = text
    def link(self):
        return '''<img src="attachments/%s" />''' % self.name

The link() method returns an <img /> tag with a src attribute that will load the appropriate attachment as an image.  The Source() class is a little different:

class Source(grok.Model):
    '''  An attachment being a text item
    '''
    grok.implements(IAttachment)

    def __init__(self, name, description, fmt, fdata, text):
        self.name = name
        self.description = description
        self.fmt  = fmt
        self.fdata = fdata
        self.text = text
        if fdata:
            self.text = fdata.replace('\r\n', '\n')

    def link(self):
        return ('''<div class='attachment' name="%s" src="attachments/%s" />''' 
                % (self.name, self.name))

 This assumes that the data content will be text, and expressly sets the text attribute to the content of fdata if file data was uploaded.  The link() method returns a div of class 'attachment' with appropriate name and src attributes.  TinyMCE does not generally allow non-standard attribute tags such as a div with a src.  The list of valid attributes is specified in the editor's init.js script.

Adding new attachments

To create a new attachment for an article, we need an adapter.  This adapter should create a new instance of IAttachment, given an instance of an IArticle:

class ArticleAttachment(grok.Adapter):
    ''' An adapter which creates an IAttachment for an IArticle
    '''
    grok.context(IArticle)
    grok.implements(IAttachment)

    name  = None
    description = None
    fmt   = u'python'
    text  = u''
    fdata = None

Notice that the above adapter does not implement a factory which spews out instances of IAttachment when it is called, but is rather itself an instance of an IAttachment.  The default __init__() dunder will assign self.context to the context of the adapter. i.e. self.context will be an instance of IArticle as identified through traversal. Having the above adapter, we can now create a form for an article which uses the input fields of an IAttachment, when presented with a traversed and identified IArticle instance.

class AddAttachment(grok.EditForm):
    grok.context(IArticle)
    form_fields = grok.Fields(IAttachment)    # Use the ArticleAttachment adapter
    grok.name('attach')

    def setUpWidgets(self, ignore_request=False):
        super(AddAttachment, self).setUpWidgets(ignore_request)
        self.widgets['text'].cssClass = 'pre'

    def update(self):
        from resource import style
        style.need()

    @grok.action(u'Add this resource')
    def addResource(self, **data):
        if self.context.attachments is None:
            attachments = self.context.attachments = Attachments()
        else:
            attachments = self.context.attachments
        name = data['name']
        if name in attachments: del attachments[name]
        if data['fmt']=='image':
            src = attachments[name] = Image(**data)
            return src.link()
        else:
            src = attachments[name] = Source(**data)
            content = getMultiAdapter((src, self.request), name='index')
            return content()

    @grok.action(u'Cancel action', validator=lambda *_a, **_k: {})
    def cancel(self):
        return ''

By specifying grok.Fields(IAttachment) for a grok.context(IArticle), we tell the form to use the adapter we prepared when initialising the context for the fields.

The name of the form view is simply 'attach'.  So navigating to an article and then appending '/attach' to the urls would bring up this add form.

A grok.EditForm default template includes the HTML header and renders a complete HTML document.  This we know will be displayed in a popup dialog.  Unless we include the style in the update() function here [style.need()], the form will not be displayed in a way consistent with the rest of our user interface.

We ensure that the textarea which represents the text attribute of an IAttachment is rendered as preformatted text, by adding in the 'pre' css class for the text attribute in setUpWidgets().

Notice also the replacement of the validator in the cancel action.  This prevents the Cancel submission from generating validation errors.

All the real work happens in addResource().  Here, if the article does not already have an attribute called 'attachments', we create an instance of the model Attachments() and assign it to the attribute.  Remember that Attachments() is a grok.Container.

If an attachment by the specified name already exists, we first delete it as we will want to replace it with our new attachment.  Then, we branch depending on whether the attachment is an image or source.

If the new attachment is an image, we assign a new instance of Image() to the attachment, and return a link to the image.

If, on the other hand, the attachment is source text, we assign a new instance of Source() and return the default view for a Source() model.

To make attachments traversable as a URL, we define the attribute as traversable in the ArticleContainer base class:

class ArticleContainer(grok.Container):
    ....
    attachments = None
    grok.traversable('attachments')

This means that if we traverse to an article, and then add the text '/attachments' to the url, we will get back an instance of the Attachments() model, or an error if there are no attachments defined.

Views on an IAttachment

Operations performed typically on attachments are to either change the attachment or to delete it.  The delete operation is rather generic and trivially implemented, but the operation of changing attachments has some rather subtle issues which we will cover in some detail.  First, deleting an attachment uses the form:

class DeleteAttachment(grok.EditForm):
    grok.context(IAttachment)
    grok.name('delete')
    form_fields = grok.Fields(IAttachment).omit('fdata', 'text')

    @grok.action(u'Yes, Delete this attachment')
    def Delete(self, **data):
        attachments = self.context.__parent__
        name = data['name']
        if name in attachments: del attachments[name]
        self.redirect(self.url(attachments))

    @grok.action(u'No, get me out of here', validator=lambda *_a, **_k: {})
    def cancel(self):
        attachments = self.context.__parent__
        self.redirect(self.url(attachments))

As is normal behaviour in handling submitted forms, the view redirects the browser to the default view for the parent after performing the updates.

The edit operation is quite similar.  However, while this updates the attachment perfectly, you will recall that the actual article does not contain a reference to the attachment, but instead contains a syntax-highlighted copy of the attachment.  This is to allow syntax highlighting to work consistently (you should not highlight already highlighted text).  It is necessary therefore to update the content of the article itself after performing an edit operation.

class EditAttachment(grok.EditForm):
    grok.context(IAttachment)
    grok.name('edit')
    camefrom = None

    def update(self, camefrom = None):
        self.camefrom = camefrom or self.url(self.context.__parent__)

    @grok.action(u'Update')
    def Change(self, **data):
        attachments = self.context.__parent__
        name = data['name']
        if name in attachments: del attachments[name]
        if data['fmt']=='image':
            attachments[name] = Image(**data)
        else:
            attachments[name] = Source(**data)
        self.redirect(self.camefrom)

    @grok.action(u'Cancel action', validator=lambda *_a, **_k: {})
    def cancel(self):
        self.redirect(self.camefrom)

We can ensure that the syntax highlighted attachments are the latest by doing an AJAX call for each instance of a div.attachment we find each time we reload the article:

$(document).ready(function(){
    $('div.attachment').on('click', function(e){
        e.preventDefault();
    });
    $('div.attachment').each(function(){
        var attach = $(this);
        $("<div />").load(attach.attr('src')+'/highlight', function(){
            attach.html($(this).html());
        });
    });
});

Since we also don't want to be able to click on text divs from the display view, the above script also prevents the default click event from propagating.

The above javascript is not included in the iframe that houses the tinyMCE editor, so there we will have to deal with things differently. The following template (showsource.pt)  renders a Source() attachment:

<div tal:attributes='class string:attachment mceNonEditable;
                     src string:attachments/${context/name};
                     name context/name'
     tal:content="structure view/html" />

From within the editor, we cannot rely on $(document).ready() since we dont keep on reloading the document as we edit it.  Instead, we need a simpler way to retrieve the highlighted text, so that our tinyMCE plugin can replace the content directly.

For this, we provide two additional views: ModifySource, and ModifyImageModifySource allows one to change the attachment type to an Image(), but ModifyImage does not let you go back.  This not really a problem as it's really easy to delete and recreate attachments.

class ModifySource(grok.EditForm):
    grok.context(Source)
    grok.name('modify')

    def setUpWidgets(self, ignore_request=False):
        super(ModifySource, self).setUpWidgets(ignore_request)
        self.widgets['text'].cssClass = 'pre'

    @grok.action(u'Update')
    def Change(self, **data):
        attachments = self.context.__parent__
        name = data['name']
        if name in attachments: del attachments[name]
        if data['fmt']=='image':
            src = attachments[name] = Image(**data)
        else:
            src = attachments[name] = Source(**data)
        content = getMultiAdapter((src, self.request), name='index')
        return content()

    @grok.action(u'Cancel action', validator=lambda *_a, **_k: {})
    def cancel(self, **data):
        attachments = self.context.__parent__
        name = self.context.name
        src = attachments[name]
        content = getMultiAdapter((src, self.request), name='index')
        return content()

As may be seen, the 'modify' view is specific to an instance of the Source() class here, and instead of a redirect to the parent page, this form returns the content of the attachment directly.  It does so by finding the MultiAdapter which is called index, and has an attachment and BrowserRequest as context and argument.  MultiAdapters with this form are views, so in short, we expect to find a view on the Source() attachment.  Calling the adapter renders and returns the result of the view.

class ShowSource(grok.View):
    grok.context(Source)
    grok.name('index')
    html = ''

    def update(self):
        self.html = self.context.highlight()  # Set highlighted text
        style.need()
        textStyle.need()

The corresponding page template for a Source() view is as follows:

<div tal:attributes='class string:attachment mceNonEditable;
                     src string:attachments/${context/name};
                     name context/name'
     tal:content="structure view/html" />

The template returns the div.attribute tag and fills it in with the highlighted text.

Images work just like Source, with a few minor differences.  Images are much simpler for one thing. The modify form for an image tailors things to suit.  It does not allow an option to type in text or specify a format at all.  Instead of returning content itself, it returns an <img /> tag with a link to content:

class ModifyImage(grok.EditForm):
    grok.context(Image)
    grok.name('modify')

    form_fields = grok.AutoFields(Image).omit('text', 'fmt')

    @grok.action(u'Update')
    def Change(self, **data):
        attachments = self.context.__parent__
        name = data['name']
        if name in attachments: del attachments[name]
        src = attachments[name] = Image(**data)
        return src.link()

    @grok.action(u'Cancel action', validator=lambda *_a, **_k: {})
    def cancel(self, **data):
        attachments = self.context.__parent__
        name = self.context.name
        src = attachments[name]
        return src.link()

The view on an image has a render() method which returns the binary image data directly:

class ShowImage(grok.View):
    grok.context(Image)
    grok.name('index')
    def render(self):
        return self.context.fdata or self.context.text

In this case we are being very specific about the index view for an image versus an index view for source text.  Grok lets us be as general or as specific as we need to be.

Wrapping it up

The addition and management of attachments turns out to be quite a complex thing to do, and the source for this module is long relative to other modules.  It demonstrates admirably however, the way the component framework can be used to extend existing components, and provide pluggability.  Subsequent support for other kinds of attachment for example, might be added with little trouble.

This chapter refers quite often to a syntax highlighter.  We talk about it, and show where we use it, but what is it?  The following section will explain in more detail.

Grok 4 Noobs

Adding attachments to articles