Index: main/waeup.kofa/trunk/CHANGES.txt
===================================================================
--- main/waeup.kofa/trunk/CHANGES.txt	(revision 12436)
+++ main/waeup.kofa/trunk/CHANGES.txt	(revision 12437)
@@ -5,5 +5,7 @@
 ===================
 
-* Do not compare ResultEntry objects but their attribtes.
+* Add documents package.
+
+* Do not compare ResultEntry objects but their attributes.
 
 * Modify getWidgetsData so that we always get the same error format.
@@ -14,5 +16,6 @@
   image file.
 
-* Remove deprecated HTML and REST widgets and use html2dict instead.
+* Remove deprecated HTML and REST widgets and use html2dict and rest2dict
+  instead.
 
 * UserProcessor: Check if imported roles do exist in the portal.
Index: main/waeup.kofa/trunk/layout/static/css/base.css
===================================================================
--- main/waeup.kofa/trunk/layout/static/css/base.css	(revision 12436)
+++ main/waeup.kofa/trunk/layout/static/css/base.css	(revision 12437)
@@ -98,5 +98,5 @@
 }
 
-.wfstatus {
+.wfstatus, .wfstatus-sub {
     text-align: center;
     background-color: #909090;
@@ -320,4 +320,18 @@
 }
 
+ /* docutils stuff */
+
+.section h1 {
+    font-size: 30px}
+
+.section h2 {
+    font-size: 24px}
+
+.section h3 {
+    font-size: 18px}
+
+.section h4 {
+    font-size: 14px}
+
 /* Smartphone styles */
 
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/__init__.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/__init__.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/__init__.py	(revision 12437)
@@ -0,0 +1,10 @@
+"""This package contains everything regarding documents.
+"""
+# Make this a package.
+from waeup.kofa.documents.container import DocumentsContainer
+from waeup.kofa.documents.document import Document
+
+__all__ = [
+    'DocumentsContainer',
+    'Document',
+    ]
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/batching.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/batching.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/batching.py	(revision 12437)
@@ -0,0 +1,162 @@
+## $Id: batching.py 12410 2015-01-07 08:49:38Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Batch processing components for document objects.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+"""
+import grok
+import unicodecsv as csv  # XXX: csv ops should move to dedicated module.
+from time import time
+from datetime import datetime
+from zope.i18n import translate
+from zope.interface import Interface
+from zope.schema import getFields
+from zope.component import queryUtility, getUtility, createObject
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from waeup.kofa.interfaces import (
+    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
+    IObjectHistory, IGNORE_MARKER)
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.documents.interfaces import (
+    IPDFDocument, IHTMLDocument, IRESTDocument)
+from waeup.kofa.utils.batching import BatchProcessor
+
+
+class DocumentProcessorBase(BatchProcessor):
+    """A base for batch processors for IDocument objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    grok.baseclass()
+
+    util_name = None
+    name = None
+    iface = None
+
+    location_fields = ['document_id',]
+    additional_fields = ['class_name',]
+
+    factory_name = None
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+                    self.additional_fields +
+                    getFields(self.iface).keys())))
+
+    def parentsExist(self, row, site):
+        return 'documents' in site.keys()
+
+    def entryExists(self, row, site):
+        document_id = row.get('document_id', None)
+        cat = queryUtility(ICatalog, name='documents_catalog')
+        results = list(cat.searchResults(document_id=(document_id, document_id)))
+        if results:
+            return True
+        return False
+
+    def getParent(self, row, site):
+        return site['documents']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['document_id'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addDocument(obj)
+        return
+
+    def delEntry(self, row, site):
+        document = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if document is not None:
+            grok.getSite().logger.info(
+                '%s - Document removed' % document.document_id)
+            del parent[document.document_id]
+        return
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = super(DocumentProcessorBase, self).updateEntry(
+            obj, row, site, filename)
+        # Log actions...
+        location_field = self.location_fields[0]
+        grok.getSite().logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, row[location_field], items_changed))
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            DocumentProcessorBase, self).checkConversion(row, mode=mode)
+        # We need to check if the class_name corresponds with the
+        # processor chosen. This is to avoid accidentally wrong imports.
+        if mode == 'create':
+            class_name = row.get('class_name', None)
+            if class_name != self.factory_name.strip('waeup.'):
+                errs.append(('class_name','wrong processor'))
+        return errs, inv_errs, conv_dict
+
+
+class PDFDocumentProcessor(DocumentProcessorBase):
+    """A batch processor for IPDFDocument objects.
+    """
+    util_name = 'pdfdocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Public PDF Document Processor')
+    iface = IPDFDocument
+
+    factory_name = 'waeup.PDFDocument'
+
+
+class HTMLDocumentProcessor(DocumentProcessorBase):
+    """A batch processor for IHTMLDocument objects.
+    """
+    util_name = 'htmldocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Public HTML Document Processor')
+    iface = IHTMLDocument
+
+    factory_name = 'waeup.HTMLDocument'
+
+
+class RESTDocumentProcessor(DocumentProcessorBase):
+    """A batch processor for IRESTDocument objects.
+    """
+    util_name = 'restdocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Public REST Document Processor')
+    iface = IRESTDocument
+
+    factory_name = 'waeup.RESTDocument'
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/browser.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/browser.py	(revision 12437)
@@ -0,0 +1,354 @@
+## $Id: browser.py 12413 2015-01-07 09:20:45Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""UI components for documents offered for download by the company.
+"""
+
+import sys
+import grok
+import pytz
+from urllib import urlencode
+from datetime import datetime
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from zope.event import notify
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
+from zope.formlib.textwidgets import BytesDisplayWidget
+from zope.security import checkPermission
+from waeup.kofa.utils.helpers import html2dict, rest2dict
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.interfaces import (
+    IContactForm, IObjectHistory, IKofaObject, IKofaUtils)
+from waeup.kofa.browser.layout import (
+    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
+    KofaForm, NullValidator, jsaction, action, UtilityView)
+from waeup.kofa.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget,
+    FriendlyDatetimeDisplayWidget)
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+from waeup.kofa.browser.pages import (
+    delSubobjects, add_local_role, del_local_roles, msave,
+    LocalRoleAssignmentUtilityView)
+
+from waeup.kofa.documents.interfaces import (
+    IDocumentsContainer, IPublicDocument,
+    IHTMLDocument, IRESTDocument, IDocumentsUtils)
+from waeup.kofa.documents.workflow import PUBLISHED
+
+grok.context(IKofaObject) # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+
+class DocumentsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customers container.
+    """
+    grok.context(IDocumentsContainer)
+    title = _('Documents')
+
+
+class DocumentBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customer container.
+    """
+    grok.context(IPublicDocument)
+
+    def title(self):
+        return self.context.title
+
+
+class DocumentsContainerPage(KofaDisplayFormPage):
+    """The standard view for document containers.
+    """
+    grok.context(IDocumentsContainer)
+    grok.name('index')
+    grok.require('waeup.viewDocuments')
+    grok.template('containerpage')
+    pnav = 2
+    label = _('Documents')
+
+
+class DocumentsContainerManageFormPage(KofaEditFormPage,
+                                      LocalRoleAssignmentUtilityView):
+    """The manage page for customer containers.
+    """
+    grok.context(IDocumentsContainer)
+    grok.name('manage')
+    grok.require('waeup.manageDocuments')
+    grok.template('containermanagepage')
+    pnav = 2
+    label = _('Manage document section')
+
+    @action(_('Add document'), validator=NullValidator, style='primary')
+    def addSubunit(self, **data):
+        self.redirect(self.url(self.context, 'adddoc'))
+        return
+
+    @jsaction(_('Remove selected documents'))
+    def delDocuments(self, **data):
+        delSubobjects(self, redirect='manage', tab='2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+class DocumentAddFormPage(KofaAddFormPage):
+    """Add-form to add a customer.
+    """
+    grok.context(IDocumentsContainer)
+    grok.require('waeup.manageDocuments')
+    grok.name('adddoc')
+    grok.template('documentaddform')
+    label = _('Add document')
+    pnav = 2
+
+    form_fields = grok.AutoFields(IPublicDocument)
+
+    @property
+    def selectable_doctypes(self):
+        doctypes = getUtility(IDocumentsUtils).SELECTABLE_DOCTYPES_DICT
+        return sorted(doctypes.items())
+
+    @action(_('Add document'), style='primary')
+    def createDocument(self, **data):
+        form = self.request.form
+        doctype = form.get('doctype', None)
+        # Here we can create various instances of PublicDocument derived
+        # classes depending on the doctype parameter given in form.
+        document = createObject('waeup.%s' % doctype)
+        self.applyData(document, **data)
+        try:
+            self.context.addDocument(document)
+        except KeyError:
+            self.flash(_('The id chosen already exists.'),
+                       type='danger')
+            return
+        doctype = getUtility(IDocumentsUtils).SELECTABLE_DOCTYPES_DICT[doctype]
+        self.flash(_('${a} added.', mapping = {'a': doctype}))
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.__parent__.logger.info(
+            '%s - added: %s %s' % (ob_class, doctype, document.document_id))
+        self.redirect(self.url(self.context) +
+            '/%s/manage' % document.document_id)
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+
+class DocumentDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display document data
+    """
+    grok.context(IPublicDocument)
+    grok.name('index')
+    grok.require('waeup.viewDocuments')
+    grok.template('documentpage')
+    pnav = 2
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface)
+
+    @property
+    def label(self):
+        return self.context.title
+
+
+class HTMLDocumentDisplayFormPage(DocumentDisplayFormPage):
+    """ Page to display html document data
+    """
+    grok.context(IHTMLDocument)
+    grok.template('htmldocumentpage')
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface).omit(
+            'html_dict', 'html_multilingual')
+
+    @property
+    def html(self):
+        lang = self.request.cookies.get('kofa.language')
+        html = self.context.html_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+            html = self.context.html_dict.get(portal_language,'')
+        return html
+
+
+class HTMLDocumentDisplayContentPage(KofaPage):
+    """ Page to display the html content of document
+    """
+    grok.context(IHTMLDocument)
+    grok.name('display')
+    grok.template('htmldisplaypage')
+    grok.require('waeup.Public')
+
+    @property
+    def label(self):
+        return self.context.title
+
+    def update(self):
+        if self.context.state != PUBLISHED:
+            self.flash(_('The document requested has not yet been published.'),
+                type="warning")
+            self.redirect(self.application_url())
+        super(HTMLDocumentDisplayContentPage, self).update()
+        return
+
+    @property
+    def content(self):
+        lang = self.request.cookies.get('kofa.language')
+        html = self.context.html_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+            html = self.context.html_dict.get(portal_language,'')
+        return html
+
+
+class RESTDocumentDisplayFormPage(HTMLDocumentDisplayFormPage):
+    """ Page to display html document data
+    """
+    grok.context(IRESTDocument)
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface).omit(
+            'html_dict', 'rest_multilingual')
+
+
+class RESTDocumentDisplayContentPage(HTMLDocumentDisplayContentPage):
+    """ Page to display the html content of document
+    """
+    grok.context(IRESTDocument)
+
+    label = None
+
+
+class DocumentManageFormPage(KofaEditFormPage,
+                            LocalRoleAssignmentUtilityView):
+    """ View to manage document data
+    """
+    grok.context(IPublicDocument)
+    grok.name('manage')
+    grok.require('waeup.manageDocuments')
+    grok.template('documentmanagepage')
+    pnav = 2
+
+    taboneactions = [_('Save'),_('Cancel')]
+    tabthreeactions1 = [_('Remove selected local roles')]
+    tabthreeactions2 = [_('Add local role')]
+
+    deletion_warning = _('Are you sure?')
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface)
+
+    def label(self):
+        return _('Manage document ') + self.context.document_id
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        return msave(self, **data)
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,2,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,2,**data)
+
+
+class HTMLDocumentManageFormPage(DocumentManageFormPage):
+    """ View to manage htmldocument data
+    """
+    grok.context(IHTMLDocument)
+    grok.template('htmldocumentmanagepage')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        html_multilingual = getattr(self.context, 'html_multilingual', None)
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.context.html_dict = html2dict(html_multilingual, portal_language)
+        return
+
+
+class RESTDocumentManageFormPage(DocumentManageFormPage):
+    """ View to manage restdocument data
+    """
+    grok.context(IRESTDocument)
+    grok.template('htmldocumentmanagepage')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        html_multilingual = getattr(self.context, 'rest_multilingual', None)
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.context.html_dict = rest2dict(html_multilingual, portal_language)
+        return
+
+
+class DocumentTriggerTransitionFormPage(KofaEditFormPage):
+    """ View to trigger public document transitions
+    """
+    grok.context(IPublicDocument)
+    grok.name('trigtrans')
+    grok.require('waeup.triggerTransition')
+    grok.template('trigtrans')
+    label = _('Trigger document transition')
+    pnav = 2
+
+    def update(self):
+        return super(KofaEditFormPage, self).update()
+
+    def getTransitions(self):
+        """Return a list of dicts of allowed transition ids and titles.
+
+        Each list entry provides keys ``name`` and ``title`` for
+        internal name and (human readable) title of a single
+        transition.
+        """
+        wf_info = IWorkflowInfo(self.context)
+        allowed_transitions = [t for t in wf_info.getManualTransitions()]
+        return [dict(name='', title=_('No transition'))] +[
+            dict(name=x, title=y) for x, y in allowed_transitions]
+
+    @action(_('Apply now'), style='primary')
+    def apply(self, **data):
+        form = self.request.form
+        if 'transition' in form and form['transition']:
+            transition_id = form['transition']
+            wf_info = IWorkflowInfo(self.context)
+            try:
+                wf_info.fireTransition(transition_id)
+                self.flash(_("Transition '%s' executed." % transition_id))
+            except InvalidTransitionError, error:
+                self.flash(error, type="warning")
+            self.redirect(self.url(self.context))
+        return
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/containermanagepage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/containermanagepage.pt	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/containermanagepage.pt	(revision 12437)
@@ -0,0 +1,32 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.kofa" method="POST" enctype="multipart/form-data">
+  <h3 i18n:translate="">Documents</h3>
+  <table class="kofa-data-table dataTableManage">
+    <thead>
+      <tr>
+        <th>&nbsp;
+        </th>
+        <th i18n:translate="">Id</th>
+        <th i18n:translate="">Title</th>
+        <th i18n:translate="">Type</th>
+        <th i18n:translate="">State</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="doc context/values"> <td>
+          <input type="checkbox" name="val_id"
+                 tal:attributes="value doc/document_id" /> </td> <td>
+          <a tal:attributes="href python: view.url(doc)"
+             tal:content="doc/__name__"> ID</a> </td>
+          <td tal:content="doc/title">TITLE</td>
+          <td tal:content="doc/translated_class_name">CLASSNAME</td>
+          <td tal:content="doc/translated_state">STATE</td>
+      </tr>
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions" tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/containerpage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/containerpage.pt	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/containerpage.pt	(revision 12437)
@@ -0,0 +1,24 @@
+<div i18n:domain="waeup.kofa" i18n:translate="" tal:condition="python: not len(context.keys())">
+There are no subobjects registered yet.
+</div>
+
+<table class="kofa-data-table dataTable" i18n:domain="waeup.kofa">
+  <thead>
+    <tr>
+      <th i18n:translate="">Id</th>
+      <th i18n:translate="">Title</th>
+      <th i18n:translate="">Type</th>
+      <th i18n:translate="">State</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value context/values">
+      <td> <a tal:attributes="href python: view.url(value)">
+          <span tal:content="value/document_id">ID</span></a>
+      </td>
+      <td tal:content="value/title">TITLE</td>
+      <td tal:content="value/translated_class_name">CLASSNAME</td>
+      <td tal:content="value/translated_state">STATE</td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/documentaddform.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/documentaddform.pt	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/documentaddform.pt	(revision 12437)
@@ -0,0 +1,27 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname">
+          <span i18n:translate="">Document Type</span>:
+        </td>
+        <td>
+          <select id="doctype" name="doctype" size="1"
+                  class="form-control half">
+            <option tal:repeat="item view/selectable_doctypes"
+                    tal:attributes="value python:item[0]">
+              <span tal:replace="python:item[1]">DOCTYPE</span>
+            </option>
+          </select>
+        </td>
+      </tr>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/documentmanagepage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/documentmanagepage.pt	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/documentmanagepage.pt	(revision 12437)
@@ -0,0 +1,101 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">Metadata</a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab" i18n:translate="">Files</a>
+    </li>
+    <li>
+      <a href="#tab3" data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="tab-pane active">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:files content="structure provider:files" />
+        </tbody>
+      </table>
+    </div>
+    <div id="tab3" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+            <input type="checkbox" name="role_id"
+                   tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <div class="form-inline">
+        <br />
+        <div class="form-group">
+          <select id="user" name="user" class="form-control">
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+            <span tal:replace="user/val/title">USERTITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                  <input tal:condition="python:action.label in view.tabthreeactions2"
+                         tal:replace="structure action/render"/>
+                </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/documentpage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/documentpage.pt	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/documentpage.pt	(revision 12437)
@@ -0,0 +1,31 @@
+<div class="wfstatus-sub"  i18n:domain="waeup.kofa">
+  Document State: <span tal:replace="context/translated_state">
+    STATE</span>
+</div>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<table class="form-table" i18n:domain="waeup.kofa"
+  tal:define="files provider:files">
+  <tbody>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Document Type:
+      </td>
+      <td>
+        <span tal:content="context/translated_class_name">CLASSNAME</span>
+      </td>
+    </tr>
+    <tal:widgets content="structure provider:widgets" />
+    <tr tal:condition="python: files.replace('\n','')">
+      <td class="separator" colspan=2 i18n:translate="">
+        Connected Files
+      </td>
+    </tr>
+    <tal:files content="structure files" />
+  </tbody>
+</table>
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt	(revision 12437)
@@ -0,0 +1,90 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">Metadata</a>
+    </li>
+    <li>
+      <a href="#tab3" data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="tab-pane active">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab3" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+            <input type="checkbox" name="role_id"
+                   tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <div class="form-inline">
+        <br />
+        <div class="form-group">
+          <select id="user" name="user" class="form-control">
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+            <span tal:replace="user/val/title">USERTITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                  <input tal:condition="python:action.label in view.tabthreeactions2"
+                         tal:replace="structure action/render"/>
+                </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/htmldocumentpage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/htmldocumentpage.pt	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/htmldocumentpage.pt	(revision 12437)
@@ -0,0 +1,43 @@
+<div class="wfstatus-sub"  i18n:domain="waeup.kofa">
+  Document State: <span tal:replace="context/translated_state">
+    STATE</span>
+</div>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<table class="form-table" i18n:domain="waeup.kofa"
+  tal:define="files provider:files">
+  <tbody>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Document Type:
+      </td>
+      <td>
+        <span tal:content="context/translated_class_name">CLASSNAME</span>
+      </td>
+    </tr>
+    <tal:widgets content="structure provider:widgets" />
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Content:
+      </td>
+      <td>
+        <span tal:content="structure view/html">HTML</span>
+      </td>
+    </tr>
+    <tr tal:condition="python: getattr(context, 'state', None) == 'published'">
+      <td i18n:translate="" class="fieldname">
+        HTML Element:
+      </td>
+      <td>
+        &lt;a href=&quot;
+        <span tal:replace="python: view.url(context, 'display')"></span>
+        &quot;&gt;<span tal:replace="context/title">FILENAME</span>&lt;/a&gt;
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/trigtrans.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/trigtrans.pt	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/browser_templates/trigtrans.pt	(revision 12437)
@@ -0,0 +1,19 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+    <br />
+
+    <select id="transition" name="transition" class="form-control half">
+      <option tal:repeat="transition view/getTransitions"
+              tal:attributes="value transition/name">
+        <span tal:replace="transition/title">TRANSITIONTITLE</span>
+      </option>
+    </select>
+    <br /><br />
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/catalog.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/catalog.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/catalog.py	(revision 12437)
@@ -0,0 +1,33 @@
+## $Id: catalog.py 12155 2014-12-05 18:30:41Z henrik $
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components to help cataloging and searching documents.
+"""
+import grok
+from waeup.kofa.interfaces import IUniversity
+from waeup.kofa.documents.interfaces import IDocument
+
+class DocumentsCatalog(grok.Indexes):
+    """A catalog for all documents.
+    """
+    grok.site(IUniversity)
+    grok.name('documents_catalog')
+    grok.context(IDocument)
+
+    document_id = grok.index.Field(attribute='document_id')
+    user_id = grok.index.Field(attribute='user_id')
+    state = grok.index.Field(attribute='state')
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/container.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/container.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/container.py	(revision 12437)
@@ -0,0 +1,60 @@
+## $Id: container.py 12213 2014-12-13 10:51:17Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Containers which contain document objects.
+"""
+import grok
+from grok import index
+from waeup.kofa.interfaces import IKofaPluggable
+from waeup.kofa.documents.interfaces import IDocumentsContainer, IDocument
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.utils.logger import Logger
+
+class DocumentsContainer(grok.Container, Logger):
+    """This is a container for all kind of documents.
+    """
+    grok.implements(IDocumentsContainer)
+    grok.provides(IDocumentsContainer)
+
+
+    def addDocument(self, document):
+        if not IDocument.providedBy(document):
+            raise TypeError(
+                'DocumentsContainers contain only IDocument instances')
+        self[document.document_id] = document
+        return
+
+DocumentsContainer = attrs_to_fields(DocumentsContainer)
+
+class DocumentsPlugin(grok.GlobalUtility):
+    """A plugin that creates container for documents inside a company.
+    """
+    grok.implements(IKofaPluggable)
+    grok.name('documents')
+
+    def setup(self, site, name, logger):
+        if 'documents' in site.keys():
+            logger.warn('Could not create container for documents.')
+            return
+        site['documents'] = DocumentsContainer()
+        logger.info('Container for documents created')
+        return
+
+    def update(self, site, name, logger):
+        if not 'documents' in site.keys():
+            self.setup(site, name, logger)
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/document.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/document.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/document.py	(revision 12437)
@@ -0,0 +1,317 @@
+## $Id: document.py 12408 2015-01-06 09:15:21Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+These are the public documents.
+"""
+import os
+import grok
+from grok import index
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.event import notify
+from zope.component import getUtility
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.i18n import translate
+
+from waeup.kofa.image import KofaImageFile
+from waeup.kofa.imagestorage import DefaultFileStoreHandler
+from waeup.kofa.interfaces import (
+    IKofaUtils, IObjectHistory,
+    IFileStoreNameChooser, IFileStoreHandler, IExtFileStore)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.helpers import attrs_to_fields, get_current_principal
+from waeup.kofa.documents.interfaces import (
+    IDocument, IPublicDocument, IDocumentsUtils,
+    IPDFDocument, IHTMLDocument, IRESTDocument)
+
+class Document(grok.Container):
+    """This is a document.
+    """
+    grok.implements(IDocument)
+    grok.provides(IDocument)
+    grok.baseclass()
+
+    form_fields_interface = None
+
+    local_roles = [
+        'waeup.local.DocumentManager',
+        ]
+
+    user_id = None
+    state = None
+    translated_state = None
+    translated_class_name = None
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    @property
+    def class_name(self):
+        return self.__class__.__name__
+
+    @property
+    def formatted_transition_date(self):
+        try:
+            return self.history.messages[-1].split(' - ')[0]
+        except IndexError:
+            return
+
+    @property
+    def connected_files(self):
+        return
+
+    @property
+    def is_verifiable(self):
+        return True, None
+
+    def setMD5(self):
+        """Determine md5 checksum of all files and store checksums as
+        document attributes.
+        """
+        return
+        
+class PublicDocumentBase(Document):
+    """This is a customer document baseclass.
+    """
+    grok.implements(IPublicDocument)
+    grok.provides(IPublicDocument)
+    grok.baseclass()
+
+    @property
+    def state(self):
+        state = IWorkflowState(self).getState()
+        return state
+
+    @property
+    def translated_state(self):
+        try:
+            TRANSLATED_STATES = getUtility(
+                IDocumentsUtils).TRANSLATED_DOCUMENT_STATES
+            return TRANSLATED_STATES[self.state]
+        except KeyError:
+            return
+
+    @property
+    def translated_class_name(self):
+        try:
+            DOCTYPES_DICT = getUtility(IDocumentsUtils).DOCTYPES_DICT
+            return DOCTYPES_DICT[self.class_name]
+        except KeyError:
+            return
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        self.__parent__.__parent__.logger.info(
+            '%s - %s - %s' % (ob_class, self.__name__, message))
+        return
+
+
+class PDFDocument(PublicDocumentBase):
+    """This is a  document for a single pdf upload file.
+    """
+    grok.implements(IPDFDocument)
+    grok.provides(IPDFDocument)
+
+    form_fields_interface = IPDFDocument
+
+PDFDocument = attrs_to_fields(PDFDocument)
+
+
+class HTMLDocument(PublicDocumentBase):
+    """This is a  document to render html-coded text.
+    """
+    grok.implements(IHTMLDocument)
+    grok.provides(IHTMLDocument)
+
+    form_fields_interface = IHTMLDocument
+
+    def __init__(self, *args, **kw):
+        super(HTMLDocument, self).__init__(*args, **kw)
+        self.html_dict = {}
+
+HTMLDocument = attrs_to_fields(HTMLDocument)
+
+
+class RESTDocument(PublicDocumentBase):
+    """This is a  document to render html-coded text.
+    """
+    grok.implements(IRESTDocument)
+    grok.provides(IRESTDocument)
+
+    form_fields_interface = IRESTDocument
+
+    def __init__(self, *args, **kw):
+        super(RESTDocument, self).__init__(*args, **kw)
+        self.html_dict = {}
+
+RESTDocument = attrs_to_fields(RESTDocument)
+
+
+class PDFDocumentFactory(grok.GlobalUtility):
+    """A factory for documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.PDFDocument')
+    title = u"Create a new PDF document.",
+    description = u"This factory instantiates new PDF documents."
+
+    def __call__(self, *args, **kw):
+        return PDFDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(PDFDocument)
+
+
+class HTMLDocumentFactory(grok.GlobalUtility):
+    """A factory for HTML documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.HTMLDocument')
+    title = u"Create a new HTML document.",
+    description = u"This factory instantiates new HTML documents."
+
+    def __call__(self, *args, **kw):
+        return HTMLDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(HTMLDocument)
+
+
+class RESTDocumentFactory(grok.GlobalUtility):
+    """A factory for REST documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.RESTDocument')
+    title = u"Create a new REST document.",
+    description = u"This factory instantiates new REST documents."
+
+    def __call__(self, *args, **kw):
+        return RESTDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(RESTDocument)
+
+
+#: The file id marker for files
+DOCUMENT_FILE_STORE_NAME = 'file-document'
+
+
+class DocumentFileNameChooser(grok.Adapter):
+    """A file id chooser for :class:`Document` objects.
+
+    `context` is an :class:`Document` instance.
+
+    The delivered file_id contains the file id marker for
+    :class:`Document` objects in the central :class:`DocumentsContainer`.
+
+    This chooser is registered as an adapter providing
+    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
+
+    File store name choosers like this one are only convenience
+    components to ease the task of creating file ids for customer document
+    objects. You are nevertheless encouraged to use them instead of
+    manually setting up filenames for customer documents.
+
+    .. seealso:: :mod:`waeup.kofa.imagestorage`
+
+    """
+
+    grok.context(IDocument)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name=None, attr=None):
+        """Check whether the given name is a valid file id for the context.
+
+        Returns ``True`` only if `name` equals the result of
+        :meth:`chooseName`.
+
+        """
+        return name == self.chooseName()
+
+    def chooseName(self, attr, name=None):
+        """Get a valid file id for customer document context.
+
+        *Example:*
+
+        For document with id 'd123'
+        with attr ``'nice_image.jpeg'`` this chooser would create:
+
+          ``'__file-document__nice_image_d123.jpeg'``
+
+        meaning that the nice image of this document would be
+        stored in the site-wide file storage in path:
+
+          ``nice_image_d123.jpeg``
+
+        """
+        basename, ext = os.path.splitext(attr)
+        doc_id = self.context.document_id
+        marked_filename = '__%s__%s_%s%s' % (
+            DOCUMENT_FILE_STORE_NAME,
+            basename, doc_id, ext)
+        return marked_filename
+
+
+class DocumentFileStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
+    """ Document specific file handling.
+
+    This handler knows in which path in a filestore to store document
+    files and how to turn this kind of data into some (browsable)
+    file object.
+
+    It is called from the global file storage, when it wants to
+    get/store a file with a file id starting with
+    ``__file-document__`` (the marker string for customer files).
+
+    Like each other file store handler it does not handle the files
+    really (this is done by the global file store) but only computes
+    paths and things like this.
+    """
+    grok.implements(IFileStoreHandler)
+    grok.name(DOCUMENT_FILE_STORE_NAME)
+
+    def pathFromFileID(self, store, root, file_id):
+        """All document files are put in directory ``documents``.
+        """
+        marker, filename, basename, ext = store.extractMarker(file_id)
+        sub_root = os.path.join(root, 'documents')
+        return super(DocumentFileStoreHandler, self).pathFromFileID(
+            store, sub_root, basename)
+
+    def createFile(self, store, root, filename, file_id, file):
+        """Create a browsable file-like object.
+        """
+        # call super method to ensure that any old files with
+        # different filename extension are deleted.
+        file, path, file_obj = super(
+            DocumentFileStoreHandler, self).createFile(
+            store, root,  filename, file_id, file)
+        return file, path, KofaImageFile(
+            file_obj.filename, file_obj.data)
+
+
+@grok.subscribe(IDocument, grok.IObjectAddedEvent)
+def handle_document_added(document, event):
+    """If a document is added the transition create is fired.
+    The latter produces a logging message.
+    """
+    if IWorkflowState(document).getState() is None:
+        IWorkflowInfo(document).fireTransition('create')
+    return
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/export.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/export.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/export.py	(revision 12437)
@@ -0,0 +1,110 @@
+## $Id: export.py 12410 2015-01-07 08:49:38Z henrik $
+##
+## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Exporters for documents.
+"""
+import grok
+from zope.securitypolicy.interfaces import IPrincipalRoleMap
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.batching import ExporterBase
+from waeup.kofa.utils.helpers import iface_names
+from waeup.kofa.documents.interfaces import (
+    IPDFDocument, IHTMLDocument, IRESTDocument)
+
+
+class DocumentExporterBase(grok.GlobalUtility, ExporterBase):
+    """Exporter for documents.
+    """
+    grok.implements(ICSVExporter)
+    grok.baseclass()
+    class_name = None
+    iface = None
+    title = None
+
+    @property
+    def fields(self):
+        return tuple(sorted(iface_names(self.iface, exclude_attribs=False,
+            omit=['is_verifiable',
+                  'translated_state',
+                  'user_id',
+                  'formatted_transition_date',
+                  'translated_class_name',
+                  'connected_files',   # Could be used to export file URLs
+                  ]
+            ))) + (
+            'users_with_local_roles',)
+
+    def mangle_value(self, value, name, context=None):
+        """Hook for mangling values in derived classes
+        """
+        if name == 'users_with_local_roles':
+            value = []
+            role_map = IPrincipalRoleMap(context)
+            for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
+                value.append({'user_name':user_name,'local_role':local_role})
+        if name == 'history':
+            value = value.messages
+        return super(DocumentExporterBase, self).mangle_value(
+            value, name, context)
+
+    def export(self, documents, filepath=None):
+        """Export `documents`, an iterable, as CSV file.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for document in documents:
+            self.write_item(document, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export documents in documentscontainer into filepath as CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        documents = site.get('documents', {}).values()
+        documents = [doc for doc in documents
+                     if doc.class_name == self.class_name]
+        return self.export(documents, filepath)
+
+
+class PDFDocumentExporter(DocumentExporterBase):
+    """Exporter for documents.
+    """
+    grok.name('pdfdocuments')
+    iface = IPDFDocument
+    class_name = 'PDFDocument'
+    title = _(u'Public PDF Documents')
+
+
+class HTMLDocumentExporter(DocumentExporterBase):
+    """Exporter for documents.
+    """
+    grok.name('htmldocuments')
+    iface = IHTMLDocument
+    class_name = 'HTMLDocument'
+    title = _(u'Public HTML Documents')
+
+
+class RESTDocumentExporter(DocumentExporterBase):
+    """Exporter for documents.
+    """
+    grok.name('restdocuments')
+    iface = IRESTDocument
+    class_name = 'RESTDocument'
+    title = _(u'Public REST Documents')
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/fileviewlets.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/fileviewlets.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/fileviewlets.py	(revision 12437)
@@ -0,0 +1,73 @@
+## $Id: fileviewlets.py 12243 2014-12-15 11:18:31Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+
+import grok
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser.layout import UtilityView
+from waeup.kofa.students.fileviewlets import (
+    FileDisplay, FileUpload, Image)
+
+from waeup.kofa.documents.document import PDFDocument
+from waeup.kofa.documents.browser import (
+    DocumentManageFormPage, DocumentDisplayFormPage)
+
+from waeup.kofa.documents.workflow import PUBLISHED
+
+
+# File viewlets for documents
+
+class PDFScanManageUpload(FileUpload):
+    """Scan upload viewlet for officers.
+    """
+    grok.view(DocumentManageFormPage)
+    grok.context(PDFDocument)
+    grok.require('waeup.manageDocuments')
+    label = _(u'PDF File')
+    title = _(u'PDF File')
+    mus = 1024 * 200
+    download_name = u'sample.pdf'
+    tab_redirect = '#tab2'
+
+
+class PDFScanDisplay(FileDisplay):
+    """Scan display viewlet.
+    """
+    grok.order(1)
+    grok.context(PDFDocument)
+    grok.require('waeup.viewDocuments')
+    grok.view(DocumentDisplayFormPage)
+    label = _(u'PDF Scan')
+    title = _(u'PDF Scan')
+    download_name = u'sample.pdf'
+
+
+class PDFScanImage(UtilityView, Image):
+    """Scan document.
+    """
+    ##: Needs to be protected if not published
+    grok.name('sample.pdf')
+    grok.context(PDFDocument)
+    grok.require('waeup.Public')
+    download_name = u'sample.pdf'
+
+    def update(self):
+        if self.context.state != PUBLISHED:
+            self.flash(_('The document requested has not yet been published.'),
+                type="warning")
+            self.redirect(self.application_url())
+        return
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/interfaces.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/interfaces.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/interfaces.py	(revision 12437)
@@ -0,0 +1,113 @@
+## $Id: interfaces.py 12408 2015-01-06 09:15:21Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+from zope.interface import Attribute, Interface
+from zope import schema
+from waeup.kofa.interfaces import (
+    IKofaObject, validate_id,
+    ContextualDictSourceFactoryBase)
+from waeup.kofa.interfaces import MessageFactory as _
+
+
+class IDocumentsContainer(IKofaObject):
+    """A container for all kind of document objects.
+
+    """
+
+    def addDocument(document):
+        """Add a document.
+        """
+
+
+class IDocument(IKofaObject):
+    """A base representation of documents.
+
+    """
+    history = Attribute('Object history, a list of messages')
+    state = Attribute('Returns the verification state of a document')
+    translated_state = Attribute(
+        'Returns a translated, more verbose verification state of a document')
+    class_name = Attribute('Name of the document class')
+    translated_class_name = Attribute('Translatable class name')
+    formatted_transition_date = Attribute('Last transition formatted date string')
+    user_id = Attribute('Id of a user')
+    connected_files = Attribute('Names of files connected to a document')
+    is_verifiable = Attribute('Contract verifiable by officer')
+
+    document_id = schema.TextLine(
+        title = _(u'Document Id'),
+        required = True,
+        constraint=validate_id,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Document Title'),
+        required = True,
+        )
+
+    def setMD5():
+        """Determine md5 checksum of selected files and store checksums as
+        document attributes.
+        """
+
+class IPublicDocument(IDocument):
+    """A base representation of public documents.
+
+    """
+
+    def writeLogMessage(view, message):
+        """Write a view specific log message into main.log.
+
+        """
+
+class IPDFDocument(IPublicDocument):
+    """A base representation of PDF documents.
+
+    """
+
+
+class IHTMLDocument(IPublicDocument):
+    """A base representation of HTML documents.
+
+    """
+
+    html_dict = Attribute('Content as language dictionary with values in HTML format')
+
+    html_multilingual = schema.Text(
+        title = _(u'Multilingual content in HTML format'),
+        required = False,
+        )
+
+class IRESTDocument(IPublicDocument):
+    """A base representation of REST documents.
+
+    """
+
+    html_dict = Attribute(
+        'Content as language dictionary with values in HTML format')
+
+    rest_multilingual = schema.Text(
+        title = _(u'Multilingual content in REST (reStructuredText) format'),
+        required = False,
+        )
+
+
+class IDocumentsUtils(Interface):
+    """A collection of methods which are subject to customization.
+
+    """
+
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/permissions.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/permissions.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/permissions.py	(revision 12437)
@@ -0,0 +1,51 @@
+## $Id: permissions.py 12206 2014-12-13 07:28:59Z henrik $
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+
+# Permissions
+
+class ViewDocuments(grok.Permission):
+    grok.name('waeup.viewDocuments')
+
+
+class ManageDocuments(grok.Permission):
+    grok.name('waeup.manageDocuments')
+
+
+# Local Roles
+
+class DocumentManager(grok.Role):
+    grok.name('waeup.local.DocumentManager')
+    grok.title(u'Document Manager')
+    grok.permissions('waeup.manageDocuments',
+                     'waeup.viewDocuments',
+                     'waeup.exportData')
+
+
+# Site Roles
+class DocumentsOfficer(grok.Role):
+    grok.name('waeup.DocumentsOfficer')
+    grok.title(u'Documents Officer (view only)')
+    grok.permissions('waeup.viewDocuments')
+
+
+class DocumentsManager(grok.Role):
+    grok.name('waeup.DocumentsManager')
+    grok.title(u'Documents Manager')
+    grok.permissions('waeup.viewDocuments',
+                     'waeup.manageDocuments')
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/__init__.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/__init__.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/__init__.py	(revision 12437)
@@ -0,0 +1,3 @@
+"""This package contains everything regarding documents.
+"""
+# Make this a package.
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/sample_document_data.csv
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/sample_document_data.csv	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/sample_document_data.csv	(revision 12437)
@@ -0,0 +1,6 @@
+document_id,title,class_name
+ABC,ABC title,PDFDocument
+CDE,CDE title,PDFDocument
+EFG,EFG title,PDFDocument
+ABC,Duplicate document,PDFDocument
+HIJ,HIJ title,HTMLDocument
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/sample_document_data_update.csv
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/sample_document_data_update.csv	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/sample_document_data_update.csv	(revision 12437)
@@ -0,0 +1,3 @@
+document_id,title
+ABC,ABC new title
+XYZ,Non-existing document
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_batching.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_batching.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_batching.py	(revision 12437)
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+## $Id: test_batching.py 12288 2014-12-21 17:01:15Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Unit tests for document data processors.
+"""
+import os
+import shutil
+import tempfile
+import unittest
+import datetime
+import grok
+from time import time
+from zope.event import notify
+from zope.component import createObject, queryUtility
+from zope.component.hooks import setSite, clearSite
+from zope.catalog.interfaces import ICatalog
+from zope.interface.verify import verifyClass, verifyObject
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IBatchProcessor, FatalCSVError, IUserAccount
+from waeup.kofa.documents.batching import PDFDocumentProcessor
+from waeup.kofa.documents.document import PDFDocument, HTMLDocument
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+DOCUMENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_document_data.csv'),
+    'rb').read()
+
+DOCUMENT_HEADER_FIELDS = DOCUMENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+DOCUMENT_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_document_data_update.csv'),
+    'rb').read()
+
+DOCUMENT_HEADER_FIELDS_UPDATE = DOCUMENT_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+class DocumentImportExportSetup(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(DocumentImportExportSetup, self).setUp()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
+        app = University()
+        app['datacenter'].setStoragePath(self.dc_root)
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(app)
+
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        return
+
+    def tearDown(self):
+        super(DocumentImportExportSetup, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def setup_for_export(self):
+        document1 = PDFDocument()
+        document1.document_id = u'DOC1'
+        self.app['documents'].addDocument(document1)
+        self.pdfdocument = document1
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        role_manager = IPrincipalRoleManager(document1)
+        role_manager.assignRoleToPrincipal(u'johnsrole', u'john')
+
+        document2 = HTMLDocument()
+        document2.document_id = u'DOC2'
+        self.app['documents'].addDocument(document2)
+        self.htmldocument = document2
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        role_manager = IPrincipalRoleManager(document2)
+        role_manager.assignRoleToPrincipal(u'johnsrole', u'john')
+        return
+
+
+class PDFDocumentProcessorTest(DocumentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PDFDocumentProcessorTest, self).setUp()
+
+        # Add document
+        document = PDFDocument()
+        document.document_id = u'DOC1'
+        self.app['documents'].addDocument(document)
+        document.title = u'Our PDF Document'
+        notify(grok.ObjectModifiedEvent(document))
+        self.document = self.app['documents'][document.document_id]
+
+        self.processor = PDFDocumentProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_document_data.csv')
+        self.csv_file_update = os.path.join(
+            self.workdir, 'sample_document_data_update.csv')
+        open(self.csv_file, 'wb').write(DOCUMENT_SAMPLE_DATA)
+        open(self.csv_file_update, 'wb').write(DOCUMENT_SAMPLE_DATA_UPDATE)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, PDFDocumentProcessor) is True
+
+    def test_parentsExist(self):
+        self.assertFalse(self.processor.parentsExist(None, dict()))
+        self.assertTrue(self.processor.parentsExist(None, self.app))
+
+    def test_entryExists(self):
+        assert self.processor.entryExists(
+            dict(document_id='ID_NONE'), self.app) is False
+        assert self.processor.entryExists(
+            dict(document_id=self.document.document_id), self.app) is True
+
+    def test_getParent(self):
+        parent = self.processor.getParent(None, self.app)
+        assert parent is self.app['documents']
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(document_id='ID_NONE'), self.app) is None
+        assert self.processor.getEntry(
+            dict(document_id=self.document.document_id), self.app) is self.document
+
+    def test_addEntry(self):
+        new_document = PDFDocument()
+        new_document.document_id = u'DOC2'
+        self.processor.addEntry(
+            new_document, {}, self.app)
+        assert len(self.app['documents'].keys()) == 2
+        self.assertEqual(self.app['documents']['DOC2'].document_id, 'DOC2')
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='DOC4', class_name='PDFDocument'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='id with spaces', class_name='PDFDocument'))
+        self.assertEqual(len(errs),1)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='DOC4', class_name='WrongDocument'), mode='create')
+        self.assertEqual(len(errs),1)
+
+    def test_delEntry(self):
+        assert self.document.document_id in self.app['documents'].keys()
+        self.processor.delEntry(
+            dict(document_id=self.document.document_id), self.app)
+        assert self.document.document_id not in self.app['documents'].keys()
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS)
+        self.assertEqual(num_warns,2)
+        self.assertEqual(len(self.app['documents']), 4)
+        self.assertEqual(self.app['documents']['ABC'].title,'ABC title')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Public PDF Document Processor - sample_document_data - '
+            'EFG - updated: document_id=EFG, title=EFG title\n'
+            in logcontent)
+        failcontent = open(fail_file).read()
+        self.assertTrue(
+            'PDFDocument,ABC,Duplicate document,This object already exists.'
+            in failcontent)
+        self.assertTrue(
+            'HTMLDocument,HIJ,HIJ title,class_name: wrong processor'
+            in failcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update, DOCUMENT_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,1)
+        # title has changed
+        self.assertEqual(self.app['documents']['ABC'].title,'ABC new title')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Public PDF Document Processor - sample_document_data_update - '
+            'ABC - updated: document_id=ABC, title=ABC new title\n'
+            in logcontent)
+        failcontent = open(fail_file).read()
+        self.assertTrue(
+            'XYZ,Non-existing document,Cannot update: no such entry'
+            in failcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_browser.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_browser.py	(revision 12437)
@@ -0,0 +1,347 @@
+## $Id: test_browser.py 12413 2015-01-07 09:20:45Z henrik $
+## 
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the customer-related UI components.
+"""
+import shutil
+import tempfile
+from datetime import datetime, timedelta, date
+from StringIO import StringIO
+import os
+from zope.event import notify
+from zope.component import createObject, queryUtility, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.schema.interfaces import ConstraintNotSatisfied
+from zope.catalog.interfaces import ICatalog
+from zope.security.interfaces import Unauthorized
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.testbrowser.testing import Browser
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import (
+    IUserAccount, IJobManager,
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler)
+from waeup.kofa.imagestorage import (
+    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
+    DefaultStorage)
+from waeup.kofa.authentication import LocalRoleSetEvent
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+from waeup.kofa.documents.workflow import PUBLISHED
+
+PH_LEN = 15911  # Length of placeholder file
+
+SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+#SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
+SAMPLE_PDF = os.path.join(os.path.dirname(__file__), 'test_pdf.pdf')
+
+class FullSetup(FunctionalTestCase):
+    # A test case that only contains a setup and teardown
+    #
+    # Complete setup for customers handlings is rather complex and
+    # requires lots of things created before we can start. This is a
+    # setup that does all this, creates a company etc.
+    # so that we do not have to bother with that in different
+    # test cases.
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FullSetup, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        self.login_path = 'http://localhost/app/login'
+        self.container_path = 'http://localhost/app/documents'
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(FullSetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+
+class DocumentUITests(FullSetup):
+    # Tests for document related views and pages
+
+    def test_manage_file_document(self):
+        # Managers can access the pages of documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Documents").click()
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['PDFDocument']
+        self.browser.getControl(name="form.title").value = 'My PDF Document'
+        self.browser.getControl(name="form.document_id").value = 'DOC1'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('PDF Document added.' in self.browser.contents)
+        document = self.app['documents']['DOC1']
+
+        # Document can be edited
+        self.browser.getControl(name="form.title").value = 'My first doc'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.container_path + '/DOC1/index')
+
+        # File can be uploaded
+        self.browser.getLink("Manage").click()
+        # Create a pseudo image file and select it to be uploaded
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.jpg')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'pdf file format expected' in self.browser.contents)
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.pdf')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'Could not determine file type' in self.browser.contents)
+        pdf = open(SAMPLE_PDF, 'rb')
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pdf, filename='my_sample_scan.pdf')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'href="http://localhost/app/documents/DOC1/sample.pdf">PDF File</a>'
+            in self.browser.contents)
+        # Browsing the link shows a real pdf only if the document
+        # has been published
+        self.browser.getLink("PDF File").click()
+        self.assertTrue(
+            'The document requested has not yet been published'
+            in self.browser.contents)
+        IWorkflowState(document).setState(PUBLISHED)
+        self.browser.open(self.container_path + '/DOC1/sample.pdf')
+        self.assertEqual(
+            self.browser.headers['content-type'], 'application/pdf')
+
+        # Transitions can be performed
+        self.assertEqual(document.state, 'published')
+        self.browser.open(self.container_path + '/DOC1')
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['retract']
+        self.browser.getControl("Apply now").click()
+        self.assertEqual(document.state, 'created')
+
+        # Documents can be removed
+        self.browser.getLink("Documents").click()
+        self.browser.getLink("Manage").click()
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=document.document_id).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+        # All actions are being logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document created' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentAddFormPage - added: PDF Document %s'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentManageFormPage - %s - saved: title'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentManageFormPage - %s - uploaded: sample.pdf (my_sample_scan.pdf)'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document retracted' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentsContainerManageFormPage - removed: %s'
+            % document.document_id in logcontent)
+
+    def test_manage_html_document(self):
+        # Managers can access the pages of documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Documents").click()
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['HTMLDocument']
+        self.browser.getControl(name="form.document_id").value = 'DOC2'
+        self.browser.getControl(name="form.title").value = 'My HTML Document'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('HTML Document added.' in self.browser.contents)
+        document = self.app['documents']['DOC2']
+
+        # Document can be edited
+        self.browser.getControl(name="form.title").value = 'My second doc'
+        self.browser.getControl(name="form.html_multilingual").value = """
+<h1>Hello World</h1>
+>>de<<
+<h1>Hallo Welt</h1>
+"""
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.container_path + '/DOC2/index')
+        self.assertTrue(
+            '<h1>Hello World</h1>' in self.browser.contents)
+        self.assertFalse(
+            '<h1>Hallo Welt</h1>' in self.browser.contents)
+        self.browser.getLink("de", index=2).click()
+        self.assertFalse(
+            '<h1>Hello World</h1>' in self.browser.contents)
+        self.assertTrue(
+            '<h1>Hallo Welt</h1>' in self.browser.contents)
+        # The content can't be rendered yet
+        self.browser.open(self.container_path + '/DOC2/display')
+        self.assertTrue(
+            'The document requested has not yet been published'
+            in self.browser.contents)
+        # We have been redirected to the portal root
+        self.assertEqual(self.browser.url, 'http://localhost/app')
+
+        # Transitions can be performed
+        self.browser.open(self.container_path + '/DOC2')
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['publish']
+        self.browser.getControl("Apply now").click() # not yet translated
+        self.assertEqual(document.state, 'published')
+
+        # The content can be rendered
+        self.browser.open(self.container_path + '/DOC2/display')
+        self.assertTrue(
+            '<h1>Hallo Welt</h1>' in self.browser.contents)
+
+        # Documents can be removed
+        self.browser.getLink("en", index=3).click()
+        self.browser.getLink("Documents").click()
+        self.browser.getLink("Manage").click()
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=document.document_id).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+        # All actions are being logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document created' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentAddFormPage - added: HTML Document %s'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.HTMLDocumentManageFormPage - %s - saved: title + html_multilingual'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document published' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentsContainerManageFormPage - removed: %s'
+            % document.document_id in logcontent)
+
+    def test_manage_rest_document(self):
+        # Managers can access the pages of documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Documents").click()
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['RESTDocument']
+        self.browser.getControl(name="form.document_id").value = 'DOC3'
+        self.browser.getControl(name="form.title").value = 'My REST Document'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('REST Document added.' in self.browser.contents)
+        document = self.app['documents']['DOC3']
+
+        # Document can be edited
+        self.browser.getControl(name="form.rest_multilingual").value = """
+----------
+Main Title
+----------
+
+Subtitle
+========
+>>de<<
+----------
+Haupttitel
+----------
+
+Untertitel
+==========
+"""
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.container_path + '/DOC3/index')
+        self.assertTrue(
+            '<h1 class="title">Main Title</h1>' in self.browser.contents)
+        self.assertTrue(
+            '<h2 class="subtitle" id="subtitle">Subtitle</h2>'
+            in self.browser.contents)
+        self.assertFalse(
+            '<h1 class="title">Haupttitel</h1>' in self.browser.contents)
+        self.browser.getLink("de", index=2).click()
+        self.assertFalse(
+            '<h1 class="title">Main Title</h1>' in self.browser.contents)
+        self.assertTrue(
+            '<h1 class="title">Haupttitel</h1>' in self.browser.contents)
+        # The content can be rendered
+        IWorkflowState(document).setState(PUBLISHED)
+        self.browser.open(self.container_path + '/DOC3/display')
+        self.assertTrue(
+            '<h1 class="title">Haupttitel</h1>' in self.browser.contents)
+        # The page label (object title) is not displayed
+        self.assertFalse(
+            '<h1 class="kofa-content-label">My REST Document</h1>'
+            in self.browser.contents)
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_document.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_document.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_document.py	(revision 12437)
@@ -0,0 +1,168 @@
+## $Id: test_document.py 12408 2015-01-06 09:15:21Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for documents.
+"""
+import shutil
+import tempfile
+from StringIO import StringIO
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import createObject, queryUtility
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.kofa.interfaces import (
+    IObjectHistory, IFileStoreNameChooser, IFileStoreHandler)
+from waeup.kofa.imagestorage import DefaultStorage
+from waeup.kofa.documents.interfaces import (
+    IDocumentsContainer, IPublicDocument, IPDFDocument,
+    IHTMLDocument, IRESTDocument)
+from waeup.kofa.documents.container import DocumentsContainer
+from waeup.kofa.documents.document import (
+    PDFDocument, HTMLDocument, RESTDocument,
+    DocumentFileNameChooser, DocumentFileStoreHandler)
+from waeup.kofa.testing import (FunctionalLayer, FunctionalTestCase)
+
+class DocumentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IDocumentsContainer, DocumentsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IDocumentsContainer, DocumentsContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IPDFDocument, PDFDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                IPDFDocument, PDFDocument())
+            )
+        self.assertTrue(
+            verifyClass(
+                IHTMLDocument, HTMLDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                IHTMLDocument, HTMLDocument())
+            )
+        self.assertTrue(
+            verifyClass(
+                IRESTDocument, RESTDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                IRESTDocument, RESTDocument())
+            )
+        return
+
+    def test_addDocument(self):
+        container = DocumentsContainer()
+        document = createObject(u'waeup.HTMLDocument')
+        document.document_id = u'DOC'
+        container.addDocument(document)
+        self.assertEqual(container[document.document_id], document)
+        self.assertRaises(TypeError, container.addDocument, object())
+        self.assertEqual(document.document_id, 'DOC')
+        return
+
+    def test_document_workflow(self):
+        document = createObject(u'waeup.HTMLDocument')
+        IWorkflowInfo(document).fireTransition('create')
+        self.assertEqual(IWorkflowState(document).getState(), 'created')
+        return
+
+    def test_document_history(self):
+        doc = createObject(u'waeup.HTMLDocument')
+        IWorkflowInfo(doc).fireTransition('create')
+        messages = ' '.join(doc.history.messages)
+        self.assertTrue('Document created by system' in messages)
+        return
+
+
+class DocumentFileTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(DocumentFileTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(DocumentFileTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_file_store_handler_util_accessible(self):
+        # we can get an IFileStoreHandler utility for documents
+        handler = queryUtility(IFileStoreHandler, name='file-document')
+        self.assertTrue(
+            isinstance(handler, DocumentFileStoreHandler))
+        return
+
+    def test_file_store_handler(self):
+        store = DefaultStorage()
+        handler = queryUtility(IFileStoreHandler, name='file-document')
+        result = handler.pathFromFileID(
+            store, '/fake-root', '__any_marker__sample.jpg')
+        self.assertEqual(
+            result, '/fake-root/documents/sample')
+        return
+
+    def test_file_store_handler_create(self):
+        # we can create files in image store with the document file
+        # store handler
+        store = DefaultStorage(self.workdir)
+        handler = queryUtility(IFileStoreHandler, name='file-document')
+        file_obj, path, waeup_file = handler.createFile(
+            store, store.root, 'sample.jpg', '__file_document__sample',
+            StringIO('I am a JPG file'))
+        self.assertEqual(path[-20:], 'documents/sample.jpg')
+        return
+
+    def test_iface(self):
+        # make sure we implement promised interfaces
+        obj = DocumentFileNameChooser(None) # needs a context normally
+        verifyClass(IFileStoreNameChooser, DocumentFileNameChooser)
+        verifyObject(IFileStoreNameChooser, obj)
+        return
+
+    def test_name_chooser_available(self):
+        # we can get a name chooser for document objects as adapter
+        doc = PDFDocument()
+        chooser = IFileStoreNameChooser(doc)
+        self.assertTrue(chooser is not None)
+        return
+
+    def test_name_chooser_document(self):
+        # we can get an image filename for documents not in a container
+        doc = PDFDocument()
+        doc.document_id = u'DOC'
+        chooser = IFileStoreNameChooser(doc)
+        result = chooser.chooseName('sample.jpg')
+        # the file would be stored in a ``_default`` directory.
+        self.assertEqual(
+            result, '__file-document__sample_DOC.jpg')
+        return
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_export.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_export.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/tests/test_export.py	(revision 12437)
@@ -0,0 +1,138 @@
+## $Id: test_export.py 12365 2015-01-02 10:09:50Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the document exporter.
+"""
+import os
+import grok
+import datetime
+from cStringIO import StringIO
+from zope.component import queryUtility, getUtility
+from zope.event import notify
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.kofa.interfaces import (
+    ICSVExporter, IExtFileStore, IFileStoreNameChooser)
+from waeup.kofa.documents.export import (
+    PDFDocumentExporter, HTMLDocumentExporter)
+from waeup.kofa.documents.document import PDFDocument, HTMLDocument
+from waeup.kofa.documents.tests.test_batching import DocumentImportExportSetup
+from waeup.kofa.testing import FunctionalLayer
+
+
+class PDFDocumentExporterTest(DocumentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PDFDocumentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = PDFDocumentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, PDFDocumentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an document exporter as utility
+        result = queryUtility(ICSVExporter, name="pdfdocuments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export a document
+        exporter = PDFDocumentExporter()
+        exporter.export([self.pdfdocument], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,document_id,history,state,title,users_with_local_roles\r\n'
+            'PDFDocument,DOC1,'
+            '[u\'2014-12-21 16:55:16 UTC - Document created by system\'],'
+            'created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all documents
+        exporter = PDFDocumentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,document_id,history,state,title,users_with_local_roles\r\n'
+            'PDFDocument,DOC1,'
+            '[u\'2014-12-21 16:55:16 UTC - Document created by system\'],'
+            'created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
+
+
+class HTMLDocumentExporterTest(DocumentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(HTMLDocumentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = HTMLDocumentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, HTMLDocumentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an document exporter as utility
+        result = queryUtility(ICSVExporter, name="htmldocuments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export a document
+        exporter = HTMLDocumentExporter()
+        exporter.export([self.htmldocument], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,document_id,history,html_dict,'
+            'html_multilingual,state,title,users_with_local_roles\r\n'
+            'HTMLDocument,DOC2,'
+            '[u\'2014-12-21 16:50:28 UTC - Document created by system\'],'
+            '{},,created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all documents
+        exporter = HTMLDocumentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,document_id,history,html_dict,'
+            'html_multilingual,state,title,users_with_local_roles\r\n'
+            'HTMLDocument,DOC2,'
+            '[u\'2014-12-21 16:50:28 UTC - Document created by system\'],'
+            '{},,created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/utils.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/utils.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/utils.py	(revision 12437)
@@ -0,0 +1,43 @@
+## $Id: utils.py 12408 2015-01-06 09:15:21Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""General helper functions and utilities for the documents.
+"""
+import grok
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.documents.workflow import (
+    CREATED, PUBLISHED)
+from waeup.kofa.documents.interfaces import IDocumentsUtils
+
+
+class DocumentsUtils(grok.GlobalUtility):
+    """A collection of methods subject to customization.
+    """
+    grok.implements(IDocumentsUtils)
+
+    TRANSLATED_DOCUMENT_STATES = {
+        CREATED: _('created'),
+        PUBLISHED:_('published')
+        }
+
+    DOCTYPES_DICT = {
+        'PDFDocument': _('PDF Document'),
+        'HTMLDocument': _('HTML Document'),
+        'RESTDocument': _('REST Document'),
+        }
+
+    SELECTABLE_DOCTYPES_DICT = DOCTYPES_DICT
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/viewlets.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/viewlets.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/viewlets.py	(revision 12437)
@@ -0,0 +1,92 @@
+## $Id: viewlets.py 12215 2014-12-13 17:21:27Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Viewlet components for documents offered for download by the company.
+"""
+
+import grok
+from waeup.kofa.browser.viewlets import (
+    PrimaryNavTab, ManageActionButton, AddActionButton)
+from waeup.kofa.interfaces import IKofaObject
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser.viewlets import PrimaryNavTab
+
+from waeup.kofa.documents.interfaces import (
+    IDocumentsContainer, IPublicDocument)
+from waeup.kofa.documents.browser import (
+    DocumentsContainerManageFormPage, DocumentsContainerPage,
+    DocumentManageFormPage, DocumentDisplayFormPage)
+
+
+grok.context(IKofaObject)  # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+
+class DocumentsTab(PrimaryNavTab):
+    """Documents tab in primary navigation.
+    """
+
+    grok.context(IKofaObject)
+    grok.order(10)
+    grok.require('waeup.viewDocuments')
+    grok.name('documentstab')
+
+    pnav = 2
+    tab_title = _(u'Documents')
+
+    @property
+    def link_target(self):
+        return self.view.application_url('documents')
+
+class DocumentsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IDocumentsContainer)
+    grok.view(DocumentsContainerPage)
+    grok.require('waeup.manageDocuments')
+    text = _('Manage')
+    target = 'manage'
+
+
+class DocumentViewActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IPublicDocument)
+    grok.view(DocumentManageFormPage)
+    grok.require('waeup.manageDocuments')
+    text = _('View')
+    target = 'index'
+    icon = 'actionicon_view.png'
+
+
+class DocumentManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IPublicDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.manageDocuments')
+    text = _('Manage')
+    target = 'manage'
+
+
+class DocumentTrigTransActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IPublicDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.manageDocuments')
+    icon = 'actionicon_trigtrans.png'
+    text = _(u'Transition')
+    target = 'trigtrans'
+
+
Index: main/waeup.kofa/trunk/src/waeup/kofa/documents/workflow.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/documents/workflow.py	(revision 12437)
+++ main/waeup.kofa/trunk/src/waeup/kofa/documents/workflow.py	(revision 12437)
@@ -0,0 +1,97 @@
+## $Id: workflow.py 12240 2014-12-15 07:44:01Z henrik $
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Workflow for documents.
+"""
+
+import grok
+from zope.component import getUtility
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import (
+    IWorkflowState, IWorkflowTransitionEvent, InvalidTransitionError)
+from waeup.kofa.interfaces import (
+    IObjectHistory, IKofaWorkflowInfo,
+    SimpleKofaVocabulary)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.workflow import KofaWorkflow, KofaWorkflowInfo
+from waeup.kofa.documents.interfaces import IPublicDocument
+
+CREATED = 'created'
+PUBLISHED = 'published'
+
+PUBLISHING_TRANSITIONS = (
+    Transition(
+        transition_id = 'create',
+        title = _('Create document'),
+        source = None,
+        condition = NullCondition,
+        msg = _('Document created'),
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'publish',
+        title = _('Publish document'),
+        source = CREATED,
+        condition = NullCondition,
+        msg = _('Document published'),
+        destination = PUBLISHED),
+
+    Transition(
+        transition_id = 'retract',
+        title = _('Retract documet'),
+        source = PUBLISHED,
+        condition = NullCondition,
+        msg = _('Document retracted'),
+        destination = CREATED),
+    )
+
+publishing_workflow = KofaWorkflow(PUBLISHING_TRANSITIONS)
+
+class PublishingWorkflowState(WorkflowState, grok.Adapter):
+    """An adapter to adapt Document objects to workflow states.
+    """
+    grok.context(IPublicDocument)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.publishing.state'
+    state_id = 'wf.publishing.id'
+
+
+class PublishingWorkflowInfo(KofaWorkflowInfo, grok.Adapter):
+    """Adapter to adapt CustomerDocument objects to workflow info objects.
+    """
+    grok.context(IPublicDocument)
+    grok.provides(IKofaWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = publishing_workflow
+
+
+@grok.subscribe(IPublicDocument, IWorkflowTransitionEvent)
+def handle_public_document_transition_event(obj, event):
+    """Append message to document history and log file.
+
+    """
+    msg = event.transition.user_data['msg']
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    try:
+        grok.getSite().logger.info('%s - %s' % (obj.document_id, msg))
+    except (TypeError, AttributeError):
+        pass
+    return
