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

Last change on this file since 14014 was 14012, checked in by Henrik Bettermann, 8 years ago

Add email_sent attribute.

Add tests.

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