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

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

Add handler for document removal. Files must be removed too.

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