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

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

Since row has passed the converter, current_level is an integer.

  • Property svn:keywords set to Id
File size: 28.1 KB
Line 
1## $Id: batching.py 9037 2012-07-21 15:25:18Z 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        Returns error messages as strings in case of requirement
449        problems.
450        """
451        current_level = row.get('current_level', None)
452        if current_level == 999 and \
453            obj.__parent__.state in FORBIDDEN_POSTGRAD_STATES:
454            return 'Not a pg student.'
455        return None
456
457class StudentStudyLevelProcessor(StudentProcessorBase):
458    """A batch processor for IStudentStudyLevel objects.
459    """
460    grok.implements(IBatchProcessor)
461    grok.provides(IBatchProcessor)
462    grok.context(Interface)
463    util_name = 'studylevelprocessor'
464    grok.name(util_name)
465
466    name = u'StudentStudyLevel Processor'
467    iface = IStudentStudyLevel
468    factory_name = 'waeup.StudentStudyLevel'
469
470    location_fields = []
471    additional_fields = ['level']
472    additional_headers = ['level']
473
474    def getParent(self, row, site):
475        student = self._getStudent(row, site)
476        if student is None:
477            return None
478        return student['studycourse']
479
480    def getEntry(self, row, site):
481        studycourse = self.getParent(row, site)
482        if studycourse is None:
483            return None
484        return studycourse.get(row['level'])
485
486    def updateEntry(self, obj, row, site):
487        """Update obj to the values given in row.
488        """
489        items_changed = super(StudentStudyLevelProcessor, self).updateEntry(
490            obj, row, site)
491        student = self.getParent(row, site).__parent__
492        student.__parent__.logger.info(
493            '%s - Study level updated: %s'
494            % (student.student_id, items_changed))
495        return
496
497    def addEntry(self, obj, row, site):
498        parent = self.getParent(row, site)
499        obj.level = int(row['level'])
500        parent[row['level']] = obj
501        return
502
503    def checkConversion(self, row, mode='ignore'):
504        """Validates all values in row.
505        """
506        errs, inv_errs, conv_dict = super(
507            StudentStudyLevelProcessor, self).checkConversion(row, mode=mode)
508
509        # We have to check if level is a valid integer.
510        # This is not done by the converter.
511        try:
512            level = int(row['level'])
513            if level not in range(0,700,10):
514                errs.append(('level','no valid integer'))
515        except ValueError:
516            errs.append(('level','no integer'))
517        return errs, inv_errs, conv_dict
518
519class CourseTicketProcessor(StudentProcessorBase):
520    """A batch processor for ICourseTicket objects.
521    """
522    grok.implements(IBatchProcessor)
523    grok.provides(IBatchProcessor)
524    grok.context(Interface)
525    util_name = 'courseticketprocessor'
526    grok.name(util_name)
527
528    name = u'CourseTicket Processor'
529    iface = ICourseTicket
530    factory_name = 'waeup.CourseTicket'
531
532    location_fields = []
533    additional_fields = ['level', 'code']
534    additional_headers = ['level', 'code']
535
536    def getParent(self, row, site):
537        student = self._getStudent(row, site)
538        if student is None:
539            return None
540        return student['studycourse'].get(row['level'])
541
542    def getEntry(self, row, site):
543        level = self.getParent(row, site)
544        if level is None:
545            return None
546        return level.get(row['code'])
547
548    def updateEntry(self, obj, row, site):
549        """Update obj to the values given in row.
550        """
551        items_changed = super(CourseTicketProcessor, self).updateEntry(
552            obj, row, site)
553        parent = self.getParent(row, site)
554        student = self.getParent(row, site).__parent__.__parent__
555        student.__parent__.logger.info(
556            '%s - Course ticket in %s updated: %s'
557            % (student.student_id,  parent.level, items_changed))
558        return
559
560    def addEntry(self, obj, row, site):
561        parent = self.getParent(row, site)
562        catalog = getUtility(ICatalog, name='courses_catalog')
563        entries = list(catalog.searchResults(code=(row['code'],row['code'])))
564        obj.fcode = entries[0].__parent__.__parent__.__parent__.code
565        obj.dcode = entries[0].__parent__.__parent__.code
566        obj.title = entries[0].title
567        obj.credits = entries[0].credits
568        obj.passmark = entries[0].passmark
569        obj.semester = entries[0].semester
570        parent[row['code']] = obj
571        return
572
573    def delEntry(self, row, site):
574        ticket = self.getEntry(row, site)
575        parent = self.getParent(row, site)
576        if ticket is not None:
577            student = self._getStudent(row, site)
578            student.__parent__.logger.info('%s - Course ticket in %s removed: %s'
579                % (student.student_id, parent.level, ticket.code))
580            del parent[ticket.code]
581        return
582
583    def checkConversion(self, row, mode='ignore'):
584        """Validates all values in row.
585        """
586        errs, inv_errs, conv_dict = super(
587            CourseTicketProcessor, self).checkConversion(row, mode=mode)
588
589        # We have to check if course really exists.
590        # This is not done by the converter.
591        catalog = getUtility(ICatalog, name='courses_catalog')
592        entries = catalog.searchResults(code=(row['code'],row['code']))
593        if len(entries) == 0:
594            errs.append(('code','non-existent'))
595            return errs, inv_errs, conv_dict
596        return errs, inv_errs, conv_dict
597
598class StudentOnlinePaymentProcessor(StudentProcessorBase):
599    """A batch processor for IStudentOnlinePayment objects.
600    """
601    grok.implements(IBatchProcessor)
602    grok.provides(IBatchProcessor)
603    grok.context(Interface)
604    util_name = 'paymentprocessor'
605    grok.name(util_name)
606
607    name = u'Student Payment Processor'
608    iface = IStudentOnlinePayment
609    factory_name = 'waeup.StudentOnlinePayment'
610
611    location_fields = []
612    additional_fields = ['p_id']
613    additional_headers = []
614
615    def checkHeaders(self, headerfields, mode='ignore'):
616        super(StudentOnlinePaymentProcessor, self).checkHeaders(headerfields)
617        if mode in ('update', 'remove') and not 'p_id' in headerfields:
618            raise FatalCSVError(
619                "Need p_id for import in update and remove modes!")
620        return True
621
622    def parentsExist(self, row, site):
623        return self.getParent(row, site) is not None
624
625    def getParent(self, row, site):
626        student = self._getStudent(row, site)
627        if student is None:
628            return None
629        return student['payments']
630
631    def getEntry(self, row, site):
632        payments = self.getParent(row, site)
633        if payments is None:
634            return None
635        p_id = row.get('p_id', None)
636        if p_id is None:
637            return None
638        # We can use the hash symbol at the end of p_id in import files
639        # to avoid annoying automatic number transformation
640        # by Excel or Calc
641        p_id = p_id.strip('#')
642        if not p_id.startswith('p'):
643            # For data migration from old SRP only
644            p_id = 'p' + p_id[7:] + '0'
645        entry = payments.get(p_id)
646        return entry
647
648    def updateEntry(self, obj, row, site):
649        """Update obj to the values given in row.
650        """
651        items_changed = super(StudentOnlinePaymentProcessor, self).updateEntry(
652            obj, row, site)
653        student = self.getParent(row, site).__parent__
654        student.__parent__.logger.info(
655            '%s - Payment ticket updated: %s'
656            % (student.student_id, items_changed))
657        return
658
659    def addEntry(self, obj, row, site):
660        parent = self.getParent(row, site)
661        p_id = row['p_id'].strip('#')
662        if not p_id.startswith('p'):
663            # For data migration from old SRP
664            obj.p_id = 'p' + p_id[7:] + '0'
665            parent[obj.p_id] = obj
666        else:
667            parent[p_id] = obj
668        return
669
670    def delEntry(self, row, site):
671        payment = self.getEntry(row, site)
672        parent = self.getParent(row, site)
673        if payment is not None:
674            student = self._getStudent(row, site)
675            student.__parent__.logger.info('%s - Payment ticket removed: %s'
676                % (student.student_id, payment.p_id))
677            del parent[payment.p_id]
678        return
679
680    def checkConversion(self, row, mode='ignore'):
681        """Validates all values in row.
682        """
683        errs, inv_errs, conv_dict = super(
684            StudentOnlinePaymentProcessor, self).checkConversion(row, mode=mode)
685
686        # We have to check p_id.
687        p_id = row.get('p_id', None)
688        if not p_id:
689            timestamp = ("%d" % int(time()*10000))[1:]
690            p_id = "p%s" % timestamp
691            conv_dict['p_id'] = p_id
692            return errs, inv_errs, conv_dict
693        else:
694            p_id = p_id.strip('#')
695        if p_id.startswith('p'):
696            if not len(p_id) == 14:
697                errs.append(('p_id','invalid length'))
698                return errs, inv_errs, conv_dict
699        else:
700            if not len(p_id) == 19:
701                errs.append(('p_id','invalid length'))
702                return errs, inv_errs, conv_dict
703        return errs, inv_errs, conv_dict
704
705class StudentVerdictProcessor(StudentStudyCourseProcessor):
706    """A batch processor for verdicts.
707
708    Import verdicts and perform workflow transitions.
709    """
710
711    util_name = 'verdictupdater'
712    grok.name(util_name)
713
714    name = u'Verdict Processor (update only)'
715    iface = IStudentVerdictUpdate
716    factory_name = 'waeup.StudentStudyCourse'
717
718    additional_fields = ['current_session', 'current_level']
719
720    def checkUpdateRequirements(self, obj, row, site):
721        """Checks requirements the studycourse and the student must fulfill
722        before being updated.
723        """
724        # Check if current_levels correspond
725        if obj.current_level != row['current_level']:
726            return 'Current level does not correspond.'
727        # Check if current_sessions correspond
728        if obj.current_session != row['current_session']:
729            return 'Current session does not correspond.'
730        # Check if student is in state REGISTERED
731        if obj.student.state != VALIDATED:
732            return 'Student in wrong state.'
733        return None
734
735    def updateEntry(self, obj, row, site):
736        """Update obj to the values given in row.
737        """
738        # Don't set current_session, current_level
739        vals_to_set = dict((key, val) for key, val in row.items()
740                           if key not in ('current_session','current_level'))
741        items_changed = super(StudentVerdictProcessor, self).updateEntry(
742            obj, vals_to_set, site)
743        parent = self.getParent(row, site)
744        parent.__parent__.logger.info(
745            '%s - Verdict updated: %s'
746            % (parent.student_id, items_changed))
747        # Fire transition
748        IWorkflowInfo(obj.__parent__).fireTransition('return')
749        # Update the students_catalog
750        notify(grok.ObjectModifiedEvent(obj.__parent__))
751        return
Note: See TracBrowser for help on using the repository browser.