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

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

Implement SIRPPrincipalInfo and SIRPPrincipal classes which provide ordinary principals with an extra email and phone attribute.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 24.5 KB
Line 
1## $Id: interfaces.py 7233 2011-11-28 21:04:57Z 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    def getSessionString():
461        """Returns the session string from the vocabulary.
462        """
463
464
465class ISessionConfigurationAdd(ISessionConfiguration):
466    """A session configuration object in add mode.
467    """
468
469    academic_session = schema.Choice(
470        title = u'Academic Session',
471        source = academic_sessions_vocab,
472        default = None,
473        required = True,
474        readonly = False,
475        )
476
477ISessionConfigurationAdd['academic_session'].order =  ISessionConfiguration[
478    'academic_session'].order
479
480class IDataCenter(IWAeUPObject):
481    """A data center.
482
483    TODO : declare methods, at least those needed by pages.
484    """
485    pass
486
487class IDataCenterFile(Interface):
488    """A data center file.
489    """
490
491    name = schema.TextLine(
492        title = u'Filename')
493
494    size = schema.TextLine(
495        title = u'Human readable file size')
496
497    uploaddate = schema.TextLine(
498        title = u'Human readable upload datetime')
499
500    lines = schema.Int(
501        title = u'Number of lines in file')
502
503    def getDate():
504        """Get creation timestamp from file in human readable form.
505        """
506
507    def getSize():
508        """Get human readable size of file.
509        """
510
511    def getLinesNumber():
512        """Get number of lines of file.
513        """
514
515class IDataCenterStorageMovedEvent(IObjectEvent):
516    """Emitted, when the storage of a datacenter changes.
517    """
518
519class IObjectUpgradeEvent(IObjectEvent):
520    """Can be fired, when an object shall be upgraded.
521    """
522
523class ILocalRoleSetEvent(IObjectEvent):
524    """A local role was granted/revoked for a principal on an object.
525    """
526    role_id = Attribute(
527        "The role id that was set.")
528    principal_id = Attribute(
529        "The principal id for which the role was granted/revoked.")
530    granted = Attribute(
531        "Boolean. If false, then the role was revoked.")
532
533class IQueryResultItem(Interface):
534    """An item in a search result.
535    """
536    url = schema.TextLine(
537        title = u'URL that links to the found item')
538    title = schema.TextLine(
539        title = u'Title displayed in search results.')
540    description = schema.Text(
541        title = u'Longer description of the item found.')
542
543class IWAeUPSIRPPluggable(Interface):
544    """A component that might be plugged into a WAeUP SIRP app.
545
546    Components implementing this interface are referred to as
547    'plugins'. They are normally called when a new
548    :class:`waeup.sirp.app.University` instance is created.
549
550    Plugins can setup and update parts of the central site without the
551    site object (normally a :class:`waeup.sirp.app.University` object)
552    needing to know about that parts. The site simply collects all
553    available plugins, calls them and the plugins care for their
554    respective subarea like the applicants area or the datacenter
555    area.
556
557    Currently we have no mechanism to define an order of plugins. A
558    plugin should therefore make no assumptions about the state of the
559    site or other plugins being run before and instead do appropriate
560    checks if necessary.
561
562    Updates can be triggered for instance by the respective form in
563    the site configuration. You normally do updates when the
564    underlying software changed.
565    """
566    def setup(site, name, logger):
567        """Create an instance of the plugin.
568
569        The method is meant to be called by the central app (site)
570        when it is created.
571
572        `site`:
573           The site that requests a setup.
574
575        `name`:
576           The name under which the plugin was registered (utility name).
577
578        `logger`:
579           A standard Python logger for the plugins use.
580        """
581
582    def update(site, name, logger):
583        """Method to update an already existing plugin.
584
585        This might be called by a site when something serious
586        changes. It is a poor-man replacement for Zope generations
587        (but probably more comprehensive and better understandable).
588
589        `site`:
590           The site that requests an update.
591
592        `name`:
593           The name under which the plugin was registered (utility name).
594
595        `logger`:
596           A standard Python logger for the plugins use.
597        """
598
599class IAuthPluginUtility(Interface):
600    """A component that cares for authentication setup at site creation.
601
602    Utilities providing this interface are looked up when a Pluggable
603    Authentication Utility (PAU) for any
604    :class:`waeup.sirp.app.University` instance is created and put
605    into ZODB.
606
607    The setup-code then calls the `register` method of the utility and
608    expects a modified (or unmodified) version of the PAU back.
609
610    This allows to define any authentication setup modifications by
611    submodules or third-party modules/packages.
612    """
613
614    def register(pau):
615        """Register any plugins wanted to be in the PAU.
616        """
617
618    def unregister(pau):
619        """Unregister any plugins not wanted to be in the PAU.
620        """
621
622class IObjectConverter(Interface):
623    """Object converters are available as simple adapters, adapting
624       interfaces (not regular instances).
625
626    """
627
628    def fromStringDict(self, data_dict, context, form_fields=None):
629        """Convert values in `data_dict`.
630
631        Converts data in `data_dict` into real values based on
632        `context` and `form_fields`.
633
634        `data_dict` is a mapping (dict) from field names to values
635        represented as strings.
636
637        The fields (keys) to convert can be given in optional
638        `form_fields`. If given, form_fields should be an instance of
639        :class:`zope.formlib.form.Fields`. Suitable instances are for
640        example created by :class:`grok.AutoFields`.
641
642        If no `form_fields` are given, a default is computed from the
643        associated interface.
644
645        The `context` can be an existing object (implementing the
646        associated interface) or a factory name. If it is a string, we
647        try to create an object using
648        :func:`zope.component.createObject`.
649
650        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
651        <DATA_DICT>)`` where
652
653        ``<FIELD_ERRORS>``
654           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
655           error that happened when validating the input data in
656           `data_dict`
657
658        ``<INVARIANT_ERRORS>``
659           is a list of invariant errors concerning several fields
660
661        ``<DATA_DICT>``
662           is a dict with the values from input dict converted.
663
664        If errors happen, i.e. the error lists are not empty, always
665        an empty ``<DATA_DICT>`` is returned.
666
667        If ``<DATA_DICT>` is non-empty, there were no errors.
668        """
669
670class IObjectHistory(Interface):
671
672    messages = schema.List(
673        title = u'List of messages stored',
674        required = True,
675        )
676
677    def addMessage(message):
678        """Add a message.
679        """
680
681class IWAeUPWorkflowInfo(IWorkflowInfo):
682    """A :class:`hurry.workflow.workflow.WorkflowInfo` with additional
683       methods for convenience.
684    """
685    def getManualTransitions():
686        """Get allowed manual transitions.
687
688        Get a sorted list of tuples containing the `transition_id` and
689        `title` of each allowed transition.
690        """
691
692class ISiteLoggers(Interface):
693
694    loggers = Attribute("A list or generator of registered WAeUPLoggers")
695
696    def register(name, filename=None, site=None, **options):
697        """Register a logger `name` which logs to `filename`.
698
699        If `filename` is not given, logfile will be `name` with
700        ``.log`` as filename extension.
701        """
702
703    def unregister(name):
704        """Unregister a once registered logger.
705        """
706
707class ILogger(Interface):
708    """A logger cares for setup, update and restarting of a Python logger.
709    """
710
711    logger = Attribute("""A :class:`logging.Logger` instance""")
712
713
714    def __init__(name, filename=None, site=None, **options):
715        """Create a WAeUP logger instance.
716        """
717
718    def setup():
719        """Create a Python :class:`logging.Logger` instance.
720
721        The created logger is based on the params given by constructor.
722        """
723
724    def update(**options):
725        """Update the logger.
726
727        Updates the logger respecting modified `options` and changed
728        paths.
729        """
730
731class ILoggerCollector(Interface):
732
733    def getLoggers(site):
734        """Return all loggers registered for `site`.
735        """
736
737    def registerLogger(site, logging_component):
738        """Register a logging component residing in `site`.
739        """
740
741    def unregisterLogger(site, logging_component):
742        """Unregister a logger.
743        """
744
745#
746# External File Storage and relatives
747#
748class IFileStoreNameChooser(INameChooser):
749    """See zope.container.interfaces.INameChooser for base methods.
750    """
751    def checkName(name, attr=None):
752        """Check whether an object name is valid.
753
754        Raises a user error if the name is not valid.
755        """
756
757    def chooseName(name, attr=None):
758        """Choose a unique valid file id for the object.
759
760        The given name may be taken into account when choosing the
761        name (file id).
762
763        chooseName is expected to always choose a valid file id (that
764        would pass the checkName test) and never raise an error.
765
766        If `attr` is not ``None`` it might been taken into account as
767        well when generating the file id. Usual behaviour is to
768        interpret `attr` as a hint for what type of file for a given
769        context should be stored if there are several types
770        possible. For instance for a certain student some file could
771        be the connected passport photograph or some certificate scan
772        or whatever. Each of them has to be stored in a different
773        location so setting `attr` to a sensible value should give
774        different file ids returned.
775        """
776
777class IExtFileStore(IFileRetrieval):
778    """A file storage that stores files in filesystem (not as blobs).
779    """
780    root = schema.TextLine(
781        title = u'Root path of file store.',
782        )
783
784    def getFile(file_id):
785        """Get raw file data stored under file with `file_id`.
786
787        Returns a file descriptor open for reading or ``None`` if the
788        file cannot be found.
789        """
790
791    def getFileByContext(context, attr=None):
792        """Get raw file data stored for the given context.
793
794        Returns a file descriptor open for reading or ``None`` if no
795        such file can be found.
796
797        Both, `context` and `attr` might be used to find (`context`)
798        and feed (`attr`) an appropriate file name chooser.
799
800        This is a convenience method.
801        """
802
803    def deleteFile(file_id):
804        """Delete file stored under `file_id`.
805
806        Remove file from filestore so, that it is not available
807        anymore on next call to getFile for the same file_id.
808
809        Should not complain if no such file exists.
810        """
811
812    def deleteFileByContext(context, attr=None):
813        """Delete file for given `context` and `attr`.
814
815        Both, `context` and `attr` might be used to find (`context`)
816        and feed (`attr`) an appropriate file name chooser.
817
818        This is a convenience method.
819        """
820
821    def createFile(filename, f):
822        """Create file given by f with filename `filename`
823
824        Returns a hurry.file.File-based object.
825        """
826
827class IFileStoreHandler(Interface):
828    """Filestore handlers handle specific files for file stores.
829
830    If a file to store/get provides a specific filename, a file store
831    looks up special handlers for that type of file.
832
833    """
834    def pathFromFileID(store, root, filename):
835        """Turn file id into path to store.
836
837        Returned path should be absolute.
838        """
839
840    def createFile(store, root, filename, file_id, file):
841        """Return some hurry.file based on `store` and `file_id`.
842
843        Some kind of callback method called by file stores to create
844        file objects from file_id.
845
846        Returns a tuple ``(raw_file, path, file_like_obj)`` where the
847        ``file_like_obj`` should be a HurryFile, a WAeUPImageFile or
848        similar. ``raw_file`` is the (maybe changed) input file and
849        ``path`` the relative internal path to store the file at.
850
851        Please make sure the ``raw_file`` is opened for reading and
852        the file descriptor set at position 0 when returned.
853
854        This method also gets the raw input file object that is about
855        to be stored and is expected to raise any exceptions if some
856        kind of validation or similar fails.
857        """
Note: See TracBrowser for help on using the repository browser.