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

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

Do not allow to add documents if customer has not yet been approved.

Rename pagetemplate.

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