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

Last change on this file since 9021 was 9001, checked in by Henrik Bettermann, 12 years ago

Update local roles by import. Test if ignore and deltion marker are doing what they are supposed to do.

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