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

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

Add test and fix checkUpdateRequirements.

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