source: main/waeup.kofa/trunk/src/waeup/kofa/students/vocabularies.py @ 16000

Last change on this file since 16000 was 15203, checked in by Henrik Bettermann, 6 years ago

Implement study level 0 (Level Zero) option for storing for
orphaned course tickets (tickets without level information).
Add ticket_session field to ICourseTicket.

  • Property svn:keywords set to Id
File size: 8.7 KB
RevLine 
[7191]1## $Id: vocabularies.py 15203 2018-10-28 17:30:45Z 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##
[13076]18"""Vocabularies and sources for the students section.
[6648]19"""
[6787]20from zope.component import getUtility, queryUtility
[6648]21from zope.catalog.interfaces import ICatalog
[6787]22from zope.interface import implements, directlyProvides
23from zope.schema.interfaces import ISource, IContextSourceBinder
24from zope.schema.interfaces import ValidationError
[6648]25from zc.sourcefactory.basic import BasicSourceFactory
26from zc.sourcefactory.contextual import BasicContextualSourceFactory
[7819]27from waeup.kofa.interfaces import SimpleKofaVocabulary
[7811]28from waeup.kofa.interfaces import MessageFactory as _
[8069]29from waeup.kofa.utils.helpers import get_sorted_preferred
30from waeup.kofa.utils.countries import COUNTRIES
[7811]31from waeup.kofa.university.vocabularies import course_levels
[14480]32from waeup.kofa.university.certificate import Certificate
[6648]33
34
[8069]35#: a tuple of tuples (<COUNTRY-NAME>, <ISO-CODE>) with Nigeria first.
36COUNTRIES = get_sorted_preferred(COUNTRIES, ['NG'])
37nats_vocab = SimpleKofaVocabulary(*COUNTRIES)
[7523]38
[9776]39def levels_from_range(start_level=None, end_level=None):
40    """Get a list of valid levels for given start/end level.
41
42    Start and end level must be numbers (or ``None``).
43
44    If no start/end level is given, return the default list of levels.
45
46    If only one of start_level or end_level is given, the result is
47    undefined.
48    """
49    if 999 in (start_level, end_level):
50        return [999]
51    levels = [10,] + [
52        level for level in range(100,1000,10) if level % 100 < 30]
53    if start_level is None and end_level is None:
54        return levels + [999]
55    if start_level == 10 == end_level:
56        return [10,]
57    start_level, end_level = (start_level or 10, end_level or 900)
58    levels = [x for x in levels
59              if x >= start_level and x <= (end_level + 120)]
60    return levels
61
[9133]62def study_levels(context):
[14480]63    if isinstance(context, Certificate):
64        certificate = context
65    else:
66        certificate = getattr(context, 'certificate', None)
[9776]67    start_level, end_level = (None, None)
[9135]68    if  certificate is not None:
69        start_level = int(certificate.start_level)
70        end_level = int(certificate.end_level)
[15203]71    return [0,] + levels_from_range(start_level, end_level)
[6787]72
[6725]73class StudyLevelSource(BasicContextualSourceFactory):
74    """The StudyLevelSource is based on and extends the
75    course_levels vocabulary defined in the university package.
76    Repeating study levels are denoted by increments of 10, the
77    first spillover level by the certificate's end level plus 100
78    and the second spillover level by the end level plus 110.
[15203]79    The level zero contains all courses without level
80    affiliation.
[6725]81    """
82    def getValues(self, context):
83        return study_levels(context)
[6724]84
[6725]85    def getToken(self, context, value):
86        return str(value)
87
88    def getTitle(self, context, value):
[14480]89        if isinstance(context, Certificate):
90            # We use this source also for various reports.
91            # In such a case the context of the source will be the
92            # certificate itself.
93            certificate = context
94        else:
95            certificate = getattr(context, 'certificate', None)
[9135]96        if certificate is not None:
97            start_level = int(certificate.start_level)
98            end_level = int(certificate.end_level)
[7532]99        else:
[7625]100            # default level range
[9162]101            start_level = 10
[8098]102            end_level = 1000
[9778]103        if 999 in (start_level, end_level):
[8471]104            if value != 999:
105                return _('Error: wrong level id ${value}',
106                    mapping={'value': value})
[9133]107        if value == 999:
[8471]108            return course_levels.by_value[999].title
[15203]109        if value == 0:
110            return _('Level Zero')
[9778]111        if start_level == 10 and end_level == 10 and value != 10:
112            return _('Error: level id ${value} out of range',
113                mapping={'value': value})
[7625]114        if value < start_level or value > end_level + 120:
[7691]115            return _('Error: level id ${value} out of range',
116                mapping={'value': value})
[7625]117        # Special treatment for pre-studies level
[7617]118        if value == 10:
119            return course_levels.by_value[value].title
[6725]120        level,repeat = divmod(value, 100)
121        level = level * 100
122        repeat = repeat//10
123        title = course_levels.by_value[level].title
[7625]124        if level > end_level and repeat == 1:
[7617]125            title = course_levels.by_value[level - 100].title
[7691]126            return _('${title} 2nd spillover', mapping={'title': title})
[7625]127        if level > end_level and repeat == 2:
128            title = course_levels.by_value[level - 100].title
[7691]129            return _('${title} 3rd spillover', mapping={'title': title})
[7617]130        if level > end_level:
131            title = course_levels.by_value[level - 100].title
[7691]132            return  _('${title} 1st spillover', mapping={'title': title})
[7625]133        if repeat == 1:
[7691]134            return _('${title} on 1st probation', mapping={'title': title})
[7625]135        if repeat == 2:
[7691]136            return _('${title} on 2nd probation', mapping={'title': title})
[6725]137        return title
138
[6648]139class GenderSource(BasicSourceFactory):
140    """A gender source delivers basically a mapping
141       ``{'m': 'Male', 'f': 'Female'}``
142
143       Using a source, we make sure that the tokens (which are
144       stored/expected for instance from CSV files) are something one
145       can expect and not cryptic IntIDs.
146    """
147    def getValues(self):
148        return ['m', 'f']
149
150    def getToken(self, value):
151        return value[0].lower()
152
153    def getTitle(self, value):
154        if value == 'm':
[7872]155            return _('male')
[6648]156        if value == 'f':
[7872]157            return _('female')
[6787]158
159class RegNumNotInSource(ValidationError):
160    """Registration number exists already
161    """
162    # The docstring of ValidationErrors is used as error description
163    # by zope.formlib.
164    pass
165
166class MatNumNotInSource(ValidationError):
167    """Matriculation number exists already
168    """
169    # The docstring of ValidationErrors is used as error description
170    # by zope.formlib.
171    pass
172
173class RegNumberSource(object):
[8766]174    """A source that accepts any entry for a certain field if not used
175    already.
176
177    Using this kind of source means a way of setting an invariant.
178
179    We accept a value iff:
180    - the value cannot be found in catalog or
181    - the value can be found as part of some item but the bound item
[12836]182    is the context object itself.
[8766]183    """
[6787]184    implements(ISource)
185    cat_name = 'students_catalog'
186    field_name = 'reg_number'
187    validation_error = RegNumNotInSource
[8766]188    comp_field = 'student_id'
[6787]189    def __init__(self, context):
190        self.context = context
191        return
192
193    def __contains__(self, value):
[8766]194        """We accept all values not already given to other students.
195        """
[6787]196        cat = queryUtility(ICatalog, self.cat_name)
197        if cat is None:
198            return True
199        kw = {self.field_name: (value, value)}
200        results = cat.searchResults(**kw)
201        for entry in results:
[8766]202            if not hasattr(self.context, self.comp_field):
203                # we have no context with comp_field (most probably
204                # while adding a new object, where the container is
205                # the context) which means that the value was given
206                # already to another object (as _something_ was found in
207                # the catalog with that value). Fail on first round.
[6787]208                raise self.validation_error(value)
[8766]209            if getattr(entry, self.comp_field) != getattr(
210                self.context, self.comp_field):
211                # An entry already given to another student is not in our
212                # range of acceptable values.
213                raise self.validation_error(value)
[6787]214                #return False
215        return True
216
217def contextual_reg_num_source(context):
218    source = RegNumberSource(context)
219    return source
220directlyProvides(contextual_reg_num_source, IContextSourceBinder)
221
222class MatNumberSource(RegNumberSource):
223    field_name = 'matric_number'
224    validation_error = MatNumNotInSource
225
226def contextual_mat_num_source(context):
227    source = MatNumberSource(context)
228    return source
229directlyProvides(contextual_mat_num_source, IContextSourceBinder)
Note: See TracBrowser for help on using the repository browser.