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

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

Fix conversion checker for payments.

  • Property svn:keywords set to Id
File size: 27.0 KB
RevLine 
[7191]1## $Id: batching.py 8942 2012-07-08 08:59:42Z 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'):
225            # Update mode: the student exists and we can get the student_id
226            parent.logger.info(
227                '%s - Student record updated: %s'
228                % (obj.student_id, items_changed))
229        else:
230            # Create mode: the student does not yet exist
231            parent.logger.info('Student data imported: %s' % items_changed)
[8221]232        return items_changed
[7497]233
[6849]234    def getMapping(self, path, headerfields, mode):
235        """Get a mapping from CSV file headerfields to actually used fieldnames.
236        """
237        result = dict()
238        reader = csv.reader(open(path, 'rb'))
239        raw_header = reader.next()
240        for num, field in enumerate(headerfields):
[8221]241            if field not in ['student_id', 'reg_number', 'matric_number'
242                             ] and mode == 'remove':
[6849]243                continue
244            if field == u'--IGNORE--':
245                # Skip ignored columns in failed and finished data files.
246                continue
247            result[raw_header[num]] = field
248        return result
249
250    def checkConversion(self, row, mode='create'):
251        """Validates all values in row.
252        """
[7643]253        iface = self.iface
[6849]254        if mode in ['update', 'remove']:
255            if self.getLocator(row) == 'reg_number':
[8581]256                iface = self.iface_byregnumber
[6849]257            elif self.getLocator(row) == 'matric_number':
[8581]258                iface = self.iface_bymatricnumber
[6849]259        converter = IObjectConverter(iface)
260        errs, inv_errs, conv_dict =  converter.fromStringDict(
[8214]261            row, self.factory_name, mode=mode)
[8309]262        if row.has_key('transition') and \
263            not row['transition'] in IMPORTABLE_TRANSITIONS:
264            if row['transition'] not in (IGNORE_MARKER, ''):
265                errs.append(('transition','not allowed'))
[8287]266        if row.has_key('state') and \
267            not row['state'] in IMPORTABLE_STATES:
268            if row['state'] not in (IGNORE_MARKER, ''):
269                errs.append(('state','not allowed'))
270            else:
271                # state is an attribute of Student and must not
272                # be changed if empty
273                conv_dict['state'] = IGNORE_MARKER
[8490]274
275        try:
276            # Correct stud_id counter. As the IConverter for students
277            # creates student objects that are not used afterwards, we
278            # have to fix the site-wide student_id counter.
279            site = grok.getSite()
280            students = site['students']
281            students._curr_stud_id -= 1
282        except (KeyError, TypeError, AttributeError):
283                pass
[6849]284        return errs, inv_errs, conv_dict
285
[8232]286
287class StudentProcessorBase(BatchProcessor):
288    """A base for student subitem processor.
289
290    Helps reducing redundancy.
[6825]291    """
[8232]292    grok.baseclass()
[6825]293
[8884]294    # additional available  fields
295    # beside 'student_id', 'reg_number' and 'matric_number'
[8232]296    additional_fields = []
[6825]297
[8884]298    #: header fields additionally required
[8232]299    additional_headers = []
[6849]300
[6825]301    @property
302    def available_fields(self):
[8232]303        fields = ['student_id','reg_number','matric_number'
304                  ] + self.additional_fields
305        return sorted(list(set(fields + getFields(
[6843]306                self.iface).keys())))
[6825]307
[6837]308    def checkHeaders(self, headerfields, mode='ignore'):
[6854]309        if not 'reg_number' in headerfields and not 'student_id' \
310            in headerfields and not 'matric_number' in headerfields:
[6825]311            raise FatalCSVError(
[6854]312                "Need at least columns student_id " +
313                "or reg_number or matric_number for import!")
[8232]314        for name in self.additional_headers:
315            if not name in headerfields:
316                raise FatalCSVError(
317                    "Need %s for import!" % name)
318
[6834]319        # Check for fields to be ignored...
[6825]320        not_ignored_fields = [x for x in headerfields
321                              if not x.startswith('--')]
322        if len(set(not_ignored_fields)) < len(not_ignored_fields):
323            raise FatalCSVError(
324                "Double headers: each column name may only appear once.")
325        return True
326
[8232]327    def _getStudent(self, row, site):
[8225]328        NON_VALUES = ['', IGNORE_MARKER]
[6846]329        if not 'students' in site.keys():
[6849]330            return None
[8225]331        if row.get('student_id', '') not in NON_VALUES:
[6825]332            if row['student_id'] in site['students']:
333                student = site['students'][row['student_id']]
334                return student
[8225]335        elif row.get('reg_number', '') not in NON_VALUES:
[6825]336            reg_number = row['reg_number']
337            cat = queryUtility(ICatalog, name='students_catalog')
338            results = list(
339                cat.searchResults(reg_number=(reg_number, reg_number)))
340            if results:
341                return results[0]
[8225]342        elif row.get('matric_number', '') not in NON_VALUES:
[6843]343            matric_number = row['matric_number']
344            cat = queryUtility(ICatalog, name='students_catalog')
345            results = list(
346                cat.searchResults(matric_number=(matric_number, matric_number)))
347            if results:
348                return results[0]
[6849]349        return None
[6825]350
[7267]351    def parentsExist(self, row, site):
352        return self.getParent(row, site) is not None
353
[6825]354    def entryExists(self, row, site):
[7534]355        return self.getEntry(row, site) is not None
[6825]356
[8232]357    def checkConversion(self, row, mode='ignore'):
358        """Validates all values in row.
359        """
360        converter = IObjectConverter(self.iface)
361        errs, inv_errs, conv_dict =  converter.fromStringDict(
362            row, self.factory_name, mode=mode)
363        return errs, inv_errs, conv_dict
364
[8885]365    def getMapping(self, path, headerfields, mode):
366        """Get a mapping from CSV file headerfields to actually used fieldnames.
367        """
368        result = dict()
369        reader = csv.reader(open(path, 'rb'))
370        raw_header = reader.next()
371        for num, field in enumerate(headerfields):
[8888]372            if field not in ['student_id', 'reg_number', 'matric_number',
373                             'p_id', 'code', 'level'
[8885]374                             ] and mode == 'remove':
375                continue
376            if field == u'--IGNORE--':
377                # Skip ignored columns in failed and finished data files.
378                continue
379            result[raw_header[num]] = field
380        return result
[8232]381
[8885]382
[8232]383class StudentStudyCourseProcessor(StudentProcessorBase):
384    """A batch processor for IStudentStudyCourse objects.
385    """
386    grok.implements(IBatchProcessor)
387    grok.provides(IBatchProcessor)
388    grok.context(Interface)
389    util_name = 'studycourseupdater'
390    grok.name(util_name)
391
392    name = u'StudentStudyCourse Processor (update only)'
393    iface = IStudentStudyCourse
394    factory_name = 'waeup.StudentStudyCourse'
395
396    location_fields = []
397    additional_fields = []
398
399    def getParent(self, row, site):
400        return self._getStudent(row, site)
401
[6825]402    def getEntry(self, row, site):
[7534]403        student = self.getParent(row, site)
[7536]404        if student is None:
[6825]405            return None
406        return student.get('studycourse')
[7429]407
408    def updateEntry(self, obj, row, site):
409        """Update obj to the values given in row.
410        """
[8221]411        items_changed = super(StudentStudyCourseProcessor, self).updateEntry(
412            obj, row, site)
[7656]413        parent = self.getParent(row, site)
414        parent.__parent__.logger.info(
415            '%s - Study course updated: %s'
416            % (parent.student_id, items_changed))
[7429]417        # Update the students_catalog
418        notify(grok.ObjectModifiedEvent(obj.__parent__))
419        return
420
[7532]421    def checkConversion(self, row, mode='ignore'):
422        """Validates all values in row.
423        """
[8232]424        errs, inv_errs, conv_dict = super(
425            StudentStudyCourseProcessor, self).checkConversion(row, mode=mode)
[7532]426        # We have to check if current_level is in range of certificate.
[8940]427        if conv_dict.has_key('certificate') and \
428            conv_dict.has_key('current_level'):
429            cert = conv_dict['certificate']
430            level = conv_dict['current_level']
431            if level < cert.start_level or level > cert.end_level+120:
432                errs.append(('current_level','not in range'))
[7532]433        return errs, inv_errs, conv_dict
434
[8232]435class StudentStudyLevelProcessor(StudentProcessorBase):
[7536]436    """A batch processor for IStudentStudyLevel objects.
437    """
438    grok.implements(IBatchProcessor)
439    grok.provides(IBatchProcessor)
440    grok.context(Interface)
[7933]441    util_name = 'studylevelprocessor'
[7536]442    grok.name(util_name)
443
[7933]444    name = u'StudentStudyLevel Processor'
[7536]445    iface = IStudentStudyLevel
446    factory_name = 'waeup.StudentStudyLevel'
447
448    location_fields = []
[8232]449    additional_fields = ['level']
450    additional_headers = ['level']
[7536]451
452    def getParent(self, row, site):
[8232]453        student = self._getStudent(row, site)
454        if student is None:
[7536]455            return None
[8232]456        return student['studycourse']
[7536]457
458    def getEntry(self, row, site):
459        studycourse = self.getParent(row, site)
460        if studycourse is None:
461            return None
462        return studycourse.get(row['level'])
463
[8626]464    def updateEntry(self, obj, row, site):
465        """Update obj to the values given in row.
466        """
467        items_changed = super(StudentStudyLevelProcessor, self).updateEntry(
468            obj, row, site)
469        student = self.getParent(row, site).__parent__
470        student.__parent__.logger.info(
471            '%s - Study level updated: %s'
472            % (student.student_id, items_changed))
473        return
474
[7536]475    def addEntry(self, obj, row, site):
476        parent = self.getParent(row, site)
477        obj.level = int(row['level'])
478        parent[row['level']] = obj
479        return
480
481    def checkConversion(self, row, mode='ignore'):
482        """Validates all values in row.
483        """
[8232]484        errs, inv_errs, conv_dict = super(
485            StudentStudyLevelProcessor, self).checkConversion(row, mode=mode)
486
[7536]487        # We have to check if level is a valid integer.
[7548]488        # This is not done by the converter.
[7536]489        try:
490            level = int(row['level'])
[7612]491            if level not in range(0,700,10):
[7536]492                errs.append(('level','no valid integer'))
493        except ValueError:
494            errs.append(('level','no integer'))
495        return errs, inv_errs, conv_dict
[7548]496
[8232]497class CourseTicketProcessor(StudentProcessorBase):
[7548]498    """A batch processor for ICourseTicket objects.
499    """
500    grok.implements(IBatchProcessor)
501    grok.provides(IBatchProcessor)
502    grok.context(Interface)
[7933]503    util_name = 'courseticketprocessor'
[7548]504    grok.name(util_name)
505
[7933]506    name = u'CourseTicket Processor'
[7548]507    iface = ICourseTicket
508    factory_name = 'waeup.CourseTicket'
509
510    location_fields = []
[8232]511    additional_fields = ['level', 'code']
512    additional_headers = ['level', 'code']
[7548]513
514    def getParent(self, row, site):
[8232]515        student = self._getStudent(row, site)
516        if student is None:
[7548]517            return None
[8232]518        return student['studycourse'].get(row['level'])
[7548]519
520    def getEntry(self, row, site):
521        level = self.getParent(row, site)
522        if level is None:
523            return None
524        return level.get(row['code'])
525
[8626]526    def updateEntry(self, obj, row, site):
527        """Update obj to the values given in row.
528        """
529        items_changed = super(CourseTicketProcessor, self).updateEntry(
530            obj, row, site)
[8888]531        parent = self.getParent(row, site)
[8626]532        student = self.getParent(row, site).__parent__.__parent__
533        student.__parent__.logger.info(
[8888]534            '%s - Course ticket in %s updated: %s'
535            % (student.student_id,  parent.level, items_changed))
[8626]536        return
537
[7548]538    def addEntry(self, obj, row, site):
539        parent = self.getParent(row, site)
540        catalog = getUtility(ICatalog, name='courses_catalog')
541        entries = list(catalog.searchResults(code=(row['code'],row['code'])))
542        obj.fcode = entries[0].__parent__.__parent__.__parent__.code
543        obj.dcode = entries[0].__parent__.__parent__.code
544        obj.title = entries[0].title
545        obj.credits = entries[0].credits
546        obj.passmark = entries[0].passmark
547        obj.semester = entries[0].semester
548        parent[row['code']] = obj
549        return
550
[8888]551    def delEntry(self, row, site):
552        ticket = self.getEntry(row, site)
553        parent = self.getParent(row, site)
554        if ticket is not None:
555            student = self._getStudent(row, site)
556            student.__parent__.logger.info('%s - Course ticket in %s removed: %s'
557                % (student.student_id, parent.level, ticket.code))
558            del parent[ticket.code]
559        return
560
[7548]561    def checkConversion(self, row, mode='ignore'):
562        """Validates all values in row.
563        """
[8232]564        errs, inv_errs, conv_dict = super(
565            CourseTicketProcessor, self).checkConversion(row, mode=mode)
566
[7548]567        # We have to check if course really exists.
568        # This is not done by the converter.
569        catalog = getUtility(ICatalog, name='courses_catalog')
570        entries = catalog.searchResults(code=(row['code'],row['code']))
571        if len(entries) == 0:
572            errs.append(('code','non-existent'))
573            return errs, inv_errs, conv_dict
[7623]574        return errs, inv_errs, conv_dict
575
[8232]576class StudentOnlinePaymentProcessor(StudentProcessorBase):
[7623]577    """A batch processor for IStudentOnlinePayment objects.
578    """
579    grok.implements(IBatchProcessor)
580    grok.provides(IBatchProcessor)
581    grok.context(Interface)
[7933]582    util_name = 'paymentprocessor'
[7623]583    grok.name(util_name)
584
[8942]585    name = u'Student Payment Processor'
[8174]586    iface = IStudentOnlinePayment
[7623]587    factory_name = 'waeup.StudentOnlinePayment'
588
589    location_fields = []
[8232]590    additional_fields = ['p_id']
[8884]591    additional_headers = []
[7623]592
[8884]593    def checkHeaders(self, headerfields, mode='ignore'):
594        super(StudentOnlinePaymentProcessor, self).checkHeaders(headerfields)
[8885]595        if mode in ('update', 'remove') and not 'p_id' in headerfields:
[8884]596            raise FatalCSVError(
[8885]597                "Need p_id for import in update and remove modes!")
[8884]598        return True
599
[8232]600    def parentsExist(self, row, site):
601        return self.getParent(row, site) is not None
[7623]602
603    def getParent(self, row, site):
[8232]604        student = self._getStudent(row, site)
605        if student is None:
[7623]606            return None
[8232]607        return student['payments']
[7623]608
609    def getEntry(self, row, site):
610        payments = self.getParent(row, site)
611        if payments is None:
612            return None
[8884]613        p_id = row.get('p_id', None)
614        if p_id is None:
615            return None
[7626]616        # We can use the hash symbol at the end of p_id in import files
617        # to avoid annoying automatic number transformation
618        # by Excel or Calc
[8884]619        p_id = p_id.strip('#')
620        if not p_id.startswith('p'):
621            # For data migration from old SRP only
622            p_id = 'p' + p_id[7:] + '0'
623        entry = payments.get(p_id)
[7623]624        return entry
625
[8626]626    def updateEntry(self, obj, row, site):
627        """Update obj to the values given in row.
628        """
629        items_changed = super(StudentOnlinePaymentProcessor, self).updateEntry(
630            obj, row, site)
631        student = self.getParent(row, site).__parent__
632        student.__parent__.logger.info(
[8886]633            '%s - Payment ticket updated: %s'
634            % (student.student_id, items_changed))
[8626]635        return
636
[7623]637    def addEntry(self, obj, row, site):
638        parent = self.getParent(row, site)
[7626]639        p_id = row['p_id'].strip('#')
640        if not p_id.startswith('p'):
[7623]641            # For data migration from old SRP
[8884]642            obj.p_id = 'p' + p_id[7:] + '0'
[7623]643            parent[obj.p_id] = obj
644        else:
[7626]645            parent[p_id] = obj
[7623]646        return
647
[8885]648    def delEntry(self, row, site):
649        payment = self.getEntry(row, site)
650        parent = self.getParent(row, site)
651        if payment is not None:
652            student = self._getStudent(row, site)
[8886]653            student.__parent__.logger.info('%s - Payment ticket removed: %s'
[8885]654                % (student.student_id, payment.p_id))
655            del parent[payment.p_id]
[8886]656        return
[8885]657
[7623]658    def checkConversion(self, row, mode='ignore'):
659        """Validates all values in row.
660        """
[8232]661        errs, inv_errs, conv_dict = super(
662            StudentOnlinePaymentProcessor, self).checkConversion(row, mode=mode)
663
[7623]664        # We have to check p_id.
[8884]665        p_id = row.get('p_id', None)
[8942]666        if not p_id:
[8884]667            timestamp = ("%d" % int(time()*10000))[1:]
668            p_id = "p%s" % timestamp
669            conv_dict['p_id'] = p_id
670            return errs, inv_errs, conv_dict
671        else:
672            p_id = p_id.strip('#')
[7626]673        if p_id.startswith('p'):
674            if not len(p_id) == 14:
[7623]675                errs.append(('p_id','invalid length'))
676                return errs, inv_errs, conv_dict
677        else:
[7626]678            if not len(p_id) == 19:
[7623]679                errs.append(('p_id','invalid length'))
680                return errs, inv_errs, conv_dict
681        return errs, inv_errs, conv_dict
[7951]682
683class StudentVerdictProcessor(StudentStudyCourseProcessor):
684    """A batch processor for verdicts.
685
686    Import verdicts and perform workflow transitions.
687    """
688
689    util_name = 'verdictupdater'
690    grok.name(util_name)
691
692    name = u'Verdict Processor (update only)'
693    iface = IStudentVerdictUpdate
694    factory_name = 'waeup.StudentStudyCourse'
695
[8232]696    additional_fields = ['current_session', 'current_level']
[7951]697
698    def checkUpdateRequirements(self, obj, row, site):
699        """Checks requirements the studycourse and the student must fulfill
700        before being updated.
701        """
702        # Check if current_levels correspond
703        if obj.current_level != row['current_level']:
704            return 'Current level does not correspond.'
705        # Check if current_sessions correspond
706        if obj.current_session != row['current_session']:
707            return 'Current session does not correspond.'
708        # Check if student is in state REGISTERED
[8736]709        if obj.student.state != VALIDATED:
[7951]710            return 'Student in wrong state.'
711        return None
712
713    def updateEntry(self, obj, row, site):
714        """Update obj to the values given in row.
715        """
[8221]716        # Don't set current_session, current_level
717        vals_to_set = dict((key, val) for key, val in row.items()
718                           if key not in ('current_session','current_level'))
719        items_changed = super(StudentVerdictProcessor, self).updateEntry(
720            obj, vals_to_set, site)
[7951]721        parent = self.getParent(row, site)
722        parent.__parent__.logger.info(
723            '%s - Verdict updated: %s'
724            % (parent.student_id, items_changed))
725        # Fire transition
726        IWorkflowInfo(obj.__parent__).fireTransition('return')
727        # Update the students_catalog
728        notify(grok.ObjectModifiedEvent(obj.__parent__))
729        return
Note: See TracBrowser for help on using the repository browser.