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

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

Customize updateEntry method of FacultyProcessor? to import local_roles.

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