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

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

Applicants in state created are blocked. They can't be edited by import.

  • Property svn:keywords set to Id
File size: 12.4 KB
Line 
1## $Id: batching.py 8336 2012-05-04 05:14:49Z henrik $
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                IUserAccount(obj).setPassword(passwd)
243                items_changed += ('%s=%s, ' % ('password', passwd))
244            row.pop('password')
245
246        # Update registration state
247        if row.has_key('state'):
248            state = row.get('state', IGNORE_MARKER)
249            if state not in (IGNORE_MARKER, ''):
250                IWorkflowState(obj).setState(state)
251                msg = _("State '${a}' set", mapping = {'a':state})
252                history = IObjectHistory(obj)
253                history.addMessage(msg)
254                items_changed += ('%s=%s, ' % ('state', state))
255            row.pop('state')
256
257        # apply other values...
258        items_changed += super(ApplicantProcessor, self).updateEntry(
259            obj, row, site)
260
261        # Log actions...
262        parent = self.getParent(row, site)
263        if self.getLocator(row) == 'container_code':
264            # Update mode: the applicant exists and we can get the applicant_id
265            parent.__parent__.logger.info(
266                'Applicant imported: %s' % items_changed)
267        else:
268            # Create mode: the applicant does not yet exist
269            parent.__parent__.logger.info(
270                'Applicant updated: %s' % items_changed)
271        return items_changed
272
273    def getMapping(self, path, headerfields, mode):
274        """Get a mapping from CSV file headerfields to actually used fieldnames.
275        """
276        result = dict()
277        reader = csv.reader(open(path, 'rb'))
278        raw_header = reader.next()
279        for num, field in enumerate(headerfields):
280            if field not in ['applicant_id', 'reg_number'] and mode == 'remove':
281                continue
282            if field == u'--IGNORE--':
283                # Skip ignored columns in failed and finished data files.
284                continue
285            result[raw_header[num]] = field
286        return result
287
288    def checkConversion(self, row, mode='create'):
289        """Validates all values in row.
290        """
291        iface = self.iface
292        if self.getLocator(row) == 'reg_number' or mode == 'remove':
293            iface = IApplicantUpdateByRegNo
294        converter = IObjectConverter(iface)
295        errs, inv_errs, conv_dict =  converter.fromStringDict(
296            row, self.factory_name, mode=mode)
297        if row.has_key('state') and \
298            not row['state'] in IMPORTABLE_STATES:
299            if row['state'] not in (IGNORE_MARKER, ''):
300                errs.append(('state','not allowed'))
301            else:
302                # state is an attribute of Applicant and must not
303                # be changed if empty
304                conv_dict['state'] = IGNORE_MARKER
305        application_number = row.get('application_number', None)
306        if application_number in (IGNORE_MARKER, ''):
307                conv_dict['application_number'] = IGNORE_MARKER
308        return errs, inv_errs, conv_dict
309
310    def checkUpdateRequirements(self, obj, row, site):
311        """Checks requirements the object must fulfill when being updated.
312
313        This method is not used in case of deleting or adding objects.
314
315        Returns error messages as strings in case of requirement
316        problems.
317        """
318        if obj.state == CREATED:
319            return 'Applicant is blocked.'
320        return None
Note: See TracBrowser for help on using the repository browser.