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

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

Lots of trash to sketch filtered imports.

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