source: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/documents.py @ 12248

Last change on this file since 12248 was 12214, checked in by Henrik Bettermann, 10 years ago

Adjust UI components in documents and customers package.

  • Property svn:keywords set to Id
File size: 12.8 KB
RevLine 
[12015]1## $Id: documents.py 12214 2014-12-13 15:46:41Z henrik $
[11989]2##
3## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""
19Customer document components.
20"""
[12035]21import os
[11989]22import grok
[12161]23from hashlib import md5
[12213]24from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
[12018]25from zope.component import queryUtility, getUtility
[11989]26from zope.component.interfaces import IFactory
27from zope.interface import implementedBy
[12194]28from zope.event import notify
[12035]29
30from waeup.ikoba.image import IkobaImageFile
31from waeup.ikoba.imagestorage import DefaultFileStoreHandler
[11989]32from waeup.ikoba.interfaces import MessageFactory as _
[12035]33from waeup.ikoba.interfaces import (
34    IFileStoreNameChooser, IFileStoreHandler,
35    IIkobaUtils, IExtFileStore)
[11989]36from waeup.ikoba.customers.interfaces import (
[12018]37    ICustomerDocumentsContainer, ICustomerNavigation, ICustomerDocument,
[12053]38    ICustomersUtils, ICustomerPDFDocument)
[11989]39from waeup.ikoba.documents import DocumentsContainer, Document
[12018]40from waeup.ikoba.documents.interfaces import IDocumentsUtils
[11989]41from waeup.ikoba.utils.helpers import attrs_to_fields
42
[12035]43from waeup.ikoba.customers.utils import path_from_custid
44
[11989]45class CustomerDocumentsContainer(DocumentsContainer):
46    """This is a container for customer documents.
47    """
48    grok.implements(ICustomerDocumentsContainer, ICustomerNavigation)
49    grok.provides(ICustomerDocumentsContainer)
50
51    def __init__(self):
52        super(CustomerDocumentsContainer, self).__init__()
53        return
54
55    @property
56    def customer(self):
57        return self.__parent__
58
59    def writeLogMessage(self, view, message):
60        return self.__parent__.writeLogMessage(view, message)
61
62CustomerDocumentsContainer = attrs_to_fields(CustomerDocumentsContainer)
63
[12057]64class CustomerDocumentBase(Document):
65    """This is a customer document baseclass.
[11989]66    """
67    grok.implements(ICustomerDocument, ICustomerNavigation)
68    grok.provides(ICustomerDocument)
[12057]69    grok.baseclass()
[11989]70
[12211]71    local_roles = []
72
[12164]73    # Ikoba can store any number of files per Document object.
74    # However, we highly recommend to associate and store
75    # only one file per Document object. Thus the following
76    # tuple should contain only a single filename string.
[12161]77    filenames = ()
78
[11989]79    @property
[12213]80    def state(self):
81        state = IWorkflowState(self).getState()
82        return state
83
84    @property
85    def translated_state(self):
86        try:
87            TRANSLATED_STATES = getUtility(
[12214]88                ICustomersUtils).TRANSLATED_DOCUMENT_STATES
[12213]89            return TRANSLATED_STATES[self.state]
90        except KeyError:
91            return
92    @property
[11989]93    def customer(self):
94        try:
95            return self.__parent__.__parent__
96        except AttributeError:
97            return None
98
[12128]99    @property
100    def user_id(self):
101        if self.customer is not None:
102            return self.customer.customer_id
103        return
104
[11989]105    def writeLogMessage(self, view, message):
106        return self.__parent__.__parent__.writeLogMessage(view, message)
107
[12018]108    @property
[12166]109    def is_editable_by_customer(self):
[12018]110        try:
111            # Customer must be approved
112            cond1 = self.customer.state in getUtility(
[12088]113                ICustomersUtils).DOCMANAGE_CUSTOMER_STATES
[12018]114            # Document must be in state created
115            cond2 = self.state in getUtility(
[12088]116                ICustomersUtils).DOCMANAGE_DOCUMENT_STATES
[12018]117            if not (cond1 and cond2):
118                return False
119        except AttributeError:
120            pass
121        return True
122
[12053]123    @property
[12166]124    def is_editable_by_manager(self):
125        try:
126            # Document must be in state created
127            cond = self.state in getUtility(
128                ICustomersUtils).DOCMANAGE_DOCUMENT_STATES
129            if not cond:
130                return False
131        except AttributeError:
132            pass
133        return True
134
135    @property
[12056]136    def translated_class_name(self):
[12053]137        try:
138            DOCTYPES_DICT = getUtility(ICustomersUtils).DOCTYPES_DICT
[12056]139            return DOCTYPES_DICT[self.class_name]
[12053]140        except KeyError:
141            return
142
[12161]143    @property
144    def connected_files(self):
145        store = getUtility(IExtFileStore)
146        files = []
147        try:
[12164]148            # Usually there is only a single element in self.filenames.
[12161]149            for filename in self.filenames:
150                attrname = filename.replace('.','_')
151                file = store.getFileByContext(self, attr=filename)
[12165]152                if file:
153                    files.append((attrname, file))
[12161]154        except AttributeError:
155            # In unit tests we don't have a customer to
156            # determine the file path.
157            return
158        return files
[11989]159
[12168]160    @property
161    def is_verifiable(self):
[12169]162        files = self.connected_files
163        if files is not None and len(files) != len(self.filenames):
164            return False, _("No file uploaded.")
[12168]165        return True, None
166
[12161]167    def setMD5(self):
[12164]168        """Set md5 checksum attribute for files connected to this document.
[12161]169        """
[12162]170        connected_files = self.connected_files
171        if connected_files:
172            for file in self.connected_files:
173                attrname = '%s_md5' % file[0]
174                checksum = md5(file[1].read()).hexdigest()
175                setattr(self, attrname, checksum)
[12161]176        return
177
178
[12057]179class CustomerSampleDocument(CustomerDocumentBase):
180    """This is a sample customer document.
[12053]181    """
[12057]182
[12164]183    # Ikoba can store any number of files per Document object.
184    # However, we highly recommend to associate and store
185    # only one file per Document object. Thus the following
186    # tuple should contain only a single filename string.
[12161]187    filenames = ('sample',)
188
[12214]189    form_fields_interface = ICustomerDocument
190
[12057]191CustomerSampleDocument = attrs_to_fields(CustomerSampleDocument)
192
193
194class CustomerPDFDocument(CustomerDocumentBase):
195    """This is a customer document for a single pdf upload file.
196    """
[12053]197    grok.implements(ICustomerPDFDocument, ICustomerNavigation)
198    grok.provides(ICustomerPDFDocument)
[11989]199
[12164]200    # Ikoba can store any number of files per Document object.
201    # However, we highly recommend to associate and store
202    # only one file per Document object. Thus the following
203    # tuple should contain only a single filename string.
204    filenames = ('sample.pdf',)
205
[12214]206    form_fields_interface = ICustomerPDFDocument
207
[12053]208CustomerPDFDocument = attrs_to_fields(CustomerPDFDocument)
209
210
[12164]211# Customer documents must be importable. So we need a factory.
[11989]212class CustomerDocumentFactory(grok.GlobalUtility):
213    """A factory for customer documents.
214    """
215    grok.implements(IFactory)
[12057]216    grok.name(u'waeup.CustomerSampleDocument')
[11989]217    title = u"Create a new document.",
[12053]218    description = u"This factory instantiates new sample document instances."
[11989]219
220    def __call__(self, *args, **kw):
[12057]221        return CustomerSampleDocument(*args, **kw)
[11989]222
223    def getInterfaces(self):
[12057]224        return implementedBy(CustomerSampleDocument)
[12035]225
[12053]226# Customer documents must be importable. So we might need a factory.
227class CustomerPDFDocumentFactory(grok.GlobalUtility):
228    """A factory for customer pdf documents.
229    """
230    grok.implements(IFactory)
231    grok.name(u'waeup.CustomerPDFDocument')
232    title = u"Create a new document.",
233    description = u"This factory instantiates new pdf document instances."
234
235    def __call__(self, *args, **kw):
236        return CustomerPDFDocument(*args, **kw)
237
238    def getInterfaces(self):
239        return implementedBy(CustomerPDFDocument)
240
[12035]241#: The file id marker for customer files
242CUSTOMERDOCUMENT_FILE_STORE_NAME = 'file-customerdocument'
243
244
245class CustomerDocumentFileNameChooser(grok.Adapter):
246    """A file id chooser for :class:`CustomerDocument` objects.
247
248    `context` is an :class:`CustomerDocument` instance.
249
250    The :class:`CustomerDocumentFileNameChooser` can build/check file ids for
251    :class:`Customer` objects suitable for use with
252    :class:`ExtFileStore` instances. The delivered file_id contains
253    the file id marker for :class:`CustomerDocument` object and the customer id
254    of the context customer.
255
256    This chooser is registered as an adapter providing
257    :class:`waeup.ikoba.interfaces.IFileStoreNameChooser`.
258
259    File store name choosers like this one are only convenience
260    components to ease the task of creating file ids for customer document
261    objects. You are nevertheless encouraged to use them instead of
262    manually setting up filenames for customer documents.
263
264    .. seealso:: :mod:`waeup.ikoba.imagestorage`
265
266    """
267
268    grok.context(ICustomerDocument)
269    grok.implements(IFileStoreNameChooser)
270
271    def checkName(self, name=None, attr=None):
272        """Check whether the given name is a valid file id for the context.
273
274        Returns ``True`` only if `name` equals the result of
275        :meth:`chooseName`.
276
277        """
278        return name == self.chooseName()
279
280    def chooseName(self, attr, name=None):
281        """Get a valid file id for customer document context.
282
283        *Example:*
284
285        For a customer with customer id ``'A123456'``
286        and document with id 'd123'
287        with attr ``'nice_image.jpeg'`` stored in
288        the customers container this chooser would create:
289
[12205]290          ``'__file-customerdocument__customers/345/c999/nice_image_d123_c999.jpeg'``
[12035]291
292        meaning that the nice image of this customer document would be
293        stored in the site-wide file storage in path:
294
[12205]295          ``customers/345/c999/nice_image_d123_c999.jpeg``
[12035]296
297        """
298        basename, ext = os.path.splitext(attr)
299        cust_id = self.context.customer.customer_id
300        doc_id = self.context.document_id
301        marked_filename = '__%s__%s/%s_%s_%s%s' % (
302            CUSTOMERDOCUMENT_FILE_STORE_NAME, path_from_custid(cust_id),
303            basename, doc_id, cust_id, ext)
304        return marked_filename
305
306
307class CustomerDocumentFileStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
308    """Customer document specific file handling.
309
310    This handler knows in which path in a filestore to store customer document
311    files and how to turn this kind of data into some (browsable)
312    file object.
313
314    It is called from the global file storage, when it wants to
315    get/store a file with a file id starting with
316    ``__file-customerdocument__`` (the marker string for customer files).
317
318    Like each other file store handler it does not handle the files
319    really (this is done by the global file store) but only computes
320    paths and things like this.
321    """
322    grok.implements(IFileStoreHandler)
323    grok.name(CUSTOMERDOCUMENT_FILE_STORE_NAME)
324
325    def pathFromFileID(self, store, root, file_id):
326        """All customer document files are put in directory ``customers``.
327        """
328        marker, filename, basename, ext = store.extractMarker(file_id)
329        sub_root = os.path.join(root, 'customers')
330        return super(CustomerDocumentFileStoreHandler, self).pathFromFileID(
331            store, sub_root, basename)
332
333    def createFile(self, store, root, filename, file_id, file):
334        """Create a browsable file-like object.
335        """
336        # call super method to ensure that any old files with
337        # different filename extension are deleted.
338        file, path, file_obj = super(
339            CustomerDocumentFileStoreHandler, self).createFile(
340            store, root,  filename, file_id, file)
341        return file, path, IkobaImageFile(
342            file_obj.filename, file_obj.data)
343
[12194]344
345@grok.subscribe(ICustomerDocument, grok.IObjectRemovedEvent)
346def handle_document_removed(document, event):
347    """If a document is deleted, we make sure that also referrers to
348    customer contract objects are removed.
349    """
350    docid = document.document_id
351
352    # Find all customer contracts that refer to given document...
353    try:
354        contracts = document.customer['contracts'].values()
355    except AttributeError:
356        # customer not available. This might happen during tests.
357        return
358    for contract in contracts:
359        # Remove that referrer...
360        for key, value in contract.__dict__.items():
361            if key.endswith('_object') and \
362                getattr(value, 'document_id', None) == docid:
363                setattr(contract, key, None)
364                notify(grok.ObjectModifiedEvent(contract))
365                contract.customer.__parent__.logger.info(
366                    'ObjectRemovedEvent - %s - %s - removed: %s' % (
367                        contract.customer.customer_id,
368                        contract.contract_id,
369                        document.document_id))
370    return
371
Note: See TracBrowser for help on using the repository browser.