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

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

Add tests for delEntry.

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