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

Last change on this file since 7390 was 7389, checked in by uli, 13 years ago

Add interface for PDFs.

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