source: main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/interfaces.py @ 18083

Last change on this file since 18083 was 18083, checked in by uli, 5 days ago

Support Google Recaptcha v3.

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