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

Last change on this file since 8581 was 8581, checked in by Henrik Bettermann, 12 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
Line 
1## $Id: batching.py 8581 2012-05-31 20:19:58Z 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    iface_byregnumber = IApplicantUpdateByRegNo
97    location_fields = ['']
98    factory_name = 'waeup.Applicant'
99
100    mode = None
101
102    @property
103    def available_fields(self):
104        return sorted(list(set(
105            ['application_number',
106            'container_code','state','password'] + getFields(
107                self.iface).keys())))
108
109    def checkHeaders(self, headerfields, mode='create'):
110        cond1 = 'container_code' in headerfields
111        cond2 = 'application_number' in headerfields
112        cond3 = 'applicant_id' in headerfields
113        cond4 = 'reg_number' in headerfields
114        if mode == 'create':
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!")
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]))
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!")
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
141
142    def getLocator(self, row):
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'
149        elif row.get('reg_number', None) not in (IGNORE_MARKER, None):
150            # update or remove
151            return 'reg_number'
152        else:
153            return None
154
155    def getParent(self, row, site):
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
173
174    def parentsExist(self, row, site):
175        return self.getParent(row, site) is not None
176
177    def getEntry(self, row, site):
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'])
184            return None
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':
193            reg_number = row['reg_number']
194            cat = queryUtility(ICatalog, name='applicants_catalog')
195            results = list(
196                cat.searchResults(reg_number=(reg_number, reg_number)))
197            if results:
198                return results[0]
199        return None
200
201    def entryExists(self, row, site):
202        return self.getEntry(row, site) is not None
203
204    def addEntry(self, obj, row, site):
205        parent = self.getParent(row, site)
206        parent.addApplicant(obj)
207        #parent.__parent__.logger.info(
208        #    'Applicant imported: %s' % obj.applicant_id)
209        history = IObjectHistory(obj)
210        history.addMessage(_('Application record imported'))
211        return
212
213    def delEntry(self, row, site):
214        applicant = self.getEntry(row, site)
215        if applicant is not None:
216            parent = applicant.__parent__
217            del parent[applicant.application_number]
218            #parent.__parent__.logger.info(
219            #    'Applicant removed: %s' % applicant.applicant_id)
220        pass
221
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'])
236            items_changed += ('%s=%s, ' % ('applicant_id', obj.applicant_id))
237            row.pop('application_number')
238
239        # Update password
240        if row.has_key('password'):
241            passwd = row.get('password', IGNORE_MARKER)
242            if passwd not in ('', IGNORE_MARKER):
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)
249                items_changed += ('%s=%s, ' % ('password', passwd))
250            row.pop('password')
251
252        # Update registration state
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))
261            row.pop('state')
262
263        # apply other values...
264        items_changed += super(ApplicantProcessor, self).updateEntry(
265            obj, row, site)
266
267        # Log actions...
268        parent = self.getParent(row, site)
269        if self.getLocator(row) == 'container_code':
270            # Update mode: the applicant exists and we can get the applicant_id
271            parent.__parent__.logger.info(
272                'Applicant imported: %s' % items_changed)
273        else:
274            # Create mode: the applicant does not yet exist
275            parent.__parent__.logger.info(
276                'Applicant updated: %s' % items_changed)
277        return items_changed
278
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):
286            if field not in ['applicant_id', 'reg_number'] and mode == 'remove':
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        """
297        iface = self.iface
298        if self.getLocator(row) == 'reg_number' or mode == 'remove':
299            iface = self.iface_byregnumber
300        converter = IObjectConverter(iface)
301        errs, inv_errs, conv_dict =  converter.fromStringDict(
302            row, self.factory_name, mode=mode)
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
311        application_number = row.get('application_number', None)
312        if application_number in (IGNORE_MARKER, ''):
313                conv_dict['application_number'] = IGNORE_MARKER
314        return errs, inv_errs, conv_dict
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.