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

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

First part of acceptance fee payment integration (under construction).

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