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
Line 
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##
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                else:
176                    # No change, no change log
177                    continue
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)
188        return
189
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):
197            if field not in [
198                'student_id', 'reg_number', 'matric_number'] and mode == 'remove':
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        """
209        iface = self.iface
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)
218        if row.has_key('reg_state') and \
219            not row['reg_state'] in IMPORTABLE_STATES:
220            if row['reg_state'] != '':
221                errs.append(('reg_state','not allowed'))
222            else:
223                errs.append(('reg_state','no value provided'))
224        return errs, inv_errs, conv_dict
225
226class StudentStudyCourseProcessor(BatchProcessor):
227    """A batch processor for IStudentStudyCourse objects.
228    """
229    grok.implements(IBatchProcessor)
230    grok.provides(IBatchProcessor)
231    grok.context(Interface)
232    util_name = 'studycourseupdater'
233    grok.name(util_name)
234
235    name = u'StudentStudyCourse Processor (update only)'
236    iface = IStudentStudyCourse
237    factory_name = 'waeup.StudentStudyCourse'
238
239    location_fields = []
240
241    mode = None
242
243    @property
244    def available_fields(self):
245        return sorted(list(set(
246            ['student_id','reg_number','matric_number'] + getFields(
247                self.iface).keys())))
248
249    def checkHeaders(self, headerfields, mode='ignore'):
250        if not 'reg_number' in headerfields and not 'student_id' \
251            in headerfields and not 'matric_number' in headerfields:
252            raise FatalCSVError(
253                "Need at least columns student_id " +
254                "or reg_number or matric_number for import!")
255        # Check for fields to be ignored...
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
263    def getParent(self, row, site):
264        if not 'students' in site.keys():
265            return None
266        if 'student_id' in row.keys() and row['student_id']:
267            if row['student_id'] in site['students']:
268                student = site['students'][row['student_id']]
269                return student
270        elif 'reg_number' in row.keys() and row['reg_number']:
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]
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]
284        return None
285
286    def parentsExist(self, row, site):
287        return self.getParent(row, site) is not None
288
289    def entryExists(self, row, site):
290        return self.getEntry(row, site) is not None
291
292    def getEntry(self, row, site):
293        student = self.getParent(row, site)
294        if student is None:
295            return None
296        return student.get('studycourse')
297
298    def updateEntry(self, obj, row, site):
299        """Update obj to the values given in row.
300        """
301        items_changed = ''
302        for key, value in row.items():
303            # Skip fields not declared in interface.
304            if hasattr(obj, key):
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
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))
317        # Update the students_catalog
318        notify(grok.ObjectModifiedEvent(obj.__parent__))
319        return
320
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.
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.
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 \
336              conv_dict['current_level'] > end_level+120:
337              errs.append(('current_level','not in range'))
338        return errs, inv_errs, conv_dict
339
340class StudentStudyLevelProcessor(BatchProcessor):
341    """A batch processor for IStudentStudyLevel objects.
342    """
343    grok.implements(IBatchProcessor)
344    grok.provides(IBatchProcessor)
345    grok.context(Interface)
346    util_name = 'studylevelprocessor'
347    grok.name(util_name)
348
349    name = u'StudentStudyLevel Processor'
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.
428        # This is not done by the converter.
429        try:
430            level = int(row['level'])
431            if level not in range(0,700,10):
432                errs.append(('level','no valid integer'))
433        except ValueError:
434            errs.append(('level','no integer'))
435        return errs, inv_errs, conv_dict
436
437class CourseTicketProcessor(BatchProcessor):
438    """A batch processor for ICourseTicket objects.
439    """
440    grok.implements(IBatchProcessor)
441    grok.provides(IBatchProcessor)
442    grok.context(Interface)
443    util_name = 'courseticketprocessor'
444    grok.name(util_name)
445
446    name = u'CourseTicket Processor'
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
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)
549    util_name = 'paymentprocessor'
550    grok.name(util_name)
551
552    name = u'Payment Processor'
553    iface = IStudentOnlinePayment
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
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)
622        else:
623            # For data migration from old SRP
624            entry = payments.get('p' + p_id[6:])
625        return entry
626
627    def addEntry(self, obj, row, site):
628        parent = self.getParent(row, site)
629        p_id = row['p_id'].strip('#')
630        if not p_id.startswith('p'):
631            # For data migration from old SRP
632            obj.p_id = 'p' + p_id[6:]
633            parent[obj.p_id] = obj
634        else:
635            parent[p_id] = obj
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.
645        p_id = row['p_id'].strip('#')
646        if p_id.startswith('p'):
647            if not len(p_id) == 14:
648                errs.append(('p_id','invalid length'))
649                return errs, inv_errs, conv_dict
650        else:
651            if not len(p_id) == 19:
652                errs.append(('p_id','invalid length'))
653                return errs, inv_errs, conv_dict
654        return errs, inv_errs, conv_dict
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
697            # current_session and current_level
698            if hasattr(obj, key) and not key in [
699                'current_session','current_level']:
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)
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)
722        return errs, inv_errs, conv_dict
Note: See TracBrowser for help on using the repository browser.