source: main/waeup.sirp/trunk/src/waeup/sirp/students/batching.py @ 7518

Last change on this file since 7518 was 7513, checked in by Henrik Bettermann, 13 years ago

Enable import of student registration states which are in IMPORTABLE_STATES.

Set workflow state directly rather than firing transitions.

  • Property svn:keywords set to Id
File size: 10.1 KB
RevLine 
[7191]1## $Id: batching.py 7513 2012-01-25 17:47:23Z 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
[6821]28from zope.interface import Interface
[6825]29from zope.schema import getFields
30from zope.component import queryUtility
[7429]31from zope.event import notify
[6825]32from zope.catalog.interfaces import ICatalog
[7513]33from hurry.workflow.interfaces import IWorkflowState
[6849]34from waeup.sirp.interfaces import (
[7497]35    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount)
[6825]36from waeup.sirp.students.interfaces import (
[7256]37    IStudent, IStudentStudyCourseImport,
[6849]38    IStudentUpdateByRegNo, IStudentUpdateByMatricNo)
[7513]39from waeup.sirp.students.workflow import  IMPORTABLE_STATES
[6821]40from waeup.sirp.utils.batching import BatchProcessor
41
42class StudentProcessor(BatchProcessor):
43    """A batch processor for IStudent objects.
44    """
45    grok.implements(IBatchProcessor)
46    grok.provides(IBatchProcessor)
47    grok.context(Interface)
48    util_name = 'studentimporter'
49    grok.name(util_name)
50
51    name = u'Student Importer'
52    iface = IStudent
53
[6849]54    location_fields = []
[6821]55    factory_name = 'waeup.Student'
56
[6841]57    mode = None
58
[6821]59    @property
[6849]60    def available_fields(self):
61        return sorted(list(set(
[7513]62            ['student_id','reg_number','matric_number',
63            'password', 'reg_state'] + getFields(
[6849]64                self.iface).keys())))
[6821]65
[6849]66    def checkHeaders(self, headerfields, mode='create'):
[6854]67        if not 'reg_number' in headerfields and not 'student_id' \
68            in headerfields and not 'matric_number' in headerfields:
[6849]69            raise FatalCSVError(
[6854]70                "Need at least columns student_id or reg_number " +
71                "or matric_number for import!")
[6849]72        if mode == 'create':
73            for field in self.required_fields:
74                if not field in headerfields:
75                    raise FatalCSVError(
76                        "Need at least columns %s for import!" %
77                        ', '.join(["'%s'" % x for x in self.required_fields]))
78        # Check for fields to be ignored...
79        not_ignored_fields = [x for x in headerfields
80                              if not x.startswith('--')]
81        if len(set(not_ignored_fields)) < len(not_ignored_fields):
82            raise FatalCSVError(
83                "Double headers: each column name may only appear once.")
84        return True
85
[6821]86    def parentsExist(self, row, site):
87        return 'students' in site.keys()
88
[6849]89    def getLocator(self, row):
[7269]90        if row.get('student_id',None):
[6849]91            return 'student_id'
[7269]92        elif row.get('reg_number',None):
[6849]93            return 'reg_number'
[7269]94        elif row.get('matric_number',None):
[6849]95            return 'matric_number'
96        else:
97            return None
98
[6821]99    # The entry never exists in create mode.
100    def entryExists(self, row, site):
[7267]101        return self.getEntry(row, site) is not None
102
103    def getParent(self, row, site):
104        return site['students']
105
106    def getEntry(self, row, site):
[6846]107        if not 'students' in site.keys():
[6849]108            return None
109        if self.getLocator(row) == 'student_id':
[6846]110            if row['student_id'] in site['students']:
111                student = site['students'][row['student_id']]
112                return student
[6849]113        elif self.getLocator(row) == 'reg_number':
[6846]114            reg_number = row['reg_number']
115            cat = queryUtility(ICatalog, name='students_catalog')
116            results = list(
117                cat.searchResults(reg_number=(reg_number, reg_number)))
118            if results:
119                return results[0]
[6849]120        elif self.getLocator(row) == 'matric_number':
[6846]121            matric_number = row['matric_number']
122            cat = queryUtility(ICatalog, name='students_catalog')
123            results = list(
124                cat.searchResults(matric_number=(matric_number, matric_number)))
125            if results:
126                return results[0]
[6849]127        return None
[6821]128
[7267]129       
[6821]130    def addEntry(self, obj, row, site):
131        parent = self.getParent(row, site)
132        parent.addStudent(obj)
133        return
134
135    def delEntry(self, row, site):
[7267]136        student = self.getEntry(row, site)
[7263]137        if student is not None:
[6846]138            parent = self.getParent(row, site)
139            del parent[student.student_id]
[6821]140        pass
[6825]141
[7497]142    def updateEntry(self, obj, row, site):
143        """Update obj to the values given in row.
144        """
145        for key, value in row.items():
146            # Set student password and all fields declared in interface.
147            if key == 'password':
148                IUserAccount(obj).setPassword(value)
[7513]149            elif key == 'reg_state':
150                IWorkflowState(obj).setState(value)
[7497]151            elif hasattr(obj, key):
152                setattr(obj, key, value)
153        return
154
[6849]155    def getMapping(self, path, headerfields, mode):
156        """Get a mapping from CSV file headerfields to actually used fieldnames.
157        """
158        result = dict()
159        reader = csv.reader(open(path, 'rb'))
160        raw_header = reader.next()
161        for num, field in enumerate(headerfields):
[6854]162            if field not in [
163                'student_id', 'reg_number', 'matric_number'] and mode == 'remove':
[6849]164                continue
165            if field == u'--IGNORE--':
166                # Skip ignored columns in failed and finished data files.
167                continue
168            result[raw_header[num]] = field
169        return result
170
171    def checkConversion(self, row, mode='create'):
172        """Validates all values in row.
173        """
174        if mode in ['update', 'remove']:
175            if self.getLocator(row) == 'reg_number':
176                iface = IStudentUpdateByRegNo
177            elif self.getLocator(row) == 'matric_number':
178                iface = IStudentUpdateByMatricNo
179        else:
180            iface = self.iface
181        converter = IObjectConverter(iface)
182        errs, inv_errs, conv_dict =  converter.fromStringDict(
183            row, self.factory_name)
[7513]184        if row.has_key('reg_state') and \
185            row['reg_state'] is not None and \
186            not row['reg_state'] in IMPORTABLE_STATES:
187            errs.append(('reg_state','not allowed'))
[6849]188        return errs, inv_errs, conv_dict
189
[6825]190class StudentStudyCourseProcessor(BatchProcessor):
191    """A batch processor for IStudentStudyCourse objects.
192    """
193    grok.implements(IBatchProcessor)
194    grok.provides(IBatchProcessor)
195    grok.context(Interface)
[6837]196    util_name = 'studycourseupdater'
[6825]197    grok.name(util_name)
198
[6837]199    name = u'StudentStudyCourse Importer (update only)'
[6825]200    iface = IStudentStudyCourseImport
201    factory_name = 'waeup.StudentStudyCourse'
202
[6849]203    location_fields = []
204
[6841]205    mode = None
206
[6825]207    @property
208    def available_fields(self):
209        return sorted(list(set(
[6843]210            ['student_id','reg_number','matric_number'] + getFields(
211                self.iface).keys())))
[6825]212
[6837]213    def checkHeaders(self, headerfields, mode='ignore'):
[6854]214        if not 'reg_number' in headerfields and not 'student_id' \
215            in headerfields and not 'matric_number' in headerfields:
[6825]216            raise FatalCSVError(
[6854]217                "Need at least columns student_id " +
218                "or reg_number or matric_number for import!")
[6834]219        # Check for fields to be ignored...
[6825]220        not_ignored_fields = [x for x in headerfields
221                              if not x.startswith('--')]
222        if len(set(not_ignored_fields)) < len(not_ignored_fields):
223            raise FatalCSVError(
224                "Double headers: each column name may only appear once.")
225        return True
226
[7267]227    def getParent(self, row, site):
[6846]228        if not 'students' in site.keys():
[6849]229            return None
[6846]230        if 'student_id' in row.keys() and row['student_id']:
[6825]231            if row['student_id'] in site['students']:
232                student = site['students'][row['student_id']]
233                return student
[6843]234        elif 'reg_number' in row.keys() and row['reg_number']:
[6825]235            reg_number = row['reg_number']
[6849]236            #import pdb; pdb.set_trace()
[6825]237            cat = queryUtility(ICatalog, name='students_catalog')
238            results = list(
239                cat.searchResults(reg_number=(reg_number, reg_number)))
240            if results:
241                return results[0]
[6843]242        elif 'matric_number' in row.keys() and row['matric_number']:
243            matric_number = row['matric_number']
244            cat = queryUtility(ICatalog, name='students_catalog')
245            results = list(
246                cat.searchResults(matric_number=(matric_number, matric_number)))
247            if results:
248                return results[0]
[6849]249        return None
[6825]250
[7267]251    def parentsExist(self, row, site):
252        return self.getParent(row, site) is not None
253
[6825]254    def entryExists(self, row, site):
[7267]255        student = self.getParent(row, site)
[6825]256        if not student:
[6849]257            return None
[6825]258        if 'studycourse' in student:
259            return student
[6849]260        return None
[6825]261
262    def getEntry(self, row, site):
263        student = self.entryExists(row, site)
264        if not student:
265            return None
266        return student.get('studycourse')
[7429]267
268    def updateEntry(self, obj, row, site):
269        """Update obj to the values given in row.
270        """
271        for key, value in row.items():
272            # Skip fields not declared in interface.
273            if hasattr(obj, key):
274                setattr(obj, key, value)
275        # Update the students_catalog
276        notify(grok.ObjectModifiedEvent(obj.__parent__))
277        return
278
Note: See TracBrowser for help on using the repository browser.