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

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

Notify customer by email after customer and contract transitions.

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