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

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

Add SessionConfigurationProcessor.
Add ConfigurationContainerProcessor.
Add ConfigurationContainerExporter.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 46.7 KB
Line 
1## $Id: interfaces.py 17787 2024-05-15 06:42:58Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
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    frontpage_dict = Attribute('Language translation dictionary with values in HTML format')
727
728    name = schema.TextLine(
729        title = _(u'Name of University'),
730        default = _(u'Sample University'),
731        required = True,
732        )
733
734    acronym = schema.TextLine(
735        title = _(u'Abbreviated Title of University'),
736        default = u'WAeUP.Kofa',
737        required = True,
738        )
739
740    frontpage = schema.Text(
741        title = _(u'Content in HTML format'),
742        required = False,
743        default = default_html_frontpage,
744        constraint=validate_html,
745        )
746
747    name_admin = schema.TextLine(
748        title = _(u'Name of Administrator'),
749        default = u'Administrator',
750        required = True,
751        )
752
753    email_admin = schema.ASCIILine(
754        title = _(u'Email Address of Administrator'),
755        default = 'contact@waeup.org',
756        required = True,
757        #constraint=validate_email,
758        )
759
760    email_subject = schema.TextLine(
761        title = _(u'Subject of Email to Administrator'),
762        default = _(u'Kofa Contact'),
763        required = True,
764        )
765
766    smtp_mailer = schema.Choice(
767        title = _(u'SMTP mailer to use when sending mail'),
768        vocabulary = 'Mail Delivery Names',
769        default = 'No email service',
770        required = True,
771        )
772
773    captcha = schema.Choice(
774        title = _(u'Captcha used for public registration pages'),
775        source = CaptchaSource(),
776        default = u'No captcha',
777        required = True,
778        )
779
780    carry_over = schema.Bool(
781        title = _(u'Carry-over Course Registration'),
782        default = False,
783        )
784
785    current_academic_session = schema.Choice(
786        title = _(u'Current Academic Session'),
787        description = _(u'Session for which score editing is allowed'),
788        source = academic_sessions_vocab,
789        default = None,
790        required = False,
791        readonly = False,
792        )
793
794    next_matric_integer = schema.Int(
795        title = _(u'Next Matriculation Number Integer'),
796        description = _(u'Integer used for constructing the next '
797                         'matriculation number'),
798        default = 0,
799        readonly = False,
800        required = False,
801        )
802
803    next_matric_integer_2 = schema.Int(
804        title = _(u'Next Matriculation Number Integer 2'),
805        description = _(u'2nd integer used for constructing the next '
806                         'matriculation number'),
807        default = 0,
808        readonly = False,
809        required = False,
810        )
811
812    next_matric_integer_3 = schema.Int(
813        title = _(u'Next Matriculation Number Integer 3'),
814        description = _(u'3rd integer used for constructing the next '
815                         'matriculation number'),
816        default = 0,
817        readonly = False,
818        required = False,
819        )
820
821    next_matric_integer_4 = schema.Int(
822        title = _(u'Next Matriculation Number Integer 4'),
823        description = _(u'4th integer used for constructing the next '
824                         'matriculation number'),
825        default = 0,
826        readonly = False,
827        required = False,
828        )
829
830    export_disabled_message = schema.Text(
831        title = _(u'Export-disabled message'),
832        description = _(u'Message which will show up if an officer tries '
833                         'to export data. All exporters are automatcally '
834                         'disabled if this field is set.'),
835        required = False,
836        )
837
838    maintmode_enabled_by = schema.TextLine(
839        title = _(u'Maintenance Mode enabled by'),
840        default = None,
841        required = False,
842        )
843
844    def addSessionConfiguration(sessionconfiguration):
845        """Add a session configuration object.
846        """
847
848class ISessionConfiguration(IKofaObject):
849    """A session configuration object.
850    """
851
852    academic_session = schema.Choice(
853        title = _(u'Academic Session'),
854        source = academic_sessions_vocab,
855        default = None,
856        required = True,
857        readonly = True,
858        )
859
860    clearance_enabled = schema.Bool(
861        title = _(u'Clearance enabled'),
862        default = False,
863        )
864
865    payment_disabled = schema.List(
866        title = _(u'Payment disabled'),
867        value_type = schema.Choice(
868            source = DisablePaymentGroupSource(),
869            ),
870        required = False,
871        defaultFactory=list,
872        )
873
874    coursereg_deadline = schema.Datetime(
875        title = _(u'Course Reg. Deadline'),
876        required = False,
877        description = _('Example: ') + u'2011-12-31 23:59:59+01:00',
878        )
879
880    clearance_fee = schema.Float(
881        title = _(u'Acceptance Fee'),
882        default = 0.0,
883        required = False,
884        )
885
886    booking_fee = schema.Float(
887        title = _(u'Bed Booking Fee'),
888        default = 0.0,
889        required = False,
890        )
891
892    maint_fee = schema.Float(
893        title = _(u'Rent (fallback)'),
894        default = 0.0,
895        required = False,
896        )
897
898    late_registration_fee = schema.Float(
899        title = _(u'Late Course Reg. Fee'),
900        default = 0.0,
901        required = False,
902        )
903
904    transcript_fee = schema.Float(
905        title = _(u'Transcript Fee'),
906        default = 0.0,
907        required = False,
908        )
909
910    transfer_fee = schema.Float(
911        title = _(u'Transfer Fee'),
912        default = 0.0,
913        required = False,
914        )
915
916    def getSessionString():
917        """Return the session string from the vocabulary.
918        """
919
920
921class ISessionConfigurationAdd(ISessionConfiguration):
922    """A session configuration object in add mode.
923    """
924
925    academic_session = schema.Choice(
926        title = _(u'Academic Session'),
927        source = academic_sessions_vocab,
928        default = None,
929        required = True,
930        readonly = False,
931        )
932
933ISessionConfigurationAdd['academic_session'].order =  ISessionConfiguration[
934    'academic_session'].order
935
936class IDataCenter(IKofaObject):
937    """A data center.
938
939    A data center manages files (uploads, downloads, etc.).
940
941    Beside providing the bare paths needed to keep files, it also
942    provides some helpers to put results of batch processing into
943    well-defined final locations (with well-defined filenames).
944
945    The main use-case is managing of site-related files, i.e. files
946    for import, export etc.
947
948    DataCenters are _not_ meant as storages for object-specific files
949    like passport photographs and similar.
950
951    It is up to the datacenter implementation how to organize data
952    (paths) inside its storage path.
953    """
954    storage = schema.Bytes(
955        title = u'Path to directory where everything is kept.'
956        )
957
958    deleted_path = schema.Bytes(
959        title = u'Path for deleted student data.'
960        )
961
962    graduated_path = schema.Bytes(
963        title = u'Path for deleted graduated student data.'
964        )
965
966    def getPendingFiles(sort='name'):
967        """Get a list of files stored in `storage` sorted by basename.
968        """
969
970    def getFinishedFiles():
971        """Get a list of files stored in `finished` subfolder of `storage`.
972        """
973
974    def setStoragePath(path, move=False, overwrite=False):
975        """Set the path where to store files.
976
977        If `move` is True, move over files from the current location
978        to the new one.
979
980        If `overwrite` is also True, overwrite any already existing
981        files of same name in target location.
982
983        Triggers a DataCenterStorageMovedEvent.
984        """
985
986    def distProcessedFiles(successful, source_path, finished_file,
987                           pending_file, mode='create', move_orig=True):
988        """Distribute processed files over final locations.
989        """
990
991
992class IDataCenterFile(Interface):
993    """A data center file.
994    """
995
996    name = schema.TextLine(
997        title = u'Filename')
998
999    size = schema.TextLine(
1000        title = u'Human readable file size')
1001
1002    uploaddate = schema.TextLine(
1003        title = u'Human readable upload datetime')
1004
1005    lines = schema.Int(
1006        title = u'Number of lines in file')
1007
1008    def getDate():
1009        """Get creation timestamp from file in human readable form.
1010        """
1011
1012    def getSize():
1013        """Get human readable size of file.
1014        """
1015
1016    def getLinesNumber():
1017        """Get number of lines of file.
1018        """
1019
1020class IDataCenterStorageMovedEvent(IObjectEvent):
1021    """Emitted, when the storage of a datacenter changes.
1022    """
1023
1024class IObjectUpgradeEvent(IObjectEvent):
1025    """Can be fired, when an object shall be upgraded.
1026    """
1027
1028class ILocalRoleSetEvent(IObjectEvent):
1029    """A local role was granted/revoked for a principal on an object.
1030    """
1031    role_id = Attribute(
1032        "The role id that was set.")
1033    principal_id = Attribute(
1034        "The principal id for which the role was granted/revoked.")
1035    granted = Attribute(
1036        "Boolean. If false, then the role was revoked.")
1037
1038class IQueryResultItem(Interface):
1039    """An item in a search result.
1040    """
1041    url = schema.TextLine(
1042        title = u'URL that links to the found item')
1043    title = schema.TextLine(
1044        title = u'Title displayed in search results.')
1045    description = schema.Text(
1046        title = u'Longer description of the item found.')
1047
1048class IKofaPluggable(Interface):
1049    """A component that might be plugged into a Kofa Kofa app.
1050
1051    Components implementing this interface are referred to as
1052    'plugins'. They are normally called when a new
1053    :class:`waeup.kofa.app.University` instance is created.
1054
1055    Plugins can setup and update parts of the central site without the
1056    site object (normally a :class:`waeup.kofa.app.University` object)
1057    needing to know about that parts. The site simply collects all
1058    available plugins, calls them and the plugins care for their
1059    respective subarea like the applicants area or the datacenter
1060    area.
1061
1062    Currently we have no mechanism to define an order of plugins. A
1063    plugin should therefore make no assumptions about the state of the
1064    site or other plugins being run before and instead do appropriate
1065    checks if necessary.
1066
1067    Updates can be triggered for instance by the respective form in
1068    the site configuration. You normally do updates when the
1069    underlying software changed.
1070    """
1071    def setup(site, name, logger):
1072        """Create an instance of the plugin.
1073
1074        The method is meant to be called by the central app (site)
1075        when it is created.
1076
1077        `site`:
1078           The site that requests a setup.
1079
1080        `name`:
1081           The name under which the plugin was registered (utility name).
1082
1083        `logger`:
1084           A standard Python logger for the plugins use.
1085        """
1086
1087    def update(site, name, logger):
1088        """Method to update an already existing plugin.
1089
1090        This might be called by a site when something serious
1091        changes. It is a poor-man replacement for Zope generations
1092        (but probably more comprehensive and better understandable).
1093
1094        `site`:
1095           The site that requests an update.
1096
1097        `name`:
1098           The name under which the plugin was registered (utility name).
1099
1100        `logger`:
1101           A standard Python logger for the plugins use.
1102        """
1103
1104class IAuthPluginUtility(Interface):
1105    """A component that cares for authentication setup at site creation.
1106
1107    Utilities providing this interface are looked up when a Pluggable
1108    Authentication Utility (PAU) for any
1109    :class:`waeup.kofa.app.University` instance is created and put
1110    into ZODB.
1111
1112    The setup-code then calls the `register` method of the utility and
1113    expects a modified (or unmodified) version of the PAU back.
1114
1115    This allows to define any authentication setup modifications by
1116    submodules or third-party modules/packages.
1117    """
1118
1119    def register(pau):
1120        """Register any plugins wanted to be in the PAU.
1121        """
1122
1123    def unregister(pau):
1124        """Unregister any plugins not wanted to be in the PAU.
1125        """
1126
1127class IObjectConverter(Interface):
1128    """Object converters are available as simple adapters, adapting
1129       interfaces (not regular instances).
1130
1131    """
1132
1133    def fromStringDict(self, data_dict, context, form_fields=None):
1134        """Convert values in `data_dict`.
1135
1136        Converts data in `data_dict` into real values based on
1137        `context` and `form_fields`.
1138
1139        `data_dict` is a mapping (dict) from field names to values
1140        represented as strings.
1141
1142        The fields (keys) to convert can be given in optional
1143        `form_fields`. If given, form_fields should be an instance of
1144        :class:`zope.formlib.form.Fields`. Suitable instances are for
1145        example created by :class:`grok.AutoFields`.
1146
1147        If no `form_fields` are given, a default is computed from the
1148        associated interface.
1149
1150        The `context` can be an existing object (implementing the
1151        associated interface) or a factory name. If it is a string, we
1152        try to create an object using
1153        :func:`zope.component.createObject`.
1154
1155        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
1156        <DATA_DICT>)`` where
1157
1158        ``<FIELD_ERRORS>``
1159           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
1160           error that happened when validating the input data in
1161           `data_dict`
1162
1163        ``<INVARIANT_ERRORS>``
1164           is a list of invariant errors concerning several fields
1165
1166        ``<DATA_DICT>``
1167           is a dict with the values from input dict converted.
1168
1169        If errors happen, i.e. the error lists are not empty, always
1170        an empty ``<DATA_DICT>`` is returned.
1171
1172        If ``<DATA_DICT>`` is non-empty, there were no errors.
1173        """
1174
1175class IFieldConverter(Interface):
1176    def request_data(name, value, schema_field, prefix='', mode='create'):
1177        """Create a dict with key-value mapping as created by a request.
1178
1179        `name` and `value` are expected to be parsed from CSV or a
1180        similar input and represent an attribute to be set to a
1181        representation of value.
1182
1183        `mode` gives the mode of import.
1184
1185        :meth:`update_request_data` is then requested to turn this
1186        name and value into vars as they would be sent by a regular
1187        form submit. This means we do not create the real values to be
1188        set but we only define the values that would be sent in a
1189        browser request to request the creation of those values.
1190
1191        The returned dict should contain names and values of a faked
1192        browser request for the given `schema_field`.
1193
1194        Field converters are normally registered as adapters to some
1195        specific zope.schema field.
1196        """
1197
1198class IObjectHistory(Interface):
1199
1200    messages = schema.List(
1201        title = u'List of messages stored',
1202        required = True,
1203        )
1204
1205    def addMessage(message):
1206        """Add a message.
1207        """
1208
1209class IKofaWorkflowInfo(IWorkflowInfo):
1210    """A :class:`hurry.workflow.workflow.WorkflowInfo` with additional
1211       methods for convenience.
1212    """
1213    def getManualTransitions():
1214        """Get allowed manual transitions.
1215
1216        Get a sorted list of tuples containing the `transition_id` and
1217        `title` of each allowed transition.
1218        """
1219
1220class ISiteLoggers(Interface):
1221
1222    loggers = Attribute("A list or generator of registered KofaLoggers")
1223
1224    def register(name, filename=None, site=None, **options):
1225        """Register a logger `name` which logs to `filename`.
1226
1227        If `filename` is not given, logfile will be `name` with
1228        ``.log`` as filename extension.
1229        """
1230
1231    def unregister(name):
1232        """Unregister a once registered logger.
1233        """
1234
1235class ILogger(Interface):
1236    """A logger cares for setup, update and restarting of a Python logger.
1237    """
1238
1239    logger = Attribute("""A :class:`logging.Logger` instance""")
1240
1241
1242    def __init__(name, filename=None, site=None, **options):
1243        """Create a Kofa logger instance.
1244        """
1245
1246    def setup():
1247        """Create a Python :class:`logging.Logger` instance.
1248
1249        The created logger is based on the params given by constructor.
1250        """
1251
1252    def update(**options):
1253        """Update the logger.
1254
1255        Updates the logger respecting modified `options` and changed
1256        paths.
1257        """
1258
1259class ILoggerCollector(Interface):
1260
1261    def getLoggers(site):
1262        """Return all loggers registered for `site`.
1263        """
1264
1265    def registerLogger(site, logging_component):
1266        """Register a logging component residing in `site`.
1267        """
1268
1269    def unregisterLogger(site, logging_component):
1270        """Unregister a logger.
1271        """
1272
1273#
1274# External File Storage and relatives
1275#
1276class IFileStoreNameChooser(INameChooser):
1277    """See zope.container.interfaces.INameChooser for base methods.
1278    """
1279    def checkName(name, attr=None):
1280        """Check whether an object name is valid.
1281
1282        Raises a user error if the name is not valid.
1283        """
1284
1285    def chooseName(name, attr=None):
1286        """Choose a unique valid file id for the object.
1287
1288        The given name may be taken into account when choosing the
1289        name (file id).
1290
1291        chooseName is expected to always choose a valid file id (that
1292        would pass the checkName test) and never raise an error.
1293
1294        If `attr` is not ``None`` it might been taken into account as
1295        well when generating the file id. Usual behaviour is to
1296        interpret `attr` as a hint for what type of file for a given
1297        context should be stored if there are several types
1298        possible. For instance for a certain student some file could
1299        be the connected passport photograph or some certificate scan
1300        or whatever. Each of them has to be stored in a different
1301        location so setting `attr` to a sensible value should give
1302        different file ids returned.
1303        """
1304
1305class IExtFileStore(IFileRetrieval):
1306    """A file storage that stores files in filesystem (not as blobs).
1307    """
1308    root = schema.TextLine(
1309        title = u'Root path of file store.',
1310        )
1311
1312    def getFile(file_id):
1313        """Get raw file data stored under file with `file_id`.
1314
1315        Returns a file descriptor open for reading or ``None`` if the
1316        file cannot be found.
1317        """
1318
1319    def getFileByContext(context, attr=None):
1320        """Get raw file data stored for the given context.
1321
1322        Returns a file descriptor open for reading or ``None`` if no
1323        such file can be found.
1324
1325        Both, `context` and `attr` might be used to find (`context`)
1326        and feed (`attr`) an appropriate file name chooser.
1327
1328        This is a convenience method.
1329        """
1330
1331    def deleteFile(file_id):
1332        """Delete file stored under `file_id`.
1333
1334        Remove file from filestore so, that it is not available
1335        anymore on next call to getFile for the same file_id.
1336
1337        Should not complain if no such file exists.
1338        """
1339
1340    def deleteFileByContext(context, attr=None):
1341        """Delete file for given `context` and `attr`.
1342
1343        Both, `context` and `attr` might be used to find (`context`)
1344        and feed (`attr`) an appropriate file name chooser.
1345
1346        This is a convenience method.
1347        """
1348
1349    def createFile(filename, f):
1350        """Create file given by f with filename `filename`
1351
1352        Returns a hurry.file.File-based object.
1353        """
1354
1355class IFileStoreHandler(Interface):
1356    """Filestore handlers handle specific files for file stores.
1357
1358    If a file to store/get provides a specific filename, a file store
1359    looks up special handlers for that type of file.
1360
1361    """
1362    def pathFromFileID(store, root, filename):
1363        """Turn file id into path to store.
1364
1365        Returned path should be absolute.
1366        """
1367
1368    def createFile(store, root, filename, file_id, file):
1369        """Return some hurry.file based on `store` and `file_id`.
1370
1371        Some kind of callback method called by file stores to create
1372        file objects from file_id.
1373
1374        Returns a tuple ``(raw_file, path, file_like_obj)`` where the
1375        ``file_like_obj`` should be a HurryFile, a KofaImageFile or
1376        similar. ``raw_file`` is the (maybe changed) input file and
1377        ``path`` the relative internal path to store the file at.
1378
1379        Please make sure the ``raw_file`` is opened for reading and
1380        the file descriptor set at position 0 when returned.
1381
1382        This method also gets the raw input file object that is about
1383        to be stored and is expected to raise any exceptions if some
1384        kind of validation or similar fails.
1385        """
1386
1387class IPDF(Interface):
1388    """A PDF representation of some context.
1389    """
1390
1391    def __call__(view=None, note=None):
1392        """Create a bytestream representing a PDF from context.
1393
1394        If `view` is passed in additional infos might be rendered into
1395        the document.
1396
1397        `note` is optional HTML rendered at bottom of the created
1398        PDF. Please consider the limited reportlab support for HTML,
1399        but using font-tags and friends you certainly can get the
1400        desired look.
1401        """
1402
1403class IMailService(Interface):
1404    """A mail service.
1405    """
1406
1407    def __call__():
1408        """Get the default mail delivery.
1409        """
1410
1411
1412class IDataCenterConfig(Interface):
1413    path = Path(
1414        title = u'Path',
1415        description = u"Directory where the datacenter should store "
1416                      u"files by default (adjustable in web UI).",
1417        required = True,
1418        )
1419
1420#
1421# Asynchronous job handling and related
1422#
1423class IJobManager(IKofaObject):
1424    """A manager for asynchronous running jobs (tasks).
1425    """
1426    def put(job, site=None):
1427        """Put a job into task queue.
1428
1429        If no `site` is given, queue job in context of current local
1430        site.
1431
1432        Returns a job_id to identify the put job. This job_id is
1433        needed for further references to the job.
1434        """
1435
1436    def jobs(site=None):
1437        """Get an iterable of jobs stored.
1438        """
1439
1440    def get(job_id, site=None):
1441        """Get the job with id `job_id`.
1442
1443        For the `site` parameter see :meth:`put`.
1444        """
1445
1446    def remove(job_id, site=None):
1447        """Remove job with `job_id` from stored jobs.
1448        """
1449
1450    def start_test_job(site=None):
1451        """Start a test job.
1452        """
1453
1454class IProgressable(Interface):
1455    """A component that can indicate its progress status.
1456    """
1457    percent = schema.Float(
1458        title = u'Percent of job done already.',
1459        )
1460
1461class IJobContainer(IContainer):
1462    """A job container contains IJob objects.
1463    """
1464
1465class IExportJob(zc.async.interfaces.IJob):
1466    def __init__(site, exporter_name):
1467        pass
1468
1469    finished = schema.Bool(
1470        title = u'`True` if the job finished.`',
1471        default = False,
1472        )
1473
1474    failed = schema.Bool(
1475        title = u"`True` iff the job finished and didn't provide a file.",
1476        default = None,
1477        )
1478
1479class IExportJobContainer(IKofaObject):
1480    """A component that contains (maybe virtually) export jobs.
1481    """
1482    def start_export_job(exporter_name, user_id, *args, **kwargs):
1483        """Start asynchronous export job.
1484
1485        `exporter_name` is the name of an exporter utility to be used.
1486
1487        `user_id` is the ID of the user that triggers the export.
1488
1489        `args` positional arguments passed to the export job created.
1490
1491        `kwargs` keyword arguments passed to the export job.
1492
1493        The job_id is stored along with exporter name and user id in a
1494        persistent list.
1495
1496        Returns the job ID of the job started.
1497        """
1498
1499    def get_running_export_jobs(user_id=None):
1500        """Get export jobs for user with `user_id` as list of tuples.
1501
1502        Each tuples holds ``<job_id>, <exporter_name>, <user_id>`` in
1503        that order. The ``<exporter_name>`` is the utility name of the
1504        used exporter.
1505
1506        If `user_id` is ``None``, all running jobs are returned.
1507        """
1508
1509    def get_export_jobs_status(user_id=None):
1510        """Get running/completed export jobs for `user_id` as list of tuples.
1511
1512        Each tuple holds ``<raw status>, <status translated>,
1513        <exporter title>`` in that order, where ``<status
1514        translated>`` and ``<exporter title>`` are translated strings
1515        representing the status of the job and the human readable
1516        title of the exporter used.
1517        """
1518
1519    def delete_export_entry(entry):
1520        """Delete the export denoted by `entry`.
1521
1522        Removes `entry` from the local `running_exports` list and also
1523        removes the regarding job via the local job manager.
1524
1525        `entry` is a tuple ``(<job id>, <exporter name>, <user id>)``
1526        as created by :meth:`start_export_job` or returned by
1527        :meth:`get_running_export_jobs`.
1528        """
1529
1530    def entry_from_job_id(job_id):
1531        """Get entry tuple for `job_id`.
1532
1533        Returns ``None`` if no such entry can be found.
1534        """
1535
1536class IExportContainerFinder(Interface):
1537    """A finder for the central export container.
1538    """
1539    def __call__():
1540        """Return the currently used global or site-wide IExportContainer.
1541        """
1542
1543class IFilteredQuery(IKofaObject):
1544    """A query for objects.
1545    """
1546
1547    defaults = schema.Dict(
1548        title = u'Default Parameters',
1549        required = True,
1550        )
1551
1552    def __init__(**parameters):
1553        """Instantiate a filtered query by passing in parameters.
1554        """
1555
1556    def query():
1557        """Get an iterable of objects denoted by the set parameters.
1558
1559        The search should be applied to objects inside current
1560        site. It's the caller's duty to set the correct site before.
1561
1562        Result can be any iterable like a catalog result set, a list,
1563        or similar.
1564        """
1565
1566class IFilteredCatalogQuery(IFilteredQuery):
1567    """A catalog-based query for objects.
1568    """
1569
1570    cat_name = schema.TextLine(
1571        title = u'Registered name of the catalog to search.',
1572        required = True,
1573        )
1574
1575    def query_catalog(catalog):
1576        """Query catalog with the parameters passed to constructor.
1577        """
Note: See TracBrowser for help on using the repository browser.