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

Last change on this file since 8176 was 8176, checked in by uli, 13 years ago

Use PhoneNumber?.

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