source: main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py @ 17376

Last change on this file since 17376 was 17376, checked in by Henrik Bettermann, 18 months ago

Enable customization of certificate select box entries also in the students section.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 46.8 KB
Line 
1## $Id: interfaces.py 17376 2023-04-06 17:29:31Z 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##
18import os
19import re
20import codecs
21import zc.async.interfaces
22import zope.i18nmessageid
23from datetime import datetime
24from hurry.file.interfaces import IFileRetrieval
25from hurry.workflow.interfaces import IWorkflowInfo
26from zc.sourcefactory.basic import BasicSourceFactory
27from zope import schema
28from zope.pluggableauth.interfaces import IPrincipalInfo
29from zope.security.interfaces import IGroupClosureAwarePrincipal as IPrincipal
30from zope.component import getUtility
31from zope.component.interfaces import IObjectEvent
32from zope.configuration.fields import Path
33from zope.container.interfaces import INameChooser, IContainer
34from zope.interface import Interface, Attribute
35from zope.schema.interfaces import IObject
36from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
37from waeup.kofa.schema import PhoneNumber
38from waeup.kofa.sourcefactory import SmartBasicContextualSourceFactory
39
40_ = MessageFactory = zope.i18nmessageid.MessageFactory('waeup.kofa')
41
42DELETION_MARKER = 'XXX'
43IGNORE_MARKER = '<IGNORE>'
44WAEUP_KEY = 'waeup.kofa'
45VIRT_JOBS_CONTAINER_NAME = 'jobs'
46DOCLINK = 'http://kofa-doc.waeup.org/userdocs'
47
48CREATED = 'created'
49ADMITTED = 'admitted'
50CLEARANCE = 'clearance started'
51REQUESTED = 'clearance requested'
52CLEARED = 'cleared'
53PAID = 'school fee paid'
54RETURNING = 'returning'
55REGISTERED = 'courses registered'
56VALIDATED = 'courses validated'
57GRADUATED = 'graduated'
58TRANSREQ = 'transcript requested'
59TRANSVAL = 'transcript validated'
60TRANSREL = 'transcript released'
61
62
63#: A dict giving job status as tuple (<STRING>, <TRANSLATED_STRING>),
64#: the latter for UI purposes.
65JOB_STATUS_MAP = {
66    zc.async.interfaces.NEW: ('new', _('new')),
67    zc.async.interfaces.COMPLETED: ('completed', _('completed')),
68    zc.async.interfaces.PENDING: ('pending', _('pending')),
69    zc.async.interfaces.ACTIVE: ('active', _('active')),
70    zc.async.interfaces.ASSIGNED: ('assigned', _('assigned')),
71    zc.async.interfaces.CALLBACKS: ('callbacks', _('callbacks')),
72    }
73
74#default_rest_frontpage = u'' + codecs.open(os.path.join(
75#        os.path.dirname(__file__), 'frontpage.rst'),
76#        encoding='utf-8', mode='rb').read()
77
78default_html_frontpage = u'' + codecs.open(os.path.join(
79        os.path.dirname(__file__), 'frontpage.html'),
80        encoding='utf-8', mode='rb').read()
81
82def SimpleKofaVocabulary(*terms):
83    """A well-buildt vocabulary provides terms with a value, token and
84       title for each term
85    """
86    return SimpleVocabulary([
87            SimpleTerm(value, value, title) for title, value in terms])
88
89def academic_sessions():
90    curr_year = datetime.now().year
91    year_range = range(1970, curr_year + 2)
92    return [('%s/%s' % (year,year+1), year) for year in year_range]
93
94academic_sessions_vocab = SimpleKofaVocabulary(*academic_sessions())
95
96registration_states_vocab = SimpleKofaVocabulary(
97    (_('created'), CREATED),
98    (_('admitted'), ADMITTED),
99    (_('clearance started'), CLEARANCE),
100    (_('clearance requested'), REQUESTED),
101    (_('cleared'), CLEARED),
102    (_('school fee paid'), PAID),
103    (_('courses registered'), REGISTERED),
104    (_('courses validated'), VALIDATED),
105    (_('returning'), RETURNING),
106    (_('graduated'), GRADUATED),
107    (_('transcript requested'), TRANSREQ),
108    (_('transcript validated'), TRANSVAL),
109    (_('transcript released'), TRANSREL),
110    )
111
112class ContextualDictSourceFactoryBase(SmartBasicContextualSourceFactory):
113    """A base for contextual sources based on KofaUtils dicts.
114
115    To create a real source, you have to set the `DICT_NAME` attribute
116    which should be the name of a dictionary in KofaUtils.
117    """
118    def getValues(self, context):
119        utils = getUtility(IKofaUtils)
120        sorted_items = sorted(getattr(utils, self.DICT_NAME).items(),
121                              key=lambda item: item[1])
122        return [item[0] for item in sorted_items]
123
124    def getToken(self, context, value):
125        return str(value)
126
127    def getTitle(self, context, value):
128        utils = getUtility(IKofaUtils)
129        return getattr(utils, self.DICT_NAME)[value]
130
131class SubjectSource(BasicSourceFactory):
132    """A source for school subjects used in exam documentation.
133    """
134    def getValues(self):
135        subjects_dict = getUtility(IKofaUtils).EXAM_SUBJECTS_DICT
136        return sorted(subjects_dict.keys())
137
138    def getTitle(self, value):
139        subjects_dict = getUtility(IKofaUtils).EXAM_SUBJECTS_DICT
140        return "%s:" % subjects_dict[value]
141
142class GradeSource(BasicSourceFactory):
143    """A source for exam grades.
144    """
145    def getValues(self):
146        for entry in getUtility(IKofaUtils).EXAM_GRADES:
147            yield entry[0]
148
149    def getTitle(self, value):
150        return dict(getUtility(IKofaUtils).EXAM_GRADES)[value]
151
152class DisablePaymentGroupSource(ContextualDictSourceFactoryBase):
153    """A source for filtering groups of students
154    """
155    #: name of dict to deliver from kofa utils.
156    DICT_NAME = 'DISABLE_PAYMENT_GROUP_DICT'
157
158class MonthSource(BasicSourceFactory):
159    """A months source delivers all months of the year.
160    """
161    def getValues(self):
162        months_dict = getUtility(IKofaUtils).MONTHS_DICT
163        return sorted(months_dict.keys(), key=lambda x: int(x))
164
165    def getToken(self, value):
166        return value
167
168    def getTitle(self, value):
169        months_dict = getUtility(IKofaUtils).MONTHS_DICT
170        return months_dict[value]
171
172# Define a validation method for email addresses
173class NotAnEmailAddress(schema.ValidationError):
174    __doc__ = u"Invalid email address"
175
176#: Regular expression to check email-address formats. As these can
177#: become rather complex (nearly everything is allowed by RFCs), we only
178#: forbid whitespaces, commas and dots following onto each other.
179check_email = re.compile(
180    r"^[^@\s,]+@[^@\.\s,]+(\.[^@\.\s,]+)*$").match
181
182def validate_email(value):
183    if not check_email(value):
184        raise NotAnEmailAddress(value)
185    return True
186
187# Define a validation method for ids
188class NotIdValue(schema.ValidationError):
189    __doc__ = u"Invalid id"
190
191#: Regular expressions to check id formats.
192check_id = re.compile(r"^[a-zA-Z0-9_-]{2,11}$").match
193
194def validate_id(value):
195    if not check_id(value):
196        raise NotIdValue(value)
197    return True
198
199# Define a validation method for HTML fields
200class NotHTMLValue(schema.ValidationError):
201    __doc__ = u"Style or script elements not allowed"
202
203def validate_html(value):
204    if '<style' in value or '<script' in value:
205        raise NotHTMLValue(value)
206    return True
207
208# Define a validation method for international phone numbers
209class InvalidPhoneNumber(schema.ValidationError):
210    __doc__ = u"Invalid phone number"
211
212# represent format +NNN-NNNN-NNNN
213RE_INT_PHONE = re.compile(r"^\+?\d+\-\d+\-[\d\-]+$")
214
215def validate_phone(value):
216    if not RE_INT_PHONE.match(value):
217        raise InvalidPhoneNumber(value)
218    return True
219
220class FatalCSVError(Exception):
221    """Some row could not be processed.
222    """
223    pass
224
225class DuplicationError(Exception):
226    """An exception that can be raised when duplicates are found.
227
228    When raising :exc:`DuplicationError` you can, beside the usual
229    message, specify a list of objects which are duplicates. These
230    values can be used by catching code to print something helpful or
231    similar.
232    """
233    def __init__(self, msg, entries=[]):
234        self.msg = msg
235        self.entries = entries
236
237    def __str__(self):
238        return '%r' % self.msg
239
240class RoleSource(BasicSourceFactory):
241    """A source for site roles.
242    """
243    def getValues(self):
244        # late import: in interfaces we should not import local modules
245        from waeup.kofa.permissions import get_waeup_role_names
246        return get_waeup_role_names()
247
248    def getTitle(self, value):
249        # late import: in interfaces we should not import local modules
250        from waeup.kofa.permissions import get_all_roles
251        roles = dict(get_all_roles())
252        if value in roles.keys():
253            title = roles[value].title
254            if '.' in title:
255                title = title.split('.', 2)[1]
256        return title
257
258class CaptchaSource(BasicSourceFactory):
259    """A source for captchas.
260    """
261    def getValues(self):
262        captchas = ['No captcha', 'Testing captcha', 'ReCaptcha']
263        try:
264            # we have to 'try' because IConfiguration can only handle
265            # interfaces from w.k.interface.
266            from waeup.kofa.browser.interfaces import ICaptchaManager
267        except:
268            return captchas
269        return sorted(getUtility(ICaptchaManager).getAvailCaptchas().keys())
270
271    def getTitle(self, value):
272        return value
273
274class IResultEntry(Interface):
275    """A school grade entry.
276    """
277
278    subject = schema.Choice(
279        title = _(u'Subject'),
280        source = SubjectSource(),
281        )
282
283    grade = schema.Choice(
284        title = _(u'Grade'),
285        source = GradeSource(),
286        )
287
288class IResultEntryField(IObject):
289    """A zope.schema-like field for usage in interfaces.
290
291    Marker interface to distuingish result entries from ordinary
292    object fields. Needed for registration of widgets.
293    """
294
295class IRefereeEntry(Interface):
296    """A referee entry.
297    """
298    email_sent = Attribute('True if email has been sent')
299
300    name = schema.TextLine(
301        title = _(u'Name'),
302        required = True,
303        description = _(u'Name'),
304        )
305
306    email = schema.ASCIILine(
307        title = _(u'Email Address'),
308        default = None,
309        required = True,
310        constraint=validate_email,
311        description = _(
312            u'Email Address'),
313        )
314
315class IRefereeEntryField(IObject):
316    """A zope.schema-like field for usage in interfaces.
317
318    Marker interface to distuingish referee entries from ordinary
319    object fields. Needed for registration of widgets.
320    """
321
322class IKofaUtils(Interface):
323    """A collection of methods which are subject to customization.
324    """
325
326    PORTAL_LANGUAGE = Attribute("Dict of global language setting")
327    MONTHS_DICT = Attribute("Dict of months of the year")
328    PREFERRED_LANGUAGES_DICT = Attribute("Dict of preferred languages")
329    EXAM_SUBJECTS_DICT = Attribute("Dict of examination subjects")
330    EXAM_GRADES = Attribute("Dict of examination grades")
331    INST_TYPES_DICT = Attribute("Dict if institution types")
332    STUDY_MODES_DICT = Attribute("Dict of study modes")
333    APP_CATS_DICT = Attribute("Dict of application categories")
334    SEMESTER_DICT = Attribute("Dict of semesters or trimesters")
335    SYSTEM_MAX_LOAD = Attribute("Dict of maximum system loads.")
336    TEMP_PASSWORD_MINUTES = Attribute(
337        "Temporary passwords and parents password validity period")
338
339    def sortCertificates(context, resultset):
340        """Sort already filtered certificates in `CertificateSource`.
341        """
342
343    def getCertTitle(context, value):
344        """Compose the titles in `CertificateSource`.
345        """
346
347    def sendContactForm(
348          from_name,from_addr,rcpt_name,rcpt_addr,
349          from_username,usertype,portal,body,subject):
350        """Send an email with data provided by forms.
351        """
352
353    def fullname(firstname,lastname,middlename):
354        """Full name constructor.
355        """
356
357    def sendCredentials(user, password, url_info, msg):
358        """Send credentials as email.
359
360        Input is the applicant for which credentials are sent and the
361        password.
362
363        Returns True or False to indicate successful operation.
364        """
365
366    def genPassword(length, chars):
367        """Generate a random password.
368        """
369
370class IKofaObject(Interface):
371    """A Kofa object.
372
373    This is merely a marker interface.
374    """
375
376class IUniversity(IKofaObject):
377    """Representation of a university.
378    """
379
380
381class IKofaContainer(IKofaObject):
382    """A container for Kofa objects.
383    """
384
385class IKofaContained(IKofaObject):
386    """An item contained in an IKofaContainer.
387    """
388
389class ICSVExporter(Interface):
390    """A CSV file exporter for objects.
391    """
392    fields = Attribute("""List of fieldnames in resulting CSV""")
393
394    title = schema.TextLine(
395        title = u'Title',
396        description = u'Description to be displayed in selections.',
397        )
398    def mangle_value(value, name, obj):
399        """Mangle `value` extracted from `obj` or suobjects thereof.
400
401        This is called by export before actually writing to the result
402        file.
403        """
404
405    def get_filtered(site, **kw):
406        """Get datasets in `site` to be exported.
407
408        The set of data is specified by keywords, which might be
409        different for any implementaion of exporter.
410
411        Returns an iterable.
412        """
413
414    def get_selected(site, selected):
415        """Get datasets in `site` to be exported.
416
417        The set of data is specified by a list of identifiers.
418
419        Returns an iterable.
420        """
421
422    def export(iterable, filepath=None):
423        """Export iterables as rows in a CSV file.
424
425        If `filepath` is not given, a string with the data should be
426        returned.
427
428        What kind of iterables are acceptable depends on the specific
429        exporter implementation.
430        """
431
432    def export_all(site, filepath=None):
433        """Export all items in `site` as CSV file.
434
435        if `filepath` is not given, a string with the data should be
436        returned.
437        """
438
439    def export_filtered(site, filepath=None, **kw):
440        """Export those items in `site` specified by `args` and `kw`.
441
442        If `filepath` is not given, a string with the data should be
443        returned.
444
445        Which special keywords are supported is up to the respective
446        exporter.
447        """
448
449    def export_selected(site, filepath=None, **kw):
450        """Export items in `site` specified by a list of identifiers
451        called `selected`.
452
453        If `filepath` is not given, a string with the data should be
454        returned.
455        """
456
457class IKofaExporter(Interface):
458    """An exporter for objects.
459    """
460    def export(obj, filepath=None):
461        """Export by pickling.
462
463        Returns a file-like object containing a representation of `obj`.
464
465        This is done using `pickle`. If `filepath` is ``None``, a
466        `cStringIO` object is returned, that contains the saved data.
467        """
468
469class IKofaXMLExporter(Interface):
470    """An XML exporter for objects.
471    """
472    def export(obj, filepath=None):
473        """Export as XML.
474
475        Returns an XML representation of `obj`.
476
477        If `filepath` is ``None``, a StringIO` object is returned,
478        that contains the transformed data.
479        """
480
481class IKofaXMLImporter(Interface):
482    """An XML import for objects.
483    """
484    def doImport(filepath):
485        """Create Python object from XML.
486
487        Returns a Python object.
488        """
489
490class IBatchProcessor(Interface):
491    """A batch processor that handles mass-operations.
492    """
493    name = schema.TextLine(
494        title = _(u'Processor name')
495        )
496
497    def doImport(path, headerfields, mode='create', user='Unknown',
498                 logger=None, ignore_empty=True):
499        """Read data from ``path`` and update connected object.
500
501        `headerfields` is a list of headerfields as read from the file
502        to import.
503
504        `mode` gives the import mode to use (``'create'``,
505        ``'update'``, or ``'remove'``.
506
507        `user` is a string describing the user performing the
508        import. Normally fetched from current principal.
509
510        `logger` is the logger to use during import.
511
512        `ignore_emtpy` in update mode ignores empty fields if true.
513        """
514
515class IContactForm(IKofaObject):
516    """A contact form.
517    """
518
519    email_from = schema.ASCIILine(
520        title = _(u'Email Address:'),
521        default = None,
522        required = True,
523        constraint=validate_email,
524        )
525
526    email_to = schema.ASCIILine(
527        title = _(u'Email to:'),
528        default = None,
529        required = True,
530        constraint=validate_email,
531        )
532
533    bcc_to = schema.Text(
534        title = _(u'Bcc to:'),
535        required = True,
536        #readonly = True,
537        )
538
539    subject = schema.TextLine(
540        title = _(u'Subject:'),
541        required = True,)
542
543    fullname = schema.TextLine(
544        title = _(u'Full Name:'),
545        required = True,)
546
547    body = schema.Text(
548        title = _(u'Text:'),
549        required = True,)
550
551
552class IKofaPrincipalInfo(IPrincipalInfo):
553    """Infos about principals that are users of Kofa Kofa.
554    """
555    email = Attribute("The email address of a user")
556    phone = Attribute("The phone number of a user")
557    public_name = Attribute("The public name of a user")
558    user_type = Attribute("The type of a user")
559
560
561class IKofaPrincipal(IPrincipal):
562    """A principle for Kofa Kofa.
563
564    This interface extends zope.security.interfaces.IPrincipal and
565    requires also an `id` and other attributes defined there.
566    """
567
568    email = schema.TextLine(
569        title = _(u'Email Address'),
570        description = u'',
571        required=False,)
572
573    phone = PhoneNumber(
574        title = _(u'Phone'),
575        description = u'',
576        required=False,)
577
578    public_name = schema.TextLine(
579        title = _(u'Public Name'),
580        required = False,)
581
582    user_type = Attribute('The user type of the principal')
583
584class IFailedLoginInfo(IKofaObject):
585    """Info about failed logins.
586
587    Timestamps are supposed to be stored as floats using time.time()
588    or similar.
589    """
590    num = schema.Int(
591        title = _(u'Number of failed logins'),
592        description = _(u'Number of failed logins'),
593        required = True,
594        default = 0,
595        )
596
597    last = schema.Float(
598        title = _(u'Timestamp'),
599        description = _(u'Timestamp of last failed login or `None`'),
600        required = False,
601        default = None,
602        )
603
604    def as_tuple():
605        """Get login info as tuple ``<NUM>, <TIMESTAMP>``.
606        """
607
608    def set_values(num=0, last=None):
609        """Set number of failed logins and timestamp of last one.
610        """
611
612    def increase():
613        """Increase the current number of failed logins and set timestamp.
614        """
615
616    def reset():
617        """Set failed login counters back to zero.
618        """
619
620
621class IUserAccount(IKofaObject):
622    """A user account.
623    """
624
625    failed_logins = Attribute('FailedLoginInfo for this account')
626
627    name = schema.TextLine(
628        title = _(u'User Id'),
629        description = _(u'Login name of user'),
630        required = True,)
631
632    title = schema.TextLine(
633        title = _(u'Full Name'),
634        required = True,)
635
636    public_name = schema.TextLine(
637        title = _(u'Public Name'),
638        description = _(u"Substitute for officer's real name "
639                       "in student object histories "
640                       "and in local roles tables."),
641        required = False,)
642
643    description = schema.Text(
644        title = _(u'Description/Notice'),
645        required = False,)
646
647    email = schema.ASCIILine(
648        title = _(u'Email Address'),
649        default = None,
650        required = True,
651        constraint=validate_email,
652        )
653
654    phone = PhoneNumber(
655        title = _(u'Phone'),
656        default = None,
657        required = False,
658        )
659
660    roles = schema.List(
661        title = _(u'Portal Roles'),
662        value_type = schema.Choice(source=RoleSource()),
663        required = False,
664        )
665
666    suspended = schema.Bool(
667        title = _(u'Account suspended'),
668        description = _(u'If set, the account is immediately blocked.'),
669        default = False,
670        required = False,
671        )
672
673
674class IPasswordValidator(Interface):
675    """A password validator utility.
676    """
677
678    def validate_password(password, password_repeat):
679        """Validates a password by comparing it with
680        control password and checking some other requirements.
681        """
682
683    def validate_secure_password(self, pw, pw_repeat):
684        """ Validates a password by comparing it with
685        control password and checks password strength by
686        matching with the regular expression:
687
688        ^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,}$
689
690        ^              Start anchor
691        (?=.*[A-Z])    Ensure password has one uppercase letters.
692        (?=.*[0-9])    Ensure password has one digit.
693        (?=.*[a-z])    Ensure password has one lowercase letter.
694        .{8,}          Ensure password is of length 8.
695        $              End anchor.
696        """
697
698
699class IUsersContainer(IKofaObject):
700    """A container for officers.
701    """
702
703    def addUser(name, password, title=None, description=None):
704        """Add a user.
705        """
706
707    def delUser(name):
708        """Delete a user if it exists.
709        """
710
711class ILocalRolesAssignable(Interface):
712    """The local roles assignable to an object.
713    """
714    def __call__():
715        """Returns a list of dicts.
716
717        Each dict contains a ``name`` referring to the role assignable
718        for the specified object and a `title` to describe the range
719        of users to which this role can be assigned.
720        """
721
722class IConfigurationContainer(IKofaObject):
723    """A container for session configuration objects.
724    """
725
726    name = schema.TextLine(
727        title = _(u'Name of University'),
728        default = _(u'Sample University'),
729        required = True,
730        )
731
732    acronym = schema.TextLine(
733        title = _(u'Abbreviated Title of University'),
734        default = u'WAeUP.Kofa',
735        required = True,
736        )
737
738    frontpage = schema.Text(
739        title = _(u'Content in HTML format'),
740        required = False,
741        default = default_html_frontpage,
742        constraint=validate_html,
743        )
744
745    frontpage_dict = schema.Dict(
746        title = u'Content as language dictionary with values in html format',
747        required = False,
748        default = {},
749        )
750
751    name_admin = schema.TextLine(
752        title = _(u'Name of Administrator'),
753        default = u'Administrator',
754        required = True,
755        )
756
757    email_admin = schema.ASCIILine(
758        title = _(u'Email Address of Administrator'),
759        default = 'contact@waeup.org',
760        required = True,
761        #constraint=validate_email,
762        )
763
764    email_subject = schema.TextLine(
765        title = _(u'Subject of Email to Administrator'),
766        default = _(u'Kofa Contact'),
767        required = True,
768        )
769
770    smtp_mailer = schema.Choice(
771        title = _(u'SMTP mailer to use when sending mail'),
772        vocabulary = 'Mail Delivery Names',
773        default = 'No email service',
774        required = True,
775        )
776
777    captcha = schema.Choice(
778        title = _(u'Captcha used for public registration pages'),
779        source = CaptchaSource(),
780        default = u'No captcha',
781        required = True,
782        )
783
784    carry_over = schema.Bool(
785        title = _(u'Carry-over Course Registration'),
786        default = False,
787        )
788
789    current_academic_session = schema.Choice(
790        title = _(u'Current Academic Session'),
791        description = _(u'Session for which score editing is allowed'),
792        source = academic_sessions_vocab,
793        default = None,
794        required = False,
795        readonly = False,
796        )
797
798    next_matric_integer = schema.Int(
799        title = _(u'Next Matriculation Number Integer'),
800        description = _(u'Integer used for constructing the next '
801                         'matriculation number'),
802        default = 0,
803        readonly = False,
804        required = False,
805        )
806
807    next_matric_integer_2 = schema.Int(
808        title = _(u'Next Matriculation Number Integer 2'),
809        description = _(u'2nd integer used for constructing the next '
810                         'matriculation number'),
811        default = 0,
812        readonly = False,
813        required = False,
814        )
815
816    next_matric_integer_3 = schema.Int(
817        title = _(u'Next Matriculation Number Integer 3'),
818        description = _(u'3rd integer used for constructing the next '
819                         'matriculation number'),
820        default = 0,
821        readonly = False,
822        required = False,
823        )
824
825    next_matric_integer_4 = schema.Int(
826        title = _(u'Next Matriculation Number Integer 4'),
827        description = _(u'4th integer used for constructing the next '
828                         'matriculation number'),
829        default = 0,
830        readonly = False,
831        required = False,
832        )
833
834    export_disabled_message = schema.Text(
835        title = _(u'Export-disabled message'),
836        description = _(u'Message which will show up if an officer tries '
837                         'to export data. All exporters are automatcally '
838                         'disabled if this field is set.'),
839        required = False,
840        )
841
842    maintmode_enabled_by = schema.TextLine(
843        title = _(u'Maintenance Mode enabled by'),
844        default = None,
845        required = False,
846        )
847
848    def addSessionConfiguration(sessionconfiguration):
849        """Add a session configuration object.
850        """
851
852class ISessionConfiguration(IKofaObject):
853    """A session configuration object.
854    """
855
856    academic_session = schema.Choice(
857        title = _(u'Academic Session'),
858        source = academic_sessions_vocab,
859        default = None,
860        required = True,
861        readonly = True,
862        )
863
864    clearance_enabled = schema.Bool(
865        title = _(u'Clearance enabled'),
866        default = False,
867        )
868
869    payment_disabled = schema.List(
870        title = _(u'Payment disabled'),
871        value_type = schema.Choice(
872            source = DisablePaymentGroupSource(),
873            ),
874        required = False,
875        defaultFactory=list,
876        )
877
878    coursereg_deadline = schema.Datetime(
879        title = _(u'Course Reg. Deadline'),
880        required = False,
881        description = _('Example: ') + u'2011-12-31 23:59:59+01:00',
882        )
883
884    clearance_fee = schema.Float(
885        title = _(u'Acceptance Fee'),
886        default = 0.0,
887        required = False,
888        )
889
890    booking_fee = schema.Float(
891        title = _(u'Bed Booking Fee'),
892        default = 0.0,
893        required = False,
894        )
895
896    maint_fee = schema.Float(
897        title = _(u'Rent (fallback)'),
898        default = 0.0,
899        required = False,
900        )
901
902    late_registration_fee = schema.Float(
903        title = _(u'Late Course Reg. Fee'),
904        default = 0.0,
905        required = False,
906        )
907
908    transcript_fee = schema.Float(
909        title = _(u'Transcript Fee'),
910        default = 0.0,
911        required = False,
912        )
913
914    transfer_fee = schema.Float(
915        title = _(u'Transfer Fee'),
916        default = 0.0,
917        required = False,
918        )
919
920    def getSessionString():
921        """Return the session string from the vocabulary.
922        """
923
924
925class ISessionConfigurationAdd(ISessionConfiguration):
926    """A session configuration object in add mode.
927    """
928
929    academic_session = schema.Choice(
930        title = _(u'Academic Session'),
931        source = academic_sessions_vocab,
932        default = None,
933        required = True,
934        readonly = False,
935        )
936
937ISessionConfigurationAdd['academic_session'].order =  ISessionConfiguration[
938    'academic_session'].order
939
940class IDataCenter(IKofaObject):
941    """A data center.
942
943    A data center manages files (uploads, downloads, etc.).
944
945    Beside providing the bare paths needed to keep files, it also
946    provides some helpers to put results of batch processing into
947    well-defined final locations (with well-defined filenames).
948
949    The main use-case is managing of site-related files, i.e. files
950    for import, export etc.
951
952    DataCenters are _not_ meant as storages for object-specific files
953    like passport photographs and similar.
954
955    It is up to the datacenter implementation how to organize data
956    (paths) inside its storage path.
957    """
958    storage = schema.Bytes(
959        title = u'Path to directory where everything is kept.'
960        )
961
962    deleted_path = schema.Bytes(
963        title = u'Path for deleted student data.'
964        )
965
966    graduated_path = schema.Bytes(
967        title = u'Path for deleted graduated student data.'
968        )
969
970    def getPendingFiles(sort='name'):
971        """Get a list of files stored in `storage` sorted by basename.
972        """
973
974    def getFinishedFiles():
975        """Get a list of files stored in `finished` subfolder of `storage`.
976        """
977
978    def setStoragePath(path, move=False, overwrite=False):
979        """Set the path where to store files.
980
981        If `move` is True, move over files from the current location
982        to the new one.
983
984        If `overwrite` is also True, overwrite any already existing
985        files of same name in target location.
986
987        Triggers a DataCenterStorageMovedEvent.
988        """
989
990    def distProcessedFiles(successful, source_path, finished_file,
991                           pending_file, mode='create', move_orig=True):
992        """Distribute processed files over final locations.
993        """
994
995
996class IDataCenterFile(Interface):
997    """A data center file.
998    """
999
1000    name = schema.TextLine(
1001        title = u'Filename')
1002
1003    size = schema.TextLine(
1004        title = u'Human readable file size')
1005
1006    uploaddate = schema.TextLine(
1007        title = u'Human readable upload datetime')
1008
1009    lines = schema.Int(
1010        title = u'Number of lines in file')
1011
1012    def getDate():
1013        """Get creation timestamp from file in human readable form.
1014        """
1015
1016    def getSize():
1017        """Get human readable size of file.
1018        """
1019
1020    def getLinesNumber():
1021        """Get number of lines of file.
1022        """
1023
1024class IDataCenterStorageMovedEvent(IObjectEvent):
1025    """Emitted, when the storage of a datacenter changes.
1026    """
1027
1028class IObjectUpgradeEvent(IObjectEvent):
1029    """Can be fired, when an object shall be upgraded.
1030    """
1031
1032class ILocalRoleSetEvent(IObjectEvent):
1033    """A local role was granted/revoked for a principal on an object.
1034    """
1035    role_id = Attribute(
1036        "The role id that was set.")
1037    principal_id = Attribute(
1038        "The principal id for which the role was granted/revoked.")
1039    granted = Attribute(
1040        "Boolean. If false, then the role was revoked.")
1041
1042class IQueryResultItem(Interface):
1043    """An item in a search result.
1044    """
1045    url = schema.TextLine(
1046        title = u'URL that links to the found item')
1047    title = schema.TextLine(
1048        title = u'Title displayed in search results.')
1049    description = schema.Text(
1050        title = u'Longer description of the item found.')
1051
1052class IKofaPluggable(Interface):
1053    """A component that might be plugged into a Kofa Kofa app.
1054
1055    Components implementing this interface are referred to as
1056    'plugins'. They are normally called when a new
1057    :class:`waeup.kofa.app.University` instance is created.
1058
1059    Plugins can setup and update parts of the central site without the
1060    site object (normally a :class:`waeup.kofa.app.University` object)
1061    needing to know about that parts. The site simply collects all
1062    available plugins, calls them and the plugins care for their
1063    respective subarea like the applicants area or the datacenter
1064    area.
1065
1066    Currently we have no mechanism to define an order of plugins. A
1067    plugin should therefore make no assumptions about the state of the
1068    site or other plugins being run before and instead do appropriate
1069    checks if necessary.
1070
1071    Updates can be triggered for instance by the respective form in
1072    the site configuration. You normally do updates when the
1073    underlying software changed.
1074    """
1075    def setup(site, name, logger):
1076        """Create an instance of the plugin.
1077
1078        The method is meant to be called by the central app (site)
1079        when it is created.
1080
1081        `site`:
1082           The site that requests a setup.
1083
1084        `name`:
1085           The name under which the plugin was registered (utility name).
1086
1087        `logger`:
1088           A standard Python logger for the plugins use.
1089        """
1090
1091    def update(site, name, logger):
1092        """Method to update an already existing plugin.
1093
1094        This might be called by a site when something serious
1095        changes. It is a poor-man replacement for Zope generations
1096        (but probably more comprehensive and better understandable).
1097
1098        `site`:
1099           The site that requests an update.
1100
1101        `name`:
1102           The name under which the plugin was registered (utility name).
1103
1104        `logger`:
1105           A standard Python logger for the plugins use.
1106        """
1107
1108class IAuthPluginUtility(Interface):
1109    """A component that cares for authentication setup at site creation.
1110
1111    Utilities providing this interface are looked up when a Pluggable
1112    Authentication Utility (PAU) for any
1113    :class:`waeup.kofa.app.University` instance is created and put
1114    into ZODB.
1115
1116    The setup-code then calls the `register` method of the utility and
1117    expects a modified (or unmodified) version of the PAU back.
1118
1119    This allows to define any authentication setup modifications by
1120    submodules or third-party modules/packages.
1121    """
1122
1123    def register(pau):
1124        """Register any plugins wanted to be in the PAU.
1125        """
1126
1127    def unregister(pau):
1128        """Unregister any plugins not wanted to be in the PAU.
1129        """
1130
1131class IObjectConverter(Interface):
1132    """Object converters are available as simple adapters, adapting
1133       interfaces (not regular instances).
1134
1135    """
1136
1137    def fromStringDict(self, data_dict, context, form_fields=None):
1138        """Convert values in `data_dict`.
1139
1140        Converts data in `data_dict` into real values based on
1141        `context` and `form_fields`.
1142
1143        `data_dict` is a mapping (dict) from field names to values
1144        represented as strings.
1145
1146        The fields (keys) to convert can be given in optional
1147        `form_fields`. If given, form_fields should be an instance of
1148        :class:`zope.formlib.form.Fields`. Suitable instances are for
1149        example created by :class:`grok.AutoFields`.
1150
1151        If no `form_fields` are given, a default is computed from the
1152        associated interface.
1153
1154        The `context` can be an existing object (implementing the
1155        associated interface) or a factory name. If it is a string, we
1156        try to create an object using
1157        :func:`zope.component.createObject`.
1158
1159        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
1160        <DATA_DICT>)`` where
1161
1162        ``<FIELD_ERRORS>``
1163           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
1164           error that happened when validating the input data in
1165           `data_dict`
1166
1167        ``<INVARIANT_ERRORS>``
1168           is a list of invariant errors concerning several fields
1169
1170        ``<DATA_DICT>``
1171           is a dict with the values from input dict converted.
1172
1173        If errors happen, i.e. the error lists are not empty, always
1174        an empty ``<DATA_DICT>`` is returned.
1175
1176        If ``<DATA_DICT>`` is non-empty, there were no errors.
1177        """
1178
1179class IFieldConverter(Interface):
1180    def request_data(name, value, schema_field, prefix='', mode='create'):
1181        """Create a dict with key-value mapping as created by a request.
1182
1183        `name` and `value` are expected to be parsed from CSV or a
1184        similar input and represent an attribute to be set to a
1185        representation of value.
1186
1187        `mode` gives the mode of import.
1188
1189        :meth:`update_request_data` is then requested to turn this
1190        name and value into vars as they would be sent by a regular
1191        form submit. This means we do not create the real values to be
1192        set but we only define the values that would be sent in a
1193        browser request to request the creation of those values.
1194
1195        The returned dict should contain names and values of a faked
1196        browser request for the given `schema_field`.
1197
1198        Field converters are normally registered as adapters to some
1199        specific zope.schema field.
1200        """
1201
1202class IObjectHistory(Interface):
1203
1204    messages = schema.List(
1205        title = u'List of messages stored',
1206        required = True,
1207        )
1208
1209    def addMessage(message):
1210        """Add a message.
1211        """
1212
1213class IKofaWorkflowInfo(IWorkflowInfo):
1214    """A :class:`hurry.workflow.workflow.WorkflowInfo` with additional
1215       methods for convenience.
1216    """
1217    def getManualTransitions():
1218        """Get allowed manual transitions.
1219
1220        Get a sorted list of tuples containing the `transition_id` and
1221        `title` of each allowed transition.
1222        """
1223
1224class ISiteLoggers(Interface):
1225
1226    loggers = Attribute("A list or generator of registered KofaLoggers")
1227
1228    def register(name, filename=None, site=None, **options):
1229        """Register a logger `name` which logs to `filename`.
1230
1231        If `filename` is not given, logfile will be `name` with
1232        ``.log`` as filename extension.
1233        """
1234
1235    def unregister(name):
1236        """Unregister a once registered logger.
1237        """
1238
1239class ILogger(Interface):
1240    """A logger cares for setup, update and restarting of a Python logger.
1241    """
1242
1243    logger = Attribute("""A :class:`logging.Logger` instance""")
1244
1245
1246    def __init__(name, filename=None, site=None, **options):
1247        """Create a Kofa logger instance.
1248        """
1249
1250    def setup():
1251        """Create a Python :class:`logging.Logger` instance.
1252
1253        The created logger is based on the params given by constructor.
1254        """
1255
1256    def update(**options):
1257        """Update the logger.
1258
1259        Updates the logger respecting modified `options` and changed
1260        paths.
1261        """
1262
1263class ILoggerCollector(Interface):
1264
1265    def getLoggers(site):
1266        """Return all loggers registered for `site`.
1267        """
1268
1269    def registerLogger(site, logging_component):
1270        """Register a logging component residing in `site`.
1271        """
1272
1273    def unregisterLogger(site, logging_component):
1274        """Unregister a logger.
1275        """
1276
1277#
1278# External File Storage and relatives
1279#
1280class IFileStoreNameChooser(INameChooser):
1281    """See zope.container.interfaces.INameChooser for base methods.
1282    """
1283    def checkName(name, attr=None):
1284        """Check whether an object name is valid.
1285
1286        Raises a user error if the name is not valid.
1287        """
1288
1289    def chooseName(name, attr=None):
1290        """Choose a unique valid file id for the object.
1291
1292        The given name may be taken into account when choosing the
1293        name (file id).
1294
1295        chooseName is expected to always choose a valid file id (that
1296        would pass the checkName test) and never raise an error.
1297
1298        If `attr` is not ``None`` it might been taken into account as
1299        well when generating the file id. Usual behaviour is to
1300        interpret `attr` as a hint for what type of file for a given
1301        context should be stored if there are several types
1302        possible. For instance for a certain student some file could
1303        be the connected passport photograph or some certificate scan
1304        or whatever. Each of them has to be stored in a different
1305        location so setting `attr` to a sensible value should give
1306        different file ids returned.
1307        """
1308
1309class IExtFileStore(IFileRetrieval):
1310    """A file storage that stores files in filesystem (not as blobs).
1311    """
1312    root = schema.TextLine(
1313        title = u'Root path of file store.',
1314        )
1315
1316    def getFile(file_id):
1317        """Get raw file data stored under file with `file_id`.
1318
1319        Returns a file descriptor open for reading or ``None`` if the
1320        file cannot be found.
1321        """
1322
1323    def getFileByContext(context, attr=None):
1324        """Get raw file data stored for the given context.
1325
1326        Returns a file descriptor open for reading or ``None`` if no
1327        such file can be found.
1328
1329        Both, `context` and `attr` might be used to find (`context`)
1330        and feed (`attr`) an appropriate file name chooser.
1331
1332        This is a convenience method.
1333        """
1334
1335    def deleteFile(file_id):
1336        """Delete file stored under `file_id`.
1337
1338        Remove file from filestore so, that it is not available
1339        anymore on next call to getFile for the same file_id.
1340
1341        Should not complain if no such file exists.
1342        """
1343
1344    def deleteFileByContext(context, attr=None):
1345        """Delete file for given `context` and `attr`.
1346
1347        Both, `context` and `attr` might be used to find (`context`)
1348        and feed (`attr`) an appropriate file name chooser.
1349
1350        This is a convenience method.
1351        """
1352
1353    def createFile(filename, f):
1354        """Create file given by f with filename `filename`
1355
1356        Returns a hurry.file.File-based object.
1357        """
1358
1359class IFileStoreHandler(Interface):
1360    """Filestore handlers handle specific files for file stores.
1361
1362    If a file to store/get provides a specific filename, a file store
1363    looks up special handlers for that type of file.
1364
1365    """
1366    def pathFromFileID(store, root, filename):
1367        """Turn file id into path to store.
1368
1369        Returned path should be absolute.
1370        """
1371
1372    def createFile(store, root, filename, file_id, file):
1373        """Return some hurry.file based on `store` and `file_id`.
1374
1375        Some kind of callback method called by file stores to create
1376        file objects from file_id.
1377
1378        Returns a tuple ``(raw_file, path, file_like_obj)`` where the
1379        ``file_like_obj`` should be a HurryFile, a KofaImageFile or
1380        similar. ``raw_file`` is the (maybe changed) input file and
1381        ``path`` the relative internal path to store the file at.
1382
1383        Please make sure the ``raw_file`` is opened for reading and
1384        the file descriptor set at position 0 when returned.
1385
1386        This method also gets the raw input file object that is about
1387        to be stored and is expected to raise any exceptions if some
1388        kind of validation or similar fails.
1389        """
1390
1391class IPDF(Interface):
1392    """A PDF representation of some context.
1393    """
1394
1395    def __call__(view=None, note=None):
1396        """Create a bytestream representing a PDF from context.
1397
1398        If `view` is passed in additional infos might be rendered into
1399        the document.
1400
1401        `note` is optional HTML rendered at bottom of the created
1402        PDF. Please consider the limited reportlab support for HTML,
1403        but using font-tags and friends you certainly can get the
1404        desired look.
1405        """
1406
1407class IMailService(Interface):
1408    """A mail service.
1409    """
1410
1411    def __call__():
1412        """Get the default mail delivery.
1413        """
1414
1415
1416class IDataCenterConfig(Interface):
1417    path = Path(
1418        title = u'Path',
1419        description = u"Directory where the datacenter should store "
1420                      u"files by default (adjustable in web UI).",
1421        required = True,
1422        )
1423
1424#
1425# Asynchronous job handling and related
1426#
1427class IJobManager(IKofaObject):
1428    """A manager for asynchronous running jobs (tasks).
1429    """
1430    def put(job, site=None):
1431        """Put a job into task queue.
1432
1433        If no `site` is given, queue job in context of current local
1434        site.
1435
1436        Returns a job_id to identify the put job. This job_id is
1437        needed for further references to the job.
1438        """
1439
1440    def jobs(site=None):
1441        """Get an iterable of jobs stored.
1442        """
1443
1444    def get(job_id, site=None):
1445        """Get the job with id `job_id`.
1446
1447        For the `site` parameter see :meth:`put`.
1448        """
1449
1450    def remove(job_id, site=None):
1451        """Remove job with `job_id` from stored jobs.
1452        """
1453
1454    def start_test_job(site=None):
1455        """Start a test job.
1456        """
1457
1458class IProgressable(Interface):
1459    """A component that can indicate its progress status.
1460    """
1461    percent = schema.Float(
1462        title = u'Percent of job done already.',
1463        )
1464
1465class IJobContainer(IContainer):
1466    """A job container contains IJob objects.
1467    """
1468
1469class IExportJob(zc.async.interfaces.IJob):
1470    def __init__(site, exporter_name):
1471        pass
1472
1473    finished = schema.Bool(
1474        title = u'`True` if the job finished.`',
1475        default = False,
1476        )
1477
1478    failed = schema.Bool(
1479        title = u"`True` iff the job finished and didn't provide a file.",
1480        default = None,
1481        )
1482
1483class IExportJobContainer(IKofaObject):
1484    """A component that contains (maybe virtually) export jobs.
1485    """
1486    def start_export_job(exporter_name, user_id, *args, **kwargs):
1487        """Start asynchronous export job.
1488
1489        `exporter_name` is the name of an exporter utility to be used.
1490
1491        `user_id` is the ID of the user that triggers the export.
1492
1493        `args` positional arguments passed to the export job created.
1494
1495        `kwargs` keyword arguments passed to the export job.
1496
1497        The job_id is stored along with exporter name and user id in a
1498        persistent list.
1499
1500        Returns the job ID of the job started.
1501        """
1502
1503    def get_running_export_jobs(user_id=None):
1504        """Get export jobs for user with `user_id` as list of tuples.
1505
1506        Each tuples holds ``<job_id>, <exporter_name>, <user_id>`` in
1507        that order. The ``<exporter_name>`` is the utility name of the
1508        used exporter.
1509
1510        If `user_id` is ``None``, all running jobs are returned.
1511        """
1512
1513    def get_export_jobs_status(user_id=None):
1514        """Get running/completed export jobs for `user_id` as list of tuples.
1515
1516        Each tuple holds ``<raw status>, <status translated>,
1517        <exporter title>`` in that order, where ``<status
1518        translated>`` and ``<exporter title>`` are translated strings
1519        representing the status of the job and the human readable
1520        title of the exporter used.
1521        """
1522
1523    def delete_export_entry(entry):
1524        """Delete the export denoted by `entry`.
1525
1526        Removes `entry` from the local `running_exports` list and also
1527        removes the regarding job via the local job manager.
1528
1529        `entry` is a tuple ``(<job id>, <exporter name>, <user id>)``
1530        as created by :meth:`start_export_job` or returned by
1531        :meth:`get_running_export_jobs`.
1532        """
1533
1534    def entry_from_job_id(job_id):
1535        """Get entry tuple for `job_id`.
1536
1537        Returns ``None`` if no such entry can be found.
1538        """
1539
1540class IExportContainerFinder(Interface):
1541    """A finder for the central export container.
1542    """
1543    def __call__():
1544        """Return the currently used global or site-wide IExportContainer.
1545        """
1546
1547class IFilteredQuery(IKofaObject):
1548    """A query for objects.
1549    """
1550
1551    defaults = schema.Dict(
1552        title = u'Default Parameters',
1553        required = True,
1554        )
1555
1556    def __init__(**parameters):
1557        """Instantiate a filtered query by passing in parameters.
1558        """
1559
1560    def query():
1561        """Get an iterable of objects denoted by the set parameters.
1562
1563        The search should be applied to objects inside current
1564        site. It's the caller's duty to set the correct site before.
1565
1566        Result can be any iterable like a catalog result set, a list,
1567        or similar.
1568        """
1569
1570class IFilteredCatalogQuery(IFilteredQuery):
1571    """A catalog-based query for objects.
1572    """
1573
1574    cat_name = schema.TextLine(
1575        title = u'Registered name of the catalog to search.',
1576        required = True,
1577        )
1578
1579    def query_catalog(catalog):
1580        """Query catalog with the parameters passed to constructor.
1581        """
Note: See TracBrowser for help on using the repository browser.