source: main/waeup.kofa/trunk/src/waeup/kofa/university/batching.py @ 17823

Last change on this file since 17823 was 17787, checked in by Henrik Bettermann, 8 months ago

Add SessionConfigurationProcessor.
Add ConfigurationContainerProcessor.
Add ConfigurationContainerExporter.

  • Property svn:keywords set to Id
File size: 15.7 KB
RevLine 
[7195]1## $Id: batching.py 17787 2024-05-15 06:42:58Z 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##
[5009]18"""Batch processing components for academics 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 academics specific objects like
24faculties, departments and the like.
25"""
26import grok
27from zope.interface import Interface
[8302]28from zope.component import queryUtility
29from zope.schema import getFields
30from zope.catalog.interfaces import ICatalog
[8995]31from zope.event import notify
[9001]32from zope.securitypolicy.interfaces import (
33    IPrincipalRoleManager, IPrincipalRoleMap)
[8995]34from waeup.kofa.authentication import LocalRoleSetEvent
[9001]35from waeup.kofa.interfaces import (
36    IBatchProcessor, IGNORE_MARKER, DELETION_MARKER, FatalCSVError)
[7811]37from waeup.kofa.university.interfaces import (
[7333]38    IFacultiesContainer, IFaculty, ICourse, IDepartment, ICertificate,
[5009]39    ICertificateCourse)
[8996]40from waeup.kofa.university import (
41    Faculty, Department, Course, Certificate)
[7811]42from waeup.kofa.utils.batching import BatchProcessor
[11891]43from waeup.kofa.interfaces import MessageFactory as _
[5009]44
45class FacultyProcessor(BatchProcessor):
[12869]46    """The Faculty Processor processes faculties in the `faculties` container.
47    The `FacultyProcessor` class also serves as a baseclass for all other
48    batch processors in the academic section.
49
50    The processor makes some efforts to set local roles.
51    If new roles are provided, the `updateEntry` method first removes
52    all existing roles and then sets the new roles as given in the import
53    file. That means the entire set of local roles is replaced.
[5009]54    """
[6628]55    grok.implements(IBatchProcessor)
[5009]56    grok.provides(IBatchProcessor)
57    grok.context(Interface)
[7933]58    util_name = 'facultyprocessor'
[5009]59    grok.name(util_name)
60
[11891]61    name = _('Faculty Processor')
[5009]62    iface = IFaculty
[8994]63    allowed_roles = Faculty.local_roles
[5009]64
65    location_fields = ['code',]
66    factory_name = 'waeup.Faculty'
67
68    def parentsExist(self, row, site):
69        return 'faculties' in site.keys()
70
[8994]71    @property
72    def available_fields(self):
73        fields = getFields(self.iface)
74        return sorted(list(set(
[8996]75            self.location_fields + fields.keys() + ['local_roles']
76            )))
[8994]77
[5009]78    def entryExists(self, row, site):
79        return row['code'] in site['faculties'].keys()
80
81    def getParent(self, row, site):
82        return site['faculties']
83
84    def getEntry(self, row, site):
85        if not self.entryExists(row, site):
86            return None
87        parent = self.getParent(row, site)
88        return parent.get(row['code'])
[6628]89
[5009]90    def addEntry(self, obj, row, site):
91        parent = self.getParent(row, site)
92        parent.addFaculty(obj)
93        return
94
95    def delEntry(self, row, site):
96        parent = self.getParent(row, site)
97        del parent[row['code']]
98        pass
99
[9706]100    def updateEntry(self, obj, row, site, filename):
[8995]101        """Update obj to the values given in row.
102        """
103        items_changed = ''
104
[9701]105        if 'local_roles' in row and row['local_roles'] not in (
[9001]106            None, IGNORE_MARKER):
107            role_manager = IPrincipalRoleManager(obj)
108            role_map = IPrincipalRoleMap(obj)
109            # Remove all existing local roles.
[12869]110            for local_role, user_name, setting in \
111                role_map.getPrincipalsAndRoles():
[9001]112                role_manager.unsetRoleForPrincipal(local_role, user_name)
113                notify(LocalRoleSetEvent(
114                        obj, local_role, user_name, granted=False))
115            # Add new local roles.
116            if row['local_roles'] != DELETION_MARKER:
117                local_roles = eval(row['local_roles'])
118                for rolemap in local_roles:
119                    user = rolemap['user_name']
120                    local_role = rolemap['local_role']
121                    role_manager.assignRoleToPrincipal(local_role, user)
[12869]122                    notify(LocalRoleSetEvent(
123                        obj, local_role, user, granted=True))
124                    items_changed += ('%s=%s, '
125                        % ('local_roles', '%s|%s' % (user,local_role)))
[8995]126            row.pop('local_roles')
127
128        # apply other values...
129        items_changed += super(FacultyProcessor, self).updateEntry(
[9706]130            obj, row, site, filename)
[8995]131
132        # Log actions...
[9087]133        location_field = self.location_fields[0]
[9706]134        grok.getSite().logger.info('%s - %s - %s - updated: %s'
135            % (self.name, filename, row[location_field], items_changed))
[8995]136        return items_changed
137
[8994]138    def checkConversion(self, row, mode='create'):
139        """Validates all values in row.
140        """
141        errs, inv_errs, conv_dict =  super(
142            FacultyProcessor, self).checkConversion(row, mode=mode)
[9701]143        if 'local_roles' in row:
[9001]144            if row['local_roles'] in (None, DELETION_MARKER, IGNORE_MARKER):
145                return errs, inv_errs, conv_dict
[8994]146            try:
147                local_roles = eval(row['local_roles'])
148            except:
149                errs.append(('local_roles','Error'))
150                return errs, inv_errs, conv_dict
151            if not isinstance(local_roles, list):
152                errs.append(('local_roles','no list'))
153                return errs, inv_errs, conv_dict
154            for rolemap in local_roles:
155                if not isinstance(rolemap, dict):
156                    errs.append(('local_roles','no dicts'))
157                    return errs, inv_errs, conv_dict
158                if not 'user_name' in rolemap.keys() or not \
159                    'local_role' in rolemap.keys():
[12869]160                    errs.append((
161                        'local_roles','user_name or local_role missing'))
[8994]162                    return errs, inv_errs, conv_dict
163                local_role = rolemap['local_role']
164                if not local_role in self.allowed_roles:
165                    errs.append(('local_roles','%s not allowed' % local_role))
166                    return errs, inv_errs, conv_dict
167                user = rolemap['user_name']
168                users = grok.getSite()['users']
169                if not user in users.keys():
170                    errs.append(('local_roles','%s does not exist' % user))
171                    return errs, inv_errs, conv_dict
172        return errs, inv_errs, conv_dict
173
[8996]174class DepartmentProcessor(FacultyProcessor):
[12869]175    """The Department Processor works in the same way as the Faculty
176    Processor. Since department codes are not necessarily unique, it needs the
177    `faculty_code` to create and update objects.
[5009]178    """
[6628]179    grok.implements(IBatchProcessor)
[5009]180    grok.provides(IBatchProcessor)
181    grok.context(Interface)
[7933]182    util_name = 'departmentprocessor'
[5009]183    grok.name(util_name)
184
[11891]185    name = _('Department Processor')
[5009]186    iface = IDepartment
[8996]187    allowed_roles = Department.local_roles
[5009]188
189    location_fields = ['code', 'faculty_code']
190    factory_name = 'waeup.Department'
191
192    def parentsExist(self, row, site):
193        if not 'faculties' in site.keys():
194            return False
195        return row['faculty_code'] in site['faculties']
196
197    def entryExists(self, row, site):
198        if not self.parentsExist(row, site):
199            return False
200        parent = self.getParent(row, site)
201        return row['code'] in parent.keys()
202
203    def getParent(self, row, site):
204        return site['faculties'][row['faculty_code']]
205
206    def getEntry(self, row, site):
207        if not self.entryExists(row, site):
208            return None
209        parent = self.getParent(row, site)
210        return parent.get(row['code'])
[6628]211
[5009]212    def addEntry(self, obj, row, site):
213        parent = self.getParent(row, site)
214        parent.addDepartment(obj)
215        return
216
217    def delEntry(self, row, site):
218        parent = self.getParent(row, site)
219        del parent[row['code']]
220        return
221
[8996]222class CertificateProcessor(FacultyProcessor):
[12869]223    """The Certificate Processor gets the parent object (the
224    `certificates` attribute of the department container) in two ways.
225    If both faculty and department codes are provided, `getPartents` uses
226    these to locate the certificate. If department code or
227    faculty code are missing, it use the certificates catalog to find the
228    certificate.
[5009]229    """
[6628]230    grok.implements(IBatchProcessor)
[5009]231    grok.provides(IBatchProcessor)
232    grok.context(Interface)
[7933]233    util_name = 'certificateprocessor'
[5009]234    grok.name(util_name)
235
[11891]236    name = _('Certificate Processor')
[5009]237    iface = ICertificate
[8996]238    allowed_roles = Certificate.local_roles
[5009]239
[8302]240    location_fields = ['code']
[5009]241    factory_name = 'waeup.Certificate'
242
[8302]243    @property
244    def available_fields(self):
245        fields = getFields(self.iface)
246        return sorted(list(set(
[8996]247            ['faculty_code','department_code'] + fields.keys()
248            + ['local_roles'])))
[8302]249
250    def checkHeaders(self, headerfields, mode='create'):
[9333]251        super(CertificateProcessor, self).checkHeaders(headerfields, mode)
[8302]252        if mode == 'create':
253            if not 'faculty_code' in headerfields \
[14724]254                or not 'department_code' in headerfields :
[8302]255                raise FatalCSVError(
256                    "Need at least columns faculty_code and department_code")
257        return True
258
[5009]259    def parentsExist(self, row, site):
[8302]260        return self.getParent(row,site) is not None
[5009]261
262    def entryExists(self, row, site):
263        parent = self.getParent(row, site)
[8302]264        if parent is not None:
265            return row['code'] in parent.keys()
266        return False
[5009]267
268    def getParent(self, row, site):
[8302]269        if not 'faculties' in site.keys():
270            return None
271        # If both faculty and department codes are provided, use
272        # these to get parent.
273        if row.get('faculty_code',None) not in (None, IGNORE_MARKER) and \
274            row.get('department_code',None) not in (None, IGNORE_MARKER):
275            if not row['faculty_code'] in site['faculties'].keys():
276                return None
277            faculty = site['faculties'][row['faculty_code']]
278            if not row['department_code'] in faculty.keys():
279                return None
280            dept = faculty[row['department_code']]
281            return dept.certificates
282        # If department code or faculty code is missing,
[12869]283        # use catalog to get parent.
[8302]284        cat = queryUtility(ICatalog, name='certificates_catalog')
285        results = list(
286            cat.searchResults(code=(row['code'], row['code'])))
287        if results:
288            return results[0].__parent__
289        return None
[5009]290
291    def getEntry(self, row, site):
292        parent = self.getParent(row, site)
[8302]293        if parent is not None:
294            return parent.get(row['code'])
295        return None
[5009]296
297    def addEntry(self, obj, row, site):
298        parent = self.getParent(row, site)
[6243]299        parent.addCertificate(obj)
300        return
[5009]301
302    def delEntry(self, row, site):
303        parent = self.getParent(row, site)
304        del parent[row['code']]
305        return
306
[9333]307class CourseProcessor(CertificateProcessor):
[12869]308    """The Course Processor works exactly in the same way as the
309    Certificate Processor. It uses the courses catalog instead of the
310    certificates catalog.
[9333]311    """
312    grok.implements(IBatchProcessor)
313    grok.provides(IBatchProcessor)
314    grok.context(Interface)
315    util_name = 'courseprocessor'
316    grok.name(util_name)
317
[11891]318    name = _('Course Processor')
[9333]319    iface = ICourse
320    allowed_roles = Course.local_roles
321
322    location_fields = ['code']
323    factory_name = 'waeup.Course'
324
325    def getParent(self, row, site):
326        if not 'faculties' in site.keys():
327            return None
328        # If both faculty and department codes are provided, use
329        # these to get parent.
330        if row.get('faculty_code',None) not in (None, IGNORE_MARKER) and \
331            row.get('department_code',None) not in (None, IGNORE_MARKER):
332            if not row['faculty_code'] in site['faculties'].keys():
333                return None
334            faculty = site['faculties'][row['faculty_code']]
335            if not row['department_code'] in faculty.keys():
336                return None
337            dept = faculty[row['department_code']]
338            return dept.courses
339        # If department code or faculty code is missing,
340        # use catalog to get parent. Makes only sense in update mode but
341        # does also work in create mode.
342        cat = queryUtility(ICatalog, name='courses_catalog')
343        results = list(
344            cat.searchResults(code=(row['code'], row['code'])))
345        if results:
346            return results[0].__parent__
347        return None
348
349    def addEntry(self, obj, row, site):
350        parent = self.getParent(row, site)
351        parent.addCourse(obj)
352        return
353
[8996]354class CertificateCourseProcessor(FacultyProcessor):
[12869]355    """The Certificate Course Processor needs more location fields.
356    Certificate courses are stored inside the certificate container.
357    Thus, `faculty_code`, `department_code` and the
358    `certificate_code` are necessary to find the parent container.
359    It furthermore needs the `course` and the `level` field to locate
360    existing objects as they are part of the object id (code).
[13271]361    Consequently, `course` and `level` can't be updated. If the level
362    has changed, the object must be replaced (removed and re-created),
363    for instance by processing two import files.
[5009]364    """
[6628]365    grok.implements(IBatchProcessor)
[5009]366    grok.provides(IBatchProcessor)
367    grok.context(Interface)
[7933]368    util_name = 'certificatecourseprocessor'
[5009]369    grok.name(util_name)
370
[11891]371    name = _('CertificateCourse Processor')
[5009]372    iface = ICertificateCourse
373
[11790]374    location_fields = ['certificate_code', 'course', 'level', 'faculty_code',
375                       'department_code',]
[5009]376    factory_name = 'waeup.CertificateCourse'
377
378    def parentsExist(self, row, site):
379        if not 'faculties' in site.keys():
380            return False
381        if not row['faculty_code'] in site['faculties'].keys():
382            return False
383        faculty = site['faculties'][row['faculty_code']]
384        if not row['department_code'] in faculty.keys():
385            return False
386        dept = faculty[row['department_code']]
387        return row['certificate_code'] in dept.certificates.keys()
388
389    def entryExists(self, row, site):
390        if not self.parentsExist(row, site):
391            return False
392        parent = self.getParent(row, site)
393        code = "%s_%s" % (row['course'].code, row['level'])
394        return code in parent.keys()
395
396    def getParent(self, row, site):
397        dept = site['faculties'][row['faculty_code']][row['department_code']]
398        return dept.certificates[row['certificate_code']]
399
400    def getEntry(self, row, site):
401        if not self.entryExists(row, site):
402            return None
403        parent = self.getParent(row, site)
[6628]404        return parent.get("%s_%s" % (row['course'].code, row['level']))
[5009]405
406    def addEntry(self, obj, row, site):
407        parent = self.getParent(row, site)
[13607]408        # mandatory is not a required and might be missing or can be
409        # the ignore-marker.
410        mandatory = row.get('mandatory', None)
411        if mandatory in (None, IGNORE_MARKER):
412            parent.addCertCourse(row['course'], row['level'])
413            return
414        parent.addCertCourse(row['course'], row['level'], mandatory)
[5009]415        return
416
417    def delEntry(self, row, site):
418        parent = self.getParent(row, site)
[9826]419        parent.delCertCourses(row['course'].code, row['level'])
[5009]420        return
Note: See TracBrowser for help on using the repository browser.