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

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

Translate all history messages.

  • Property svn:keywords set to Id
File size: 26.8 KB
Line 
1## $Id: batching.py 7959 2012-03-22 15:51: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##
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 zope.interface import Interface
29from zope.schema import getFields
30from zope.component import queryUtility, getUtility
31from zope.event import notify
32from zope.catalog.interfaces import ICatalog
33from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
34from waeup.kofa.interfaces import (
35    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
36    IObjectHistory, VALIDATED)
37from waeup.kofa.interfaces import MessageFactory as _
38from waeup.kofa.students.interfaces import (
39    IStudent, IStudentStudyCourse,
40    IStudentUpdateByRegNo, IStudentUpdateByMatricNo,
41    IStudentStudyLevel, ICourseTicket,
42    IStudentOnlinePayment, IStudentVerdictUpdate)
43from waeup.kofa.students.workflow import  IMPORTABLE_STATES
44from waeup.kofa.utils.batching import BatchProcessor
45
46class StudentProcessor(BatchProcessor):
47    """A batch processor for IStudent objects.
48    """
49    grok.implements(IBatchProcessor)
50    grok.provides(IBatchProcessor)
51    grok.context(Interface)
52    util_name = 'studentprocessor'
53    grok.name(util_name)
54
55    name = u'Student Processor'
56    iface = IStudent
57
58    location_fields = []
59    factory_name = 'waeup.Student'
60
61    mode = None
62
63    @property
64    def available_fields(self):
65        return sorted(list(set(
66            ['student_id','reg_number','matric_number',
67            'password', 'reg_state'] + getFields(
68                self.iface).keys())))
69
70    def checkHeaders(self, headerfields, mode='create'):
71        if not 'reg_number' in headerfields and not 'student_id' \
72            in headerfields and not 'matric_number' in headerfields:
73            raise FatalCSVError(
74                "Need at least columns student_id or reg_number " +
75                "or matric_number for import!")
76        if mode == 'create':
77            for field in self.required_fields:
78                if not field in headerfields:
79                    raise FatalCSVError(
80                        "Need at least columns %s for import!" %
81                        ', '.join(["'%s'" % x for x in self.required_fields]))
82        # Check for fields to be ignored...
83        not_ignored_fields = [x for x in headerfields
84                              if not x.startswith('--')]
85        if len(set(not_ignored_fields)) < len(not_ignored_fields):
86            raise FatalCSVError(
87                "Double headers: each column name may only appear once.")
88        return True
89
90    def parentsExist(self, row, site):
91        return 'students' in site.keys()
92
93    def getLocator(self, row):
94        if row.get('student_id',None):
95            return 'student_id'
96        elif row.get('reg_number',None):
97            return 'reg_number'
98        elif row.get('matric_number',None):
99            return 'matric_number'
100        else:
101            return None
102
103    # The entry never exists in create mode.
104    def entryExists(self, row, site):
105        return self.getEntry(row, site) is not None
106
107    def getParent(self, row, site):
108        return site['students']
109
110    def getEntry(self, row, site):
111        if not 'students' in site.keys():
112            return None
113        if self.getLocator(row) == 'student_id':
114            if row['student_id'] in site['students']:
115                student = site['students'][row['student_id']]
116                return student
117        elif self.getLocator(row) == 'reg_number':
118            reg_number = row['reg_number']
119            cat = queryUtility(ICatalog, name='students_catalog')
120            results = list(
121                cat.searchResults(reg_number=(reg_number, reg_number)))
122            if results:
123                return results[0]
124        elif self.getLocator(row) == 'matric_number':
125            matric_number = row['matric_number']
126            cat = queryUtility(ICatalog, name='students_catalog')
127            results = list(
128                cat.searchResults(matric_number=(matric_number, matric_number)))
129            if results:
130                return results[0]
131        return None
132
133    def addEntry(self, obj, row, site):
134        parent = self.getParent(row, site)
135        parent.addStudent(obj)
136        # We have to log this if reg_state is provided. If not,
137        # logging is done by the event handler handle_student_added
138        if row.has_key('reg_state'):
139            parent.logger.info('%s - Student record created' % obj.student_id)
140        history = IObjectHistory(obj)
141        history.addMessage(_('Student record created'))
142        return
143
144    def delEntry(self, row, site):
145        student = self.getEntry(row, site)
146        if student is not None:
147            parent = self.getParent(row, site)
148            parent.logger.info('%s - Student removed' % student.student_id)
149            del parent[student.student_id]
150        pass
151
152    def updateEntry(self, obj, row, site):
153        """Update obj to the values given in row.
154        """
155        # Remove student_id from row if empty
156        if row.has_key('student_id') and row['student_id'] is None:
157            row.pop('student_id')
158        items_changed = ''
159        for key, value in row.items():
160            # Set student password and all fields declared in interface.
161            if key == 'password' and value != '':
162                IUserAccount(obj).setPassword(value)
163            elif key == 'reg_state':
164                IWorkflowState(obj).setState(value)
165                msg = _("State '${a}' set", mapping = {'a':value})
166                history = IObjectHistory(obj)
167                history.addMessage(msg)
168            elif hasattr(obj, key):
169                setattr(obj, key, value)
170            items_changed += '%s=%s, ' % (key,value)
171        parent = self.getParent(row, site)
172        if hasattr(obj,'student_id'):
173            # Update mode: the student exists and we can get the student_id
174            parent.logger.info(
175                '%s - Student record updated: %s'
176                % (obj.student_id, items_changed))
177        else:
178            # Create mode: the student does not yet exist
179            parent.logger.info('Student data imported: %s' % items_changed)
180        return
181
182    def getMapping(self, path, headerfields, mode):
183        """Get a mapping from CSV file headerfields to actually used fieldnames.
184        """
185        result = dict()
186        reader = csv.reader(open(path, 'rb'))
187        raw_header = reader.next()
188        for num, field in enumerate(headerfields):
189            if field not in [
190                'student_id', 'reg_number', 'matric_number'] and mode == 'remove':
191                continue
192            if field == u'--IGNORE--':
193                # Skip ignored columns in failed and finished data files.
194                continue
195            result[raw_header[num]] = field
196        return result
197
198    def checkConversion(self, row, mode='create'):
199        """Validates all values in row.
200        """
201        iface = self.iface
202        if mode in ['update', 'remove']:
203            if self.getLocator(row) == 'reg_number':
204                iface = IStudentUpdateByRegNo
205            elif self.getLocator(row) == 'matric_number':
206                iface = IStudentUpdateByMatricNo
207        converter = IObjectConverter(iface)
208        errs, inv_errs, conv_dict =  converter.fromStringDict(
209            row, self.factory_name)
210        if row.has_key('reg_state') and \
211            not row['reg_state'] in IMPORTABLE_STATES:
212            if row['reg_state'] != '':
213                errs.append(('reg_state','not allowed'))
214            else:
215                errs.append(('reg_state','no value provided'))
216        return errs, inv_errs, conv_dict
217
218class StudentStudyCourseProcessor(BatchProcessor):
219    """A batch processor for IStudentStudyCourse objects.
220    """
221    grok.implements(IBatchProcessor)
222    grok.provides(IBatchProcessor)
223    grok.context(Interface)
224    util_name = 'studycourseupdater'
225    grok.name(util_name)
226
227    name = u'StudentStudyCourse Processor (update only)'
228    iface = IStudentStudyCourse
229    factory_name = 'waeup.StudentStudyCourse'
230
231    location_fields = []
232
233    mode = None
234
235    @property
236    def available_fields(self):
237        return sorted(list(set(
238            ['student_id','reg_number','matric_number'] + getFields(
239                self.iface).keys())))
240
241    def checkHeaders(self, headerfields, mode='ignore'):
242        if not 'reg_number' in headerfields and not 'student_id' \
243            in headerfields and not 'matric_number' in headerfields:
244            raise FatalCSVError(
245                "Need at least columns student_id " +
246                "or reg_number or matric_number for import!")
247        # Check for fields to be ignored...
248        not_ignored_fields = [x for x in headerfields
249                              if not x.startswith('--')]
250        if len(set(not_ignored_fields)) < len(not_ignored_fields):
251            raise FatalCSVError(
252                "Double headers: each column name may only appear once.")
253        return True
254
255    def getParent(self, row, site):
256        if not 'students' in site.keys():
257            return None
258        if 'student_id' in row.keys() and row['student_id']:
259            if row['student_id'] in site['students']:
260                student = site['students'][row['student_id']]
261                return student
262        elif 'reg_number' in row.keys() and row['reg_number']:
263            reg_number = row['reg_number']
264            cat = queryUtility(ICatalog, name='students_catalog')
265            results = list(
266                cat.searchResults(reg_number=(reg_number, reg_number)))
267            if results:
268                return results[0]
269        elif 'matric_number' in row.keys() and row['matric_number']:
270            matric_number = row['matric_number']
271            cat = queryUtility(ICatalog, name='students_catalog')
272            results = list(
273                cat.searchResults(matric_number=(matric_number, matric_number)))
274            if results:
275                return results[0]
276        return None
277
278    def parentsExist(self, row, site):
279        return self.getParent(row, site) is not None
280
281    def entryExists(self, row, site):
282        return self.getEntry(row, site) is not None
283
284    def getEntry(self, row, site):
285        student = self.getParent(row, site)
286        if student is None:
287            return None
288        return student.get('studycourse')
289
290    def updateEntry(self, obj, row, site):
291        """Update obj to the values given in row.
292        """
293        items_changed = ''
294        for key, value in row.items():
295            # Skip fields not declared in interface.
296            if hasattr(obj, key):
297                setattr(obj, key, value)
298                if key == 'certificate':
299                    value = value.code
300            items_changed += '%s=%s, ' % (key,value)
301        parent = self.getParent(row, site)
302        parent.__parent__.logger.info(
303            '%s - Study course updated: %s'
304            % (parent.student_id, items_changed))
305        # Update the students_catalog
306        notify(grok.ObjectModifiedEvent(obj.__parent__))
307        return
308
309    def checkConversion(self, row, mode='ignore'):
310        """Validates all values in row.
311        """
312        converter = IObjectConverter(self.iface)
313        errs, inv_errs, conv_dict =  converter.fromStringDict(
314            row, self.factory_name)
315        # We have to check if current_level is in range of certificate.
316        # This is not done by the converter. This kind of conversion
317        # checking does only work if a combination of certificate and
318        # current_level is provided.
319        if conv_dict.has_key('certificate'):
320          certificate = conv_dict['certificate']
321          start_level = certificate.start_level
322          end_level = certificate.end_level
323          if conv_dict['current_level'] < start_level or \
324              conv_dict['current_level'] > end_level+120:
325              errs.append(('current_level','not in range'))
326        return errs, inv_errs, conv_dict
327
328class StudentStudyLevelProcessor(BatchProcessor):
329    """A batch processor for IStudentStudyLevel objects.
330    """
331    grok.implements(IBatchProcessor)
332    grok.provides(IBatchProcessor)
333    grok.context(Interface)
334    util_name = 'studylevelprocessor'
335    grok.name(util_name)
336
337    name = u'StudentStudyLevel Processor'
338    iface = IStudentStudyLevel
339    factory_name = 'waeup.StudentStudyLevel'
340
341    location_fields = []
342
343    mode = None
344
345    @property
346    def available_fields(self):
347        return sorted(list(set(
348            ['student_id','reg_number','matric_number','level'] + getFields(
349                self.iface).keys())))
350
351    def checkHeaders(self, headerfields, mode='ignore'):
352        if not 'reg_number' in headerfields and not 'student_id' \
353            in headerfields and not 'matric_number' in headerfields:
354            raise FatalCSVError(
355                "Need at least columns student_id " +
356                "or reg_number or matric_number for import!")
357        if not 'level' in headerfields:
358            raise FatalCSVError(
359                "Need level for import!")
360        # Check for fields to be ignored...
361        not_ignored_fields = [x for x in headerfields
362                              if not x.startswith('--')]
363        if len(set(not_ignored_fields)) < len(not_ignored_fields):
364            raise FatalCSVError(
365                "Double headers: each column name may only appear once.")
366        return True
367
368    def getParent(self, row, site):
369        if not 'students' in site.keys():
370            return None
371        if 'student_id' in row.keys() and row['student_id']:
372            if row['student_id'] in site['students']:
373                student = site['students'][row['student_id']]
374                return student['studycourse']
375        elif 'reg_number' in row.keys() and row['reg_number']:
376            reg_number = row['reg_number']
377            cat = queryUtility(ICatalog, name='students_catalog')
378            results = list(
379                cat.searchResults(reg_number=(reg_number, reg_number)))
380            if results:
381                return results[0]['studycourse']
382        elif 'matric_number' in row.keys() and row['matric_number']:
383            matric_number = row['matric_number']
384            cat = queryUtility(ICatalog, name='students_catalog')
385            results = list(
386                cat.searchResults(matric_number=(matric_number, matric_number)))
387            if results:
388                return results[0]['studycourse']
389        return None
390
391    def parentsExist(self, row, site):
392        return self.getParent(row, site) is not None
393
394    def entryExists(self, row, site):
395        return self.getEntry(row, site) is not None
396
397    def getEntry(self, row, site):
398        studycourse = self.getParent(row, site)
399        if studycourse is None:
400            return None
401        return studycourse.get(row['level'])
402
403    def addEntry(self, obj, row, site):
404        parent = self.getParent(row, site)
405        obj.level = int(row['level'])
406        parent[row['level']] = obj
407        return
408
409    def checkConversion(self, row, mode='ignore'):
410        """Validates all values in row.
411        """
412        converter = IObjectConverter(self.iface)
413        errs, inv_errs, conv_dict =  converter.fromStringDict(
414            row, self.factory_name)
415        # We have to check if level is a valid integer.
416        # This is not done by the converter.
417        try:
418            level = int(row['level'])
419            if level not in range(0,700,10):
420                errs.append(('level','no valid integer'))
421        except ValueError:
422            errs.append(('level','no integer'))
423        return errs, inv_errs, conv_dict
424
425class CourseTicketProcessor(BatchProcessor):
426    """A batch processor for ICourseTicket objects.
427    """
428    grok.implements(IBatchProcessor)
429    grok.provides(IBatchProcessor)
430    grok.context(Interface)
431    util_name = 'courseticketprocessor'
432    grok.name(util_name)
433
434    name = u'CourseTicket Processor'
435    iface = ICourseTicket
436    factory_name = 'waeup.CourseTicket'
437
438    location_fields = []
439
440    mode = None
441
442    @property
443    def available_fields(self):
444        return sorted(list(set(
445            ['student_id','reg_number','matric_number','level','code'] + getFields(
446                self.iface).keys())))
447
448    def checkHeaders(self, headerfields, mode='ignore'):
449        if not 'reg_number' in headerfields and not 'student_id' \
450            in headerfields and not 'matric_number' in headerfields:
451            raise FatalCSVError(
452                "Need at least columns student_id " +
453                "or reg_number or matric_number for import!")
454        if not 'level' in headerfields:
455            raise FatalCSVError(
456                "Need level for import!")
457        if not 'code' in headerfields:
458            raise FatalCSVError(
459                "Need code for import!")
460        # Check for fields to be ignored...
461        not_ignored_fields = [x for x in headerfields
462                              if not x.startswith('--')]
463        if len(set(not_ignored_fields)) < len(not_ignored_fields):
464            raise FatalCSVError(
465                "Double headers: each column name may only appear once.")
466        return True
467
468    def getParent(self, row, site):
469        if not 'students' in site.keys():
470            return None
471        if 'student_id' in row.keys() and row['student_id']:
472            if row['student_id'] in site['students']:
473                student = site['students'][row['student_id']]
474                return student['studycourse'].get(row['level'])
475        elif 'reg_number' in row.keys() and row['reg_number']:
476            reg_number = row['reg_number']
477            cat = queryUtility(ICatalog, name='students_catalog')
478            results = list(
479                cat.searchResults(reg_number=(reg_number, reg_number)))
480            if results:
481                return results[0]['studycourse'].get(row['level'])
482        elif 'matric_number' in row.keys() and row['matric_number']:
483            matric_number = row['matric_number']
484            cat = queryUtility(ICatalog, name='students_catalog')
485            results = list(
486                cat.searchResults(matric_number=(matric_number, matric_number)))
487            if results:
488                return results[0]['studycourse'].get(row['level'])
489        return None
490
491    def parentsExist(self, row, site):
492        return self.getParent(row, site) is not None
493
494    def entryExists(self, row, site):
495        return self.getEntry(row, site) is not None
496
497    def getEntry(self, row, site):
498        level = self.getParent(row, site)
499        if level is None:
500            return None
501        return level.get(row['code'])
502
503    def addEntry(self, obj, row, site):
504        parent = self.getParent(row, site)
505        catalog = getUtility(ICatalog, name='courses_catalog')
506        entries = list(catalog.searchResults(code=(row['code'],row['code'])))
507        obj.fcode = entries[0].__parent__.__parent__.__parent__.code
508        obj.dcode = entries[0].__parent__.__parent__.code
509        obj.title = entries[0].title
510        obj.credits = entries[0].credits
511        obj.passmark = entries[0].passmark
512        obj.semester = entries[0].semester
513        parent[row['code']] = obj
514        return
515
516    def checkConversion(self, row, mode='ignore'):
517        """Validates all values in row.
518        """
519        converter = IObjectConverter(self.iface)
520        errs, inv_errs, conv_dict =  converter.fromStringDict(
521            row, self.factory_name)
522        # We have to check if course really exists.
523        # This is not done by the converter.
524        catalog = getUtility(ICatalog, name='courses_catalog')
525        entries = catalog.searchResults(code=(row['code'],row['code']))
526        if len(entries) == 0:
527            errs.append(('code','non-existent'))
528            return errs, inv_errs, conv_dict
529        return errs, inv_errs, conv_dict
530
531class StudentOnlinePaymentProcessor(BatchProcessor):
532    """A batch processor for IStudentOnlinePayment objects.
533    """
534    grok.implements(IBatchProcessor)
535    grok.provides(IBatchProcessor)
536    grok.context(Interface)
537    util_name = 'paymentprocessor'
538    grok.name(util_name)
539
540    name = u'Payment Processor'
541    iface = IStudentOnlinePayment
542    factory_name = 'waeup.StudentOnlinePayment'
543
544    location_fields = []
545
546    mode = None
547
548    @property
549    def available_fields(self):
550        return sorted(list(set(
551            ['student_id','reg_number','matric_number','p_id'] + getFields(
552                self.iface).keys())))
553
554    def checkHeaders(self, headerfields, mode='ignore'):
555        if not 'reg_number' in headerfields and not 'student_id' \
556            in headerfields and not 'matric_number' in headerfields:
557            raise FatalCSVError(
558                "Need at least columns student_id " +
559                "or reg_number or matric_number for import!")
560        if not 'p_id' in headerfields:
561            raise FatalCSVError(
562                "Need p_id for import!")
563        # Check for fields to be ignored...
564        not_ignored_fields = [x for x in headerfields
565                              if not x.startswith('--')]
566        if len(set(not_ignored_fields)) < len(not_ignored_fields):
567            raise FatalCSVError(
568                "Double headers: each column name may only appear once.")
569        return True
570
571    def getParent(self, row, site):
572        if not 'students' in site.keys():
573            return None
574        if 'student_id' in row.keys() and row['student_id']:
575            if row['student_id'] in site['students']:
576                student = site['students'][row['student_id']]
577                return student['payments']
578        elif 'reg_number' in row.keys() and row['reg_number']:
579            reg_number = row['reg_number']
580            cat = queryUtility(ICatalog, name='students_catalog')
581            results = list(
582                cat.searchResults(reg_number=(reg_number, reg_number)))
583            if results:
584                return results[0]['payments']
585        elif 'matric_number' in row.keys() and row['matric_number']:
586            matric_number = row['matric_number']
587            cat = queryUtility(ICatalog, name='students_catalog')
588            results = list(
589                cat.searchResults(matric_number=(matric_number, matric_number)))
590            if results:
591                return results[0]['payments']
592        return None
593
594    def parentsExist(self, row, site):
595        return self.getParent(row, site) is not None
596
597    def entryExists(self, row, site):
598        return self.getEntry(row, site) is not None
599
600    def getEntry(self, row, site):
601        payments = self.getParent(row, site)
602        if payments is None:
603            return None
604        # We can use the hash symbol at the end of p_id in import files
605        # to avoid annoying automatic number transformation
606        # by Excel or Calc
607        p_id = row['p_id'].strip('#')
608        if p_id.startswith('p'):
609            entry = payments.get(p_id)
610        else:
611            # For data migration from old SRP
612            entry = payments.get('p' + p_id[6:])
613        return entry
614
615    def addEntry(self, obj, row, site):
616        parent = self.getParent(row, site)
617        p_id = row['p_id'].strip('#')
618        if not p_id.startswith('p'):
619            # For data migration from old SRP
620            obj.p_id = 'p' + p_id[6:]
621            parent[obj.p_id] = obj
622        else:
623            parent[p_id] = obj
624        return
625
626    def checkConversion(self, row, mode='ignore'):
627        """Validates all values in row.
628        """
629        converter = IObjectConverter(self.iface)
630        errs, inv_errs, conv_dict =  converter.fromStringDict(
631            row, self.factory_name)
632        # We have to check p_id.
633        p_id = row['p_id'].strip('#')
634        if p_id.startswith('p'):
635            if not len(p_id) == 14:
636                errs.append(('p_id','invalid length'))
637                return errs, inv_errs, conv_dict
638        else:
639            if not len(p_id) == 19:
640                errs.append(('p_id','invalid length'))
641                return errs, inv_errs, conv_dict
642        return errs, inv_errs, conv_dict
643
644class StudentVerdictProcessor(StudentStudyCourseProcessor):
645    """A batch processor for verdicts.
646
647    Import verdicts and perform workflow transitions.
648    """
649
650    util_name = 'verdictupdater'
651    grok.name(util_name)
652
653    name = u'Verdict Processor (update only)'
654    iface = IStudentVerdictUpdate
655    factory_name = 'waeup.StudentStudyCourse'
656
657    @property
658    def available_fields(self):
659        return sorted(list(set(
660            ['student_id','reg_number','matric_number',
661                'current_session', 'current_level'] + getFields(
662                self.iface).keys())))
663
664    def checkUpdateRequirements(self, obj, row, site):
665        """Checks requirements the studycourse and the student must fulfill
666        before being updated.
667        """
668        # Check if current_levels correspond
669        if obj.current_level != row['current_level']:
670            return 'Current level does not correspond.'
671        # Check if current_sessions correspond
672        if obj.current_session != row['current_session']:
673            return 'Current session does not correspond.'
674        # Check if student is in state REGISTERED
675        if obj.getStudent().state != VALIDATED:
676            return 'Student in wrong state.'
677        return None
678
679    def updateEntry(self, obj, row, site):
680        """Update obj to the values given in row.
681        """
682        items_changed = ''
683        for key, value in row.items():
684            # Skip fields not declared in interface plus
685            # current_session and current_level
686            if hasattr(obj, key) and not key in [
687                'current_session','current_level']:
688                setattr(obj, key, value)
689            items_changed += '%s=%s, ' % (key,value)
690        parent = self.getParent(row, site)
691        parent.__parent__.logger.info(
692            '%s - Verdict updated: %s'
693            % (parent.student_id, items_changed))
694        # Fire transition
695        IWorkflowInfo(obj.__parent__).fireTransition('return')
696        # Update the students_catalog
697        notify(grok.ObjectModifiedEvent(obj.__parent__))
698        return
699
700    def checkConversion(self, row, mode='ignore'):
701        """Validates all values in row.
702        """
703        converter = IObjectConverter(self.iface)
704        errs, inv_errs, conv_dict =  converter.fromStringDict(
705            row, self.factory_name)
706        return errs, inv_errs, conv_dict
Note: See TracBrowser for help on using the repository browser.