source: main/waeup.kofa/trunk/src/waeup/kofa/applicants/batching.py @ 8547

Last change on this file since 8547 was 8348, checked in by uli, 13 years ago

Handle encrypted and unencrypted passwords different on import (UNTESTED!)

  • Property svn:keywords set to Id
File size: 12.6 KB
Line 
1## $Id: batching.py 8348 2012-05-04 22:48:31Z uli $
2##
3## Copyright (C) 2011 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"""Batch processing for applicants.
19"""
20import csv
21import grok
22from zope.schema import getFields
23from zope.interface import Interface
24from zope.component import queryUtility
25from hurry.workflow.interfaces import IWorkflowState
26from zope.catalog.interfaces import ICatalog
27from waeup.kofa.interfaces import (
28    IBatchProcessor, IObjectConverter, FatalCSVError, IGNORE_MARKER,
29    IObjectHistory, IUserAccount)
30from waeup.kofa.interfaces import MessageFactory as _
31from waeup.kofa.utils.batching import BatchProcessor
32from waeup.kofa.applicants.interfaces import (
33    IApplicantsContainer, IApplicant, IApplicantUpdateByRegNo)
34from waeup.kofa.applicants.workflow import  IMPORTABLE_STATES, CREATED
35
36class ApplicantsContainerProcessor(BatchProcessor):
37    """A processor for applicants containers.
38    """
39    grok.implements(IBatchProcessor)
40    grok.provides(IBatchProcessor)
41    grok.context(Interface)
42    util_name = 'applicants container processor'
43    grok.name(util_name)
44
45    name = u'Applicants Container Processor'
46    mode = u'create'
47    iface = IApplicantsContainer
48
49    location_fields = ['code',]
50    factory_name = 'waeup.ApplicantsContainer'
51
52    def parentsExist(self, row, site):
53        return 'applicants' in site.keys()
54
55    def entryExists(self, row, site):
56        return row['code'] in site['applicants'].keys()
57
58    def getParent(self, row, site):
59        return site['applicants']
60
61    def getEntry(self, row, site):
62        if not self.entryExists(row, site):
63            return None
64        parent = self.getParent(row, site)
65        return parent.get(row['code'])
66
67    def addEntry(self, obj, row, site):
68        parent = self.getParent(row, site)
69        parent[row['code']] = obj
70        return
71
72    def delEntry(self, row, site):
73        parent = self.getParent(row, site)
74        del parent[row['code']]
75        return
76
77class ApplicantProcessor(BatchProcessor):
78    """A batch processor for IApplicant objects.
79
80    In create mode container_code is required. If application_number is given
81    an applicant with this number is created in the designated container.
82    If application_number is not given a random application_number is assigned.
83    applicant_id is being determined by the system and can't be imported.
84
85    In update or remove mode container_code and application_number columns
86    must not exist. The applicant object is solely searched by its applicant_id
87    or reg_number.
88    """
89    grok.implements(IBatchProcessor)
90    grok.provides(IBatchProcessor)
91    grok.context(Interface)
92    util_name = 'applicantprocessor'
93    grok.name(util_name)
94    name = u'Applicant Processor'
95    iface = IApplicant
96    location_fields = ['']
97    factory_name = 'waeup.Applicant'
98
99    mode = None
100
101    @property
102    def available_fields(self):
103        return sorted(list(set(
104            ['application_number',
105            'container_code','state','password'] + getFields(
106                self.iface).keys())))
107
108    def checkHeaders(self, headerfields, mode='create'):
109        cond1 = 'container_code' in headerfields
110        cond2 = 'application_number' in headerfields
111        cond3 = 'applicant_id' in headerfields
112        cond4 = 'reg_number' in headerfields
113        if mode == 'create':
114            if not cond1:
115                raise FatalCSVError(
116                    "Need at least container_code column!")
117            if cond3:
118                raise FatalCSVError(
119                    "applicant_id can't be imported in create mode!")
120            for field in self.required_fields:
121                if not field in headerfields:
122                    raise FatalCSVError(
123                        "Need at least columns %s for import!" %
124                        ', '.join(["'%s'" % x for x in self.required_fields]))
125        if mode in ('update', 'remove'):
126            if not cond3 and not cond4:
127                raise FatalCSVError(
128                    "Need at least column reg_number or applicant_id!")
129            if cond1 or cond2:
130                raise FatalCSVError(
131                    "container_code or application_number can't be imported " +
132                    "in update or remove mode!")
133        # Check for fields to be ignored...
134        not_ignored_fields = [x for x in headerfields
135                              if not x.startswith('--')]
136        if len(set(not_ignored_fields)) < len(not_ignored_fields):
137            raise FatalCSVError(
138                "Double headers: each column name may only appear once.")
139        return True
140
141    def getLocator(self, row):
142        if row.get('container_code', None) not in (IGNORE_MARKER, None):
143            # create, update or remove
144            return 'container_code'
145        elif row.get('applicant_id', None) not in (IGNORE_MARKER, None):
146            # update or remove
147            return 'applicant_id'
148        elif row.get('reg_number', None) not in (IGNORE_MARKER, None):
149            # update or remove
150            return 'reg_number'
151        else:
152            return None
153
154    def getParent(self, row, site):
155        if self.getLocator(row) == 'container_code':
156            return site['applicants'].get(row['container_code'], None)
157        if self.getLocator(row) == 'reg_number':
158            reg_number = row['reg_number']
159            cat = queryUtility(ICatalog, name='applicants_catalog')
160            results = list(
161                cat.searchResults(reg_number=(reg_number, reg_number)))
162            if results:
163                return results[0].__parent__
164        if self.getLocator(row) == 'applicant_id':
165            applicant_id = row['applicant_id']
166            cat = queryUtility(ICatalog, name='applicants_catalog')
167            results = list(
168                cat.searchResults(applicant_id=(applicant_id, applicant_id)))
169            if results:
170                return results[0].__parent__
171        return None
172
173    def parentsExist(self, row, site):
174        return self.getParent(row, site) is not None
175
176    def getEntry(self, row, site):
177        if self.getLocator(row) == 'container_code':
178            if row.get('application_number', None) not in (IGNORE_MARKER, None):
179                if not self.parentsExist(row, site):
180                    return None
181                parent = self.getParent(row, site)
182                return parent.get(row['application_number'])
183            return None
184        if self.getLocator(row) == 'applicant_id':
185            applicant_id = row['applicant_id']
186            cat = queryUtility(ICatalog, name='applicants_catalog')
187            results = list(
188                cat.searchResults(applicant_id=(applicant_id, applicant_id)))
189            if results:
190                return results[0]
191        if self.getLocator(row) == 'reg_number':
192            reg_number = row['reg_number']
193            cat = queryUtility(ICatalog, name='applicants_catalog')
194            results = list(
195                cat.searchResults(reg_number=(reg_number, reg_number)))
196            if results:
197                return results[0]
198        return None
199
200    def entryExists(self, row, site):
201        return self.getEntry(row, site) is not None
202
203    def addEntry(self, obj, row, site):
204        parent = self.getParent(row, site)
205        parent.addApplicant(obj)
206        #parent.__parent__.logger.info(
207        #    'Applicant imported: %s' % obj.applicant_id)
208        history = IObjectHistory(obj)
209        history.addMessage(_('Application record imported'))
210        return
211
212    def delEntry(self, row, site):
213        applicant = self.getEntry(row, site)
214        if applicant is not None:
215            parent = applicant.__parent__
216            del parent[applicant.application_number]
217            #parent.__parent__.logger.info(
218            #    'Applicant removed: %s' % applicant.applicant_id)
219        pass
220
221    def updateEntry(self, obj, row, site):
222        """Update obj to the values given in row.
223        """
224        items_changed = ''
225        # Remove application_number from row if empty
226        if row.has_key('application_number') and row['application_number'] in (
227            None, IGNORE_MARKER):
228            row.pop('application_number')
229
230        # Update applicant_id fom application_number and container code
231        # if application_number is given
232        if row.has_key('application_number'):
233            obj.applicant_id = u'%s_%s' % (
234                row['container_code'], row['application_number'])
235            items_changed += ('%s=%s, ' % ('applicant_id', obj.applicant_id))
236            row.pop('application_number')
237
238        # Update password
239        if row.has_key('password'):
240            passwd = row.get('password', IGNORE_MARKER)
241            if passwd not in ('', IGNORE_MARKER):
242                if passwd.startswith('{SSHA}'):
243                    # already encrypted password
244                    obj.password = passwd
245                else:
246                    # not yet encrypted password
247                    IUserAccount(obj).setPassword(passwd)
248                items_changed += ('%s=%s, ' % ('password', passwd))
249            row.pop('password')
250
251        # Update registration state
252        if row.has_key('state'):
253            state = row.get('state', IGNORE_MARKER)
254            if state not in (IGNORE_MARKER, ''):
255                IWorkflowState(obj).setState(state)
256                msg = _("State '${a}' set", mapping = {'a':state})
257                history = IObjectHistory(obj)
258                history.addMessage(msg)
259                items_changed += ('%s=%s, ' % ('state', state))
260            row.pop('state')
261
262        # apply other values...
263        items_changed += super(ApplicantProcessor, self).updateEntry(
264            obj, row, site)
265
266        # Log actions...
267        parent = self.getParent(row, site)
268        if self.getLocator(row) == 'container_code':
269            # Update mode: the applicant exists and we can get the applicant_id
270            parent.__parent__.logger.info(
271                'Applicant imported: %s' % items_changed)
272        else:
273            # Create mode: the applicant does not yet exist
274            parent.__parent__.logger.info(
275                'Applicant updated: %s' % items_changed)
276        return items_changed
277
278    def getMapping(self, path, headerfields, mode):
279        """Get a mapping from CSV file headerfields to actually used fieldnames.
280        """
281        result = dict()
282        reader = csv.reader(open(path, 'rb'))
283        raw_header = reader.next()
284        for num, field in enumerate(headerfields):
285            if field not in ['applicant_id', 'reg_number'] and mode == 'remove':
286                continue
287            if field == u'--IGNORE--':
288                # Skip ignored columns in failed and finished data files.
289                continue
290            result[raw_header[num]] = field
291        return result
292
293    def checkConversion(self, row, mode='create'):
294        """Validates all values in row.
295        """
296        iface = self.iface
297        if self.getLocator(row) == 'reg_number' or mode == 'remove':
298            iface = IApplicantUpdateByRegNo
299        converter = IObjectConverter(iface)
300        errs, inv_errs, conv_dict =  converter.fromStringDict(
301            row, self.factory_name, mode=mode)
302        if row.has_key('state') and \
303            not row['state'] in IMPORTABLE_STATES:
304            if row['state'] not in (IGNORE_MARKER, ''):
305                errs.append(('state','not allowed'))
306            else:
307                # state is an attribute of Applicant and must not
308                # be changed if empty
309                conv_dict['state'] = IGNORE_MARKER
310        application_number = row.get('application_number', None)
311        if application_number in (IGNORE_MARKER, ''):
312                conv_dict['application_number'] = IGNORE_MARKER
313        return errs, inv_errs, conv_dict
314
315    def checkUpdateRequirements(self, obj, row, site):
316        """Checks requirements the object must fulfill when being updated.
317
318        This method is not used in case of deleting or adding objects.
319
320        Returns error messages as strings in case of requirement
321        problems.
322        """
323        if obj.state == CREATED:
324            return 'Applicant is blocked.'
325        return None
Note: See TracBrowser for help on using the repository browser.