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

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

Use new attrs_to_fields function to display gpa and total_credits on level pages.

  • Property svn:keywords set to Id
File size: 31.6 KB
Line 
1## $Id: batching.py 9690 2012-11-20 06:17:36Z 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 row.has_key('state'):
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):
188        """Update obj to the values given in row.
189        """
190        items_changed = ''
191
192        # Remove student_id from row if empty
193        if row.has_key('student_id') and row['student_id'] in (
194            None, IGNORE_MARKER):
195            row.pop('student_id')
196
197        # Update password
198        # XXX: Tale DELETION_MARKER into consideration
199        if row.has_key('password'):
200            passwd = row.get('password', IGNORE_MARKER)
201            if passwd not in ('', IGNORE_MARKER):
202                if passwd.startswith('{SSHA}'):
203                    # already encrypted password
204                    obj.password = passwd
205                else:
206                    # not yet encrypted password
207                    IUserAccount(obj).setPassword(passwd)
208                items_changed += ('%s=%s, ' % ('password', passwd))
209            row.pop('password')
210
211        # Update registration state
212        if row.has_key('state'):
213            state = row.get('state', IGNORE_MARKER)
214            if state not in (IGNORE_MARKER, ''):
215                value = row['state']
216                IWorkflowState(obj).setState(value)
217                msg = _("State '${a}' set", mapping = {'a':value})
218                history = IObjectHistory(obj)
219                history.addMessage(msg)
220                items_changed += ('%s=%s, ' % ('state', state))
221            row.pop('state')
222
223        if row.has_key('transition'):
224            transition = row.get('transition', IGNORE_MARKER)
225            if transition not in (IGNORE_MARKER, ''):
226                value = row['transition']
227                IWorkflowInfo(obj).fireTransition(value)
228                items_changed += ('%s=%s, ' % ('transition', transition))
229            row.pop('transition')
230
231        # apply other values...
232        items_changed += super(StudentProcessor, self).updateEntry(
233            obj, row, site)
234
235        # Log actions...
236        parent = self.getParent(row, site)
237        if hasattr(obj,'student_id'):
238            # Update mode: the student exists and we can get the student_id.
239            # Create mode: the record contains the student_id
240            parent.logger.info(
241                '%s - Student record updated: %s'
242                % (obj.student_id, items_changed))
243        else:
244            # Create mode: the student does not yet exist
245            parent.logger.info('Student data imported: %s' % items_changed)
246        return items_changed
247
248    def getMapping(self, path, headerfields, mode):
249        """Get a mapping from CSV file headerfields to actually used fieldnames.
250        """
251        result = dict()
252        reader = csv.reader(open(path, 'rb'))
253        raw_header = reader.next()
254        for num, field in enumerate(headerfields):
255            if field not in ['student_id', 'reg_number', 'matric_number'
256                             ] and mode == 'remove':
257                continue
258            if field == u'--IGNORE--':
259                # Skip ignored columns in failed and finished data files.
260                continue
261            result[raw_header[num]] = field
262        return result
263
264    def checkConversion(self, row, mode='create'):
265        """Validates all values in row.
266        """
267        iface = self.iface
268        if mode in ['update', 'remove']:
269            if self.getLocator(row) == 'reg_number':
270                iface = self.iface_byregnumber
271            elif self.getLocator(row) == 'matric_number':
272                iface = self.iface_bymatricnumber
273        converter = IObjectConverter(iface)
274        errs, inv_errs, conv_dict =  converter.fromStringDict(
275            row, self.factory_name, mode=mode)
276        if row.has_key('transition'):
277            if row['transition'] not in IMPORTABLE_TRANSITIONS:
278                if row['transition'] not in (IGNORE_MARKER, ''):
279                    errs.append(('transition','not allowed'))
280        if row.has_key('state'):
281            if row['state'] not in IMPORTABLE_STATES:
282                if row['state'] not in (IGNORE_MARKER, ''):
283                    errs.append(('state','not allowed'))
284                else:
285                    # State is an attribute of Student and must not
286                    # be changed if empty.
287                    conv_dict['state'] = IGNORE_MARKER
288        try:
289            # Correct stud_id counter. As the IConverter for students
290            # creates student objects that are not used afterwards, we
291            # have to fix the site-wide student_id counter.
292            site = grok.getSite()
293            students = site['students']
294            students._curr_stud_id -= 1
295        except (KeyError, TypeError, AttributeError):
296                pass
297        return errs, inv_errs, conv_dict
298
299
300class StudentProcessorBase(BatchProcessor):
301    """A base for student subitem processor.
302
303    Helps reducing redundancy.
304    """
305    grok.baseclass()
306
307    # additional available fields
308    # beside 'student_id', 'reg_number' and 'matric_number'
309    additional_fields = []
310
311    #: header fields additionally required
312    additional_headers = []
313
314    @property
315    def available_fields(self):
316        fields = ['student_id','reg_number','matric_number'
317                  ] + self.additional_fields
318        return sorted(list(set(fields + getFields(
319                self.iface).keys())))
320
321    def checkHeaders(self, headerfields, mode='ignore'):
322        if not 'reg_number' in headerfields and not 'student_id' \
323            in headerfields and not 'matric_number' in headerfields:
324            raise FatalCSVError(
325                "Need at least columns student_id " +
326                "or reg_number or matric_number for import!")
327        for name in self.additional_headers:
328            if not name in headerfields:
329                raise FatalCSVError(
330                    "Need %s for import!" % name)
331
332        # Check for fields to be ignored...
333        not_ignored_fields = [x for x in headerfields
334                              if not x.startswith('--')]
335        if len(set(not_ignored_fields)) < len(not_ignored_fields):
336            raise FatalCSVError(
337                "Double headers: each column name may only appear once.")
338        return True
339
340    def _getStudent(self, row, site):
341        NON_VALUES = ['', IGNORE_MARKER]
342        if not 'students' in site.keys():
343            return None
344        if row.get('student_id', '') not in NON_VALUES:
345            if row['student_id'] in site['students']:
346                student = site['students'][row['student_id']]
347                return student
348        elif row.get('reg_number', '') not in NON_VALUES:
349            reg_number = row['reg_number']
350            cat = queryUtility(ICatalog, name='students_catalog')
351            results = list(
352                cat.searchResults(reg_number=(reg_number, reg_number)))
353            if results:
354                return results[0]
355        elif row.get('matric_number', '') not in NON_VALUES:
356            matric_number = row['matric_number']
357            cat = queryUtility(ICatalog, name='students_catalog')
358            results = list(
359                cat.searchResults(matric_number=(matric_number, matric_number)))
360            if results:
361                return results[0]
362        return None
363
364    def parentsExist(self, row, site):
365        return self.getParent(row, site) is not None
366
367    def entryExists(self, row, site):
368        return self.getEntry(row, site) is not None
369
370    def checkConversion(self, row, mode='ignore'):
371        """Validates all values in row.
372        """
373        converter = IObjectConverter(self.iface)
374        errs, inv_errs, conv_dict =  converter.fromStringDict(
375            row, self.factory_name, mode=mode)
376        return errs, inv_errs, conv_dict
377
378    def getMapping(self, path, headerfields, mode):
379        """Get a mapping from CSV file headerfields to actually used fieldnames.
380        """
381        result = dict()
382        reader = csv.reader(open(path, 'rb'))
383        raw_header = reader.next()
384        for num, field in enumerate(headerfields):
385            if field not in ['student_id', 'reg_number', 'matric_number',
386                             'p_id', 'code', 'level'
387                             ] and mode == 'remove':
388                continue
389            if field == u'--IGNORE--':
390                # Skip ignored columns in failed and finished data files.
391                continue
392            result[raw_header[num]] = field
393        return result
394
395
396class StudentStudyCourseProcessor(StudentProcessorBase):
397    """A batch processor for IStudentStudyCourse objects.
398    """
399    grok.implements(IBatchProcessor)
400    grok.provides(IBatchProcessor)
401    grok.context(Interface)
402    util_name = 'studycourseupdater'
403    grok.name(util_name)
404
405    name = u'StudentStudyCourse Processor (update only)'
406    iface = IStudentStudyCourse
407    factory_name = 'waeup.StudentStudyCourse'
408
409    location_fields = []
410    additional_fields = []
411
412    def getParent(self, row, site):
413        return self._getStudent(row, site)
414
415    def getEntry(self, row, site):
416        student = self.getParent(row, site)
417        if student is None:
418            return None
419        return student.get('studycourse')
420
421    def updateEntry(self, obj, row, site):
422        """Update obj to the values given in row.
423        """
424        items_changed = super(StudentStudyCourseProcessor, self).updateEntry(
425            obj, row, site)
426        parent = self.getParent(row, site)
427        parent.__parent__.logger.info(
428            '%s - Study course updated: %s'
429            % (parent.student_id, items_changed))
430        # Update the students_catalog
431        notify(grok.ObjectModifiedEvent(obj.__parent__))
432        return
433
434    def checkConversion(self, row, mode='ignore'):
435        """Validates all values in row.
436        """
437        errs, inv_errs, conv_dict = super(
438            StudentStudyCourseProcessor, self).checkConversion(row, mode=mode)
439        # We have to check if current_level is in range of certificate.
440        if conv_dict.has_key('certificate') and \
441            conv_dict.has_key('current_level'):
442            cert = conv_dict['certificate']
443            level = conv_dict['current_level']
444            if level < cert.start_level or level > cert.end_level+120:
445                errs.append(('current_level','not in range'))
446        return errs, inv_errs, conv_dict
447
448    def checkUpdateRequirements(self, obj, row, site):
449        """Checks requirements the object must fulfill when being updated.
450
451        Returns error messages as strings in case of requirement
452        problems.
453        """
454        current_level = row.get('current_level', None)
455        if current_level == 999 and \
456            obj.__parent__.state in FORBIDDEN_POSTGRAD_STATES:
457            return 'Not a pg student.'
458        return None
459
460class StudentStudyLevelProcessor(StudentProcessorBase):
461    """A batch processor for IStudentStudyLevel objects.
462    """
463    grok.implements(IBatchProcessor)
464    grok.provides(IBatchProcessor)
465    grok.context(Interface)
466    util_name = 'studylevelprocessor'
467    grok.name(util_name)
468
469    name = u'StudentStudyLevel Processor'
470    iface = IStudentStudyLevel
471    factory_name = 'waeup.StudentStudyLevel'
472
473    location_fields = []
474
475    additional_fields = ['level']
476    additional_headers = ['level']
477
478    @property
479    def available_fields(self):
480        fields = super(StudentStudyLevelProcessor, self).available_fields
481        fields.remove('total_credits')
482        fields.remove('gpa')
483        return  fields
484
485    def getParent(self, row, site):
486        student = self._getStudent(row, site)
487        if student is None:
488            return None
489        return student['studycourse']
490
491    def getEntry(self, row, site):
492        studycourse = self.getParent(row, site)
493        if studycourse is None:
494            return None
495        return studycourse.get(row['level'])
496
497    def delEntry(self, row, site):
498        studylevel = self.getEntry(row, site)
499        parent = self.getParent(row, site)
500        if studylevel is not None:
501            student = self._getStudent(row, site)
502            student.__parent__.logger.info('%s - Level removed: %s'
503                % (student.student_id, studylevel.__name__))
504            del parent[studylevel.__name__]
505        return
506
507    def updateEntry(self, obj, row, site):
508        """Update obj to the values given in row.
509        """
510        items_changed = super(StudentStudyLevelProcessor, self).updateEntry(
511            obj, row, site)
512        student = self.getParent(row, site).__parent__
513        student.__parent__.logger.info(
514            '%s - Study level updated: %s'
515            % (student.student_id, items_changed))
516        return
517
518    def addEntry(self, obj, row, site):
519        parent = self.getParent(row, site)
520        obj.level = int(row['level'])
521        parent[row['level']] = obj
522        return
523
524    def checkConversion(self, row, mode='ignore'):
525        """Validates all values in row.
526        """
527        errs, inv_errs, conv_dict = super(
528            StudentStudyLevelProcessor, self).checkConversion(row, mode=mode)
529
530        # We have to check if level is a valid integer.
531        # This is not done by the converter.
532        try:
533            level = int(row['level'])
534            if level not in range(0,1000,10) + [999]:
535                errs.append(('level','no valid integer'))
536        except ValueError:
537            errs.append(('level','no integer'))
538        return errs, inv_errs, conv_dict
539
540class CourseTicketProcessor(StudentProcessorBase):
541    """A batch processor for ICourseTicket objects.
542    """
543    grok.implements(IBatchProcessor)
544    grok.provides(IBatchProcessor)
545    grok.context(Interface)
546    util_name = 'courseticketprocessor'
547    grok.name(util_name)
548
549    name = u'CourseTicket Processor'
550    iface = ICourseTicketImport
551    factory_name = 'waeup.CourseTicket'
552
553    location_fields = []
554    additional_fields = ['level', 'code']
555    additional_headers = ['level', 'code']
556
557    @property
558    def available_fields(self):
559        fields = [
560            'student_id','reg_number','matric_number',
561            'mandatory', 'score', 'carry_over', 'automatic',
562            'level_session'
563            ] + self.additional_fields
564        return sorted(fields)
565
566    def getParent(self, row, site):
567        student = self._getStudent(row, site)
568        if student is None:
569            return None
570        return student['studycourse'].get(row['level'])
571
572    def getEntry(self, row, site):
573        level = self.getParent(row, site)
574        if level is None:
575            return None
576        return level.get(row['code'])
577
578    def updateEntry(self, obj, row, site):
579        """Update obj to the values given in row.
580        """
581        items_changed = super(CourseTicketProcessor, self).updateEntry(
582            obj, row, site)
583        parent = self.getParent(row, site)
584        student = self.getParent(row, site).__parent__.__parent__
585        student.__parent__.logger.info(
586            '%s - Course ticket in %s updated: %s'
587            % (student.student_id,  parent.level, items_changed))
588        return
589
590    def addEntry(self, obj, row, site):
591        parent = self.getParent(row, site)
592        catalog = getUtility(ICatalog, name='courses_catalog')
593        entries = list(catalog.searchResults(code=(row['code'],row['code'])))
594        obj.fcode = entries[0].__parent__.__parent__.__parent__.code
595        obj.dcode = entries[0].__parent__.__parent__.code
596        obj.title = entries[0].title
597        #if getattr(obj, 'credits', None) is None:
598        obj.credits = entries[0].credits
599        #if getattr(obj, 'passmark', None) is None:
600        obj.passmark = entries[0].passmark
601        obj.semester = entries[0].semester
602        parent[row['code']] = obj
603        return
604
605    def delEntry(self, row, site):
606        ticket = self.getEntry(row, site)
607        parent = self.getParent(row, site)
608        if ticket is not None:
609            student = self._getStudent(row, site)
610            student.__parent__.logger.info('%s - Course ticket in %s removed: %s'
611                % (student.student_id, parent.level, ticket.code))
612            del parent[ticket.code]
613        return
614
615    def checkConversion(self, row, mode='ignore'):
616        """Validates all values in row.
617        """
618        errs, inv_errs, conv_dict = super(
619            CourseTicketProcessor, self).checkConversion(row, mode=mode)
620        if mode == 'remove':
621            return errs, inv_errs, conv_dict
622        # In update and create mode we have to check if course really exists.
623        # This is not done by the converter.
624        catalog = getUtility(ICatalog, name='courses_catalog')
625        entries = catalog.searchResults(code=(row['code'],row['code']))
626        if len(entries) == 0:
627            errs.append(('code','non-existent'))
628            return errs, inv_errs, conv_dict
629        # If level_session is provided in row we have to check if
630        # the parent studylevel exists and if its level_session
631        # attribute corresponds with the expected value in row.
632        level_session = conv_dict.get('level_session', IGNORE_MARKER)
633        if level_session not in (IGNORE_MARKER, None):
634            site = grok.getSite()
635            studylevel = self.getParent(row, site)
636            if studylevel is not None:
637                if studylevel.level_session != level_session:
638                    errs.append(('level_session','does not match %s'
639                        % studylevel.level_session))
640            else:
641                errs.append(('level','does not exist'))
642        return errs, inv_errs, conv_dict
643
644class StudentOnlinePaymentProcessor(StudentProcessorBase):
645    """A batch processor for IStudentOnlinePayment objects.
646    """
647    grok.implements(IBatchProcessor)
648    grok.provides(IBatchProcessor)
649    grok.context(Interface)
650    util_name = 'paymentprocessor'
651    grok.name(util_name)
652
653    name = u'Student Payment Processor'
654    iface = IStudentOnlinePayment
655    factory_name = 'waeup.StudentOnlinePayment'
656
657    location_fields = []
658    additional_fields = ['p_id']
659    additional_headers = []
660
661    def checkHeaders(self, headerfields, mode='ignore'):
662        super(StudentOnlinePaymentProcessor, self).checkHeaders(headerfields)
663        if mode in ('update', 'remove') and not 'p_id' in headerfields:
664            raise FatalCSVError(
665                "Need p_id for import in update and remove modes!")
666        return True
667
668    def parentsExist(self, row, site):
669        return self.getParent(row, site) is not None
670
671    def getParent(self, row, site):
672        student = self._getStudent(row, site)
673        if student is None:
674            return None
675        return student['payments']
676
677    def getEntry(self, row, site):
678        payments = self.getParent(row, site)
679        if payments is None:
680            return None
681        p_id = row.get('p_id', None)
682        if p_id is None:
683            return None
684        # We can use the hash symbol at the end of p_id in import files
685        # to avoid annoying automatic number transformation
686        # by Excel or Calc
687        p_id = p_id.strip('#')
688        if len(p_id.split('-')) != 3 and not p_id.startswith('p'):
689            # For data migration from old SRP only
690            p_id = 'p' + p_id[7:] + '0'
691        entry = payments.get(p_id)
692        return entry
693
694    def updateEntry(self, obj, row, site):
695        """Update obj to the values given in row.
696        """
697        items_changed = super(StudentOnlinePaymentProcessor, self).updateEntry(
698            obj, row, site)
699        student = self.getParent(row, site).__parent__
700        student.__parent__.logger.info(
701            '%s - Payment ticket updated: %s'
702            % (student.student_id, items_changed))
703        return
704
705    def addEntry(self, obj, row, site):
706        parent = self.getParent(row, site)
707        p_id = row['p_id'].strip('#')
708        if len(p_id.split('-')) != 3 and not p_id.startswith('p'):
709            # For data migration from old SRP
710            obj.p_id = 'p' + p_id[7:] + '0'
711            parent[obj.p_id] = obj
712        else:
713            parent[p_id] = obj
714        return
715
716    def delEntry(self, row, site):
717        payment = self.getEntry(row, site)
718        parent = self.getParent(row, site)
719        if payment is not None:
720            student = self._getStudent(row, site)
721            student.__parent__.logger.info('%s - Payment ticket removed: %s'
722                % (student.student_id, payment.p_id))
723            del parent[payment.p_id]
724        return
725
726    def checkConversion(self, row, mode='ignore'):
727        """Validates all values in row.
728        """
729        errs, inv_errs, conv_dict = super(
730            StudentOnlinePaymentProcessor, self).checkConversion(row, mode=mode)
731
732        # We have to check p_id.
733        p_id = row.get('p_id', None)
734        if not p_id:
735            timestamp = ("%d" % int(time()*10000))[1:]
736            p_id = "p%s" % timestamp
737            conv_dict['p_id'] = p_id
738            return errs, inv_errs, conv_dict
739        else:
740            p_id = p_id.strip('#')
741        if p_id.startswith('p'):
742            if not len(p_id) == 14:
743                errs.append(('p_id','invalid length'))
744                return errs, inv_errs, conv_dict
745        elif len(p_id.split('-')) == 3:
746            # The SRP used either pins as keys ...
747            if len(p_id.split('-')[2]) not in (9, 10):
748                errs.append(('p_id','invalid pin'))
749                return errs, inv_errs, conv_dict
750        else:
751            # ... or order_ids.
752            if not len(p_id) == 19:
753                errs.append(('p_id','invalid length'))
754                return errs, inv_errs, conv_dict
755        return errs, inv_errs, conv_dict
756
757class StudentVerdictProcessor(StudentStudyCourseProcessor):
758    """A special batch processor for verdicts.
759
760    Import verdicts and perform workflow transitions.
761    """
762
763    util_name = 'verdictupdater'
764    grok.name(util_name)
765
766    name = u'Verdict Processor (special processor, update only)'
767    iface = IStudentVerdictUpdate
768    factory_name = 'waeup.StudentStudyCourse'
769
770    additional_fields = [
771        'current_session',
772        'current_level',
773        'bypass_validation',
774        'validated_by']
775
776    def checkUpdateRequirements(self, obj, row, site):
777        """Checks requirements the studycourse and the student must fulfill
778        before being updated.
779        """
780        # Check if current_levels correspond
781        if obj.current_level != row['current_level']:
782            return 'Current level does not correspond.'
783        # Check if current_sessions correspond
784        if obj.current_session != row['current_session']:
785            return 'Current session does not correspond.'
786        # Check if new verdict is provided
787        if row['current_verdict'] in (IGNORE_MARKER, ''):
788            return 'No verdict in import file.'
789        # Check if studylevel exists
790        level_string = str(obj.current_level)
791        if obj.get(level_string) is None:
792            return 'Study level object is missing.'
793        # Check if student is in state REGISTERED or VALIDATED
794        if row.get('bypass_validation'):
795            if obj.student.state not in (VALIDATED, REGISTERED):
796                return 'Student in wrong state.'
797        else:
798            if obj.student.state != VALIDATED:
799                return 'Student in wrong state.'
800        return None
801
802    def updateEntry(self, obj, row, site):
803        """Update obj to the values given in row.
804        """
805        # Don't set current_session, current_level
806        vals_to_set = dict((key, val) for key, val in row.items()
807                           if key not in ('current_session','current_level'))
808        super(StudentVerdictProcessor, self).updateEntry(obj, vals_to_set, site)
809        parent = self.getParent(row, site)
810        # Set current_vedict in corresponding studylevel
811        level_string = str(obj.current_level)
812        obj[level_string].level_verdict = row['current_verdict']
813        # Fire transition and set studylevel attributes
814        # depending on student's state
815        if obj.__parent__.state == REGISTERED:
816            validated_by = row.get('validated_by', '')
817            if validated_by in (IGNORE_MARKER, ''):
818                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
819                system = translate(_('System'),'waeup.kofa',
820                                  target_language=portal_language)
821                obj[level_string].validated_by = system
822            else:
823                obj[level_string].validated_by = validated_by
824            obj[level_string].validation_date = datetime.utcnow()
825            IWorkflowInfo(obj.__parent__).fireTransition('bypass_validation')
826        else:
827            IWorkflowInfo(obj.__parent__).fireTransition('return')
828        # Update the students_catalog
829        notify(grok.ObjectModifiedEvent(obj.__parent__))
830        return
Note: See TracBrowser for help on using the repository browser.