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

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

The term 'application' should really not be used in Python-based portal software.

Replace 'application' by 'contract': batch 1

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