source: main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py @ 8219

Last change on this file since 8219 was 8214, checked in by uli, 13 years ago

Lots of trash to sketch filtered imports.

  • Property svn:keywords set to Id
File size: 27.9 KB
RevLine 
[7191]1## $Id: batching.py 8214 2012-04-19 12:43:37Z 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##
[7433]18"""Batch processing components for student objects.
[6821]19
20Batch processors eat CSV files to add, update or remove large numbers
21of certain kinds of objects at once.
22
[7261]23Here we define the processors for students specific objects like
24students, studycourses, payment tickets and accommodation tickets.
[6821]25"""
26import grok
[6849]27import csv
[6821]28from zope.interface import Interface
[6825]29from zope.schema import getFields
[7548]30from zope.component import queryUtility, getUtility
[7429]31from zope.event import notify
[6825]32from zope.catalog.interfaces import ICatalog
[7951]33from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
[7811]34from waeup.kofa.interfaces import (
[7522]35    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
[8214]36    IObjectHistory, VALIDATED, DELETION_MARKER, IGNORE_MARKER)
[7959]37from waeup.kofa.interfaces import MessageFactory as _
[7811]38from waeup.kofa.students.interfaces import (
[7532]39    IStudent, IStudentStudyCourse,
[7536]40    IStudentUpdateByRegNo, IStudentUpdateByMatricNo,
[7623]41    IStudentStudyLevel, ICourseTicket,
[8174]42    IStudentOnlinePayment, IStudentVerdictUpdate)
[7811]43from waeup.kofa.students.workflow import  IMPORTABLE_STATES
44from waeup.kofa.utils.batching import BatchProcessor
[6821]45
46class StudentProcessor(BatchProcessor):
47    """A batch processor for IStudent objects.
48    """
49    grok.implements(IBatchProcessor)
50    grok.provides(IBatchProcessor)
51    grok.context(Interface)
[7933]52    util_name = 'studentprocessor'
[6821]53    grok.name(util_name)
54
[7933]55    name = u'Student Processor'
[6821]56    iface = IStudent
57
[6849]58    location_fields = []
[6821]59    factory_name = 'waeup.Student'
60
[6841]61    mode = None
62
[6821]63    @property
[6849]64    def available_fields(self):
[8176]65        fields = getFields(self.iface)
[6849]66        return sorted(list(set(
[7513]67            ['student_id','reg_number','matric_number',
68            'password', 'reg_state'] + getFields(
[6849]69                self.iface).keys())))
[6821]70
[6849]71    def checkHeaders(self, headerfields, mode='create'):
[6854]72        if not 'reg_number' in headerfields and not 'student_id' \
73            in headerfields and not 'matric_number' in headerfields:
[6849]74            raise FatalCSVError(
[6854]75                "Need at least columns student_id or reg_number " +
76                "or matric_number for import!")
[6849]77        if mode == 'create':
78            for field in self.required_fields:
79                if not field in headerfields:
80                    raise FatalCSVError(
81                        "Need at least columns %s for import!" %
82                        ', '.join(["'%s'" % x for x in self.required_fields]))
83        # Check for fields to be ignored...
84        not_ignored_fields = [x for x in headerfields
85                              if not x.startswith('--')]
86        if len(set(not_ignored_fields)) < len(not_ignored_fields):
87            raise FatalCSVError(
88                "Double headers: each column name may only appear once.")
89        return True
90
[6821]91    def parentsExist(self, row, site):
92        return 'students' in site.keys()
93
[6849]94    def getLocator(self, row):
[7269]95        if row.get('student_id',None):
[6849]96            return 'student_id'
[7269]97        elif row.get('reg_number',None):
[6849]98            return 'reg_number'
[7269]99        elif row.get('matric_number',None):
[6849]100            return 'matric_number'
101        else:
102            return None
103
[6821]104    # The entry never exists in create mode.
105    def entryExists(self, row, site):
[7267]106        return self.getEntry(row, site) is not None
107
108    def getParent(self, row, site):
109        return site['students']
110
111    def getEntry(self, row, site):
[6846]112        if not 'students' in site.keys():
[6849]113            return None
114        if self.getLocator(row) == 'student_id':
[6846]115            if row['student_id'] in site['students']:
116                student = site['students'][row['student_id']]
117                return student
[6849]118        elif self.getLocator(row) == 'reg_number':
[6846]119            reg_number = row['reg_number']
120            cat = queryUtility(ICatalog, name='students_catalog')
121            results = list(
122                cat.searchResults(reg_number=(reg_number, reg_number)))
123            if results:
124                return results[0]
[6849]125        elif self.getLocator(row) == 'matric_number':
[6846]126            matric_number = row['matric_number']
127            cat = queryUtility(ICatalog, name='students_catalog')
128            results = list(
129                cat.searchResults(matric_number=(matric_number, matric_number)))
130            if results:
131                return results[0]
[6849]132        return None
[6821]133
134    def addEntry(self, obj, row, site):
135        parent = self.getParent(row, site)
136        parent.addStudent(obj)
[7959]137        # We have to log this if reg_state is provided. If not,
138        # logging is done by the event handler handle_student_added
139        if row.has_key('reg_state'):
140            parent.logger.info('%s - Student record created' % obj.student_id)
[7656]141        history = IObjectHistory(obj)
[7959]142        history.addMessage(_('Student record created'))
[6821]143        return
144
145    def delEntry(self, row, site):
[7267]146        student = self.getEntry(row, site)
[7263]147        if student is not None:
[6846]148            parent = self.getParent(row, site)
[7656]149            parent.logger.info('%s - Student removed' % student.student_id)
[6846]150            del parent[student.student_id]
[6821]151        pass
[6825]152
[7497]153    def updateEntry(self, obj, row, site):
154        """Update obj to the values given in row.
155        """
[7643]156        # Remove student_id from row if empty
157        if row.has_key('student_id') and row['student_id'] is None:
158            row.pop('student_id')
[7656]159        items_changed = ''
[7497]160        for key, value in row.items():
161            # Set student password and all fields declared in interface.
[7522]162            if key == 'password' and value != '':
[7497]163                IUserAccount(obj).setPassword(value)
[7513]164            elif key == 'reg_state':
165                IWorkflowState(obj).setState(value)
[7959]166                msg = _("State '${a}' set", mapping = {'a':value})
[7522]167                history = IObjectHistory(obj)
168                history.addMessage(msg)
[7497]169            elif hasattr(obj, key):
[8202]170                # Set attribute to None if value is marked for deletion
[8214]171                #if value == DELETION_MARKER:
172                #    setattr(obj, key, None)
173                #elif value is not None:
174                #    setattr(obj, key, value)
175                #else:
176                #    # No change, no change log
177                #    continue
178                if value != '<IGNORE>':
[8202]179                    setattr(obj, key, value)
[8214]180               
181                    items_changed += '%s=%s, ' % (key,value)
[7656]182        parent = self.getParent(row, site)
183        if hasattr(obj,'student_id'):
184            # Update mode: the student exists and we can get the student_id
185            parent.logger.info(
186                '%s - Student record updated: %s'
187                % (obj.student_id, items_changed))
188        else:
189            # Create mode: the student does not yet exist
190            parent.logger.info('Student data imported: %s' % items_changed)
[7497]191        return
192
[6849]193    def getMapping(self, path, headerfields, mode):
194        """Get a mapping from CSV file headerfields to actually used fieldnames.
195        """
196        result = dict()
197        reader = csv.reader(open(path, 'rb'))
198        raw_header = reader.next()
199        for num, field in enumerate(headerfields):
[6854]200            if field not in [
201                'student_id', 'reg_number', 'matric_number'] and mode == 'remove':
[6849]202                continue
203            if field == u'--IGNORE--':
204                # Skip ignored columns in failed and finished data files.
205                continue
206            result[raw_header[num]] = field
207        return result
208
209    def checkConversion(self, row, mode='create'):
210        """Validates all values in row.
211        """
[7643]212        iface = self.iface
[6849]213        if mode in ['update', 'remove']:
214            if self.getLocator(row) == 'reg_number':
215                iface = IStudentUpdateByRegNo
216            elif self.getLocator(row) == 'matric_number':
217                iface = IStudentUpdateByMatricNo
218        converter = IObjectConverter(iface)
219        errs, inv_errs, conv_dict =  converter.fromStringDict(
[8214]220            row, self.factory_name, mode=mode)
[7513]221        if row.has_key('reg_state') and \
222            not row['reg_state'] in IMPORTABLE_STATES:
[7522]223            if row['reg_state'] != '':
224                errs.append(('reg_state','not allowed'))
225            else:
226                errs.append(('reg_state','no value provided'))
[6849]227        return errs, inv_errs, conv_dict
228
[6825]229class StudentStudyCourseProcessor(BatchProcessor):
230    """A batch processor for IStudentStudyCourse objects.
231    """
232    grok.implements(IBatchProcessor)
233    grok.provides(IBatchProcessor)
234    grok.context(Interface)
[6837]235    util_name = 'studycourseupdater'
[6825]236    grok.name(util_name)
237
[7933]238    name = u'StudentStudyCourse Processor (update only)'
[7532]239    iface = IStudentStudyCourse
[6825]240    factory_name = 'waeup.StudentStudyCourse'
241
[6849]242    location_fields = []
243
[6841]244    mode = None
245
[6825]246    @property
247    def available_fields(self):
248        return sorted(list(set(
[6843]249            ['student_id','reg_number','matric_number'] + getFields(
250                self.iface).keys())))
[6825]251
[6837]252    def checkHeaders(self, headerfields, mode='ignore'):
[6854]253        if not 'reg_number' in headerfields and not 'student_id' \
254            in headerfields and not 'matric_number' in headerfields:
[6825]255            raise FatalCSVError(
[6854]256                "Need at least columns student_id " +
257                "or reg_number or matric_number for import!")
[6834]258        # Check for fields to be ignored...
[6825]259        not_ignored_fields = [x for x in headerfields
260                              if not x.startswith('--')]
261        if len(set(not_ignored_fields)) < len(not_ignored_fields):
262            raise FatalCSVError(
263                "Double headers: each column name may only appear once.")
264        return True
265
[7267]266    def getParent(self, row, site):
[6846]267        if not 'students' in site.keys():
[6849]268            return None
[6846]269        if 'student_id' in row.keys() and row['student_id']:
[6825]270            if row['student_id'] in site['students']:
271                student = site['students'][row['student_id']]
272                return student
[6843]273        elif 'reg_number' in row.keys() and row['reg_number']:
[6825]274            reg_number = row['reg_number']
275            cat = queryUtility(ICatalog, name='students_catalog')
276            results = list(
277                cat.searchResults(reg_number=(reg_number, reg_number)))
278            if results:
279                return results[0]
[6843]280        elif 'matric_number' in row.keys() and row['matric_number']:
281            matric_number = row['matric_number']
282            cat = queryUtility(ICatalog, name='students_catalog')
283            results = list(
284                cat.searchResults(matric_number=(matric_number, matric_number)))
285            if results:
286                return results[0]
[6849]287        return None
[6825]288
[7267]289    def parentsExist(self, row, site):
290        return self.getParent(row, site) is not None
291
[6825]292    def entryExists(self, row, site):
[7534]293        return self.getEntry(row, site) is not None
[6825]294
295    def getEntry(self, row, site):
[7534]296        student = self.getParent(row, site)
[7536]297        if student is None:
[6825]298            return None
299        return student.get('studycourse')
[7429]300
301    def updateEntry(self, obj, row, site):
302        """Update obj to the values given in row.
303        """
[7656]304        items_changed = ''
[7429]305        for key, value in row.items():
306            # Skip fields not declared in interface.
307            if hasattr(obj, key):
[8202]308                # Set attribute to None if value is marked for deletion
[8214]309                setattr(obj, key, value)
310                if key == 'certificate':
311                    value = value.code
312
313                #if value == DELETIONMARKER:
314                #    setattr(obj, key, None)
315                #elif value is not None:
316                #    setattr(obj, key, value)
317                #    if key == 'certificate':
318                #        value = value.code
319                items_changed += '%s=%s, ' % (key,value)
[7656]320        parent = self.getParent(row, site)
321        parent.__parent__.logger.info(
322            '%s - Study course updated: %s'
323            % (parent.student_id, items_changed))
[7429]324        # Update the students_catalog
325        notify(grok.ObjectModifiedEvent(obj.__parent__))
326        return
327
[7532]328    def checkConversion(self, row, mode='ignore'):
329        """Validates all values in row.
330        """
331        converter = IObjectConverter(self.iface)
332        errs, inv_errs, conv_dict =  converter.fromStringDict(
333            row, self.factory_name)
334        # We have to check if current_level is in range of certificate.
[7548]335        # This is not done by the converter. This kind of conversion
336        # checking does only work if a combination of certificate and
337        # current_level is provided.
[7534]338        if conv_dict.has_key('certificate'):
339          certificate = conv_dict['certificate']
340          start_level = certificate.start_level
341          end_level = certificate.end_level
342          if conv_dict['current_level'] < start_level or \
[7612]343              conv_dict['current_level'] > end_level+120:
[7534]344              errs.append(('current_level','not in range'))
[7532]345        return errs, inv_errs, conv_dict
346
[7536]347class StudentStudyLevelProcessor(BatchProcessor):
348    """A batch processor for IStudentStudyLevel objects.
349    """
350    grok.implements(IBatchProcessor)
351    grok.provides(IBatchProcessor)
352    grok.context(Interface)
[7933]353    util_name = 'studylevelprocessor'
[7536]354    grok.name(util_name)
355
[7933]356    name = u'StudentStudyLevel Processor'
[7536]357    iface = IStudentStudyLevel
358    factory_name = 'waeup.StudentStudyLevel'
359
360    location_fields = []
361
362    mode = None
363
364    @property
365    def available_fields(self):
366        return sorted(list(set(
367            ['student_id','reg_number','matric_number','level'] + getFields(
368                self.iface).keys())))
369
370    def checkHeaders(self, headerfields, mode='ignore'):
371        if not 'reg_number' in headerfields and not 'student_id' \
372            in headerfields and not 'matric_number' in headerfields:
373            raise FatalCSVError(
374                "Need at least columns student_id " +
375                "or reg_number or matric_number for import!")
376        if not 'level' in headerfields:
377            raise FatalCSVError(
378                "Need level for import!")
379        # Check for fields to be ignored...
380        not_ignored_fields = [x for x in headerfields
381                              if not x.startswith('--')]
382        if len(set(not_ignored_fields)) < len(not_ignored_fields):
383            raise FatalCSVError(
384                "Double headers: each column name may only appear once.")
385        return True
386
387    def getParent(self, row, site):
388        if not 'students' in site.keys():
389            return None
390        if 'student_id' in row.keys() and row['student_id']:
391            if row['student_id'] in site['students']:
392                student = site['students'][row['student_id']]
393                return student['studycourse']
394        elif 'reg_number' in row.keys() and row['reg_number']:
395            reg_number = row['reg_number']
396            cat = queryUtility(ICatalog, name='students_catalog')
397            results = list(
398                cat.searchResults(reg_number=(reg_number, reg_number)))
399            if results:
400                return results[0]['studycourse']
401        elif 'matric_number' in row.keys() and row['matric_number']:
402            matric_number = row['matric_number']
403            cat = queryUtility(ICatalog, name='students_catalog')
404            results = list(
405                cat.searchResults(matric_number=(matric_number, matric_number)))
406            if results:
407                return results[0]['studycourse']
408        return None
409
410    def parentsExist(self, row, site):
411        return self.getParent(row, site) is not None
412
413    def entryExists(self, row, site):
414        return self.getEntry(row, site) is not None
415
416    def getEntry(self, row, site):
417        studycourse = self.getParent(row, site)
418        if studycourse is None:
419            return None
420        return studycourse.get(row['level'])
421
422    def addEntry(self, obj, row, site):
423        parent = self.getParent(row, site)
424        obj.level = int(row['level'])
425        parent[row['level']] = obj
426        return
427
428    def checkConversion(self, row, mode='ignore'):
429        """Validates all values in row.
430        """
431        converter = IObjectConverter(self.iface)
432        errs, inv_errs, conv_dict =  converter.fromStringDict(
433            row, self.factory_name)
434        # We have to check if level is a valid integer.
[7548]435        # This is not done by the converter.
[7536]436        try:
437            level = int(row['level'])
[7612]438            if level not in range(0,700,10):
[7536]439                errs.append(('level','no valid integer'))
440        except ValueError:
441            errs.append(('level','no integer'))
442        return errs, inv_errs, conv_dict
[7548]443
444class CourseTicketProcessor(BatchProcessor):
445    """A batch processor for ICourseTicket objects.
446    """
447    grok.implements(IBatchProcessor)
448    grok.provides(IBatchProcessor)
449    grok.context(Interface)
[7933]450    util_name = 'courseticketprocessor'
[7548]451    grok.name(util_name)
452
[7933]453    name = u'CourseTicket Processor'
[7548]454    iface = ICourseTicket
455    factory_name = 'waeup.CourseTicket'
456
457    location_fields = []
458
459    mode = None
460
461    @property
462    def available_fields(self):
463        return sorted(list(set(
464            ['student_id','reg_number','matric_number','level','code'] + getFields(
465                self.iface).keys())))
466
467    def checkHeaders(self, headerfields, mode='ignore'):
468        if not 'reg_number' in headerfields and not 'student_id' \
469            in headerfields and not 'matric_number' in headerfields:
470            raise FatalCSVError(
471                "Need at least columns student_id " +
472                "or reg_number or matric_number for import!")
473        if not 'level' in headerfields:
474            raise FatalCSVError(
475                "Need level for import!")
476        if not 'code' in headerfields:
477            raise FatalCSVError(
478                "Need code for import!")
479        # Check for fields to be ignored...
480        not_ignored_fields = [x for x in headerfields
481                              if not x.startswith('--')]
482        if len(set(not_ignored_fields)) < len(not_ignored_fields):
483            raise FatalCSVError(
484                "Double headers: each column name may only appear once.")
485        return True
486
487    def getParent(self, row, site):
488        if not 'students' in site.keys():
489            return None
490        if 'student_id' in row.keys() and row['student_id']:
491            if row['student_id'] in site['students']:
492                student = site['students'][row['student_id']]
493                return student['studycourse'].get(row['level'])
494        elif 'reg_number' in row.keys() and row['reg_number']:
495            reg_number = row['reg_number']
496            cat = queryUtility(ICatalog, name='students_catalog')
497            results = list(
498                cat.searchResults(reg_number=(reg_number, reg_number)))
499            if results:
500                return results[0]['studycourse'].get(row['level'])
501        elif 'matric_number' in row.keys() and row['matric_number']:
502            matric_number = row['matric_number']
503            cat = queryUtility(ICatalog, name='students_catalog')
504            results = list(
505                cat.searchResults(matric_number=(matric_number, matric_number)))
506            if results:
507                return results[0]['studycourse'].get(row['level'])
508        return None
509
510    def parentsExist(self, row, site):
511        return self.getParent(row, site) is not None
512
513    def entryExists(self, row, site):
514        return self.getEntry(row, site) is not None
515
516    def getEntry(self, row, site):
517        level = self.getParent(row, site)
518        if level is None:
519            return None
520        return level.get(row['code'])
521
522    def addEntry(self, obj, row, site):
523        parent = self.getParent(row, site)
524        catalog = getUtility(ICatalog, name='courses_catalog')
525        entries = list(catalog.searchResults(code=(row['code'],row['code'])))
526        obj.fcode = entries[0].__parent__.__parent__.__parent__.code
527        obj.dcode = entries[0].__parent__.__parent__.code
528        obj.title = entries[0].title
529        obj.credits = entries[0].credits
530        obj.passmark = entries[0].passmark
531        obj.semester = entries[0].semester
532        parent[row['code']] = obj
533        return
534
535    def checkConversion(self, row, mode='ignore'):
536        """Validates all values in row.
537        """
538        converter = IObjectConverter(self.iface)
539        errs, inv_errs, conv_dict =  converter.fromStringDict(
540            row, self.factory_name)
541        # We have to check if course really exists.
542        # This is not done by the converter.
543        catalog = getUtility(ICatalog, name='courses_catalog')
544        entries = catalog.searchResults(code=(row['code'],row['code']))
545        if len(entries) == 0:
546            errs.append(('code','non-existent'))
547            return errs, inv_errs, conv_dict
[7623]548        return errs, inv_errs, conv_dict
549
550class StudentOnlinePaymentProcessor(BatchProcessor):
551    """A batch processor for IStudentOnlinePayment objects.
552    """
553    grok.implements(IBatchProcessor)
554    grok.provides(IBatchProcessor)
555    grok.context(Interface)
[7933]556    util_name = 'paymentprocessor'
[7623]557    grok.name(util_name)
558
[7933]559    name = u'Payment Processor'
[8174]560    iface = IStudentOnlinePayment
[7623]561    factory_name = 'waeup.StudentOnlinePayment'
562
563    location_fields = []
564
565    mode = None
566
567    @property
568    def available_fields(self):
569        return sorted(list(set(
570            ['student_id','reg_number','matric_number','p_id'] + getFields(
571                self.iface).keys())))
572
573    def checkHeaders(self, headerfields, mode='ignore'):
574        if not 'reg_number' in headerfields and not 'student_id' \
575            in headerfields and not 'matric_number' in headerfields:
576            raise FatalCSVError(
577                "Need at least columns student_id " +
578                "or reg_number or matric_number for import!")
579        if not 'p_id' in headerfields:
580            raise FatalCSVError(
581                "Need p_id for import!")
582        # Check for fields to be ignored...
583        not_ignored_fields = [x for x in headerfields
584                              if not x.startswith('--')]
585        if len(set(not_ignored_fields)) < len(not_ignored_fields):
586            raise FatalCSVError(
587                "Double headers: each column name may only appear once.")
588        return True
589
590    def getParent(self, row, site):
591        if not 'students' in site.keys():
592            return None
593        if 'student_id' in row.keys() and row['student_id']:
594            if row['student_id'] in site['students']:
595                student = site['students'][row['student_id']]
596                return student['payments']
597        elif 'reg_number' in row.keys() and row['reg_number']:
598            reg_number = row['reg_number']
599            cat = queryUtility(ICatalog, name='students_catalog')
600            results = list(
601                cat.searchResults(reg_number=(reg_number, reg_number)))
602            if results:
603                return results[0]['payments']
604        elif 'matric_number' in row.keys() and row['matric_number']:
605            matric_number = row['matric_number']
606            cat = queryUtility(ICatalog, name='students_catalog')
607            results = list(
608                cat.searchResults(matric_number=(matric_number, matric_number)))
609            if results:
610                return results[0]['payments']
611        return None
612
613    def parentsExist(self, row, site):
614        return self.getParent(row, site) is not None
615
616    def entryExists(self, row, site):
617        return self.getEntry(row, site) is not None
618
619    def getEntry(self, row, site):
620        payments = self.getParent(row, site)
621        if payments is None:
622            return None
[7626]623        # We can use the hash symbol at the end of p_id in import files
624        # to avoid annoying automatic number transformation
625        # by Excel or Calc
626        p_id = row['p_id'].strip('#')
627        if p_id.startswith('p'):
628            entry = payments.get(p_id)
[7623]629        else:
630            # For data migration from old SRP
[7626]631            entry = payments.get('p' + p_id[6:])
[7623]632        return entry
633
634    def addEntry(self, obj, row, site):
635        parent = self.getParent(row, site)
[7626]636        p_id = row['p_id'].strip('#')
637        if not p_id.startswith('p'):
[7623]638            # For data migration from old SRP
[7626]639            obj.p_id = 'p' + p_id[6:]
[7623]640            parent[obj.p_id] = obj
641        else:
[7626]642            parent[p_id] = obj
[7623]643        return
644
645    def checkConversion(self, row, mode='ignore'):
646        """Validates all values in row.
647        """
648        converter = IObjectConverter(self.iface)
649        errs, inv_errs, conv_dict =  converter.fromStringDict(
650            row, self.factory_name)
651        # We have to check p_id.
[7626]652        p_id = row['p_id'].strip('#')
653        if p_id.startswith('p'):
654            if not len(p_id) == 14:
[7623]655                errs.append(('p_id','invalid length'))
656                return errs, inv_errs, conv_dict
657        else:
[7626]658            if not len(p_id) == 19:
[7623]659                errs.append(('p_id','invalid length'))
660                return errs, inv_errs, conv_dict
661        return errs, inv_errs, conv_dict
[7951]662
663class StudentVerdictProcessor(StudentStudyCourseProcessor):
664    """A batch processor for verdicts.
665
666    Import verdicts and perform workflow transitions.
667    """
668
669    util_name = 'verdictupdater'
670    grok.name(util_name)
671
672    name = u'Verdict Processor (update only)'
673    iface = IStudentVerdictUpdate
674    factory_name = 'waeup.StudentStudyCourse'
675
676    @property
677    def available_fields(self):
678        return sorted(list(set(
679            ['student_id','reg_number','matric_number',
680                'current_session', 'current_level'] + getFields(
681                self.iface).keys())))
682
683    def checkUpdateRequirements(self, obj, row, site):
684        """Checks requirements the studycourse and the student must fulfill
685        before being updated.
686        """
687        # Check if current_levels correspond
688        if obj.current_level != row['current_level']:
689            return 'Current level does not correspond.'
690        # Check if current_sessions correspond
691        if obj.current_session != row['current_session']:
692            return 'Current session does not correspond.'
693        # Check if student is in state REGISTERED
694        if obj.getStudent().state != VALIDATED:
695            return 'Student in wrong state.'
696        return None
697
698    def updateEntry(self, obj, row, site):
699        """Update obj to the values given in row.
700        """
701        items_changed = ''
702        for key, value in row.items():
703            # Skip fields not declared in interface plus
[7952]704            # current_session and current_level
[7951]705            if hasattr(obj, key) and not key in [
[7952]706                'current_session','current_level']:
[8214]707                setattr(obj, key, value)
[8202]708                # Set attribute to None if value is marked for deletion
[8214]709                #if value == DELETIONMARKER:
710                #    setattr(obj, key, None)
711                #elif value is not None:
712                #    setattr(obj, key, value)
713                items_changed += '%s=%s, ' % (key,value)
[7951]714        parent = self.getParent(row, site)
715        parent.__parent__.logger.info(
716            '%s - Verdict updated: %s'
717            % (parent.student_id, items_changed))
718        # Fire transition
719        IWorkflowInfo(obj.__parent__).fireTransition('return')
720        # Update the students_catalog
721        notify(grok.ObjectModifiedEvent(obj.__parent__))
722        return
723
724    def checkConversion(self, row, mode='ignore'):
725        """Validates all values in row.
726        """
727        converter = IObjectConverter(self.iface)
728        errs, inv_errs, conv_dict =  converter.fromStringDict(
729            row, self.factory_name)
[8176]730        return errs, inv_errs, conv_dict
Note: See TracBrowser for help on using the repository browser.