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

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

Fix fullname property.

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