source: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/customer.py @ 12060

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

Add document file viewlets. Tests will follow.

  • Property svn:keywords set to Id
File size: 13.6 KB
Line 
1## $Id: customer.py 12035 2014-11-22 10:14: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"""
19Container for the various objects owned by customers.
20"""
21import os
22import shutil
23import grok
24from datetime import datetime, timedelta
25from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
26from zope.password.interfaces import IPasswordManager
27from zope.component import getUtility, createObject
28from zope.component.interfaces import IFactory
29from zope.interface import implementedBy
30from zope.securitypolicy.interfaces import IPrincipalRoleManager
31from zope.schema.interfaces import ConstraintNotSatisfied
32
33from waeup.ikoba.image import IkobaImageFile
34from waeup.ikoba.imagestorage import DefaultFileStoreHandler
35from waeup.ikoba.interfaces import (
36    IObjectHistory, IUserAccount, IFileStoreNameChooser, IFileStoreHandler,
37    IIkobaUtils, IExtFileStore,
38    CREATED, REQUESTED, APPROVED)
39from waeup.ikoba.customers.export import EXPORTER_NAMES
40from waeup.ikoba.customers.interfaces import (
41    ICustomer, ICustomerNavigation, ICSVCustomerExporter,
42    ICustomersUtils)
43from waeup.ikoba.customers.utils import generate_customer_id, path_from_custid
44from waeup.ikoba.customers.documents import CustomerDocumentsContainer
45from waeup.ikoba.utils.helpers import attrs_to_fields, now, copy_filesystem_tree
46
47class Customer(grok.Container):
48    """This is a customer container for the various objects
49    owned by customers.
50    """
51    grok.implements(ICustomer, ICustomerNavigation)
52    grok.provides(ICustomer)
53
54    temp_password_minutes = 10
55
56    def __init__(self):
57        super(Customer, self).__init__()
58        # The site doesn't exist in unit tests
59        try:
60            self.customer_id = generate_customer_id()
61        except TypeError:
62            self.customer_id = u'Z654321'
63        self.password = None
64        self.temp_password = None
65        return
66
67    def setTempPassword(self, user, password):
68        """Set a temporary password (LDAP-compatible) SSHA encoded for
69        officers.
70
71        """
72        passwordmanager = getUtility(IPasswordManager, 'SSHA')
73        self.temp_password = {}
74        self.temp_password[
75            'password'] = passwordmanager.encodePassword(password)
76        self.temp_password['user'] = user
77        self.temp_password['timestamp'] = datetime.utcnow()  # offset-naive datetime
78
79    def getTempPassword(self):
80        """Check if a temporary password has been set and if it
81        is not expired.
82
83        Return the temporary password if valid,
84        None otherwise. Unset the temporary password if expired.
85        """
86        temp_password_dict = getattr(self, 'temp_password', None)
87        if temp_password_dict is not None:
88            delta = timedelta(minutes=self.temp_password_minutes)
89            now = datetime.utcnow()
90            if now < temp_password_dict.get('timestamp') + delta:
91                return temp_password_dict.get('password')
92            else:
93                # Unset temporary password if expired
94                self.temp_password = None
95        return None
96
97    def writeLogMessage(self, view, message):
98        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
99        self.__parent__.logger.info(
100            '%s - %s - %s' % (ob_class, self.__name__, message))
101        return
102
103    @property
104    def display_fullname(self):
105        middlename = getattr(self, 'middlename', None)
106        ikoba_utils = getUtility(IIkobaUtils)
107        return ikoba_utils.fullname(self.firstname, self.lastname, middlename)
108
109    @property
110    def fullname(self):
111        middlename = getattr(self, 'middlename', None)
112        if middlename:
113            return '%s-%s-%s' % (self.firstname.lower(),
114                middlename.lower(), self.lastname.lower())
115        else:
116            return '%s-%s' % (self.firstname.lower(), self.lastname.lower())
117
118    @property
119    def state(self):
120        state = IWorkflowState(self).getState()
121        return state
122
123    @property
124    def translated_state(self):
125        try:
126            TRANSLATED_STATES = getUtility(ICustomersUtils).TRANSLATED_STATES
127            ts = TRANSLATED_STATES[self.state]
128            return ts
129        except KeyError:
130            return
131
132    @property
133    def history(self):
134        history = IObjectHistory(self)
135        return history
136
137    @property
138    def customer(self):
139        return self
140
141    @property
142    def personal_data_expired(self):
143        return False
144
145# Set all attributes of Customer required in ICustomer as field
146# properties. Doing this, we do not have to set initial attributes
147# ourselves and as a bonus we get free validation when an attribute is
148# set.
149Customer = attrs_to_fields(Customer)
150
151
152class CustomerFactory(grok.GlobalUtility):
153    """A factory for customers.
154    """
155    grok.implements(IFactory)
156    grok.name(u'waeup.Customer')
157    title = u"Create a new customer.",
158    description = u"This factory instantiates new customer instances."
159
160    def __call__(self, *args, **kw):
161        return Customer()
162
163    def getInterfaces(self):
164        return implementedBy(Customer)
165
166
167@grok.subscribe(ICustomer, grok.IObjectAddedEvent)
168def handle_customer_added(customer, event):
169    """If a customer is added all subcontainers are automatically added
170    and the transition create is fired. The latter produces a logging
171    message.
172    """
173    # Assign global customer role for new customer
174    account = IUserAccount(customer)
175    account.roles = ['waeup.Customer']
176    # Assign local CustomerRecordOwner role
177    role_manager = IPrincipalRoleManager(customer)
178    role_manager.assignRoleToPrincipal(
179        'waeup.local.CustomerRecordOwner', customer.customer_id)
180    if customer.state is None:
181        IWorkflowInfo(customer).fireTransition('create')
182    documents = CustomerDocumentsContainer()
183    customer['documents'] = documents
184    return
185
186def move_customer_files(customer, del_dir):
187    """Move files belonging to `customer` to `del_dir`.
188
189    `del_dir` is expected to be the path to the site-wide directory
190    for storing backup data.
191
192    The current files of the customer are removed after backup.
193
194    If the customer has no associated files stored, nothing is done.
195    """
196    cust_id = customer.customer_id
197
198    src = getUtility(IExtFileStore).root
199    src = os.path.join(src, 'customers', path_from_custid(cust_id))
200
201    dst = os.path.join(
202        del_dir, 'media', 'customers', path_from_custid(cust_id))
203
204    if not os.path.isdir(src):
205        # Do not copy if no files were stored.
206        return
207    if not os.path.exists(dst):
208        os.makedirs(dst, 0755)
209    copy_filesystem_tree(src, dst)
210    shutil.rmtree(src)
211    return
212
213
214def update_customer_deletion_csvs(customer, del_dir):
215    """Update deletion CSV files with data from customer.
216
217    `del_dir` is expected to be the path to the site-wide directory
218    for storing backup data.
219
220    Each exporter available for customers (and their many subobjects)
221    is called in order to export CSV data of the given customer to csv
222    files in the site-wide backup directory for object data (see
223    DataCenter).
224
225    Each exported row is appended a column giving the deletion date
226    (column `del_date`) as a UTC timestamp.
227    """
228    for name in EXPORTER_NAMES:
229        exporter = getUtility(ICSVCustomerExporter, name=name)
230        csv_data = exporter.export_customer(customer)
231        csv_data = csv_data.split('\r\n')
232
233        # append a deletion timestamp on each data row
234        timestamp = str(now().replace(microsecond=0))  # store UTC timestamp
235        for num, row in enumerate(csv_data[1:-1]):
236            csv_data[num+1] = csv_data[num+1] + ',' + timestamp
237        csv_path = os.path.join(del_dir, '%s.csv' % name)
238
239        # write data to CSV file
240        if not os.path.exists(csv_path):
241            # create new CSV file (including header line)
242            csv_data[0] = csv_data[0] + ',del_date'
243            open(csv_path, 'wb').write('\r\n'.join(csv_data))
244        else:
245            # append existing CSV file (omitting headerline)
246            open(csv_path, 'a').write('\r\n'.join(csv_data[1:]))
247    return
248
249
250@grok.subscribe(ICustomer, grok.IObjectRemovedEvent)
251def handle_customer_removed(customer, event):
252    """If a customer is removed a message is logged and data is put
253       into a backup location.
254
255    The data of the removed customer is appended to CSV files in local
256    datacenter and any existing external files (passport images, etc.)
257    are copied over to this location as well.
258
259    Documents in the file storage refering to the given customer are
260    removed afterwards (if they exist). Please make no assumptions
261    about how the deletion takes place. Files might be deleted
262    individually (leaving the customers file directory intact) or the
263    whole customer directory might be deleted completely.
264
265    All CSV rows created/appended contain a timestamp with the
266    datetime of removal in an additional `del_date` column.
267
268    XXX: blocking of used customer_ids yet not implemented.
269    """
270    comment = 'Customer record removed'
271    target = customer.customer_id
272    try:
273        site = grok.getSite()
274        site['customers'].logger.info('%s - %s' % (
275            target, comment))
276    except KeyError:
277        # If we delete an entire university instance there won't be
278        # a customers subcontainer
279        return
280
281    del_dir = site['datacenter'].deleted_path
282
283    # save files of the customer
284    move_customer_files(customer, del_dir)
285
286    # update CSV files
287    update_customer_deletion_csvs(customer, del_dir)
288    return
289
290#: The file id marker for customer files
291CUSTOMER_FILE_STORE_NAME = 'file-customer'
292
293
294class CustomerFileNameChooser(grok.Adapter):
295    """A file id chooser for :class:`Customer` objects.
296
297    `context` is an :class:`Customer` instance.
298
299    The :class:`CustomerFileNameChooser` can build/check file ids for
300    :class:`Customer` objects suitable for use with
301    :class:`ExtFileStore` instances. The delivered file_id contains
302    the file id marker for :class:`Customer` object and the customer id
303    of the context customer.
304
305    This chooser is registered as an adapter providing
306    :class:`waeup.ikoba.interfaces.IFileStoreNameChooser`.
307
308    File store name choosers like this one are only convenience
309    components to ease the task of creating file ids for customer
310    objects. You are nevertheless encouraged to use them instead of
311    manually setting up filenames for customers.
312
313    .. seealso:: :mod:`waeup.ikoba.imagestorage`
314
315    """
316    grok.context(ICustomer)
317    grok.implements(IFileStoreNameChooser)
318
319    def checkName(self, name=None, attr=None):
320        """Check whether the given name is a valid file id for the context.
321
322        Returns ``True`` only if `name` equals the result of
323        :meth:`chooseName`.
324
325        """
326        return name == self.chooseName()
327
328    def chooseName(self, attr, name=None):
329        """Get a valid file id for customer context.
330
331        *Example:*
332
333        For a customer with customer id ``'A123456'`` and
334        with attr ``'nice_image.jpeg'`` stored in
335        the customers container this chooser would create:
336
337          ``'__file-customer__customers/A/A123456/nice_image_A123456.jpeg'``
338
339        meaning that the nice image of this customer would be
340        stored in the site-wide file storage in path:
341
342          ``customers/A/A123456/nice_image_A123456.jpeg``
343
344        """
345        basename, ext = os.path.splitext(attr)
346        cust_id = self.context.customer_id
347        marked_filename = '__%s__%s/%s_%s%s' % (
348            CUSTOMER_FILE_STORE_NAME, path_from_custid(cust_id), basename,
349            cust_id, ext)
350        return marked_filename
351
352
353class CustomerFileStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
354    """Customer specific file handling.
355
356    This handler knows in which path in a filestore to store customer
357    files and how to turn this kind of data into some (browsable)
358    file object.
359
360    It is called from the global file storage, when it wants to
361    get/store a file with a file id starting with
362    ``__file-customer__`` (the marker string for customer files).
363
364    Like each other file store handler it does not handle the files
365    really (this is done by the global file store) but only computes
366    paths and things like this.
367    """
368    grok.implements(IFileStoreHandler)
369    grok.name(CUSTOMER_FILE_STORE_NAME)
370
371    def pathFromFileID(self, store, root, file_id):
372        """All customer files are put in directory ``customers``.
373        """
374        marker, filename, basename, ext = store.extractMarker(file_id)
375        sub_root = os.path.join(root, 'customers')
376        return super(CustomerFileStoreHandler, self).pathFromFileID(
377            store, sub_root, basename)
378
379    def createFile(self, store, root, filename, file_id, file):
380        """Create a browsable file-like object.
381        """
382        # call super method to ensure that any old files with
383        # different filename extension are deleted.
384        file, path, file_obj = super(
385            CustomerFileStoreHandler, self).createFile(
386            store, root,  filename, file_id, file)
387        return file, path, IkobaImageFile(
388            file_obj.filename, file_obj.data)
Note: See TracBrowser for help on using the repository browser.