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

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

Make EXPORTER_NAMES tuples customizable. We have many new subobject classes in custom packages.

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