## $Id: batching.py 8290 2012-04-26 17:01:32Z henrik $ ## ## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## """Batch processing for applicants. """ import csv import grok from zope.schema import getFields from zope.interface import Interface from zope.component import queryUtility from hurry.workflow.interfaces import IWorkflowState from zope.catalog.interfaces import ICatalog from waeup.kofa.interfaces import ( IBatchProcessor, IObjectConverter, FatalCSVError, IGNORE_MARKER, IObjectHistory, IUserAccount) from waeup.kofa.interfaces import MessageFactory as _ from waeup.kofa.utils.batching import BatchProcessor from waeup.kofa.applicants.interfaces import ( IApplicantsContainer, IApplicant, IApplicantUpdateByRegNo) from waeup.kofa.applicants.workflow import IMPORTABLE_STATES class ApplicantsContainerProcessor(BatchProcessor): """A processor for applicants containers. """ grok.implements(IBatchProcessor) grok.provides(IBatchProcessor) grok.context(Interface) util_name = 'applicants container processor' grok.name(util_name) name = u'Applicants Container Processor' mode = u'create' iface = IApplicantsContainer location_fields = ['code',] factory_name = 'waeup.ApplicantsContainer' def parentsExist(self, row, site): return 'applicants' in site.keys() def entryExists(self, row, site): return row['code'] in site['applicants'].keys() def getParent(self, row, site): return site['applicants'] def getEntry(self, row, site): if not self.entryExists(row, site): return None parent = self.getParent(row, site) return parent.get(row['code']) def addEntry(self, obj, row, site): parent = self.getParent(row, site) parent[row['code']] = obj return def delEntry(self, row, site): parent = self.getParent(row, site) del parent[row['code']] return class ApplicantProcessor(BatchProcessor): """A batch processor for IApplicant objects. """ grok.implements(IBatchProcessor) grok.provides(IBatchProcessor) grok.context(Interface) util_name = 'applicantprocessor' grok.name(util_name) name = u'Applicant Processor' iface = IApplicant location_fields = ['container_code'] factory_name = 'waeup.Applicant' mode = None @property def available_fields(self): return sorted(list(set( ['application_number','reg_number', 'container_code','state','password'] + getFields( self.iface).keys()))) def checkHeaders(self, headerfields, mode='create'): if not 'reg_number' in headerfields and not 'application_number' \ in headerfields: raise FatalCSVError( "Need at least columns application_number or reg_number ") if mode == 'create': for field in self.required_fields: if not field in headerfields: raise FatalCSVError( "Need at least columns %s for import!" % ', '.join(["'%s'" % x for x in self.required_fields])) # Check for fields to be ignored... not_ignored_fields = [x for x in headerfields if not x.startswith('--')] if len(set(not_ignored_fields)) < len(not_ignored_fields): raise FatalCSVError( "Double headers: each column name may only appear once.") return True def getLocator(self, row): if row.get('application_number', None) not in (IGNORE_MARKER, None): return 'application_number' elif row.get('reg_number', None) not in (IGNORE_MARKER, None): return 'reg_number' else: return None def getParent(self, row, site): if not 'applicants' in site.keys(): return False return site['applicants'][row['container_code']] def parentsExist(self, row, site): return self.getParent(row, site) is not None def getEntry(self, row, site): if not self.parentsExist(row, site): return None parent = self.getParent(row, site) if self.getLocator(row) == 'application_number': if row['application_number'] in parent: applicant = parent[row['application_number']] return applicant elif self.getLocator(row) == 'reg_number': reg_number = row['reg_number'] cat = queryUtility(ICatalog, name='applicants_catalog') results = list( cat.searchResults(reg_number=(reg_number, reg_number))) if results: return results[0] return None def entryExists(self, row, site): return self.getEntry(row, site) is not None def addEntry(self, obj, row, site): parent = self.getParent(row, site) parent.addApplicant(obj) # We have to log this if state is provided. If not, # logging is done by the event handler handle_applicant_added if row.has_key('state'): parent.__parent__.logger.info( '%s - Application initialized' % obj.applicant_id) history = IObjectHistory(obj) history.addMessage(_('Application initialized')) return def delEntry(self, row, site): applicant = self.getEntry(row, site) if applicant is not None: parent = self.getParent(row, site) del parent[applicant.application_number] pass def updateEntry(self, obj, row, site): """Update obj to the values given in row. """ items_changed = '' # Remove application_number from row if empty if row.has_key('application_number') and row['application_number'] in ( None, IGNORE_MARKER): row.pop('application_number') # Update applicant_id fom application_number and container code # if application_number is given if row.has_key('application_number'): obj.applicant_id = u'%s_%s' % ( row['container_code'], row['application_number']) row.pop('application_number') # Update password passwd = row.get('password', IGNORE_MARKER) if passwd not in ('', IGNORE_MARKER): IUserAccount(obj).setPassword(passwd) row.pop('password') # Update registration state state = row.get('state', IGNORE_MARKER) if state not in (IGNORE_MARKER, ''): value = row['state'] IWorkflowState(obj).setState(value) msg = _("State '${a}' set", mapping = {'a':value}) history = IObjectHistory(obj) history.addMessage(msg) row.pop('state') # apply other values... items_changed = super(ApplicantProcessor, self).updateEntry( obj, row, site) # Log actions... parent = self.getParent(row, site) if hasattr(obj,'application_number'): # Update mode: the applicant exists and we can get the applicant_id parent.__parent__.logger.info( '%s - Application record updated: %s' % (obj.applicant_id, items_changed)) else: # Create mode: the applicant does not yet exist parent.logger.info('Application record imported: %s' % items_changed) return items_changed def getMapping(self, path, headerfields, mode): """Get a mapping from CSV file headerfields to actually used fieldnames. """ result = dict() reader = csv.reader(open(path, 'rb')) raw_header = reader.next() for num, field in enumerate(headerfields): if field not in ['container_code', 'application_number', 'reg_number'] and mode == 'remove': continue if field == u'--IGNORE--': # Skip ignored columns in failed and finished data files. continue result[raw_header[num]] = field return result def checkConversion(self, row, mode='create'): """Validates all values in row. """ if mode in ['update', 'remove']: if self.getLocator(row) == 'reg_number': iface = IApplicantUpdateByRegNo else: iface = self.iface converter = IObjectConverter(iface) errs, inv_errs, conv_dict = converter.fromStringDict( row, self.factory_name, mode=mode) if row.has_key('state') and \ not row['state'] in IMPORTABLE_STATES: if row['state'] not in (IGNORE_MARKER, ''): errs.append(('state','not allowed')) else: # state is an attribute of Applicant and must not # be changed if empty conv_dict['state'] = IGNORE_MARKER application_number = row.get('application_number', IGNORE_MARKER) if application_number in (IGNORE_MARKER, ''): conv_dict['application_number'] = IGNORE_MARKER return errs, inv_errs, conv_dict