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

Last change on this file since 17421 was 13261, checked in by Henrik Bettermann, 9 years ago

Remove global role when removing a customer.

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