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

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

Current level must be in range of certificate. This must be checked in checkUpdateRequirements.

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