source: main/waeup.kofa/branches/uli-rm-bootstrap/src/waeup/kofa/interfaces.py @ 17660

Last change on this file since 17660 was 14699, checked in by Henrik Bettermann, 7 years ago

Revert changes of previous revision.

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