source: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/batching.py @ 10322

Last change on this file since 10322 was 9169, checked in by uli, 12 years ago

Merge changes from trunk, r8786-HEAD

  • Property svn:keywords set to Id
File size: 28.1 KB
Line 
1## $Id: batching.py 9169 2012-09-10 11:05:07Z uli $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""Batch processing components for student objects.
19
20Batch processors eat CSV files to add, update or remove large numbers
21of certain kinds of objects at once.
22
23Here we define the processors for students specific objects like
24students, studycourses, payment tickets and accommodation tickets.
25"""
26import grok
27import csv
28from 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
472    additional_fields = ['level']
473    additional_headers = ['level']
474
475    def getParent(self, row, site):
476        student = self._getStudent(row, site)
477        if student is None:
478            return None
479        return student['studycourse']
480
481    def getEntry(self, row, site):
482        studycourse = self.getParent(row, site)
483        if studycourse is None:
484            return None
485        return studycourse.get(row['level'])
486
487    def updateEntry(self, obj, row, site):
488        """Update obj to the values given in row.
489        """
490        items_changed = super(StudentStudyLevelProcessor, self).updateEntry(
491            obj, row, site)
492        student = self.getParent(row, site).__parent__
493        student.__parent__.logger.info(
494            '%s - Study level updated: %s'
495            % (student.student_id, items_changed))
496        return
497
498    def addEntry(self, obj, row, site):
499        parent = self.getParent(row, site)
500        obj.level = int(row['level'])
501        parent[row['level']] = obj
502        return
503
504    def checkConversion(self, row, mode='ignore'):
505        """Validates all values in row.
506        """
507        errs, inv_errs, conv_dict = super(
508            StudentStudyLevelProcessor, self).checkConversion(row, mode=mode)
509
510        # We have to check if level is a valid integer.
511        # This is not done by the converter.
512        try:
513            level = int(row['level'])
514            if level not in range(0,700,10):
515                errs.append(('level','no valid integer'))
516        except ValueError:
517            errs.append(('level','no integer'))
518        return errs, inv_errs, conv_dict
519
520class CourseTicketProcessor(StudentProcessorBase):
521    """A batch processor for ICourseTicket objects.
522    """
523    grok.implements(IBatchProcessor)
524    grok.provides(IBatchProcessor)
525    grok.context(Interface)
526    util_name = 'courseticketprocessor'
527    grok.name(util_name)
528
529    name = u'CourseTicket Processor'
530    iface = ICourseTicket
531    factory_name = 'waeup.CourseTicket'
532
533    location_fields = []
534    additional_fields = ['level', 'code']
535    additional_headers = ['level', 'code']
536
537    def getParent(self, row, site):
538        student = self._getStudent(row, site)
539        if student is None:
540            return None
541        return student['studycourse'].get(row['level'])
542
543    def getEntry(self, row, site):
544        level = self.getParent(row, site)
545        if level is None:
546            return None
547        return level.get(row['code'])
548
549    def updateEntry(self, obj, row, site):
550        """Update obj to the values given in row.
551        """
552        items_changed = super(CourseTicketProcessor, self).updateEntry(
553            obj, row, site)
554        parent = self.getParent(row, site)
555        student = self.getParent(row, site).__parent__.__parent__
556        student.__parent__.logger.info(
557            '%s - Course ticket in %s updated: %s'
558            % (student.student_id,  parent.level, items_changed))
559        return
560
561    def addEntry(self, obj, row, site):
562        parent = self.getParent(row, site)
563        catalog = getUtility(ICatalog, name='courses_catalog')
564        entries = list(catalog.searchResults(code=(row['code'],row['code'])))
565        obj.fcode = entries[0].__parent__.__parent__.__parent__.code
566        obj.dcode = entries[0].__parent__.__parent__.code
567        obj.title = entries[0].title
568        obj.credits = entries[0].credits
569        obj.passmark = entries[0].passmark
570        obj.semester = entries[0].semester
571        parent[row['code']] = obj
572        return
573
574    def delEntry(self, row, site):
575        ticket = self.getEntry(row, site)
576        parent = self.getParent(row, site)
577        if ticket is not None:
578            student = self._getStudent(row, site)
579            student.__parent__.logger.info('%s - Course ticket in %s removed: %s'
580                % (student.student_id, parent.level, ticket.code))
581            del parent[ticket.code]
582        return
583
584    def checkConversion(self, row, mode='ignore'):
585        """Validates all values in row.
586        """
587        errs, inv_errs, conv_dict = super(
588            CourseTicketProcessor, self).checkConversion(row, mode=mode)
589
590        # We have to check if course really exists.
591        # This is not done by the converter.
592        catalog = getUtility(ICatalog, name='courses_catalog')
593        entries = catalog.searchResults(code=(row['code'],row['code']))
594        if len(entries) == 0:
595            errs.append(('code','non-existent'))
596            return errs, inv_errs, conv_dict
597        return errs, inv_errs, conv_dict
598
599class StudentOnlinePaymentProcessor(StudentProcessorBase):
600    """A batch processor for IStudentOnlinePayment objects.
601    """
602    grok.implements(IBatchProcessor)
603    grok.provides(IBatchProcessor)
604    grok.context(Interface)
605    util_name = 'paymentprocessor'
606    grok.name(util_name)
607
608    name = u'Student Payment Processor'
609    iface = IStudentOnlinePayment
610    factory_name = 'waeup.StudentOnlinePayment'
611
612    location_fields = []
613    additional_fields = ['p_id']
614    additional_headers = []
615
616    def checkHeaders(self, headerfields, mode='ignore'):
617        super(StudentOnlinePaymentProcessor, self).checkHeaders(headerfields)
618        if mode in ('update', 'remove') and not 'p_id' in headerfields:
619            raise FatalCSVError(
620                "Need p_id for import in update and remove modes!")
621        return True
622
623    def parentsExist(self, row, site):
624        return self.getParent(row, site) is not None
625
626    def getParent(self, row, site):
627        student = self._getStudent(row, site)
628        if student is None:
629            return None
630        return student['payments']
631
632    def getEntry(self, row, site):
633        payments = self.getParent(row, site)
634        if payments is None:
635            return None
636        p_id = row.get('p_id', None)
637        if p_id is None:
638            return None
639        # We can use the hash symbol at the end of p_id in import files
640        # to avoid annoying automatic number transformation
641        # by Excel or Calc
642        p_id = p_id.strip('#')
643        if not p_id.startswith('p'):
644            # For data migration from old SRP only
645            p_id = 'p' + p_id[7:] + '0'
646        entry = payments.get(p_id)
647        return entry
648
649    def updateEntry(self, obj, row, site):
650        """Update obj to the values given in row.
651        """
652        items_changed = super(StudentOnlinePaymentProcessor, self).updateEntry(
653            obj, row, site)
654        student = self.getParent(row, site).__parent__
655        student.__parent__.logger.info(
656            '%s - Payment ticket updated: %s'
657            % (student.student_id, items_changed))
658        return
659
660    def addEntry(self, obj, row, site):
661        parent = self.getParent(row, site)
662        p_id = row['p_id'].strip('#')
663        if not p_id.startswith('p'):
664            # For data migration from old SRP
665            obj.p_id = 'p' + p_id[7:] + '0'
666            parent[obj.p_id] = obj
667        else:
668            parent[p_id] = obj
669        return
670
671    def delEntry(self, row, site):
672        payment = self.getEntry(row, site)
673        parent = self.getParent(row, site)
674        if payment is not None:
675            student = self._getStudent(row, site)
676            student.__parent__.logger.info('%s - Payment ticket removed: %s'
677                % (student.student_id, payment.p_id))
678            del parent[payment.p_id]
679        return
680
681    def checkConversion(self, row, mode='ignore'):
682        """Validates all values in row.
683        """
684        errs, inv_errs, conv_dict = super(
685            StudentOnlinePaymentProcessor, self).checkConversion(row, mode=mode)
686
687        # We have to check p_id.
688        p_id = row.get('p_id', None)
689        if not p_id:
690            timestamp = ("%d" % int(time()*10000))[1:]
691            p_id = "p%s" % timestamp
692            conv_dict['p_id'] = p_id
693            return errs, inv_errs, conv_dict
694        else:
695            p_id = p_id.strip('#')
696        if p_id.startswith('p'):
697            if not len(p_id) == 14:
698                errs.append(('p_id','invalid length'))
699                return errs, inv_errs, conv_dict
700        else:
701            if not len(p_id) == 19:
702                errs.append(('p_id','invalid length'))
703                return errs, inv_errs, conv_dict
704        return errs, inv_errs, conv_dict
705
706class StudentVerdictProcessor(StudentStudyCourseProcessor):
707    """A batch processor for verdicts.
708
709    Import verdicts and perform workflow transitions.
710    """
711
712    util_name = 'verdictupdater'
713    grok.name(util_name)
714
715    name = u'Verdict Processor (update only)'
716    iface = IStudentVerdictUpdate
717    factory_name = 'waeup.StudentStudyCourse'
718
719    additional_fields = ['current_session', 'current_level']
720
721    def checkUpdateRequirements(self, obj, row, site):
722        """Checks requirements the studycourse and the student must fulfill
723        before being updated.
724        """
725        # Check if current_levels correspond
726        if obj.current_level != row['current_level']:
727            return 'Current level does not correspond.'
728        # Check if current_sessions correspond
729        if obj.current_session != row['current_session']:
730            return 'Current session does not correspond.'
731        # Check if student is in state REGISTERED
732        if obj.student.state != VALIDATED:
733            return 'Student in wrong state.'
734        return None
735
736    def updateEntry(self, obj, row, site):
737        """Update obj to the values given in row.
738        """
739        # Don't set current_session, current_level
740        vals_to_set = dict((key, val) for key, val in row.items()
741                           if key not in ('current_session','current_level'))
742        items_changed = super(StudentVerdictProcessor, self).updateEntry(
743            obj, vals_to_set, site)
744        parent = self.getParent(row, site)
745        parent.__parent__.logger.info(
746            '%s - Verdict updated: %s'
747            % (parent.student_id, items_changed))
748        # Fire transition
749        IWorkflowInfo(obj.__parent__).fireTransition('return')
750        # Update the students_catalog
751        notify(grok.ObjectModifiedEvent(obj.__parent__))
752        return
Note: See TracBrowser for help on using the repository browser.