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

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

Set validation_date and validated_by in study level objects when importing verdicts.

  • Property svn:keywords set to Id
File size: 29.0 KB
Line 
1## $Id: batching.py 9295 2012-10-04 20:40:29Z 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 datetime import datetime
30from zope.interface import Interface
31from zope.schema import getFields
32from zope.component import queryUtility, getUtility
33from zope.event import notify
34from zope.catalog.interfaces import ICatalog
35from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
36from waeup.kofa.interfaces import (
37    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
38    IObjectHistory, VALIDATED, REGISTERED, IGNORE_MARKER)
39from waeup.kofa.interfaces import MessageFactory as _
40from waeup.kofa.students.interfaces import (
41    IStudent, IStudentStudyCourse,
42    IStudentUpdateByRegNo, IStudentUpdateByMatricNo,
43    IStudentStudyLevel, ICourseTicket,
44    IStudentOnlinePayment, IStudentVerdictUpdate)
45from waeup.kofa.students.workflow import  (
46    IMPORTABLE_STATES, IMPORTABLE_TRANSITIONS,
47    FORBIDDEN_POSTGRAD_TRANS, FORBIDDEN_POSTGRAD_STATES)
48from waeup.kofa.utils.batching import BatchProcessor
49
50class StudentProcessor(BatchProcessor):
51    """A batch processor for IStudent objects.
52    """
53    grok.implements(IBatchProcessor)
54    grok.provides(IBatchProcessor)
55    grok.context(Interface)
56    util_name = 'studentprocessor'
57    grok.name(util_name)
58
59    name = u'Student Processor'
60    iface = IStudent
61    iface_byregnumber = IStudentUpdateByRegNo
62    iface_bymatricnumber = IStudentUpdateByMatricNo
63
64    location_fields = []
65    factory_name = 'waeup.Student'
66
67    @property
68    def available_fields(self):
69        fields = getFields(self.iface)
70        return sorted(list(set(
71            ['student_id','reg_number','matric_number',
72            'password', 'state', 'transition'] + fields.keys())))
73
74    def checkHeaders(self, headerfields, mode='create'):
75        if 'state' in headerfields and 'transition' in headerfields:
76            raise FatalCSVError(
77                "State and transition can't be  imported at the same time!")
78        if not 'reg_number' in headerfields and not 'student_id' \
79            in headerfields and not 'matric_number' in headerfields:
80            raise FatalCSVError(
81                "Need at least columns student_id or reg_number " +
82                "or matric_number for import!")
83        if mode == 'create':
84            for field in self.required_fields:
85                if not field in headerfields:
86                    raise FatalCSVError(
87                        "Need at least columns %s for import!" %
88                        ', '.join(["'%s'" % x for x in self.required_fields]))
89        # Check for fields to be ignored...
90        not_ignored_fields = [x for x in headerfields
91                              if not x.startswith('--')]
92        if len(set(not_ignored_fields)) < len(not_ignored_fields):
93            raise FatalCSVError(
94                "Double headers: each column name may only appear once.")
95        return True
96
97    def parentsExist(self, row, site):
98        return 'students' in site.keys()
99
100    def getLocator(self, row):
101        if row.get('student_id',None) not in (None, IGNORE_MARKER):
102            return 'student_id'
103        elif row.get('reg_number',None) not in (None, IGNORE_MARKER):
104            return 'reg_number'
105        elif row.get('matric_number',None) not in (None, IGNORE_MARKER):
106            return 'matric_number'
107        else:
108            return None
109
110    # The entry never exists in create mode.
111    def entryExists(self, row, site):
112        return self.getEntry(row, site) is not None
113
114    def getParent(self, row, site):
115        return site['students']
116
117    def getEntry(self, row, site):
118        if not 'students' in site.keys():
119            return None
120        if self.getLocator(row) == 'student_id':
121            if row['student_id'] in site['students']:
122                student = site['students'][row['student_id']]
123                return student
124        elif self.getLocator(row) == 'reg_number':
125            reg_number = row['reg_number']
126            cat = queryUtility(ICatalog, name='students_catalog')
127            results = list(
128                cat.searchResults(reg_number=(reg_number, reg_number)))
129            if results:
130                return results[0]
131        elif self.getLocator(row) == 'matric_number':
132            matric_number = row['matric_number']
133            cat = queryUtility(ICatalog, name='students_catalog')
134            results = list(
135                cat.searchResults(matric_number=(matric_number, matric_number)))
136            if results:
137                return results[0]
138        return None
139
140    def addEntry(self, obj, row, site):
141        parent = self.getParent(row, site)
142        parent.addStudent(obj)
143        # Reset _curr_stud_id if student_id has been imported
144        if self.getLocator(row) == 'student_id':
145            parent._curr_stud_id -= 1
146        # We have to log this if state is provided. If not,
147        # logging is done by the event handler handle_student_added
148        if row.has_key('state'):
149            parent.logger.info('%s - Student record created' % obj.student_id)
150        history = IObjectHistory(obj)
151        history.addMessage(_('Student record created'))
152        return
153
154    def delEntry(self, row, site):
155        student = self.getEntry(row, site)
156        if student is not None:
157            parent = self.getParent(row, site)
158            parent.logger.info('%s - Student removed' % student.student_id)
159            del parent[student.student_id]
160        pass
161
162    def checkUpdateRequirements(self, obj, row, site):
163        """Checks requirements the object must fulfill when being updated.
164
165        This method is not used in case of deleting or adding objects.
166
167        Returns error messages as strings in case of requirement
168        problems.
169        """
170        transition = row.get('transition', IGNORE_MARKER)
171        if transition not in (IGNORE_MARKER, ''):
172            allowed_transitions = IWorkflowInfo(obj).getManualTransitionIds()
173            if transition not in allowed_transitions:
174                return 'Transition not allowed.'
175            if transition in FORBIDDEN_POSTGRAD_TRANS and \
176                obj.is_postgrad:
177                return 'Transition not allowed (pg student).'
178        state = row.get('state', IGNORE_MARKER)
179        if state not in (IGNORE_MARKER, ''):
180            if state in FORBIDDEN_POSTGRAD_STATES and \
181                obj.is_postgrad:
182                return 'State not allowed (pg student).'
183        return None
184
185    def updateEntry(self, obj, row, site):
186        """Update obj to the values given in row.
187        """
188        items_changed = ''
189
190        # Remove student_id from row if empty
191        if row.has_key('student_id') and row['student_id'] in (
192            None, IGNORE_MARKER):
193            row.pop('student_id')
194
195        # Update password
196        # XXX: Tale DELETION_MARKER into consideration
197        if row.has_key('password'):
198            passwd = row.get('password', IGNORE_MARKER)
199            if passwd not in ('', IGNORE_MARKER):
200                if passwd.startswith('{SSHA}'):
201                    # already encrypted password
202                    obj.password = passwd
203                else:
204                    # not yet encrypted password
205                    IUserAccount(obj).setPassword(passwd)
206                items_changed += ('%s=%s, ' % ('password', passwd))
207            row.pop('password')
208
209        # Update registration state
210        if row.has_key('state'):
211            state = row.get('state', IGNORE_MARKER)
212            if state not in (IGNORE_MARKER, ''):
213                value = row['state']
214                IWorkflowState(obj).setState(value)
215                msg = _("State '${a}' set", mapping = {'a':value})
216                history = IObjectHistory(obj)
217                history.addMessage(msg)
218                items_changed += ('%s=%s, ' % ('state', state))
219            row.pop('state')
220
221        if row.has_key('transition'):
222            transition = row.get('transition', IGNORE_MARKER)
223            if transition not in (IGNORE_MARKER, ''):
224                value = row['transition']
225                IWorkflowInfo(obj).fireTransition(value)
226                items_changed += ('%s=%s, ' % ('transition', transition))
227            row.pop('transition')
228
229        # apply other values...
230        items_changed += super(StudentProcessor, self).updateEntry(
231            obj, row, site)
232
233        # Log actions...
234        parent = self.getParent(row, site)
235        if hasattr(obj,'student_id'):
236            # Update mode: the student exists and we can get the student_id.
237            # Create mode: the record contains the student_id
238            parent.logger.info(
239                '%s - Student record updated: %s'
240                % (obj.student_id, items_changed))
241        else:
242            # Create mode: the student does not yet exist
243            parent.logger.info('Student data imported: %s' % items_changed)
244        return items_changed
245
246    def getMapping(self, path, headerfields, mode):
247        """Get a mapping from CSV file headerfields to actually used fieldnames.
248        """
249        result = dict()
250        reader = csv.reader(open(path, 'rb'))
251        raw_header = reader.next()
252        for num, field in enumerate(headerfields):
253            if field not in ['student_id', 'reg_number', 'matric_number'
254                             ] and mode == 'remove':
255                continue
256            if field == u'--IGNORE--':
257                # Skip ignored columns in failed and finished data files.
258                continue
259            result[raw_header[num]] = field
260        return result
261
262    def checkConversion(self, row, mode='create'):
263        """Validates all values in row.
264        """
265        iface = self.iface
266        if mode in ['update', 'remove']:
267            if self.getLocator(row) == 'reg_number':
268                iface = self.iface_byregnumber
269            elif self.getLocator(row) == 'matric_number':
270                iface = self.iface_bymatricnumber
271        converter = IObjectConverter(iface)
272        errs, inv_errs, conv_dict =  converter.fromStringDict(
273            row, self.factory_name, mode=mode)
274        if row.has_key('transition'):
275            if row['transition'] not in IMPORTABLE_TRANSITIONS:
276                if row['transition'] not in (IGNORE_MARKER, ''):
277                    errs.append(('transition','not allowed'))
278        if row.has_key('state'):
279            if row['state'] not in IMPORTABLE_STATES:
280                if row['state'] not in (IGNORE_MARKER, ''):
281                    errs.append(('state','not allowed'))
282                else:
283                    # State is an attribute of Student and must not
284                    # be changed if empty.
285                    conv_dict['state'] = IGNORE_MARKER
286        try:
287            # Correct stud_id counter. As the IConverter for students
288            # creates student objects that are not used afterwards, we
289            # have to fix the site-wide student_id counter.
290            site = grok.getSite()
291            students = site['students']
292            students._curr_stud_id -= 1
293        except (KeyError, TypeError, AttributeError):
294                pass
295        return errs, inv_errs, conv_dict
296
297
298class StudentProcessorBase(BatchProcessor):
299    """A base for student subitem processor.
300
301    Helps reducing redundancy.
302    """
303    grok.baseclass()
304
305    # additional available  fields
306    # beside 'student_id', 'reg_number' and 'matric_number'
307    additional_fields = []
308
309    #: header fields additionally required
310    additional_headers = []
311
312    @property
313    def available_fields(self):
314        fields = ['student_id','reg_number','matric_number'
315                  ] + self.additional_fields
316        return sorted(list(set(fields + getFields(
317                self.iface).keys())))
318
319    def checkHeaders(self, headerfields, mode='ignore'):
320        if not 'reg_number' in headerfields and not 'student_id' \
321            in headerfields and not 'matric_number' in headerfields:
322            raise FatalCSVError(
323                "Need at least columns student_id " +
324                "or reg_number or matric_number for import!")
325        for name in self.additional_headers:
326            if not name in headerfields:
327                raise FatalCSVError(
328                    "Need %s for import!" % name)
329
330        # Check for fields to be ignored...
331        not_ignored_fields = [x for x in headerfields
332                              if not x.startswith('--')]
333        if len(set(not_ignored_fields)) < len(not_ignored_fields):
334            raise FatalCSVError(
335                "Double headers: each column name may only appear once.")
336        return True
337
338    def _getStudent(self, row, site):
339        NON_VALUES = ['', IGNORE_MARKER]
340        if not 'students' in site.keys():
341            return None
342        if row.get('student_id', '') not in NON_VALUES:
343            if row['student_id'] in site['students']:
344                student = site['students'][row['student_id']]
345                return student
346        elif row.get('reg_number', '') not in NON_VALUES:
347            reg_number = row['reg_number']
348            cat = queryUtility(ICatalog, name='students_catalog')
349            results = list(
350                cat.searchResults(reg_number=(reg_number, reg_number)))
351            if results:
352                return results[0]
353        elif row.get('matric_number', '') not in NON_VALUES:
354            matric_number = row['matric_number']
355            cat = queryUtility(ICatalog, name='students_catalog')
356            results = list(
357                cat.searchResults(matric_number=(matric_number, matric_number)))
358            if results:
359                return results[0]
360        return None
361
362    def parentsExist(self, row, site):
363        return self.getParent(row, site) is not None
364
365    def entryExists(self, row, site):
366        return self.getEntry(row, site) is not None
367
368    def checkConversion(self, row, mode='ignore'):
369        """Validates all values in row.
370        """
371        converter = IObjectConverter(self.iface)
372        errs, inv_errs, conv_dict =  converter.fromStringDict(
373            row, self.factory_name, mode=mode)
374        return errs, inv_errs, conv_dict
375
376    def getMapping(self, path, headerfields, mode):
377        """Get a mapping from CSV file headerfields to actually used fieldnames.
378        """
379        result = dict()
380        reader = csv.reader(open(path, 'rb'))
381        raw_header = reader.next()
382        for num, field in enumerate(headerfields):
383            if field not in ['student_id', 'reg_number', 'matric_number',
384                             'p_id', 'code', 'level'
385                             ] and mode == 'remove':
386                continue
387            if field == u'--IGNORE--':
388                # Skip ignored columns in failed and finished data files.
389                continue
390            result[raw_header[num]] = field
391        return result
392
393
394class StudentStudyCourseProcessor(StudentProcessorBase):
395    """A batch processor for IStudentStudyCourse objects.
396    """
397    grok.implements(IBatchProcessor)
398    grok.provides(IBatchProcessor)
399    grok.context(Interface)
400    util_name = 'studycourseupdater'
401    grok.name(util_name)
402
403    name = u'StudentStudyCourse Processor (update only)'
404    iface = IStudentStudyCourse
405    factory_name = 'waeup.StudentStudyCourse'
406
407    location_fields = []
408    additional_fields = []
409
410    def getParent(self, row, site):
411        return self._getStudent(row, site)
412
413    def getEntry(self, row, site):
414        student = self.getParent(row, site)
415        if student is None:
416            return None
417        return student.get('studycourse')
418
419    def updateEntry(self, obj, row, site):
420        """Update obj to the values given in row.
421        """
422        items_changed = super(StudentStudyCourseProcessor, self).updateEntry(
423            obj, row, site)
424        parent = self.getParent(row, site)
425        parent.__parent__.logger.info(
426            '%s - Study course updated: %s'
427            % (parent.student_id, items_changed))
428        # Update the students_catalog
429        notify(grok.ObjectModifiedEvent(obj.__parent__))
430        return
431
432    def checkConversion(self, row, mode='ignore'):
433        """Validates all values in row.
434        """
435        errs, inv_errs, conv_dict = super(
436            StudentStudyCourseProcessor, self).checkConversion(row, mode=mode)
437        # We have to check if current_level is in range of certificate.
438        if conv_dict.has_key('certificate') and \
439            conv_dict.has_key('current_level'):
440            cert = conv_dict['certificate']
441            level = conv_dict['current_level']
442            if level < cert.start_level or level > cert.end_level+120:
443                errs.append(('current_level','not in range'))
444        return errs, inv_errs, conv_dict
445
446    def checkUpdateRequirements(self, obj, row, site):
447        """Checks requirements the object must fulfill when being updated.
448
449        Returns error messages as strings in case of requirement
450        problems.
451        """
452        current_level = row.get('current_level', None)
453        if current_level == 999 and \
454            obj.__parent__.state in FORBIDDEN_POSTGRAD_STATES:
455            return 'Not a pg student.'
456        return None
457
458class StudentStudyLevelProcessor(StudentProcessorBase):
459    """A batch processor for IStudentStudyLevel objects.
460    """
461    grok.implements(IBatchProcessor)
462    grok.provides(IBatchProcessor)
463    grok.context(Interface)
464    util_name = 'studylevelprocessor'
465    grok.name(util_name)
466
467    name = u'StudentStudyLevel Processor'
468    iface = IStudentStudyLevel
469    factory_name = 'waeup.StudentStudyLevel'
470
471    location_fields = []
472
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 = [
721        'current_session',
722        'current_level',
723        'bypass_validation']
724
725    def checkUpdateRequirements(self, obj, row, site):
726        """Checks requirements the studycourse and the student must fulfill
727        before being updated.
728        """
729        # Check if current_levels correspond
730        if obj.current_level != row['current_level']:
731            return 'Current level does not correspond.'
732        # Check if current_sessions correspond
733        if obj.current_session != row['current_session']:
734            return 'Current session does not correspond.'
735        # Check if new verdict is provided
736        if row['current_verdict'] in (IGNORE_MARKER, ''):
737            return 'No verdict in import file.'
738        # Check if studylevel exists#
739        level_string = str(obj.current_level)
740        if obj.get(level_string) is None:
741            return 'Study level object is missing.'
742        # Check if student is in state REGISTERED or VALIDATED
743        if row.get('bypass_validation') in ('1', 'True', 'TRUE', 'yes'):
744            if obj.student.state not in (VALIDATED, REGISTERED):
745                return 'Student in wrong state.'
746        else:
747            if obj.student.state != VALIDATED:
748                return 'Student in wrong state.'
749        return None
750
751    def updateEntry(self, obj, row, site):
752        """Update obj to the values given in row.
753        """
754        # Don't set current_session, current_level
755        vals_to_set = dict((key, val) for key, val in row.items()
756                           if key not in ('current_session','current_level'))
757        super(StudentVerdictProcessor, self).updateEntry(obj, vals_to_set, site)
758        parent = self.getParent(row, site)
759        # Set current_vedict in corresponding studylevel
760        level_string = str(obj.current_level)
761        obj[level_string].level_verdict = row['current_verdict']
762        # Fire transition depending on student's state
763        if obj.__parent__.state == REGISTERED:
764            IWorkflowInfo(obj.__parent__).fireTransition('bypass_validation')
765            obj[level_string].validated_by = _('System')
766            obj[level_string].validation_date = datetime.utcnow()
767        else:
768            IWorkflowInfo(obj.__parent__).fireTransition('return')
769        # Update the students_catalog
770        notify(grok.ObjectModifiedEvent(obj.__parent__))
771        return
Note: See TracBrowser for help on using the repository browser.