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

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

Fix handle_product_remove. The attribute is meanwhile called product_object.

Add handle_document_removed with test.

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