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

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

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

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