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

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

Don't mark unchanged fields as changed.

  • Property svn:keywords set to Id
File size: 27.5 KB
RevLine 
[7191]1## $Id: batching.py 8209 2012-04-19 00:36:39Z 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,
[8202]36    IObjectHistory, VALIDATED, DELETIONMARKER)
[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
171                if value == DELETIONMARKER:
172                    setattr(obj, key, None)
173                elif value is not None:
174                    setattr(obj, key, value)
[8209]175                else:
176                    # No change, no change log
177                    continue
[7656]178            items_changed += '%s=%s, ' % (key,value)
179        parent = self.getParent(row, site)
180        if hasattr(obj,'student_id'):
181            # Update mode: the student exists and we can get the student_id
182            parent.logger.info(
183                '%s - Student record updated: %s'
184                % (obj.student_id, items_changed))
185        else:
186            # Create mode: the student does not yet exist
187            parent.logger.info('Student data imported: %s' % items_changed)
[7497]188        return
189
[6849]190    def getMapping(self, path, headerfields, mode):
191        """Get a mapping from CSV file headerfields to actually used fieldnames.
192        """
193        result = dict()
194        reader = csv.reader(open(path, 'rb'))
195        raw_header = reader.next()
196        for num, field in enumerate(headerfields):
[6854]197            if field not in [
198                'student_id', 'reg_number', 'matric_number'] and mode == 'remove':
[6849]199                continue
200            if field == u'--IGNORE--':
201                # Skip ignored columns in failed and finished data files.
202                continue
203            result[raw_header[num]] = field
204        return result
205
206    def checkConversion(self, row, mode='create'):
207        """Validates all values in row.
208        """
[7643]209        iface = self.iface
[6849]210        if mode in ['update', 'remove']:
211            if self.getLocator(row) == 'reg_number':
212                iface = IStudentUpdateByRegNo
213            elif self.getLocator(row) == 'matric_number':
214                iface = IStudentUpdateByMatricNo
215        converter = IObjectConverter(iface)
216        errs, inv_errs, conv_dict =  converter.fromStringDict(
217            row, self.factory_name)
[7513]218        if row.has_key('reg_state') and \
219            not row['reg_state'] in IMPORTABLE_STATES:
[7522]220            if row['reg_state'] != '':
221                errs.append(('reg_state','not allowed'))
222            else:
223                errs.append(('reg_state','no value provided'))
[6849]224        return errs, inv_errs, conv_dict
225
[6825]226class StudentStudyCourseProcessor(BatchProcessor):
227    """A batch processor for IStudentStudyCourse objects.
228    """
229    grok.implements(IBatchProcessor)
230    grok.provides(IBatchProcessor)
231    grok.context(Interface)
[6837]232    util_name = 'studycourseupdater'
[6825]233    grok.name(util_name)
234
[7933]235    name = u'StudentStudyCourse Processor (update only)'
[7532]236    iface = IStudentStudyCourse
[6825]237    factory_name = 'waeup.StudentStudyCourse'
238
[6849]239    location_fields = []
240
[6841]241    mode = None
242
[6825]243    @property
244    def available_fields(self):
245        return sorted(list(set(
[6843]246            ['student_id','reg_number','matric_number'] + getFields(
247                self.iface).keys())))
[6825]248
[6837]249    def checkHeaders(self, headerfields, mode='ignore'):
[6854]250        if not 'reg_number' in headerfields and not 'student_id' \
251            in headerfields and not 'matric_number' in headerfields:
[6825]252            raise FatalCSVError(
[6854]253                "Need at least columns student_id " +
254                "or reg_number or matric_number for import!")
[6834]255        # Check for fields to be ignored...
[6825]256        not_ignored_fields = [x for x in headerfields
257                              if not x.startswith('--')]
258        if len(set(not_ignored_fields)) < len(not_ignored_fields):
259            raise FatalCSVError(
260                "Double headers: each column name may only appear once.")
261        return True
262
[7267]263    def getParent(self, row, site):
[6846]264        if not 'students' in site.keys():
[6849]265            return None
[6846]266        if 'student_id' in row.keys() and row['student_id']:
[6825]267            if row['student_id'] in site['students']:
268                student = site['students'][row['student_id']]
269                return student
[6843]270        elif 'reg_number' in row.keys() and row['reg_number']:
[6825]271            reg_number = row['reg_number']
272            cat = queryUtility(ICatalog, name='students_catalog')
273            results = list(
274                cat.searchResults(reg_number=(reg_number, reg_number)))
275            if results:
276                return results[0]
[6843]277        elif 'matric_number' in row.keys() and row['matric_number']:
278            matric_number = row['matric_number']
279            cat = queryUtility(ICatalog, name='students_catalog')
280            results = list(
281                cat.searchResults(matric_number=(matric_number, matric_number)))
282            if results:
283                return results[0]
[6849]284        return None
[6825]285
[7267]286    def parentsExist(self, row, site):
287        return self.getParent(row, site) is not None
288
[6825]289    def entryExists(self, row, site):
[7534]290        return self.getEntry(row, site) is not None
[6825]291
292    def getEntry(self, row, site):
[7534]293        student = self.getParent(row, site)
[7536]294        if student is None:
[6825]295            return None
296        return student.get('studycourse')
[7429]297
298    def updateEntry(self, obj, row, site):
299        """Update obj to the values given in row.
300        """
[7656]301        items_changed = ''
[7429]302        for key, value in row.items():
303            # Skip fields not declared in interface.
304            if hasattr(obj, key):
[8202]305                # Set attribute to None if value is marked for deletion
306                if value == DELETIONMARKER:
307                    setattr(obj, key, None)
308                elif value is not None:
309                    setattr(obj, key, value)
310                    if key == 'certificate':
311                        value = value.code
[7656]312            items_changed += '%s=%s, ' % (key,value)
313        parent = self.getParent(row, site)
314        parent.__parent__.logger.info(
315            '%s - Study course updated: %s'
316            % (parent.student_id, items_changed))
[7429]317        # Update the students_catalog
318        notify(grok.ObjectModifiedEvent(obj.__parent__))
319        return
320
[7532]321    def checkConversion(self, row, mode='ignore'):
322        """Validates all values in row.
323        """
324        converter = IObjectConverter(self.iface)
325        errs, inv_errs, conv_dict =  converter.fromStringDict(
326            row, self.factory_name)
327        # We have to check if current_level is in range of certificate.
[7548]328        # This is not done by the converter. This kind of conversion
329        # checking does only work if a combination of certificate and
330        # current_level is provided.
[7534]331        if conv_dict.has_key('certificate'):
332          certificate = conv_dict['certificate']
333          start_level = certificate.start_level
334          end_level = certificate.end_level
335          if conv_dict['current_level'] < start_level or \
[7612]336              conv_dict['current_level'] > end_level+120:
[7534]337              errs.append(('current_level','not in range'))
[7532]338        return errs, inv_errs, conv_dict
339
[7536]340class StudentStudyLevelProcessor(BatchProcessor):
341    """A batch processor for IStudentStudyLevel objects.
342    """
343    grok.implements(IBatchProcessor)
344    grok.provides(IBatchProcessor)
345    grok.context(Interface)
[7933]346    util_name = 'studylevelprocessor'
[7536]347    grok.name(util_name)
348
[7933]349    name = u'StudentStudyLevel Processor'
[7536]350    iface = IStudentStudyLevel
351    factory_name = 'waeup.StudentStudyLevel'
352
353    location_fields = []
354
355    mode = None
356
357    @property
358    def available_fields(self):
359        return sorted(list(set(
360            ['student_id','reg_number','matric_number','level'] + getFields(
361                self.iface).keys())))
362
363    def checkHeaders(self, headerfields, mode='ignore'):
364        if not 'reg_number' in headerfields and not 'student_id' \
365            in headerfields and not 'matric_number' in headerfields:
366            raise FatalCSVError(
367                "Need at least columns student_id " +
368                "or reg_number or matric_number for import!")
369        if not 'level' in headerfields:
370            raise FatalCSVError(
371                "Need level for import!")
372        # Check for fields to be ignored...
373        not_ignored_fields = [x for x in headerfields
374                              if not x.startswith('--')]
375        if len(set(not_ignored_fields)) < len(not_ignored_fields):
376            raise FatalCSVError(
377                "Double headers: each column name may only appear once.")
378        return True
379
380    def getParent(self, row, site):
381        if not 'students' in site.keys():
382            return None
383        if 'student_id' in row.keys() and row['student_id']:
384            if row['student_id'] in site['students']:
385                student = site['students'][row['student_id']]
386                return student['studycourse']
387        elif 'reg_number' in row.keys() and row['reg_number']:
388            reg_number = row['reg_number']
389            cat = queryUtility(ICatalog, name='students_catalog')
390            results = list(
391                cat.searchResults(reg_number=(reg_number, reg_number)))
392            if results:
393                return results[0]['studycourse']
394        elif 'matric_number' in row.keys() and row['matric_number']:
395            matric_number = row['matric_number']
396            cat = queryUtility(ICatalog, name='students_catalog')
397            results = list(
398                cat.searchResults(matric_number=(matric_number, matric_number)))
399            if results:
400                return results[0]['studycourse']
401        return None
402
403    def parentsExist(self, row, site):
404        return self.getParent(row, site) is not None
405
406    def entryExists(self, row, site):
407        return self.getEntry(row, site) is not None
408
409    def getEntry(self, row, site):
410        studycourse = self.getParent(row, site)
411        if studycourse is None:
412            return None
413        return studycourse.get(row['level'])
414
415    def addEntry(self, obj, row, site):
416        parent = self.getParent(row, site)
417        obj.level = int(row['level'])
418        parent[row['level']] = obj
419        return
420
421    def checkConversion(self, row, mode='ignore'):
422        """Validates all values in row.
423        """
424        converter = IObjectConverter(self.iface)
425        errs, inv_errs, conv_dict =  converter.fromStringDict(
426            row, self.factory_name)
427        # We have to check if level is a valid integer.
[7548]428        # This is not done by the converter.
[7536]429        try:
430            level = int(row['level'])
[7612]431            if level not in range(0,700,10):
[7536]432                errs.append(('level','no valid integer'))
433        except ValueError:
434            errs.append(('level','no integer'))
435        return errs, inv_errs, conv_dict
[7548]436
437class CourseTicketProcessor(BatchProcessor):
438    """A batch processor for ICourseTicket objects.
439    """
440    grok.implements(IBatchProcessor)
441    grok.provides(IBatchProcessor)
442    grok.context(Interface)
[7933]443    util_name = 'courseticketprocessor'
[7548]444    grok.name(util_name)
445
[7933]446    name = u'CourseTicket Processor'
[7548]447    iface = ICourseTicket
448    factory_name = 'waeup.CourseTicket'
449
450    location_fields = []
451
452    mode = None
453
454    @property
455    def available_fields(self):
456        return sorted(list(set(
457            ['student_id','reg_number','matric_number','level','code'] + getFields(
458                self.iface).keys())))
459
460    def checkHeaders(self, headerfields, mode='ignore'):
461        if not 'reg_number' in headerfields and not 'student_id' \
462            in headerfields and not 'matric_number' in headerfields:
463            raise FatalCSVError(
464                "Need at least columns student_id " +
465                "or reg_number or matric_number for import!")
466        if not 'level' in headerfields:
467            raise FatalCSVError(
468                "Need level for import!")
469        if not 'code' in headerfields:
470            raise FatalCSVError(
471                "Need code for import!")
472        # Check for fields to be ignored...
473        not_ignored_fields = [x for x in headerfields
474                              if not x.startswith('--')]
475        if len(set(not_ignored_fields)) < len(not_ignored_fields):
476            raise FatalCSVError(
477                "Double headers: each column name may only appear once.")
478        return True
479
480    def getParent(self, row, site):
481        if not 'students' in site.keys():
482            return None
483        if 'student_id' in row.keys() and row['student_id']:
484            if row['student_id'] in site['students']:
485                student = site['students'][row['student_id']]
486                return student['studycourse'].get(row['level'])
487        elif 'reg_number' in row.keys() and row['reg_number']:
488            reg_number = row['reg_number']
489            cat = queryUtility(ICatalog, name='students_catalog')
490            results = list(
491                cat.searchResults(reg_number=(reg_number, reg_number)))
492            if results:
493                return results[0]['studycourse'].get(row['level'])
494        elif 'matric_number' in row.keys() and row['matric_number']:
495            matric_number = row['matric_number']
496            cat = queryUtility(ICatalog, name='students_catalog')
497            results = list(
498                cat.searchResults(matric_number=(matric_number, matric_number)))
499            if results:
500                return results[0]['studycourse'].get(row['level'])
501        return None
502
503    def parentsExist(self, row, site):
504        return self.getParent(row, site) is not None
505
506    def entryExists(self, row, site):
507        return self.getEntry(row, site) is not None
508
509    def getEntry(self, row, site):
510        level = self.getParent(row, site)
511        if level is None:
512            return None
513        return level.get(row['code'])
514
515    def addEntry(self, obj, row, site):
516        parent = self.getParent(row, site)
517        catalog = getUtility(ICatalog, name='courses_catalog')
518        entries = list(catalog.searchResults(code=(row['code'],row['code'])))
519        obj.fcode = entries[0].__parent__.__parent__.__parent__.code
520        obj.dcode = entries[0].__parent__.__parent__.code
521        obj.title = entries[0].title
522        obj.credits = entries[0].credits
523        obj.passmark = entries[0].passmark
524        obj.semester = entries[0].semester
525        parent[row['code']] = obj
526        return
527
528    def checkConversion(self, row, mode='ignore'):
529        """Validates all values in row.
530        """
531        converter = IObjectConverter(self.iface)
532        errs, inv_errs, conv_dict =  converter.fromStringDict(
533            row, self.factory_name)
534        # We have to check if course really exists.
535        # This is not done by the converter.
536        catalog = getUtility(ICatalog, name='courses_catalog')
537        entries = catalog.searchResults(code=(row['code'],row['code']))
538        if len(entries) == 0:
539            errs.append(('code','non-existent'))
540            return errs, inv_errs, conv_dict
[7623]541        return errs, inv_errs, conv_dict
542
543class StudentOnlinePaymentProcessor(BatchProcessor):
544    """A batch processor for IStudentOnlinePayment objects.
545    """
546    grok.implements(IBatchProcessor)
547    grok.provides(IBatchProcessor)
548    grok.context(Interface)
[7933]549    util_name = 'paymentprocessor'
[7623]550    grok.name(util_name)
551
[7933]552    name = u'Payment Processor'
[8174]553    iface = IStudentOnlinePayment
[7623]554    factory_name = 'waeup.StudentOnlinePayment'
555
556    location_fields = []
557
558    mode = None
559
560    @property
561    def available_fields(self):
562        return sorted(list(set(
563            ['student_id','reg_number','matric_number','p_id'] + getFields(
564                self.iface).keys())))
565
566    def checkHeaders(self, headerfields, mode='ignore'):
567        if not 'reg_number' in headerfields and not 'student_id' \
568            in headerfields and not 'matric_number' in headerfields:
569            raise FatalCSVError(
570                "Need at least columns student_id " +
571                "or reg_number or matric_number for import!")
572        if not 'p_id' in headerfields:
573            raise FatalCSVError(
574                "Need p_id for import!")
575        # Check for fields to be ignored...
576        not_ignored_fields = [x for x in headerfields
577                              if not x.startswith('--')]
578        if len(set(not_ignored_fields)) < len(not_ignored_fields):
579            raise FatalCSVError(
580                "Double headers: each column name may only appear once.")
581        return True
582
583    def getParent(self, row, site):
584        if not 'students' in site.keys():
585            return None
586        if 'student_id' in row.keys() and row['student_id']:
587            if row['student_id'] in site['students']:
588                student = site['students'][row['student_id']]
589                return student['payments']
590        elif 'reg_number' in row.keys() and row['reg_number']:
591            reg_number = row['reg_number']
592            cat = queryUtility(ICatalog, name='students_catalog')
593            results = list(
594                cat.searchResults(reg_number=(reg_number, reg_number)))
595            if results:
596                return results[0]['payments']
597        elif 'matric_number' in row.keys() and row['matric_number']:
598            matric_number = row['matric_number']
599            cat = queryUtility(ICatalog, name='students_catalog')
600            results = list(
601                cat.searchResults(matric_number=(matric_number, matric_number)))
602            if results:
603                return results[0]['payments']
604        return None
605
606    def parentsExist(self, row, site):
607        return self.getParent(row, site) is not None
608
609    def entryExists(self, row, site):
610        return self.getEntry(row, site) is not None
611
612    def getEntry(self, row, site):
613        payments = self.getParent(row, site)
614        if payments is None:
615            return None
[7626]616        # We can use the hash symbol at the end of p_id in import files
617        # to avoid annoying automatic number transformation
618        # by Excel or Calc
619        p_id = row['p_id'].strip('#')
620        if p_id.startswith('p'):
621            entry = payments.get(p_id)
[7623]622        else:
623            # For data migration from old SRP
[7626]624            entry = payments.get('p' + p_id[6:])
[7623]625        return entry
626
627    def addEntry(self, obj, row, site):
628        parent = self.getParent(row, site)
[7626]629        p_id = row['p_id'].strip('#')
630        if not p_id.startswith('p'):
[7623]631            # For data migration from old SRP
[7626]632            obj.p_id = 'p' + p_id[6:]
[7623]633            parent[obj.p_id] = obj
634        else:
[7626]635            parent[p_id] = obj
[7623]636        return
637
638    def checkConversion(self, row, mode='ignore'):
639        """Validates all values in row.
640        """
641        converter = IObjectConverter(self.iface)
642        errs, inv_errs, conv_dict =  converter.fromStringDict(
643            row, self.factory_name)
644        # We have to check p_id.
[7626]645        p_id = row['p_id'].strip('#')
646        if p_id.startswith('p'):
647            if not len(p_id) == 14:
[7623]648                errs.append(('p_id','invalid length'))
649                return errs, inv_errs, conv_dict
650        else:
[7626]651            if not len(p_id) == 19:
[7623]652                errs.append(('p_id','invalid length'))
653                return errs, inv_errs, conv_dict
654        return errs, inv_errs, conv_dict
[7951]655
656class StudentVerdictProcessor(StudentStudyCourseProcessor):
657    """A batch processor for verdicts.
658
659    Import verdicts and perform workflow transitions.
660    """
661
662    util_name = 'verdictupdater'
663    grok.name(util_name)
664
665    name = u'Verdict Processor (update only)'
666    iface = IStudentVerdictUpdate
667    factory_name = 'waeup.StudentStudyCourse'
668
669    @property
670    def available_fields(self):
671        return sorted(list(set(
672            ['student_id','reg_number','matric_number',
673                'current_session', 'current_level'] + getFields(
674                self.iface).keys())))
675
676    def checkUpdateRequirements(self, obj, row, site):
677        """Checks requirements the studycourse and the student must fulfill
678        before being updated.
679        """
680        # Check if current_levels correspond
681        if obj.current_level != row['current_level']:
682            return 'Current level does not correspond.'
683        # Check if current_sessions correspond
684        if obj.current_session != row['current_session']:
685            return 'Current session does not correspond.'
686        # Check if student is in state REGISTERED
687        if obj.getStudent().state != VALIDATED:
688            return 'Student in wrong state.'
689        return None
690
691    def updateEntry(self, obj, row, site):
692        """Update obj to the values given in row.
693        """
694        items_changed = ''
695        for key, value in row.items():
696            # Skip fields not declared in interface plus
[7952]697            # current_session and current_level
[7951]698            if hasattr(obj, key) and not key in [
[7952]699                'current_session','current_level']:
[8202]700                # Set attribute to None if value is marked for deletion
701                if value == DELETIONMARKER:
702                    setattr(obj, key, None)
703                elif value is not None:
704                    setattr(obj, key, value)
[7951]705            items_changed += '%s=%s, ' % (key,value)
706        parent = self.getParent(row, site)
707        parent.__parent__.logger.info(
708            '%s - Verdict updated: %s'
709            % (parent.student_id, items_changed))
710        # Fire transition
711        IWorkflowInfo(obj.__parent__).fireTransition('return')
712        # Update the students_catalog
713        notify(grok.ObjectModifiedEvent(obj.__parent__))
714        return
715
716    def checkConversion(self, row, mode='ignore'):
717        """Validates all values in row.
718        """
719        converter = IObjectConverter(self.iface)
720        errs, inv_errs, conv_dict =  converter.fromStringDict(
721            row, self.factory_name)
[8176]722        return errs, inv_errs, conv_dict
Note: See TracBrowser for help on using the repository browser.