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

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

Define is_verifiable as probably requested. Adjust test.

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