# Copyright (c) 2005 Infrae. All rights reserved.
# See also LICENSE.txt
import datetime
import os.path, mimetypes, zipfile
from StringIO import StringIO

from zope.interface import implements, Invalid

from zope import event

from zope.app import zapi
from zope.security.management import getInteraction

from zope.app.pagetemplate import ViewPageTemplateFile
from zope.app.event import objectevent
from zope.app.component.hooks import getSite
from zope.app.container.interfaces import INameChooser
from zope.app.intid.interfaces import IIntIds
from zope.app.publisher.browser.menu import getMenu
from zope.app.form.interfaces import ConversionError

from zope.formlib import form, page
from zope.formlib.interfaces import IActions

from hurry.file.schema import File
from hurry.file import HurryFile

from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo,\
     IWorkflow, IWorkflowVersions

from documentlibrary.core import interfaces, document, category, flow
from documentlibrary.core.browser import widgets, filewidget
     
from documentlibrary.core.browser.feedback import IFeedback     
from documentlibrary.core import conversion
from documentlibrary.core.version import getDocumentLibraryVersion

DO_NOT_RENDER = ''

validation_error_text = (
    'Some values are incorrect or missing. '
    'Please look at the form for further details.')

class FormMixin(object):

    adapters = None

    def updateFormErrors(self):
        if not getattr(self, 'errors', False):
            return
        for error in self.errors:
            if isinstance(error, Invalid):
                IFeedback(self.request).addError(unicode(error))
        
    def addFeedback(self, message):
        IFeedback(self.request).addFeedback(message)

    def addError(self, message):
        IFeedback(self.request).addError(message)
        
    def useFeedback(self):
        return IFeedback(self.request).useFeedback()

    def useErrors(self):
        return IFeedback(self.request).useErrors()

    def clearFeedbackAndErrors(self):
        self.useFeedback()
        self.useErrors()
        
    def redirect(self, url):
        self.request.response.redirect(url)
        return DO_NOT_RENDER
                
    def editAction(self, data, add_feedback=True):
        if form.applyChanges(
            self.context, self.form_fields, data, self.adapters):
            event.notify(
                objectevent.ObjectModifiedEvent(self.context))
            formatter = self.request.locale.dates.getFormatter(
                'dateTime', 'medium')
            if add_feedback:
                self.addFeedback('Updated')
        else:
            if add_feedback:
                self.addFeedback('No changes')

# little hack to allow any template to display feedback
class FeedbackPage(FormMixin):
    pass

class DocumentLibraryPage(FeedbackPage):
    def getFrontpageText(self):
        return zapi.getUtility(interfaces.ICustomText).frontpage_text

def createDocumentFields(iface):
    form_fields = form.Fields(iface)
    form_fields = form_fields.select(
        'file',
        'file_available',
        'pdf',
        'generate_pdf',
        'plaintext',
        'generate_plaintext',
        'title',
        'description',
        'category',
        'foi_topic',
        'authors',
        'modificationdate',
        'versionstring',
        'availabledate',
        'expirydate',
        'access',
        'note')
    form_fields['modificationdate'].custom_widget = widgets.FancyDateWidget
    form_fields['availabledate'].custom_widget = widgets.FancyDateWidget
    form_fields['expirydate'].custom_widget = widgets.FancyDateWidget
    form_fields['authors'].custom_widget = widgets.AuthorsWidget
    form_fields['category'].custom_widget = widgets.CategoriesWidget
    form_fields['foi_topic'].custom_widget = widgets.FOITopicsWidget
    return form_fields

class DocumentBaseForm(FormMixin):
    
    def processFilename(self, data):
        file = data.get('file')
        if file is None:
            return

        # handle nasty Windows/IE pathnames with backslashes and the like
        filename = file.filename
        filename = filename.replace('\\', '/')
        rest, filename = os.path.split(filename)
        file.filename = filename
        
        # if we have no input for title, derive from filename
        if data['title'] is None:
            # strip off extension
            name, dummy = os.path.splitext(file.filename)
            # and now set it as title
            data['title'] = name.replace('-', ' ')
            self.addFeedback(
                u'Title has been set according to filename '
                u'of uploaded file.')

    def processFoiTopic(self, data):
        # if a foi_topic is selected, can only be unrestricted
        foi_topic = data.get('foi_topic')
        if foi_topic is None or 'zope.Everybody' in data['access']:
            return
        data['access'] = ['zope.Everybody']
        self.addFeedback((
            u'Access has been changed to everybody, since an FOI topic '
            u'has been selected.'))

    def processRetentionPeriod(self, data):
        # if there is a retention period, adjust expiry date
        retention = self.category(data['category']).retention_period
        # hard cap with maximum retention
        maximum_retention = zapi.getUtility(
            interfaces.IMaximumRetentionPeriod).maximum_retention_period
        if retention == 0:
            retention = maximum_retention
        elif retention > maximum_retention:
            retention = maximum_retention
        delta = datetime.timedelta(days=retention)
        maximum_expiry = data['availabledate'] + delta
        # only adjust expiries that are above maximum
        if data['expirydate'] < maximum_expiry:
            return
        data['expirydate'] = maximum_expiry
        self.addFeedback((
            u'The expiry date has been set according to the maximum allowed '
            u'retention of the category (%s days).'
            ) % retention)

    def _convertToPDFHelper(self, data):
        file = data['file']
        try:
            value, filename = conversion.convertToPDF(
                file.data, file.filename)
            data['pdf'] = HurryFile(filename, value)
        except conversion.ConversionError, e:
            return 'Could not convert to PDF. Reason: %s' % str(e)
        return None
    
    def convertToPDF(self, data, errors):
        error_msg = self._convertToPDFHelper(data)
        if error_msg is None:
            self.addFeedback('Created PDF')
        else:
            error = ConversionError(error_msg)
            self.widgets['pdf']._error = error
            errors.append(error)

    def _convertToPlaintextHelper(self, data):
        file = data['file']
        try:
            value, filename = conversion.convertToTxt(
                file.data, file.filename)
            data['plaintext'] = HurryFile(filename, value)
        except conversion.ConversionError, e:
            return 'Could not convert to plain text. Reason: %s' % str(e)
        return None
        
    def convertToPlaintext(self, data, errors):
        error_msg = self._convertToPlaintextHelper(data)
        if error_msg is None:
            self.addFeedback('Created plain text')
        else:
            error = ConversionError(error_msg)
            self.widgets['plaintext']._error = error
            errors.append(error)

    def category(self, category_id):
        intid = zapi.getUtility(IIntIds)
        return intid.getObject(category_id)

# XXX a hack to make sure we have the file field required
# in the add form
class IRequiredFileDocument(interfaces.IDocument):
    file = File(
        title=u'file',
        description=(
            u'Browse and select a document to upload to the library.'),
        required=True
        )

# XXX can this class be simplified (with nextURL and the like)
# if we don't make this a PageAddForm anymore?
class DocumentAddForm(DocumentBaseForm, form.PageAddForm):

    form_fields = createDocumentFields(IRequiredFileDocument)

    # no download option, only upload
    form_fields['file'].custom_widget = filewidget.FileWidget
    form_fields['pdf'].custom_widget = filewidget.FileWidget
    form_fields['plaintext'].custom_widget = filewidget.FileWidget

    label = u'Add document'
   
    template = ViewPageTemplateFile('templates/document_editform.pt')

    _document = None 
    _to_submissions = False
    
    __finished_add = False

    def validate(self, action, data):
        # do normal validation
        errors = form.PageAddForm.validate(self, action, data)
        # if we got any errors, report those first
        # this is to prevent delay of feedback by conversions
        if errors:
            self.addError(validation_error_text)
            return errors
        # if we're dealing with a zip file, special case and
        # don't try conversion immediately
        if data['file'].filename.lower().endswith('.zip'):
            return errors
        # now do conversion
        if data['generate_pdf']:
            self.convertToPDF(data, errors)
        if data['generate_plaintext']:
            self.convertToPlaintext(data, errors)
        if errors:
            self.addError(validation_error_text)
        return errors
    
    @form.action("submit")
    def handle_submit(self, action, data):
        if data['file'].filename.lower().endswith('.zip'):
            # XXX using StringIO here isn't very efficient
            # XXX won't work with tramlined file
            self.createAndAddZipfile(StringIO(data['file'].data), data)
            self._to_submissions = True
            return
        self.createAndAdd(data)
        self.request.response.addHeader('tramline_ok', 'OK')
    
    def createAndAdd(self, data):
        self.processAddInput(data)
        doc = document.Document(**data)
        self._document = doc
        event.notify(objectevent.ObjectCreatedEvent(doc))

        category = self.category(data['category'])

        # have to add it in the right category by hand, as
        # cannot use adding view
        documents = category['documents']
        chooser = INameChooser(documents)
        name = chooser.chooseName('', doc)
        chooser.checkName(name, documents)
        documents[name] = doc
        self.__finished_add = True

        IWorkflowInfo(doc).fireTransition('submit')
        
        self.addFeedback('"%s" has been submitted to the "%s" category' % (
            doc.title, category.name))
        return doc

    def createAndAddZipfile(self, file, data):
        # ignore title as title needs to be derived from filenames
        data['title'] = None
        # ignore custom pdf and plaintext versions as this doesn't make
        # sense
        data['pdf'] = None
        data['plaintext'] = None
        
        for file, pdf, plaintext in ZippedFiles(file).getFiles():
            self.addFeedback("(From zipfile) %s:" % file.filename)
            d = data.copy()
            d['file'] = file
            d['pdf'] = pdf
            d['plaintext'] = plaintext
            # now do conversion
            if data['generate_pdf']:
                error_msg = self._convertToPDFHelper(d)
                if error_msg is not None:
                    self.addError("%s: %s" % (file.filename, error_msg))
            if data['generate_plaintext']:
                error_msg = self._convertToPlaintextHelper(d)
                if error_msg is not None:
                    self.addError("%s: %s" % (file.filename, error_msg))
            self.createAndAdd(d)
            
    def processAddInput(self, data):
        self.processFilename(data)
        self.processFoiTopic(data)
        self.processRetentionPeriod(data)
        self.processUniformFilenames(data)

    def processUniformFilenames(self, data):
        """Make sure all filenames are based on that of original.
        """
        original_filename = data['file'].filename
        filename, ext = os.path.splitext(original_filename)
        pdf = data.get('pdf')
        if pdf is not None:
            pdf.filename = filename + '.pdf'
        plaintext = data.get('plaintext')
        if plaintext is not None:
            plaintext.filename = filename + '.txt'
    
    # XXX unfortunately I have to override this as well as
    # __finished_add in AddFormBase gets name-mangled by python and is thus not
    # settable from subclasses
    def render(self):
        if self.__finished_add:
            self.request.response.redirect(self.nextURL())
            return ""
        return super(DocumentAddForm, self).render()

    def nextURL(self):
        if self._to_submissions:
            return zapi.absoluteURL(getSite(), self.request) + '/@@my_submissions'
        # Override nextURL() from AddForm base class
        return zapi.absoluteURL(
            self._document, self.request) + '/@@display_document'

    def getHintHeading(self):
        return "New submission"
    
def editCondition(form, action):
    context = form.context
    state = IWorkflowState(context).getState()
    if state in (flow.SUBMITTED, flow.APPROVED):
        # if submitted or approved, you can still edit if you
        # have approve document permission
        return getInteraction().checkPermission(
            'documentlibrary.ApproveDocument', context)
    # cannot edit otherwise
    return False

class WorkflowActions(object):
    """Represent form actions based on workflow.
    """

    implements(IActions)

    def __init__(self, form=None, normal_actions=None):
        self.form = form
        self.normal_actions = normal_actions or []
        
    def __iter__(self):
        # Get to the context of the view (which is the document)
        context = self.form.context
        state = IWorkflowState(context)
        transition_ids = IWorkflowInfo(context).getManualTransitionIds()
        wf = zapi.getUtility(IWorkflow)
        for transition_id in transition_ids:
            transition = wf.getTransition(state.getState(), transition_id)
            action = form.Action(
                transition.title,
                success=TransitionFirer(context, transition_id),
                data={'no_edit': transition.user_data.get('no_edit')}
                )
            action = action.__get__(self.form)
            yield action
        # yield any normal actions
        for action in self.normal_actions:
            action = action.__get__(self.form)
            yield action

    def __getitem__(self, name):
        raise NotImplementedError

    def __add__(self, other):
        raise NotImplementedError

    def append(self, action):
        self.normal_actions += (action,)
        
    def __get__(self, inst, class_):
        if inst is None:
            return self
        return self.__class__(inst, self.normal_actions)

class TransitionFirer(object):
    """Fire workflow transition after editing document.
    """
    def __init__(self, context, transition_id):
        self.context = context
        self.transition_id = transition_id

    def __call__(self, form, action, data):
        if not action.data.get('no_edit'):
            # set up side effect for editing during workflow transition
            def side_effect(context):
                # We *have* to create a new document edit form, so as to
                # bind to the *new* context, which could be a new version
                DocumentEditForm(context, form.request).editDocument(data)
        else:
            # no editing is taking place, so don't do any editing after
            # workflow transition
            side_effect = None
        # fire workflow transition
        created = IWorkflowInfo(self.context).fireTransition(
            self.transition_id, side_effect=side_effect)

        # now determine next url
        next = created or self.context
    
        # we don't have a parent, so we probably just got
        # deleted, go back to doclib root
        if getattr(next, '__parent__', None) is None:
            # clear out any other messages that might've been added..
            form.clearFeedbackAndErrors()
            form.addFeedback('Document was deleted')
            return form.redirect(zapi.absoluteURL(getSite(), form.request))

        # feedback
        wf = zapi.getUtility(IWorkflow)
        transition = wf.getTransitionById(self.transition_id)
        form.addFeedback("Action %s succeeded" % transition.title)
        
        return form.redirect(zapi.absoluteURL(
            next, form.request) + '/@@display_document')        


def deletion_request_condition(form, action):
    # if deletion already requested, we can't request it again
    if form.context.deletion_requested:
        return False
    return isDeleteRequestCandidate(form.context)

def isDeleteRequestCandidate(context):
    wf_state = IWorkflowState(context)
    state = wf_state.getState()
    id = wf_state.getId()
    if state in [flow.AVAILABLE, flow.ALERTED]:
        return True
    if state == flow.EXPIRED:
        # if expired, deletion requests aren't needed if there's
        # already an available/alerted version
        wf_versions = zapi.getUtility(IWorkflowVersions)
        if wf_versions.hasVersion(flow.AVAILABLE, id):
            return False
        if wf_versions.hasVersion(flow.ALERTED, id):
            return False
        return True
    # if not available, alerted or expired, no deletion request possible
    return False

def delete_receipt_condition(form, action):
    if not isDeleteRequestCandidate(form.context):
        return False
    interaction = getInteraction()
    return interaction.checkPermission('documentlibrary.ApproveDocument',
                                       form.context)

class DocumentEditForm(DocumentBaseForm, form.PageEditForm):
    form_fields = createDocumentFields(interfaces.IDocument)
    form_fields['file'].custom_widget = filewidget.FileUploadDownloadWidget
    form_fields['pdf'].custom_widget = filewidget.FileUploadDownloadWidget
    form_fields['plaintext'].custom_widget = filewidget.\
                                             FileUploadDownloadWidget
    
    label = u'Edit document'
    
    template = ViewPageTemplateFile('templates/document_editform.pt')

    actions = WorkflowActions()

    def validate(self, action, data):
        # skip validation if we got 'no edit' (this is just to
        # trigger the workflow transition)
        if action.data.get('no_edit'):
            return []
        # do normal validation
        errors = form.PageEditForm.validate(self, action, data)
        # if we got any errors, report those first
        # this is to prevent delay of feedback by conversions
        if errors:
            self.addError(validation_error_text)
            return errors
        # some conversion related post-processing that's very form specific
        if self.mustRegeneratePDF(data):
            self.convertToPDF(data, errors)
        if self.mustRegeneratePlaintext(data):
            self.convertToPlaintext(data, errors)
        if errors:
            self.addError(validation_error_text)
        return errors

    def editDocument(self, data):
        orig_category = self.context.category
        self.processEditInput(data)
        self.editAction(data, add_feedback=False)
        self.moveCategory(orig_category, self.context.category)
        
    def moveCategory(self, from_category, to_category):
        if from_category == to_category:
            return
        # get actual category object we're moving to
        category = self.category(to_category)
        # do the actual move
        # XXX rip off security, otherwise no way to get
        # to __parent__?
        from zope.security.proxy import removeSecurityProxy
        doc = removeSecurityProxy(self.context)
        category['documents'][self.context.__name__] = doc

    def processEditInput(self, data):
        self.processFilename(data)
        self.processFoiTopic(data)
        self.processRetentionPeriod(data)
        self.processStableFilenames(data)

    def processStableFilenames(self, data):
        """Make sure filenames are the same as before and don't change.
        """
        original_filename = self.context.file.filename
        filename, ext = os.path.splitext(original_filename)
        file = data.get('file')
        if file is not None:
            file.filename = original_filename
        pdf = data.get('pdf')
        if pdf is not None:
            pdf.filename = filename + '.pdf'
        plaintext = data.get('plaintext')
        if plaintext is not None:
            plaintext.filename = filename + '.txt'
        
    def mustRegeneratePDF(self, data):
        return self.mustRegenerateFile(data, 'generate_pdf')
    
    def mustRegeneratePlaintext(self, data):
        return self.mustRegenerateFile(data, 'generate_plaintext')

    def mustRegenerateFile(self, data, generate_name):
        # if file is the same
        if data['file'] == self.context.file:
            # only regenerate if we suddenly want a converted file
            # while before we didn't
            return (data[generate_name] and
                    not getattr(self.context, generate_name))
        # file is not the same, so regenerate PDF is generate_pdf is chosen
        return data[generate_name]

    def getHintHeading(self):
        if self.context.deletion_requested:
            return 'deletion requested'
        return flow.getStateName(IWorkflowState(self.context).getState())
        
    @form.action('deletion request', condition=deletion_request_condition,
                 data={'no_edit': True})
    def handle_deletion_request(self, action, data):
        return self.redirect(zapi.absoluteURL(self.context, self.request) +
                             '/@@request_deletion')

    @form.action('delete (via receipt)', condition=delete_receipt_condition,
                 data={'no_edit': True})
    def handle_delete(self, action, data):
        return self.redirect(zapi.absoluteURL(self.context, self.request) +
                             '/@@deletion_receipt')
    
class DocumentDeletionRequest(FormMixin, form.PageEditForm):

    label = u'Document deletion request'
    
    form_fields = form.Fields(interfaces.IDocument)
    form_fields = form_fields.select('title', 'description', 'note')
    form_fields['title'].for_display = True
    form_fields['description'].for_display = True

    template = ViewPageTemplateFile('templates/document_editform.pt')

    def getHintText(self):
        return (
            "'available' or 'expired' documents (where no new version is "
            "available yet) can only be deleted by "
            "Librarians. You can request deletion by completing this form. "
            "When the document is deleted, a deletion receipt will remain "
            "in the library for future reference.")

    @form.action('confirm deletion request')
    def handle_deletion_request_confirm(self, action, data):
        wf_state = IWorkflowState(self.context)
        state = wf_state.getState()
        # refuse to operate if the state is not right.
        # XXX this is a hack; should somehow fit this in proper workflow engine
        if state not in [flow.AVAILABLE, flow.ALERTED, flow.EXPIRED]:
            return
        self.context.note = data['note']
        self.context.deletion_requested = True
        wf_info = IWorkflowInfo(self.context)
        # fire deletion request
        # XXX this is a hack; firing transition towards oneself will
        # result in the only transition possible: a deletion request,
        # but this may not be always true...
        wf_info.fireTransitionToward(state)
        self.addFeedback('Deletion for this document was requested')
        return self.redirect(zapi.absoluteURL(self.context, self.request) +
                             '/@@display_document')

class DocumentDeletionReceipt(FormMixin, form.PageEditForm):

    label = u'Document deletion receipt'

    form_fields = form.Fields(interfaces.IDeletionReceipt,
                              interfaces.IDocument)
    form_fields = form_fields.select(
        'requester_name',
        'requester_email',
        'title',
        'description',
        'versionstring',
        'handle_id',
        'category',
        'foi_topic',
        'explanation')

    form_fields['requester_email'].custom_widget = widgets.EmailWidget
    form_fields['title'].for_display = True
    form_fields['description'].for_display = True
    form_fields['versionstring'].for_display = True
    form_fields['handle_id'].for_display = True
    form_fields['handle_id'].custom_widget = widgets.HandleUrlWidget
    form_fields['category'].for_display = True
    form_fields['category'].custom_widget = widgets.CategoryDisplayWidget
    form_fields['foi_topic'].for_display = True
    form_fields['foi_topic'].custom_widget = widgets.CategoryDisplayWidget
    
    template = ViewPageTemplateFile('templates/document_editform.pt')
    
    @form.action('confirm deletion')
    def handle_deletion_receipt_confirm(self, action, data):
        # transition to deleted status
        info = IWorkflowInfo(self.context)
        info.fireTransitionToward(flow.DELETED)

        # edit deletion receipt
        receipt = self.context.deletion_receipt
        for key, value in data.items():
            setattr(receipt, key, value)
        self.addFeedback('Document deleted')
        return self.redirect(zapi.absoluteURL(self.context, self.request) +
                             '/@@edit_document')

class DocumentDeletionReceiptDisplayForm(FormMixin, form.PageDisplayForm):
    label = u'Document deletion receipt'

    template = ViewPageTemplateFile('templates/document_displayform.pt')

    form_fields = form.Fields(interfaces.IDeletionReceipt,
                              interfaces.IDocument)
    form_fields = form_fields.select(
        'requester_name',
        'requester_email',
        'title',
        'handle_id',
        'description',
        'versionstring',
        'category',
        'foi_topic',
        'explanation',
        'last_changed')
    form_fields['description'].for_display = True
    form_fields['versionstring'].for_display = True
    form_fields['handle_id'].custom_widget = widgets.HandleUrlWidget
    form_fields['last_changed'].custom_widget = widgets.DatetimeDisplayWidget
    
class DocumentDisplayForm(FormMixin, form.PageDisplayForm):
    
    label = u'Document information'
    
    template = ViewPageTemplateFile('templates/document_displayform.pt')

    form_fields = form.Fields(interfaces.IDocument)
    form_fields = form_fields.select(
        'title',
        'description',
        'file',
        'file_available',
        'pdf',
        'generate_pdf',
        'plaintext',
        'handle_id',
        'category',
        'foi_topic',
        'authors',
        'modificationdate',
        'versionstring',
        'availabledate',
        'expirydate',
        'access',
        'note')
    form_fields = form_fields.omit('file_available', 'generate_pdf')
    form_fields['authors'].custom_widget = widgets.AuthorsDisplayWidget
    form_fields['file'].custom_widget = filewidget.DownloadWidget
    form_fields['pdf'].custom_widget = filewidget.DownloadWidget
    form_fields['plaintext'].custom_widget = filewidget.DownloadWidget
    form_fields['handle_id'].custom_widget = widgets.HandleUrlWidget
    form_fields['note'].custom_widget = widgets.PreformattedWidget
    form_fields['modificationdate'].custom_widget = widgets.DateDisplayWidget
    form_fields['availabledate'].custom_widget = widgets.DateDisplayWidget
    form_fields['expirydate'].custom_widget = widgets.DateDisplayWidget
    
class GenericDownloadPage(page.Page):
    def __call__(self):
        response = self.request.response
        mimetype, dummy = mimetypes.guess_type(self.context.filename)
        response.setHeader('Content-Type', mimetype)
        # should not be necessary
        #response.setHeader(
        #    'Content-Disposition', 'inline;filename=%s' %
        # self.context.filename)
        response.addHeader(
            'tramline_file', '')
        return self.context.data

class UnavailableDocumentPage(page.Page):
    def __call__(self):
        return (
            'The document you requested is not available. '
            'Please contact the webmaster for further details.')

class UnavailableFilePage(page.Page):
    def __call__(self):
        return (
            'That file is no longer available. Please contact the '
            'webmaster for further details.')
            
class CategoryBaseForm(FormMixin):
    
    form_fields = form.Fields(interfaces.ICategory, render_context=False)

    form_fields = form_fields.select(
        'name', 'retention_period', 'librarians', 'submitters')
    form_fields['librarians'].custom_widget = widgets.UsersEditWidget
    form_fields['submitters'].custom_widget = widgets.UsersEditWidget
    
class CategoryAddForm(CategoryBaseForm, form.PageAddForm):
    
    label = u'Add category'
    
    template = ViewPageTemplateFile('templates/form.pt')
   
    _category = None
    
    def create(self, data):
        cat = category.Category(**data)
        interfaces.ICategoryRoleManager(cat).addRoles(
            data['librarians'], data['submitters'])
        self.addFeedback('Category created')
        self._category = cat
        return cat
    
    def nextURL(self):
        return zapi.absoluteURL(self._category, self.request)

class SubCategoryAddForm(CategoryAddForm):
    label = u'Add sub-category'
    
class CategoryEditForm(CategoryBaseForm, form.PageEditForm):
    
    label = u'Edit category &#xab;%s&#xbb;'
    
    template = ViewPageTemplateFile('templates/form.pt')

    def __init__(self, *args, **kwargs):
        super(CategoryEditForm, self).__init__(*args, **kwargs)
        self.label = self.label % self.context.name
    
    @form.action("save changes")
    def handleEditAction(self, action, data):
        self.editAction(data)
        interfaces.ICategoryRoleManager(self.context).setRoles(
            data['librarians'], data['submitters'])
        return self.redirect(zapi.absoluteURL(self.context, self.request))

class CategoryDeletePage(page.Page, FormMixin):
    def __call__(self):
        if not self.context.isEmpty():
            self.addFeedback(
                "Categories and sub-categories cannot be deleted unless "
                "they are empty.")
            return self.request.response.redirect(
                zapi.absoluteURL(self.context, self.request))
        parent = self.context.__parent__
        self.addFeedback('Category deleted: %s' % self.context.name)
        del parent[self.context.__name__]
        return self.request.response.redirect(
            zapi.absoluteURL(
            parent, self.request))
    
class HistoryPage(page.Page):

    label = u'History'
    
    template = ViewPageTemplateFile('templates/history.pt')
    
    def __call__(self):
        return self.template()    

    def items(self):
        entries = getSite()['history'].values()
        intid = zapi.getUtility(IIntIds)
        for entry in entries:
            yield {'id': intid.getId(entry), 'entry': entry}
    
class HistoryEntryPage(form.PageDisplayForm):

    form_fields = form.Fields(interfaces.IHistoryEntry)
    form_fields = form_fields.omit('doc_id')

    form_fields['message'].custom_widget = widgets.UnescapedDisplayWidget
    
    label = u'History entry'
    
    template = ViewPageTemplateFile('templates/display_form.pt')

class LibrarianCategoriesPage(page.Page, FormMixin):
    label = u'My categories'

    template = ViewPageTemplateFile('templates/librarian_categories.pt')

    def __call__(self):
        return self.template()

    def getMenu(self):
        return getMenu('categorytree_menu', self.context, self.request)
    
class RedirectToParentPage(page.Page):

    def __call__(self):
        return self.request.response.redirect(
            zapi.absoluteURL(
            self.context.__parent__, self.request))

class AboutPage(page.Page):
    __call__ = ViewPageTemplateFile('templates/about.pt')

    def getDocumentLibraryVersion(self):
        return getDocumentLibraryVersion()
        
class WorkflowAuto(page.Page):
    def __call__(self):
        wf_versions = zapi.getUtility(IWorkflowVersions)
        wf_versions.fireAutomatic()
        return 'OK'
        
class ZippedFiles(object):
    def __init__(self, file):
        self.zfile = zipfile.ZipFile(file, 'r')

    def getRawFilenames(self):
        # collect filenames in zipfile by rootname
        result = {}
        for name in self.zfile.namelist():
            rootname, extension = os.path.splitext(name)
            d = result.setdefault(rootname, {})
            if extension == '.pdf':
                d['pdf'] = name
            elif extension == '.txt':
                d['txt'] = name
            else:
                d['file'] = name
        for d in result.values():
            yield d.get('file'), d.get('pdf'), d.get('txt')

    def getProcessedFilenames(self):
        # process filesnames so that there's at least always a file even
        # if a pdf is uploaded
        result = {}
        for file, pdf, txt in self.getRawFilenames():
            if file is None:
                if pdf is not None:
                    file = pdf
                    pdf = None
                elif txt is not None:
                    file = txt
                    txt = None
                else:
                    # all None should never occur, just skip
                    continue
            yield file, pdf, txt

    def getFiles(self):
        z = self.zfile
        for file, pdf, txt in self.getProcessedFilenames():
            file_data = HurryFile(file, z.read(file))
            if pdf is not None:
                pdf_data = HurryFile(pdf, z.read(pdf))
            else:
                pdf_data = None
            if txt is not None:
                txt_data = HurryFile(txt, z.read(txt))
            else:
                txt_data = None
            yield file_data, pdf_data, txt_data
    
