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

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

We need different workflows for customer documents and central 'public' documents.

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