source: main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py @ 7226

Last change on this file since 7226 was 7225, checked in by Henrik Bettermann, 13 years ago

Reorganise contact form pages. Use megrok.layout.Form.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 23.7 KB
Line 
1## $Id: interfaces.py 7225 2011-11-27 18:55:06Z 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
20from datetime import datetime
21from hurry.file.interfaces import IFileRetrieval
22from hurry.workflow.interfaces import IWorkflow, IWorkflowInfo
23from zc.sourcefactory.basic import BasicSourceFactory
24from zope import schema
25from zope.component import getUtility
26from zope.component.interfaces import IObjectEvent
27from zope.container.interfaces import INameChooser
28from zope.interface import Interface, Attribute, implements
29from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
30
31CREATED = 'created'
32ADMITTED = 'admitted'
33CLEARANCE = 'clearance started'
34REQUESTED = 'clearance requested'
35CLEARED = 'cleared'
36PAID = 'school fee paid'
37RETURNING = 'returning'
38REGISTERED = 'courses registered'
39VALIDATED = 'courses validated'
40
41default_frontpage = u'' + open(os.path.join(
42        os.path.dirname(__file__), 'frontpage.rst'), 'rb').read()
43
44def SimpleWAeUPVocabulary(*terms):
45    """A well-buildt vocabulary provides terms with a value, token and
46       title for each term
47    """
48    return SimpleVocabulary([
49            SimpleTerm(value, value, title) for title, value in terms])
50
51def year_range():
52    curr_year = datetime.now().year
53    return range(curr_year - 2, curr_year + 5)
54
55def academic_sessions():
56    curr_year = datetime.now().year
57    year_range = range(curr_year - 10, curr_year + 2)
58    return [('%s/%s' % (year,year+1), year) for year in year_range]
59
60academic_sessions_vocab = SimpleWAeUPVocabulary(*academic_sessions())
61
62registration_states_vocab = SimpleWAeUPVocabulary(
63    ('created', CREATED),
64    ('admitted', ADMITTED),
65    ('clearance started', CLEARANCE),
66    ('clearance requested', REQUESTED),
67    ('cleared', CLEARED),
68    ('school fee paid', PAID),
69    ('returning', RETURNING),
70    ('courses registered', REGISTERED),
71    ('courses validated', VALIDATED),
72    )
73
74# Define a valiation method for email addresses
75class NotAnEmailAddress(schema.ValidationError):
76    __doc__ = u"Invalid email address"
77
78check_email = re.compile(
79    r"[a-zA-Z0-9._%-]+@([a-zA-Z0-9-]+.)*[a-zA-Z]{2,4}").match
80
81def validate_email(value):
82    if not check_email(value):
83        raise NotAnEmailAddress(value)
84    return True
85
86class FatalCSVError(Exception):
87    """Some row could not be processed.
88    """
89    pass
90
91class DuplicationError(Exception):
92    """An exception that can be raised when duplicates are found.
93
94    When raising :exc:`DuplicationError` you can, beside the usual
95    message, specify a list of objects which are duplicates. These
96    values can be used by catching code to print something helpful or
97    similar.
98    """
99    def __init__(self, msg, entries=[]):
100        self.msg = msg
101        self.entries = entries
102
103    def __str__(self):
104        return '%r' % self.msg
105
106class RoleSource(BasicSourceFactory):
107    """A source for site roles.
108    """
109    def getValues(self):
110        # late import: in interfaces we should not import local modules
111        from waeup.sirp.permissions import get_waeup_role_names
112        return get_waeup_role_names()
113
114    def getTitle(self, value):
115        # late import: in interfaces we should not import local modules
116        from waeup.sirp.permissions import get_all_roles
117        roles = dict(get_all_roles())
118        if value in roles.keys():
119            title = roles[value].title
120            if '.' in title:
121                title = title.split('.', 2)[1]
122        return title
123
124class IWAeUPObject(Interface):
125    """A WAeUP object.
126
127    This is merely a marker interface.
128    """
129
130class IUniversity(IWAeUPObject):
131    """Representation of a university.
132    """
133
134
135class IWAeUPContainer(IWAeUPObject):
136    """A container for WAeUP objects.
137    """
138
139class IWAeUPContained(IWAeUPObject):
140    """An item contained in an IWAeUPContainer.
141    """
142
143class IWAeUPExporter(Interface):
144    """An exporter for objects.
145    """
146    def export(obj, filepath=None):
147        """Export by pickling.
148
149        Returns a file-like object containing a representation of `obj`.
150
151        This is done using `pickle`. If `filepath` is ``None``, a
152        `cStringIO` object is returned, that contains the saved data.
153        """
154
155class IWAeUPXMLExporter(Interface):
156    """An XML exporter for objects.
157    """
158    def export(obj, filepath=None):
159        """Export as XML.
160
161        Returns an XML representation of `obj`.
162
163        If `filepath` is ``None``, a StringIO` object is returned,
164        that contains the transformed data.
165        """
166
167class IWAeUPXMLImporter(Interface):
168    """An XML import for objects.
169    """
170    def doImport(filepath):
171        """Create Python object from XML.
172
173        Returns a Python object.
174        """
175
176class IBatchProcessor(Interface):
177    """A batch processor that handles mass-operations.
178    """
179    name = schema.TextLine(
180        title = u'Importer name'
181        )
182
183    mode = schema.Choice(
184        title = u'Import mode',
185        values = ['create', 'update', 'remove']
186        )
187
188    def doImport(path, headerfields, mode='create', user='Unknown',
189                 logger=None):
190        """Read data from ``path`` and update connected object.
191
192        `headerfields` is a list of headerfields as read from the file
193        to import.
194
195        `mode` gives the import mode to use (``'create'``,
196        ``'update'``, or ``'remove'``.
197
198        `user` is a string describing the user performing the
199        import. Normally fetched from current principal.
200
201        `logger` is the logger to use during import.
202        """
203
204class IContactForm(IWAeUPObject):
205    """A contact form.
206    """
207
208    email_from = schema.ASCIILine(
209        title = u'Email Address:',
210        default = None,
211        required = True,
212        constraint=validate_email,
213        )
214
215    email_to = schema.ASCIILine(
216        title = u'Email to:',
217        default = None,
218        required = True,
219        constraint=validate_email,
220        )
221
222    subject = schema.TextLine(
223        title = u'Subject:',
224        required = True,)
225
226    fullname = schema.TextLine(
227        title = u'Full Name:',
228        required = True,)
229
230    body = schema.Text(
231        title = u'Text:',
232        required = True,)
233
234
235class IUserAccount(IWAeUPObject):
236    """A user account.
237    """
238    name = schema.TextLine(
239        title = u'User ID',
240        description = u'Login name of user',
241        required = True,)
242
243    title = schema.TextLine(
244        title = u'Name',
245        description = u'Real name of user',
246        required = False,)
247
248    description = schema.Text(
249        title = u'Description/Notice',
250        required = False,)
251
252    email = schema.ASCIILine(
253        title = u'Email',
254        default = None,
255        required = True,
256        constraint=validate_email,
257        )
258
259    roles = schema.List(
260        title = u'Portal roles',
261        value_type = schema.Choice(source=RoleSource()))
262
263class IPasswordValidator(Interface):
264    """A password validator utility.
265    """
266
267    def validate_password(password, password_repeat):
268        """Validates a password by comparing it with
269        control password and checking some other requirements.
270        """
271
272
273class IUsersContainer(IWAeUPObject):
274    """A container for users (principals).
275
276    These users are used for authentication purposes.
277    """
278
279    def addUser(name, password, title=None, description=None):
280        """Add a user.
281        """
282
283    def delUser(name):
284        """Delete a user if it exists.
285        """
286
287class ILocalRolesAssignable(Interface):
288    """The local roles assignable to an object.
289    """
290    def __call__():
291        """Returns a list of dicts.
292
293        Each dict contains a ``name`` referring to the role assignable
294        for the specified object and a `title` to describe the range
295        of users to which this role can be assigned.
296        """
297
298class IConfigurationContainer(IWAeUPObject):
299    """A container for session configuration objects.
300    """
301
302    name = schema.TextLine(
303        title = u'Name of University',
304        default = u'Sample University',
305        required = True,
306        )
307
308    title = schema.TextLine(
309        title = u'Title of Frontpage',
310        default = u'Welcome to the Student Information and Registration ' +
311                  u'Portal of Sample University',
312        required = False,
313        )
314
315    skin = schema.Choice(
316        title = u'Skin',
317        default = u'gray waeup theme',
318        vocabulary = 'waeup.sirp.browser.theming.ThemesVocabulary',
319        required = True,
320        )
321
322    frontpage = schema.Text(
323        title = u'Content in reST format',
324        required = False,
325        default = default_frontpage,
326        )
327
328    accommodation_session = schema.Choice(
329        title = u'Accommodation Booking Session',
330        source = academic_sessions_vocab,
331        default = datetime.now().year,
332        required = False,
333        readonly = False,
334        )
335
336    accommodation_states = schema.List(
337        title = u'Allowed States for Accommodation Booking',
338        value_type = schema.Choice(
339            vocabulary = registration_states_vocab,
340            ),
341        default = [],
342        )
343
344    name_admin = schema.TextLine(
345        title = u'Name of Administrator',
346        default = u'Administrator',
347        required = False,
348        )
349
350    email_admin = schema.ASCIILine(
351        title = u'Email Address of Administrator',
352        default = 'contact@waeup.org',
353        required = False,
354        constraint=validate_email,
355        )
356
357    email_subject = schema.TextLine(
358        title = u'Subject of Email to Administrator',
359        default = u'SIRP Contact',
360        required = False,
361        )
362
363    smtp_server = schema.TextLine(
364        title = u'Adress of SMTP Server',
365        default = u'localhost',
366        required = False,
367        )
368
369    smtp_requires_login = schema.Bool(
370        title = u'Server requires login',
371        default = False,
372        )
373
374    smtp_username = schema.TextLine(
375        title = u'Username for SMTP Account',
376        default = None,
377        required = False,
378        )
379
380    smtp_password = schema.Password(
381        title = u'Password for SMTP Account',
382        default = None,
383        required = False,
384        )
385
386
387class ISessionConfiguration(IWAeUPObject):
388    """A session configuration object.
389    """
390
391    academic_session = schema.Choice(
392        title = u'Academic Session',
393        source = academic_sessions_vocab,
394        default = None,
395        required = True,
396        readonly = True,
397        )
398
399    school_fee_base = schema.Int(
400        title = u'School Fee',
401        default = 0,
402        )
403
404    surcharge_1 = schema.Int(
405        title = u'Surcharge 1',
406        default = 0,
407        )
408
409    surcharge_2 = schema.Int(
410        title = u'Surcharge 2',
411        default = 0,
412        )
413
414    surcharge_3 = schema.Int(
415        title = u'Surcharge 3',
416        default = 0,
417        )
418
419    clearance_fee = schema.Int(
420        title = u'Clearance Fee',
421        default = 0,
422        )
423
424    booking_fee = schema.Int(
425        title = u'Booking Fee',
426        default = 0,
427        )
428
429    def getSessionString():
430        """Returns the session string from the vocabulary.
431        """
432
433
434class ISessionConfigurationAdd(ISessionConfiguration):
435    """A session configuration object in add mode.
436    """
437
438    academic_session = schema.Choice(
439        title = u'Academic Session',
440        source = academic_sessions_vocab,
441        default = None,
442        required = True,
443        readonly = False,
444        )
445
446ISessionConfigurationAdd['academic_session'].order =  ISessionConfiguration[
447    'academic_session'].order
448
449class IDataCenter(IWAeUPObject):
450    """A data center.
451
452    TODO : declare methods, at least those needed by pages.
453    """
454    pass
455
456class IDataCenterFile(Interface):
457    """A data center file.
458    """
459
460    name = schema.TextLine(
461        title = u'Filename')
462
463    size = schema.TextLine(
464        title = u'Human readable file size')
465
466    uploaddate = schema.TextLine(
467        title = u'Human readable upload datetime')
468
469    lines = schema.Int(
470        title = u'Number of lines in file')
471
472    def getDate():
473        """Get creation timestamp from file in human readable form.
474        """
475
476    def getSize():
477        """Get human readable size of file.
478        """
479
480    def getLinesNumber():
481        """Get number of lines of file.
482        """
483
484class IDataCenterStorageMovedEvent(IObjectEvent):
485    """Emitted, when the storage of a datacenter changes.
486    """
487
488class IObjectUpgradeEvent(IObjectEvent):
489    """Can be fired, when an object shall be upgraded.
490    """
491
492class ILocalRoleSetEvent(IObjectEvent):
493    """A local role was granted/revoked for a principal on an object.
494    """
495    role_id = Attribute(
496        "The role id that was set.")
497    principal_id = Attribute(
498        "The principal id for which the role was granted/revoked.")
499    granted = Attribute(
500        "Boolean. If false, then the role was revoked.")
501
502class IQueryResultItem(Interface):
503    """An item in a search result.
504    """
505    url = schema.TextLine(
506        title = u'URL that links to the found item')
507    title = schema.TextLine(
508        title = u'Title displayed in search results.')
509    description = schema.Text(
510        title = u'Longer description of the item found.')
511
512class IWAeUPSIRPPluggable(Interface):
513    """A component that might be plugged into a WAeUP SIRP app.
514
515    Components implementing this interface are referred to as
516    'plugins'. They are normally called when a new
517    :class:`waeup.sirp.app.University` instance is created.
518
519    Plugins can setup and update parts of the central site without the
520    site object (normally a :class:`waeup.sirp.app.University` object)
521    needing to know about that parts. The site simply collects all
522    available plugins, calls them and the plugins care for their
523    respective subarea like the applicants area or the datacenter
524    area.
525
526    Currently we have no mechanism to define an order of plugins. A
527    plugin should therefore make no assumptions about the state of the
528    site or other plugins being run before and instead do appropriate
529    checks if necessary.
530
531    Updates can be triggered for instance by the respective form in
532    the site configuration. You normally do updates when the
533    underlying software changed.
534    """
535    def setup(site, name, logger):
536        """Create an instance of the plugin.
537
538        The method is meant to be called by the central app (site)
539        when it is created.
540
541        `site`:
542           The site that requests a setup.
543
544        `name`:
545           The name under which the plugin was registered (utility name).
546
547        `logger`:
548           A standard Python logger for the plugins use.
549        """
550
551    def update(site, name, logger):
552        """Method to update an already existing plugin.
553
554        This might be called by a site when something serious
555        changes. It is a poor-man replacement for Zope generations
556        (but probably more comprehensive and better understandable).
557
558        `site`:
559           The site that requests an update.
560
561        `name`:
562           The name under which the plugin was registered (utility name).
563
564        `logger`:
565           A standard Python logger for the plugins use.
566        """
567
568class IAuthPluginUtility(Interface):
569    """A component that cares for authentication setup at site creation.
570
571    Utilities providing this interface are looked up when a Pluggable
572    Authentication Utility (PAU) for any
573    :class:`waeup.sirp.app.University` instance is created and put
574    into ZODB.
575
576    The setup-code then calls the `register` method of the utility and
577    expects a modified (or unmodified) version of the PAU back.
578
579    This allows to define any authentication setup modifications by
580    submodules or third-party modules/packages.
581    """
582
583    def register(pau):
584        """Register any plugins wanted to be in the PAU.
585        """
586
587    def unregister(pau):
588        """Unregister any plugins not wanted to be in the PAU.
589        """
590
591class IObjectConverter(Interface):
592    """Object converters are available as simple adapters, adapting
593       interfaces (not regular instances).
594
595    """
596
597    def fromStringDict(self, data_dict, context, form_fields=None):
598        """Convert values in `data_dict`.
599
600        Converts data in `data_dict` into real values based on
601        `context` and `form_fields`.
602
603        `data_dict` is a mapping (dict) from field names to values
604        represented as strings.
605
606        The fields (keys) to convert can be given in optional
607        `form_fields`. If given, form_fields should be an instance of
608        :class:`zope.formlib.form.Fields`. Suitable instances are for
609        example created by :class:`grok.AutoFields`.
610
611        If no `form_fields` are given, a default is computed from the
612        associated interface.
613
614        The `context` can be an existing object (implementing the
615        associated interface) or a factory name. If it is a string, we
616        try to create an object using
617        :func:`zope.component.createObject`.
618
619        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
620        <DATA_DICT>)`` where
621
622        ``<FIELD_ERRORS>``
623           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
624           error that happened when validating the input data in
625           `data_dict`
626
627        ``<INVARIANT_ERRORS>``
628           is a list of invariant errors concerning several fields
629
630        ``<DATA_DICT>``
631           is a dict with the values from input dict converted.
632
633        If errors happen, i.e. the error lists are not empty, always
634        an empty ``<DATA_DICT>`` is returned.
635
636        If ``<DATA_DICT>` is non-empty, there were no errors.
637        """
638
639class IObjectHistory(Interface):
640
641    messages = schema.List(
642        title = u'List of messages stored',
643        required = True,
644        )
645
646    def addMessage(message):
647        """Add a message.
648        """
649
650class IWAeUPWorkflowInfo(IWorkflowInfo):
651    """A :class:`hurry.workflow.workflow.WorkflowInfo` with additional
652       methods for convenience.
653    """
654    def getManualTransitions():
655        """Get allowed manual transitions.
656
657        Get a sorted list of tuples containing the `transition_id` and
658        `title` of each allowed transition.
659        """
660
661class ISiteLoggers(Interface):
662
663    loggers = Attribute("A list or generator of registered WAeUPLoggers")
664
665    def register(name, filename=None, site=None, **options):
666        """Register a logger `name` which logs to `filename`.
667
668        If `filename` is not given, logfile will be `name` with
669        ``.log`` as filename extension.
670        """
671
672    def unregister(name):
673        """Unregister a once registered logger.
674        """
675
676class ILogger(Interface):
677    """A logger cares for setup, update and restarting of a Python logger.
678    """
679
680    logger = Attribute("""A :class:`logging.Logger` instance""")
681
682
683    def __init__(name, filename=None, site=None, **options):
684        """Create a WAeUP logger instance.
685        """
686
687    def setup():
688        """Create a Python :class:`logging.Logger` instance.
689
690        The created logger is based on the params given by constructor.
691        """
692
693    def update(**options):
694        """Update the logger.
695
696        Updates the logger respecting modified `options` and changed
697        paths.
698        """
699
700class ILoggerCollector(Interface):
701
702    def getLoggers(site):
703        """Return all loggers registered for `site`.
704        """
705
706    def registerLogger(site, logging_component):
707        """Register a logging component residing in `site`.
708        """
709
710    def unregisterLogger(site, logging_component):
711        """Unregister a logger.
712        """
713
714#
715# External File Storage and relatives
716#
717class IFileStoreNameChooser(INameChooser):
718    """See zope.container.interfaces.INameChooser for base methods.
719    """
720    def checkName(name, attr=None):
721        """Check whether an object name is valid.
722
723        Raises a user error if the name is not valid.
724        """
725
726    def chooseName(name, attr=None):
727        """Choose a unique valid file id for the object.
728
729        The given name may be taken into account when choosing the
730        name (file id).
731
732        chooseName is expected to always choose a valid file id (that
733        would pass the checkName test) and never raise an error.
734
735        If `attr` is not ``None`` it might been taken into account as
736        well when generating the file id. Usual behaviour is to
737        interpret `attr` as a hint for what type of file for a given
738        context should be stored if there are several types
739        possible. For instance for a certain student some file could
740        be the connected passport photograph or some certificate scan
741        or whatever. Each of them has to be stored in a different
742        location so setting `attr` to a sensible value should give
743        different file ids returned.
744        """
745
746class IExtFileStore(IFileRetrieval):
747    """A file storage that stores files in filesystem (not as blobs).
748    """
749    root = schema.TextLine(
750        title = u'Root path of file store.',
751        )
752
753    def getFile(file_id):
754        """Get raw file data stored under file with `file_id`.
755
756        Returns a file descriptor open for reading or ``None`` if the
757        file cannot be found.
758        """
759
760    def getFileByContext(context, attr=None):
761        """Get raw file data stored for the given context.
762
763        Returns a file descriptor open for reading or ``None`` if no
764        such file can be found.
765
766        Both, `context` and `attr` might be used to find (`context`)
767        and feed (`attr`) an appropriate file name chooser.
768
769        This is a convenience method.
770        """
771
772    def deleteFile(file_id):
773        """Delete file stored under `file_id`.
774
775        Remove file from filestore so, that it is not available
776        anymore on next call to getFile for the same file_id.
777
778        Should not complain if no such file exists.
779        """
780
781    def deleteFileByContext(context, attr=None):
782        """Delete file for given `context` and `attr`.
783
784        Both, `context` and `attr` might be used to find (`context`)
785        and feed (`attr`) an appropriate file name chooser.
786
787        This is a convenience method.
788        """
789
790    def createFile(filename, f):
791        """Create file given by f with filename `filename`
792
793        Returns a hurry.file.File-based object.
794        """
795
796class IFileStoreHandler(Interface):
797    """Filestore handlers handle specific files for file stores.
798
799    If a file to store/get provides a specific filename, a file store
800    looks up special handlers for that type of file.
801
802    """
803    def pathFromFileID(store, root, filename):
804        """Turn file id into path to store.
805
806        Returned path should be absolute.
807        """
808
809    def createFile(store, root, filename, file_id, file):
810        """Return some hurry.file based on `store` and `file_id`.
811
812        Some kind of callback method called by file stores to create
813        file objects from file_id.
814
815        Returns a tuple ``(raw_file, path, file_like_obj)`` where the
816        ``file_like_obj`` should be a HurryFile, a WAeUPImageFile or
817        similar. ``raw_file`` is the (maybe changed) input file and
818        ``path`` the relative internal path to store the file at.
819
820        Please make sure the ``raw_file`` is opened for reading and
821        the file descriptor set at position 0 when returned.
822
823        This method also gets the raw input file object that is about
824        to be stored and is expected to raise any exceptions if some
825        kind of validation or similar fails.
826        """
Note: See TracBrowser for help on using the repository browser.