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

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

Empty If no value is provided in import files, attributes must not be cleared. Clear attribute only if value == DELETIONMARKER.

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