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

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

Change document_id generation algorithm. Use Universally Unique IDentifiers instead of consecutive numbers.

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