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

Last change on this file since 8583 was 8581, checked in by Henrik Bettermann, 13 years ago

We have to customize also those interfaces which are used for string conversion only. Prepare ApplicantProcessor? and StudentProcessor? for customization.

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