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

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

See previous revision comment.

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