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

Last change on this file since 17763 was 16825, checked in by Henrik Bettermann, 3 years ago

Fix end_level.

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