Changeset 7063 for main/waeup.sirp/trunk/src
- Timestamp:
- 9 Nov 2011, 15:42:45 (13 years ago)
- Location:
- main/waeup.sirp/trunk
- Files:
-
- 25 edited
- 1 copied
Legend:
- Unmodified
- Added
- Removed
-
main/waeup.sirp/trunk
- Property svn:mergeinfo changed
/main/waeup.sirp/branches/ulif-extimgstore (added) merged: 7001-7002,7010-7011,7016,7031-7041,7043-7044,7046-7055
- Property svn:mergeinfo changed
-
main/waeup.sirp/trunk/src/waeup/sirp/app.py
r6952 r7063 1 1 import grok 2 from hurry.file.interfaces import IFileRetrieval3 2 from zope.authentication.interfaces import IAuthentication 4 3 from zope.component import getUtilitiesFor … … 9 8 from waeup.sirp.students.container import StudentsContainer 10 9 from waeup.sirp.hostels.container import HostelsContainer 11 from waeup.sirp.imagestorage import ImageStorageFileRetrieval, ImageStorage12 10 from waeup.sirp.interfaces import ( 13 11 IUniversity, IWAeUPSIRPPluggable, IObjectUpgradeEvent, ) … … 28 26 setup = setup_authentication,) 29 27 30 grok.local_utility(31 ImageStorageFileRetrieval, provides = IFileRetrieval)32 33 28 def __init__(self, *args, **kw): 34 29 super(University, self).__init__(*args, **kw) … … 44 39 self['users'] = UserContainer() 45 40 self['datacenter'] = DataCenter() 46 self['images'] = ImageStorage()47 41 self['students'] = StudentsContainer() 48 42 self['configuration'] = ConfigurationContainer() -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py
r6632 r7063 3 3 # Make this a package. 4 4 from waeup.sirp.applicants.applicant import ( 5 ResultEntry, Applicant, ApplicantFactory, ApplicantTraverser, 5 ResultEntry, Applicant, ApplicantFactory, ApplicantImageStoreHandler, 6 get_regno_or_ac, ApplicantImageNameChooser, 6 7 ) 7 8 from waeup.sirp.applicants.container import ApplicantsContainer … … 14 15 'Applicant', 15 16 'ApplicantFactory', 17 'ApplicantImageNameChooser', 18 'ApplicantImageStoreHandler', 16 19 'ApplicantsContainer', 17 20 'ApplicantsRoot', 18 'ApplicantTraverser',19 21 'application_exists', 20 22 'get_applicant_data', 23 'get_regno_or_ac', 21 24 ] -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/applicant.py
r6632 r7063 20 20 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 21 ## 22 import os 22 23 import grok 23 24 from grok import index … … 25 26 from zope.interface import implementedBy 26 27 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState 27 from waeup.sirp.interfaces import IObjectHistory28 28 from waeup.sirp.app import University 29 from waeup.sirp.image import WAeUPImageFile 30 from waeup.sirp.imagestorage import DefaultFileStoreHandler 31 from waeup.sirp.interfaces import ( 32 IObjectHistory, IFileStoreHandler, IFileStoreNameChooser) 33 from waeup.sirp.utils.helpers import attrs_to_fields 29 34 from waeup.sirp.applicants.interfaces import ( 30 IResultEntry, IApplicant, IApplicantEdit, default_passport_image,35 IResultEntry, IApplicant, IApplicantEdit, 31 36 ) 32 from waeup.sirp.utils.helpers import attrs_to_fields 37 38 39 def get_regno_or_ac(context): 40 reg_no = getattr(context, 'reg_no', None) 41 if reg_no is None: 42 return getattr(context, 'access_code', None) 43 return reg_no 33 44 34 45 class ResultEntry(grok.Context): … … 49 60 return 50 61 51 #def getApplicantsRootLogger(self):52 # return grok.getSite()['applicants'].logger53 54 62 def loggerInfo(self, ob_class, comment=None): 55 63 target = self.__name__ … … 81 89 access_code = index.Field(attribute='access_code') 82 90 83 class ApplicantTraverser(grok.Traverser):84 """Get image of the context applicant.85 86 Each applicant can provide a passport photograph which will be87 returned by this traverser if:88 89 - we request the exact filename of the picture or90 91 - ask for a picture named 'passport.jpg'.92 93 If no picture was stored yet, we get a placeholder image when94 asking for `passport.jpg`.95 96 If none of the above applies, we return ``None``, most probably97 resulting a :exc:`NotFound` exception.98 99 """100 grok.context(IApplicant)101 def traverse(self, name):102 passport_filename = getattr(self.context.passport, 'filename', None)103 if name == passport_filename:104 return self.context.passport105 if name == 'passport.jpg':106 if self.context.passport is not None:107 return self.context.passport108 return109 110 91 class ApplicantFactory(grok.GlobalUtility): 111 92 """A factory for applicants. … … 121 102 def getInterfaces(self): 122 103 return implementedBy(Applicant) 104 105 106 #: The file id marker for applicant passport images 107 APPLICANT_IMAGE_STORE_NAME = 'img-applicant' 108 109 class ApplicantImageNameChooser(grok.Adapter): 110 """A file id chooser for :class:`Applicant` objects. 111 112 `context` is an :class:`Applicant` instance. 113 114 The :class:`ApplicantImageNameChooser` can build/check file ids 115 for :class:`Applicant` objects suitable for use with 116 :class:`ExtFileStore` instances. The delivered file_id contains 117 the file id marker for :class:`Applicant` object and the 118 registration number or access code of the context applicant. Also 119 the name of the connected applicant container will be part of the 120 generated file id. 121 122 This chooser is registered as an adapter providing 123 :class:`waeup.sirp.interfaces.IFileStoreNameChooser`. 124 125 File store name choosers like this one are only convenience 126 components to ease the task of creating file ids for applicant 127 objects. You are nevertheless encouraged to use them instead of 128 manually setting up filenames for applicants. 129 130 .. seealso:: :mod:`waeup.sirp.imagestorage` 131 132 """ 133 grok.context(IApplicant) 134 grok.implements(IFileStoreNameChooser) 135 136 def checkName(self, name=None): 137 """Check whether the given name is a valid file id for the context. 138 139 Returns ``True`` only if `name` equals the result of 140 :meth:`chooseName`. 141 """ 142 return name == self.chooseName() 143 144 def chooseName(self, name=None): 145 """Get a valid file id for applicant context. 146 147 *Example:* 148 149 For an applicant with registration no. ``'My_reg_no_1234'`` 150 and stored in an applicants container called 151 ``'mycontainer'``, this chooser would create: 152 153 ``'__img-applicant__mycontainer/My_reg_no_1234.jpg'`` 154 155 meaning that the passport image of this applicant would be 156 stored in the site-wide file storage in path: 157 158 ``mycontainer/My_reg_no_1234.jpg`` 159 160 If the context applicant has no parent, ``'_default'`` is used 161 as parent name. 162 """ 163 parent_name = getattr( 164 getattr(self.context, '__parent__', None), 165 '__name__', '_default') 166 marked_filename = '__%s__%s/%s.jpg' % ( 167 APPLICANT_IMAGE_STORE_NAME, 168 parent_name, get_regno_or_ac(self.context)) 169 return marked_filename 170 171 172 class ApplicantImageStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility): 173 """Applicant specific image handling. 174 175 This handler knows in which path in a filestore to store applicant 176 images and how to turn this kind of data into some (browsable) 177 file object. 178 179 It is called from the global file storage, when it wants to 180 get/store a file with a file id starting with 181 ``__img-applicant__`` (the marker string for applicant images). 182 183 Like each other file store handler it does not handle the files 184 really (this is done by the global file store) but only computes 185 paths and things like this. 186 """ 187 grok.implements(IFileStoreHandler) 188 grok.name(APPLICANT_IMAGE_STORE_NAME) 189 190 def pathFromFileID(self, store, root, file_id): 191 """All applicants images are filed in directory ``applicants``. 192 """ 193 marker, filename, basename, ext = store.extractMarker(file_id) 194 return os.path.join(root, 'applicants', filename) 195 196 def createFile(self, store, root, filename, file_id, file): 197 """Create a browsable file-like object. 198 """ 199 # possible other actions: check for jpeg format 200 path = self.pathFromFileID(store, root, file_id) 201 return file, path, WAeUPImageFile(filename, file_id) -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/authentication.py
r6670 r7063 232 232 if ac.state == 'used' and appl_ac != ac.representation: 233 233 return None 234 if appl_ac is not None and appl_ac != ac.representation:235 # This should never happen. It means a catalog error.236 return None234 # If the following fails we have a catalog error. Bad enough 235 # to pull emergency break. 236 assert appl_ac is None or appl_ac == ac.representation 237 237 return ApplicantPrincipalInfo(accesscode) 238 238 … … 256 256 systems (regular users, officers, etc.) were set up. 257 257 """ 258 grok. provides(IAuthPluginUtility)258 grok.implements(IAuthPluginUtility) 259 259 grok.name('applicants_auth_setup') 260 260 … … 285 285 286 286 def unregister(self, pau): 287 """Unregister applicant specific authentication components from PAU. 288 """ 289 pau.credentialsPlugins = tuple( 290 [x for x in list(pau.credentialsPlugins) 291 if x != 'applicant_credentials']) 292 pau.authenticatorPlugins = tuple( 293 [x for x in list(pau.authenticatorPlugins) 294 if x != 'applicants']) 287 295 return pau 288 296 -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py
r6816 r7063 22 22 """UI components for basic applicants and related components. 23 23 """ 24 import os 24 25 import sys 25 26 import grok … … 53 54 ManageActionButton, PrimaryNavTab, LeftSidebarLink 54 55 ) 55 from waeup.sirp.image.browser.widget import ( 56 ThumbnailWidget, EncodingImageFileWidget, 57 ) 58 from waeup.sirp.image.image import createWAeUPImageFile 59 from waeup.sirp.interfaces import IWAeUPObject, ILocalRolesAssignable 56 from waeup.sirp.interfaces import ( 57 IWAeUPObject, ILocalRolesAssignable, IExtFileStore, IFileStoreNameChooser) 60 58 from waeup.sirp.permissions import get_users_with_local_roles 61 59 from waeup.sirp.university.interfaces import ICertificate … … 68 66 from waeup.sirp.applicants.interfaces import ( 69 67 IApplicant, IApplicantPrincipal,IApplicantEdit, IApplicantsRoot, 70 IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab 68 IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab, 69 IMAGE_PATH, 71 70 ) 72 71 from waeup.sirp.applicants.workflow import INITIALIZED, STARTED … … 425 424 # If application has ended and applicant record doesn't exist, 426 425 # logout without marking AC as used 427 if not pin in self.context.keys() and self.context.enddate < date.today(): 426 if not pin in self.context.keys() and ( 427 self.context.enddate < date.today()): 428 428 self.flash('Application has ended.') 429 429 auth = getUtility(IAuthentication) … … 433 433 434 434 # Mark AC as used (this also fires a pin related transition) 435 if get_access_code(pin).state == USED: 436 pass 437 else: 435 if get_access_code(pin).state != USED: 438 436 comment = u"AC invalidated" 439 437 # Here we know that the ac is in state initialized so we do not … … 514 512 applicant_object = get_applicant_data(access_code) 515 513 return absoluteURL(applicant_object, self.request) + self.target 516 #else: 517 # return '' 514 return '' 518 515 519 516 class AccessCodeEditLink(AccessCodeViewLink): … … 534 531 return '' 535 532 return absoluteURL(applicant_object, self.request) + self.target 536 #else: 537 # return '' 533 return '' 538 534 539 535 class AccessCodeSlipLink(AccessCodeViewLink): … … 550 546 form_fields = grok.AutoFields(IApplicant).omit( 551 547 'locked').omit('course_admitted') 552 #form_fields['fst_sit_results'].custom_widget = list_results_display_widget553 form_fields['passport'].custom_widget = ThumbnailWidget554 548 form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le') 555 549 label = 'Applicant' 556 550 grok.template('form_display') 557 551 pnav = 3 552 553 def update(self): 554 self.passport_url = self.url(self.context, 'passport.jpg') 555 return 558 556 559 557 @property … … 582 580 class PDFActionButton(ManageActionButton): 583 581 grok.context(IApplicant) 584 #grok.view(DisplayApplicant)585 582 grok.require('waeup.manageApplications') 586 583 icon = 'actionicon_pdf.png' … … 624 621 625 622 def render(self): 626 # (0,0),(-1,-1) = whole table627 # (0,0),(0,-1) = first column628 # (-1,0),(-1,-1) = last column629 # (0,0),(-1,0) = first row630 # (0,-1),(-1,-1) = last row631 623 SLIP_STYLE = TableStyle( 632 624 [('VALIGN',(0,0),(-1,-1),'TOP')] … … 652 644 frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm)) 653 645 story.append(Paragraph(self.label, style["Heading2"])) 654 #story.append(HRFlowable())655 646 story.append(Spacer(1, 18)) 656 647 for msg in self.context.history.messages: … … 658 649 story.append(Paragraph(f_msg, style["Normal"])) 659 650 story.append(Spacer(1, 24)) 651 652 data = [] 653 # insert passport photograph 654 img = getUtility(IExtFileStore).getFileByContext(self.context) 655 if img is None: 656 img = open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg'), 'rb') 657 doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound') 658 data.append(['', doc_img]) 659 660 # render widget fields 660 661 self.setUpWidgets() 661 data = []662 662 for widget in self.widgets: 663 663 f_label = '<font size=12>%s</font>:' % widget.label.strip() 664 664 f_label = Paragraph(f_label, style["Normal"]) 665 if widget.name != 'form.passport': 666 f_text = '<font size=12>%s</font>' % widget() 667 f_text = Paragraph(f_text, style["Normal"]) 668 data.append([f_label,f_text]) 669 else: 670 filename = widget._data.file.name 671 im = Image(filename,width=4*cm, height=3*cm,kind='bound') 672 data.append([f_label,im]) 665 f_text = '<font size=12>%s</font>' % widget() 666 f_text = Paragraph(f_text, style["Normal"]) 667 data.append([f_label,f_text]) 673 668 f_label = '<font size=12>Admitted Course of Study:</font>' 674 669 f_text = '<font size=12>%s</font>' % self.getCourseAdmitted() … … 698 693 target = 'edit_full' 699 694 695 def handle_img_upload(upload, context): 696 """Handle upload of applicant image. 697 """ 698 store = getUtility(IExtFileStore) 699 file_id = IFileStoreNameChooser(context).chooseName() 700 store.createFile(file_id, upload) 701 upload.seek(0) # XXX: really neccessary? 702 return 703 700 704 class EditApplicantFull(WAeUPEditFormPage): 701 705 """A full edit view for applicant data. … … 705 709 grok.require('waeup.manageApplications') 706 710 form_fields = grok.AutoFields(IApplicant) 707 form_fields['passport'].custom_widget = EncodingImageFileWidget708 711 form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year') 709 712 grok.template('form_edit') … … 718 721 if upload: 719 722 # We got a fresh upload 720 image = createWAeUPImageFile(upload.filename, upload) 721 # This would normally be totally illegal. We set context 722 # data without the complete form data being 723 # verified. Normally data is set in methods like `save` 724 # only, which is only called when all form data is correct 725 # (and if not, the context is not touched). With images 726 # and their uploads the problem then is, that the uploaded 727 # image won't be shown in a partially erranous form 728 # because the new uploaded image is still not part of the 729 # context obeject and the image widget cannot work around 730 # that (it has no access to the new data). For this reason 731 # we set the image here and not in 'save()' or other 732 # methods. 733 self.context.passport = image 734 upload.seek(0) 723 handle_img_upload(upload, self.context) 735 724 self.passport_changed = True 736 725 return … … 760 749 changed_fields = self.applyData(self.context, **data) 761 750 changed_fields = changed_fields.values() 762 fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields) 751 fields_string = '+'.join( 752 ' + '.join(str(i) for i in b) for b in changed_fields) 763 753 self.context._p_changed = True 764 754 form = self.request.form … … 782 772 'screening_score', 783 773 ) 784 form_fields['passport'].custom_widget = EncodingImageFileWidget785 774 form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year') 786 775 grok.template('form_edit') … … 788 777 title = u'Your Application Form' 789 778 790 791 779 def emitLockMessage(self): 792 780 self.flash('The requested form is locked (read-only).') … … 802 790 if upload: 803 791 # We got a fresh upload 804 image = createWAeUPImageFile(upload.filename, upload) 805 # This would normally be totally illegal. We set context 806 # data without the complete form data being 807 # verified. Normally data is set in methods like `save` 808 # only, which is only called when all form data is correct 809 # (and if not, the context is not touched). With images 810 # and their uploads the problem then is, that the uploaded 811 # image won't be shown in a partially erranous form 812 # because the new uploaded image is still not part of the 813 # context obeject and the image widget cannot work around 814 # that (it has no access to the new data). For this reason 815 # we set the image here and not in 'save()' or other 816 # methods. 817 self.context.passport = image 818 upload.seek(0) 792 handle_img_upload(upload, self.context) 819 793 self.passport_changed = True 820 794 super(EditApplicantStudent, self).update() … … 824 798 if not self.request.form.get('confirm_passport', False): 825 799 return 'Passport confirmation box not ticked.' 826 #if len(self.errors) > 0:827 # return 'Form has errors.'828 800 return False 829 801 830 802 @grok.action('Save') 831 803 def save(self, **data): 832 #if self.context.locked:833 # self.emitLockMessage()834 # return835 804 self.applyData(self.context, **data) 836 805 self.context._p_changed = True … … 840 809 @grok.action('Final Submit') 841 810 def finalsubmit(self, **data): 842 #if self.context.locked:843 # self.emitLockMessage()844 # return845 811 self.applyData(self.context, **data) 846 812 self.context._p_changed = True … … 868 834 text = 'View application record' 869 835 target = 'index' 836 837 class PassportImage(grok.View): 838 """Renders the passport image for applicants. 839 """ 840 grok.name('passport.jpg') 841 grok.context(IApplicant) 842 grok.require('waeup.handleApplication') 843 844 def render(self): 845 # A filename chooser turns a context into a filename suitable 846 # for file storage. 847 image = getUtility(IExtFileStore).getFileByContext(self.context) 848 self.response.setHeader( 849 'Content-Type', 'image/jpeg') 850 if image is None: 851 # show placeholder image 852 return open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg'), 'rb') 853 return image -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser_templates/form_display.pt
r6350 r7063 13 13 <table class="zebra"> 14 14 <tbody> 15 <tr><td></td> 16 <td><img src="" tal:attributes="src view/passport_url" /></td> 17 </tr> 15 18 <tal:block repeat="widget view/widgets"> 16 19 <tr> -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser_templates/form_edit.pt
r6756 r7063 46 46 </tr> 47 47 </tal:block> 48 <tr> 49 <td class="label">Photograph</td> 50 <td class="field"> 51 <span class="widget"> 52 <img src="passport.jpg" /><br /> 53 <input type="file" name="form.passport" /> 54 </span> 55 </tr> 48 56 <tr tal:condition="view/manage_applications"> 49 57 <td class="label"><label>Application Transition:</label></td> -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py
r6915 r7063 29 29 30 30 from zope import schema 31 from zope.interface import Interface, Attribute , provider31 from zope.interface import Interface, Attribute 32 32 from zope.component import getUtilitiesFor 33 33 from zope.pluggableauth.interfaces import IPrincipalInfo 34 34 from zope.security.interfaces import IGroupClosureAwarePrincipal as IPrincipal 35 35 from zc.sourcefactory.basic import BasicSourceFactory 36 from waeup.sirp.image import createWAeUPImageFile37 from waeup.sirp.image.schema import ImageFile38 36 from waeup.sirp.interfaces import IWAeUPObject, year_range 39 37 from waeup.sirp.university.vocabularies import application_categories … … 66 64 return True 67 65 68 @provider(schema.interfaces.IContextAwareDefaultFactory)69 def default_passport_image(context):70 """A default value factory for ImageFile fields.71 72 Returns some default image as WAeUPImageFile. We cannot set the73 default directly in ImageFile fields, as, if we want to set74 max_size or min_size as well, some utility lookups are needed75 which are not possible during startup.76 77 Developers which use IContextAwareDefaultFactories like this one78 always should make sure that the delivered default meets all79 constraints of the field that makes use of this default value80 provider.81 """82 imagefile = createWAeUPImageFile(83 'placeholder_m.jpg',84 open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg'), 'r')85 )86 return imagefile87 88 66 class ApplicantContainerProviderSource(BasicSourceFactory): 89 67 """A source offering all available applicants container types. … … 370 348 required = False, 371 349 ) 372 passport = ImageFile(373 title = u'Passport Photograph',374 #default = DEFAULT_PASSPORT_IMAGE_MALE,375 defaultFactory = default_passport_image,376 description = u'Maximun file size is 20 kB.',377 required = True,378 max_size = 20480,379 )350 #passport = ImageFile( 351 # title = u'Passport Photograph', 352 # #default = DEFAULT_PASSPORT_IMAGE_MALE, 353 # defaultFactory = default_passport_image, 354 # description = u'Maximun file size is 20 kB.', 355 # required = True, 356 # max_size = 20480, 357 # ) 380 358 381 359 # -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/root.py
r6582 r7063 47 47 """ 48 48 user = get_current_principal() 49 if user is None: 50 user = 'system' 51 elif user.title == 'Applicant': 52 user = 'applicant' 53 else: 54 user = user.id 49 user = getattr(user, 'id', 'system') 55 50 self.logger.info('%s - %s - %s - %s' % ( 56 51 user, target, ob_class, comment)) -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_applicant.py
r6657 r7063 22 22 """Tests for applicants and related. 23 23 """ 24 import grok 24 25 import unittest 25 from StringIO import StringIO 26 from hurry.file.interfaces import IFileRetrieval 27 from zope.component import ( 28 provideAdapter, adapts, getGlobalSiteManager, provideUtility) 29 from zope.component.hooks import setSite 26 from zope.component import adapts, queryUtility 30 27 from zope.component.interfaces import IFactory 31 28 from zope.interface import verify, implements 32 29 from zope.location.interfaces import ILocation 33 from zope.publisher.base import TestRequest34 from zope.publisher.interfaces import NotFound35 from waeup.sirp.app import University36 from waeup.sirp.image import WAeUPImageFile, createWAeUPImageFile37 30 from waeup.sirp.image.interfaces import IWAeUPImageFile 31 from waeup.sirp.imagestorage import DefaultStorage 32 from waeup.sirp.interfaces import IFileStoreHandler, IFileStoreNameChooser 38 33 from waeup.sirp.applicants import ( 39 ResultEntry, Applicant, ApplicantFactory, ApplicantTraverser, 34 ResultEntry, Applicant, ApplicantFactory, get_regno_or_ac, 35 ApplicantImageStoreHandler, ApplicantImageNameChooser, 40 36 ) 41 from waeup.sirp.applicants.interfaces import ( 42 IResultEntry, IApplicant, 43 ) 44 from waeup.sirp.imagestorage import ImageStorageFileRetrieval 45 from waeup.sirp.testing import (FunctionalTestCase, FunctionalLayer) 37 from waeup.sirp.applicants.interfaces import IResultEntry, IApplicant 38 from waeup.sirp.testing import FunctionalTestCase, FunctionalLayer 46 39 47 40 class FakeImageLocation(object): … … 51 44 pass 52 45 46 class HelperTests(FunctionalTestCase): 47 48 layer = FunctionalLayer 49 50 def test_get_regno_or_ac(self): 51 # we can get reg_no or access_code of an applicants if it is set 52 appl1 = Applicant() 53 appl2 = Applicant() 54 appl2.reg_no = u'foo' 55 appl3 = Applicant() 56 appl3.access_code = u'bar' 57 appl4 = Applicant() 58 appl4.reg_no = u'foo' 59 appl4.access_code = u'bar' 60 self.assertTrue( 61 get_regno_or_ac(appl1) is None) 62 self.assertEqual( 63 get_regno_or_ac(appl2), u'foo') 64 self.assertEqual( 65 get_regno_or_ac(appl3), u'bar') 66 self.assertEqual( 67 get_regno_or_ac(appl4), u'foo') 68 return 69 70 def test_image_store_handler_util_accessible(self): 71 # we can get an IFileStoreHandler utility for applicants 72 handler = queryUtility(IFileStoreHandler, name='img-applicant') 73 self.assertTrue( 74 isinstance(handler, ApplicantImageStoreHandler)) 75 return 76 77 def test_image_store_handler(self): 78 store = DefaultStorage() 79 handler = queryUtility(IFileStoreHandler, name='img-applicant') 80 result1 = handler.pathFromFileID( 81 store, '/fake-root', '__img-applicant__sample.jpg') 82 result2 = handler.pathFromFileID( 83 store, '/fake-root', '__img-applicant__dir1/sample.jpg') 84 result3 = handler.pathFromFileID( 85 store, '/fake-root', '__img-applicant__dir1/dir2/sample.jpg') 86 self.assertEqual( 87 result1, '/fake-root/applicants/sample.jpg') 88 self.assertEqual( 89 result2, '/fake-root/applicants/dir1/sample.jpg') 90 self.assertEqual( 91 result3, '/fake-root/applicants/dir1/dir2/sample.jpg') 92 return 93 94 class ApplicantImageNameChooserTests(FunctionalTestCase): 95 96 layer = FunctionalLayer 97 98 def test_iface(self): 99 # make sure we implement promised interfaces 100 obj = ApplicantImageNameChooser(None) # needs a context normally 101 verify.verifyClass(IFileStoreNameChooser, ApplicantImageNameChooser) 102 verify.verifyObject(IFileStoreNameChooser, obj) 103 return 104 105 def test_name_chooser_available(self): 106 # we can get a name chooser for applicant objects as adapter 107 appl = Applicant() 108 chooser = IFileStoreNameChooser(appl) 109 self.assertTrue(chooser is not None) 110 return 111 112 def test_name_chooser_applicant_wo_container(self): 113 # we can get an image filename for applicants not in a container 114 appl = Applicant() 115 appl.reg_no = u'MY_REG_NO' 116 chooser = IFileStoreNameChooser(appl) 117 result = chooser.chooseName() 118 # the file would be stored in a ``_default`` directory. 119 self.assertEqual( 120 result, '__img-applicant___default/MY_REG_NO.jpg') 121 return 122 123 def test_name_chooser_applicant_w_container(self): 124 appl = Applicant() 125 appl.reg_no = u'MY_REG_NO' 126 fake_container = grok.Container() 127 fake_container.__name__ = 'folder' 128 fake_container['appl'] = appl 129 appl.__parent__ = fake_container 130 chooser = IFileStoreNameChooser(appl) 131 result = chooser.chooseName() 132 self.assertEqual( 133 result, '__img-applicant__folder/MY_REG_NO.jpg') 134 return 135 136 def test_name_chooser_check_name(self): 137 # we can check file ids for applicants 138 appl = Applicant() 139 appl.reg_no = u'MY_REG_NO' 140 fake_container = grok.Container() 141 fake_container.__name__ = 'folder' 142 fake_container['appl'] = appl 143 appl.__parent__ = fake_container 144 chooser = IFileStoreNameChooser(appl) 145 result1 = chooser.checkName('foo') 146 result2 = chooser.checkName('__img-applicant__folder/MY_REG_NO.jpg') 147 self.assertEqual(result1, False) 148 self.assertEqual(result2, True) 149 return 150 53 151 class ResultEntryTest(unittest.TestCase): 54 152 … … 87 185 return 88 186 89 def test_passport_no_site(self):90 # make sure we get a real image stored in passport attr91 image = self.applicant.passport.file.read()92 self.assertEqual(len(image), 2059)93 return94 95 def test_passport_insite(self):96 # make sure we get a real image in passport attr when inside a site.97 # When an applicant is created 'inside a site', its passport98 # photograph should be put inside the images folder of the99 # site, even if it is only a placeholder.100 self.getRootFolder()['app'] = University()101 app = self.getRootFolder()['app']102 setSite(app)103 applicant = Applicant()104 image = self.applicant.passport.file.read()105 self.assertEqual(len(image), 2059)106 # The image contains a file_id instead of real image-data107 self.assertEqual(self.applicant.passport.data,108 u'b48a1d39bbcb32e955d9ff2dea4ed0e6-1')109 assert u'b48a1d39bbcb32e955d9ff2dea4ed0e6' in app['images'].keys()110 return111 112 187 class ApplicantFactoryTest(FunctionalTestCase): 113 188 … … 116 191 def setUp(self): 117 192 super(ApplicantFactoryTest, self).setUp() 118 # Install a IFileRetrieval utility that returns WAeUPImageFiles.119 storage = ImageStorageFileRetrieval()120 provideUtility(storage, IFileRetrieval)121 193 self.factory = ApplicantFactory() 122 194 return … … 137 209 implemented_by = self.factory.getInterfaces() 138 210 assert implemented_by.isOrExtends(IApplicant) 139 140 141 class ApplicantTraverserTest(FunctionalTestCase):142 143 layer = FunctionalLayer144 145 def setUp(self):146 super(ApplicantTraverserTest, self).setUp()147 provideAdapter(FakeImageLocation)148 self.applicant = Applicant()149 150 self.request = TestRequest('')151 return152 153 def tearDown(self):154 gsm = getGlobalSiteManager()155 gsm.unregisterAdapter(FakeImageLocation)156 super(ApplicantTraverserTest, self).tearDown()157 return158 159 def test_traverse_wo_passport(self):160 # Ask for some attribute not provided161 traverser = ApplicantTraverser(162 self.applicant, self.request163 )164 self.assertRaises(165 NotFound,166 traverser.publishTraverse, self.request, 'passport'167 )168 return169 170 def test_traverse_wo_image_passport_jpg(self):171 # Ask for applicant pic if we didn't provided one172 # We get a placeholder.173 traverser = ApplicantTraverser(174 self.applicant, self.request175 )176 result = traverser.publishTraverse(self.request, 'passport.jpg')177 self.assertTrue(isinstance(result, FakeImageLocation))178 return179 180 def test_traverse_w_image_passport_jpg(self):181 # Ask for applicant pic that's named 'passport.jpg'182 traverser = ApplicantTraverser(183 self.applicant, self.request184 )185 self.applicant.passport = createWAeUPImageFile(186 'nofile.jpg', StringIO('no-content'))187 self.applicant.passport.filename = 'mypic.jpg'188 result = traverser.publishTraverse(self.request, 'passport.jpg')189 self.assertTrue(isinstance(result, FakeImageLocation))190 return191 192 def test_traverse_w_image_some_jpg(self):193 # Ask for applicant pic by correct name194 traverser = ApplicantTraverser(195 self.applicant, self.request196 )197 self.applicant.passport = WAeUPImageFile('nofile.jpg', '')198 self.applicant.passport.filename = 'mypic.jpg'199 result = traverser.publishTraverse(self.request, 'mypic.jpg')200 self.assertTrue(isinstance(result, FakeImageLocation))201 return -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_authentication.py
r6730 r7063 24 24 import unittest 25 25 from zope.authentication.interfaces import IAuthentication 26 from zope.component import provideAdapter, getUtility 26 from zope.component import provideAdapter, getUtility, queryUtility 27 27 from zope.component.hooks import setSite 28 28 from zope.interface import verify … … 35 35 from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase 36 36 from waeup.sirp.app import University 37 from waeup.sirp.interfaces import IAuthPluginUtility 37 38 from waeup.sirp.applicants import ApplicantsContainer, Applicant 38 39 from waeup.sirp.applicants.authentication import ( 39 40 ApplicantsAuthenticatorPlugin, WAeUPApplicantCredentialsPlugin, 40 41 ApplicantCredentials, AuthenticatedApplicantPrincipalFactory, 41 ApplicantPrincipalInfo, ApplicantPrincipal, )42 ApplicantPrincipalInfo, ApplicantPrincipal, ApplicantsAuthUtility) 42 43 43 44 … … 367 368 return 368 369 369 def test_suite(): 370 suite = unittest.TestSuite() 371 for testcase in [ 372 AuthenticatorPluginTest, CredentialsPluginTest, 373 ApplicantCredentialsTest, PrincipalFactoryTest, 374 PAUSetupTest, 375 ]: 376 suite.addTest(unittest.TestLoader().loadTestsFromTestCase( 377 testcase 378 ) 379 ) 380 return suite 370 class FakePAU(object): 371 credentialsPlugins = () 372 authenticatorPlugins = () 373 374 class ApplicantsAuthUtilityTests(FunctionalTestCase): 375 376 layer = FunctionalLayer 377 378 def test_ifaces(self): 379 # make sure we fullfill the interface promises 380 obj = ApplicantsAuthUtility() 381 verify.verifyClass(IAuthPluginUtility, ApplicantsAuthUtility) 382 verify.verifyObject(IAuthPluginUtility, obj) 383 return 384 385 def test_utility_available(self): 386 # we can get an applicant auth utility by lookup 387 util = queryUtility(IAuthPluginUtility, 388 name='applicants_auth_setup') 389 self.assertTrue(util is not None) 390 return 391 392 def test_register(self): 393 # make sure we can register additional components 394 pau = FakePAU() 395 result = ApplicantsAuthUtility().register(pau) 396 self.assertEqual( 397 result.credentialsPlugins, ('applicant_credentials',)) 398 self.assertEqual( 399 result.authenticatorPlugins, ('applicants',)) 400 return 401 402 def test_unregister(self): 403 # make sure we can unregister applicant auth components 404 pau = FakePAU() 405 util = ApplicantsAuthUtility() 406 pau = util.register(pau) 407 result = util.unregister(pau) 408 self.assertEqual( 409 result.credentialsPlugins, ()) 410 self.assertEqual( 411 result.authenticatorPlugins, ()) 412 return -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py
r6816 r7063 28 28 from datetime import datetime, date, timedelta 29 29 from mechanize import LinkNotFoundError 30 from zope.component import createObject 30 from zope.component import createObject, getUtility 31 31 from zope.component.hooks import setSite, clearSite 32 32 from zope.security.interfaces import Unauthorized … … 37 37 from waeup.sirp.applicants.container import ApplicantsContainer 38 38 from waeup.sirp.applicants.applicant import Applicant 39 from waeup.sirp.interfaces import IExtFileStore, IFileStoreNameChooser 39 40 from waeup.sirp.university.faculty import Faculty 40 41 from waeup.sirp.university.department import Department … … 350 351 self.browser.getControl(name="SUBMIT").click() 351 352 pin = self.pins[2] 352 appl = self.getRootFolder()['app']['applicants']['app2009']353 appl = appl[pin]354 passp = appl.passport355 passp_len = len(passp.file.read())356 self.assertEqual(passp_len, PH_LEN)353 #appl = self.getRootFolder()['app']['applicants']['app2009'] 354 #appl = appl[pin] 355 #passp = appl.passport 356 #passp_len = len(passp.file.read()) 357 #self.assertEqual(passp_len, PH_LEN) 357 358 #image_url = "%s/%s" % (self.browser.url, 'placeholder.jpg') 358 359 image_url = "%s/%s" % (self.browser.url, 'passport.jpg') … … 639 640 # There is a correct <img> link included 640 641 self.assertTrue( 641 '<img src="p laceholder_m.jpg" />' in self.browser.contents)642 '<img src="passport.jpg" />' in self.browser.contents) 642 643 # Browsing the link shows a real image 643 self.browser.open(self.image_url('p laceholder_m.jpg'))644 self.browser.open(self.image_url('passport.jpg')) 644 645 self.assertEqual( 645 646 self.browser.headers['content-type'], 'image/jpeg') 646 647 self.assertEqual(len(self.browser.contents), PH_LEN) 647 648 648 def test_after_login_default_stored_in_imagestorage(self):649 def DISABLEDtest_after_login_default_stored_in_imagestorage(self): 649 650 # After login the applicants placeholder image is stored in 650 651 # an imagestorage … … 672 673 # There is a correct <img> link included 673 674 self.assertTrue( 674 '<img src="p laceholder_m.jpg" />' in self.browser.contents)675 '<img src="passport.jpg" />' in self.browser.contents) 675 676 # Browsing the link shows a real image 676 self.browser.open(self.image_url('p laceholder_m.jpg'))677 self.browser.open(self.image_url('passport.jpg')) 677 678 self.assertEqual( 678 679 self.browser.headers['content-type'], 'image/jpeg') 679 680 self.assertEqual(len(self.browser.contents), PH_LEN) 680 681 def test_after_submit_default_stored_in_imagestorage(self):682 # After submitting an applicant form the default image is683 # correctly stored in an imagestorage684 self.login()685 self.browser.getControl("Save").click() # submit form686 storage = self.app['images']687 self.assertEqual(688 [x for x in storage.keys()],689 [u'b48a1d39bbcb32e955d9ff2dea4ed0e6'])690 file_id = self.applicant.passport.data691 self.assertEqual(692 file_id, u'b48a1d39bbcb32e955d9ff2dea4ed0e6-1')693 # The stored image can be fetched694 fd = storage.retrieveFile(file_id)695 file_len = len(fd.read())696 self.assertEqual(file_len, PH_LEN)697 681 698 682 def test_uploaded_image_browsable_w_errors(self): … … 709 693 # There is a correct <img> link included 710 694 self.assertTrue( 711 '<img src=" myphoto.jpg" />' in self.browser.contents)695 '<img src="passport.jpg" />' in self.browser.contents) 712 696 # Browsing the link shows a real image 713 self.browser.open(self.image_url(' myphoto.jpg'))697 self.browser.open(self.image_url('passport.jpg')) 714 698 self.assertEqual( 715 699 self.browser.headers['content-type'], 'image/jpeg') … … 726 710 file_ctrl.add_file(pseudo_image, filename='myphoto.jpg') 727 711 self.browser.getControl("Save").click() # submit form 728 storage = self.app['images'] 729 self.assertTrue( 730 u'18e57c7eac6ca7fb15b54b5b2bd4106d' in storage.keys()) 731 # The stored image can be fetched 732 fd = storage.retrieveFile(u'18e57c7eac6ca7fb15b54b5b2bd4106d-1') 733 file_len = len(fd.read()) 734 self.assertEqual(file_len, 31) 735 # The image uploaded belongs to the applicant 736 file_id = self.applicant.passport.data 712 storage = getUtility(IExtFileStore) 713 file_id = IFileStoreNameChooser(self.applicant).chooseName() 714 pseudo_image.seek(0) # reset our file data source 737 715 self.assertEqual( 738 file_id, u'18e57c7eac6ca7fb15b54b5b2bd4106d-1') 716 storage.getFile(file_id).read(), pseudo_image.read()) 717 return 739 718 740 719 def test_uploaded_image_browsable_wo_errors(self): … … 751 730 # There is a correct <img> link included 752 731 self.assertTrue( 753 '<img src=" myphoto.jpg" />' in self.browser.contents)732 '<img src="passport.jpg" />' in self.browser.contents) 754 733 # Browsing the link shows a real image 755 self.browser.open(self.image_url(' myphoto.jpg'))734 self.browser.open(self.image_url('passport.jpg')) 756 735 self.assertEqual( 757 736 self.browser.headers['content-type'], 'image/jpeg') … … 769 748 file_ctrl.add_file(pseudo_image, filename='myphoto.jpg') 770 749 self.browser.getControl("Save").click() # submit form 771 storage = self.app['images'] 772 self.assertTrue( 773 u'18e57c7eac6ca7fb15b54b5b2bd4106d' in storage.keys()) 750 storage = getUtility(IExtFileStore) 751 file_id = IFileStoreNameChooser(self.applicant).chooseName() 774 752 # The stored image can be fetched 775 fd = storage.retrieveFile(u'18e57c7eac6ca7fb15b54b5b2bd4106d-1') 776 #fd = storage.retrieveFile(file_id) 753 fd = storage.getFile(file_id) 777 754 file_len = len(fd.read()) 778 755 self.assertEqual(file_len, 31) 779 # The image uploaded belongs to the applicant780 file_id = self.applicant.passport.data781 self.assertEqual(782 file_id, u'18e57c7eac6ca7fb15b54b5b2bd4106d-1')783 756 784 757 def test_uploaded_images_equal(self): … … 797 770 file_ctrl = ctrl.mech_control 798 771 file_ctrl.add_file(pseudo_image, filename='myphoto.jpg') 799 passport0 = self.applicant.passport 772 file_id = IFileStoreNameChooser(self.applicant).chooseName() 773 setSite(self.app) 774 passport0 = getUtility(IExtFileStore).getFile(file_id) 800 775 self.browser.getControl("Save").click() # submit form with changed pic 801 passport1 = self.applicant.passport776 passport1 = getUtility(IExtFileStore).getFile(file_id).read() 802 777 self.browser.getControl("Save").click() # submit form w/o changes 803 passport2 = self.applicant.passport 778 passport2 = getUtility(IExtFileStore).getFile(file_id).read() 779 self.assertTrue(passport0 is None) 804 780 self.assertTrue(passport0 != passport1) 805 781 self.assertTrue(passport1 == passport2) 806 self.assertTrue(passport1 is passport2)807 782 return 808 783 -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_interfaces.py
r6340 r7063 25 25 import unittest 26 26 from zc.sourcefactory.browser.source import FactoredTerms 27 from zope.browser.interfaces import ITerms28 from zope.component import getMultiAdapter29 from zope.interface.verify import verifyClass, verifyObject30 27 from zope.publisher.browser import TestRequest 31 from waeup.sirp.applicants import interfaces 32 from waeup.sirp.applicants.vocabularies import ( 33 APPLICATION_TYPES, application_types_vocab, GenderSource, 34 ) 28 from waeup.sirp.applicants.vocabularies import application_types_vocab 29 from waeup.sirp.students.vocabularies import GenderSource 35 30 36 31 class ApplicationCategoriesTestCase(unittest.TestCase): -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_root.py
r6659 r7063 39 39 ) 40 40 from waeup.sirp.testing import ( 41 FunctionalLayer, FunctionalTestCase, get_all_loggers, remove_new_loggers, 42 remove_logger) 41 FunctionalLayer, FunctionalTestCase, remove_logger) 43 42 44 43 -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/vocabularies.py
r6744 r7063 1 1 """Vocabularies and sources for the application section. 2 2 """ 3 from datetime import datetime4 3 from zope.component import getUtility 5 4 from zope.catalog.interfaces import ICatalog 6 from zc.sourcefactory.basic import BasicSourceFactory7 from zc.sourcefactory.contextual import BasicContextualSourceFactory8 5 from waeup.sirp.interfaces import SimpleWAeUPVocabulary 9 from waeup.sirp.students.lgas import LGAS 10 from waeup.sirp.students.vocabularies import ( 11 CertificateSource, GenderSource) 6 from waeup.sirp.students.vocabularies import CertificateSource 12 7 13 8 #: Types of applications we support. -
main/waeup.sirp/trunk/src/waeup/sirp/applicants/workflow.py
r6644 r7063 2 2 """ 3 3 import grok 4 from datetime import datetime5 4 from hurry.workflow.workflow import Transition, WorkflowState, NullCondition 6 5 from hurry.workflow.interfaces import IWorkflowState, IWorkflowTransitionEvent -
main/waeup.sirp/trunk/src/waeup/sirp/browser/browser.txt
r6917 r7063 1064 1064 1065 1065 >>> sorted(os.listdir(uploadpath)) 1066 ['finished', 'logs', ' unfinished']1066 ['finished', 'logs', 'media', 'unfinished'] 1067 1067 1068 1068 … … 1105 1105 1106 1106 >>> sorted(os.listdir(uploadpath)) 1107 ['finished', 'logs', 'm yfaculties_zope.mgr.csv', 'unfinished']1107 ['finished', 'logs', 'media', 'myfaculties_zope.mgr.csv', 'unfinished'] 1108 1108 1109 1109 We create and upload also a CSV file containing departments: -
main/waeup.sirp/trunk/src/waeup/sirp/browser/resources.py
r6894 r7063 38 38 #: 39 39 #: * In the `update()` method of the responsible view/page/form 40 #: require the JavaScript code to be rendered into the page: 40 #: require the JavaScript code to be rendered into the page:: 41 41 #: 42 42 #: from waeup.sirp.browser.resources import datepicker 43 #: ...44 #: class MyPage(...): 45 #: ...43 #: # ... 44 #: class MyPage(...): 45 #: # ... 46 46 #: def update(self): 47 47 #: datepicker.need() … … 51 51 #: 52 52 #: * In your HTML code add some ``<input>`` or ``<div>`` tag with 53 #: ``class`` set to ``datepicker``, like this:: 53 #: ``class`` set to ``datepicker``, like this: 54 #: 55 #: .. code-block:: html 54 56 #: 55 57 #: <input type="text" name="entrydate" class="datepicker" /> … … 85 87 #: ``"datepicker-de-year"`` 86 88 #: same but also adds a select field for year 87 #: 89 #: 88 90 #: ``"datepicker-us"`` 89 91 #: renders date in format ``MM/DD/YYYY`` (U.S. format) … … 97 99 #: If you want generate alert or confirm messages then you have to perform 98 100 #: two steps: 99 #: * In the `update()` method of the responsible view/page/form 100 #: require the JavaScript code to be rendered into the page: 101 #: 102 #: * In the `update()` method of the responsible view/page/form 103 #: require the JavaScript code to be rendered into the page:: 101 104 #: 102 105 #: from waeup.sirp.browser.resources import datepicker 103 #: ...104 #: class MyPage(...): 105 #: ...106 #: # ... 107 #: class MyPage(...): 108 #: # ... 106 109 #: def update(self): 107 110 #: warning.need() … … 109 112 #: This way all required JavaScripts will be rendered correctly 110 113 #: into the HTML page generated. 111 #: 114 #: 112 115 #: In your HTML code add some <input> or <a> tag with onClick event: 113 116 #: 114 #: <input type="submit" name="xyz" value="abc"/> 115 #: onclick="return confirmPost('Are you sure?')"> 117 #: .. code-block:: html 118 #: 119 #: <input type="submit" name="xyz" value="abc"/> 120 #: onclick="return confirmPost('Are you sure?')"> 116 121 warning = ResourceInclusion(waeup_sirp, 'warning.js') 117 122 118 123 #: If you have many select boxes in a from which have to be selected at the same 119 # time you have to perform120 #: two steps:121 #: * In the `update()` method of the responsible view/page/form 122 #: require the JavaScript code to be rendered into the page: 124 #: time you have to perform two steps: 125 #: 126 #: * In the `update()` method of the responsible view/page/form 127 #: require the JavaScript code to be rendered into the page:: 123 128 #: 124 129 #: from waeup.sirp.browser.resources import toggleall 125 #: ...126 #: class MyPage(...): 127 #: ...130 #: # ... 131 #: class MyPage(...): 132 #: # ... 128 133 #: def update(self): 129 134 #: toggleall.need() … … 134 139 #: In your HTML code add some <input> tag with onClick event: 135 140 #: 136 #: <input type="checkbox" onClick="toggle(this, 'entries')" /> 137 141 #: .. code-block:: html 142 #: 143 #: <input type="checkbox" onClick="toggle(this, 'entries')" /> 138 144 toggleall = ResourceInclusion(waeup_sirp, 'toggleall.js') 139 145 … … 141 147 #: ``tabs``. 142 148 #: 143 #:144 149 #: If you want jQuery tabs for some <div>s in your page then you 145 150 #: have to perform two steps: 146 151 #: 147 152 #: * In the `update()` method of the responsible view/page/form 148 #: require the JavaScript code to be rendered into the page: 153 #: require the JavaScript code to be rendered into the page:: 149 154 #: 150 155 #: from waeup.sirp.browser.resources import tabs 151 #: ...152 #: class MyPage(...): 153 #: ...156 #: # ... 157 #: class MyPage(...): 158 #: # ... 154 159 #: def update(self): 155 160 #: tabs.need() … … 159 164 #: 160 165 #: * In your HTML code add some ``<div>`` tag with ``class`` set to 161 # ``tabs``, like this:: 162 #: 166 #: ``tabs``, like this: 167 #: 168 #: .. code-block:: html 169 #: 163 170 #: <div id="tabs"> 164 171 #: <ul> … … 195 202 #: A stylesheet for datatables 196 203 datatables_css = ResourceInclusion(waeup_sirp, 'datatables.css') 197 204 198 205 #: A resource that turns HTML tables into sortable, searchable and : 199 206 #: multi-page widgets using jQuery and the dataTables plugin available … … 204 211 #: 205 212 #: * In the `update()` method of the responsible view/page/form 206 #: require the JavaScript code to be rendered into the page: 213 #: require the JavaScript code to be rendered into the page:: 207 214 #: 208 215 #: from waeup.sirp.browser.resources import datatable 209 #: ...210 #: class MyPage(...): 211 #: ...216 #: # ... 217 #: class MyPage(...): 218 #: # ... 212 219 #: def update(self): 213 220 #: datatable.need() … … 217 224 #: 218 225 #: * Assign a ``display dataTable`` class to the HTML table you want to tune: 219 #: 226 #: 227 #: .. code-block:: html 228 #: 220 229 #: <table class="display dataTable"> 221 230 #: <thead> … … 246 255 #: Register basic YUI based CSS as a resource. 247 256 reset_fonts_grids = ResourceInclusion( 248 waeup_sirp, 'yuirfg.css') 249 257 waeup_sirp, 'yuirfg.css') 258 250 259 yuiapp_css = ResourceInclusion( 251 260 waeup_sirp, 'yuiapp.css', 252 depends=[reset_fonts_grids]) 261 depends=[reset_fonts_grids]) 253 262 254 263 #: Register basic WAeUP base CSS (which is based on ``yuiapp.css`` as a -
main/waeup.sirp/trunk/src/waeup/sirp/browser/theming.py
r6894 r7063 88 88 A named global utility implementing 89 89 90 :class:`zope.schema.interfaces.IVocabularyFactory 90 :class:`zope.schema.interfaces.IVocabularyFactory` 91 91 92 92 and registered under the name 93 93 94 94 'waeup.sirp.browser.theming.ThemesVocabulary' 95 95 96 96 Interface fields that wish to provide a list of available themes 97 97 can require a 'named vocabulary', i.e. set: … … 107 107 grok.implements(IVocabularyFactory) 108 108 grok.name('waeup.sirp.browser.theming.ThemesVocabulary') 109 109 110 110 def __call__(self, context): 111 111 """Deliver a vocabulary of available themes. -
main/waeup.sirp/trunk/src/waeup/sirp/imagestorage.py
r6980 r7063 21 21 ## 22 22 """A storage for image files. 23 24 A few words about storing files with ``waeup.sirp``. The need for this 25 feature arised initially from the need to store passport files for 26 applicants and students. These files are dynamic (can be changed 27 anytime), mean a lot of traffic and cost a lot of memory/disk space. 28 29 **Design Basics** 30 31 While one *can* store images and similar 'large binary objects' aka 32 blobs in the ZODB, this approach quickly becomes cumbersome and 33 difficult to understand. The worst approach here would be to store 34 images as regular byte-stream objects. ZODB supports this but 35 obviously access is slow (data must be looked up in the one 36 ``Data.fs`` file, each file has to be sent to the ZEO server and back, 37 etc.). 38 39 A bit less worse is the approach to store images in the ZODB but as 40 Blobs. ZODB supports storing blobs in separate files in order to 41 accelerate lookup/retrieval of these files. The files, however, have 42 to be sent to the ZEO server (and back on lookups) which means a 43 bottleneck and will easily result in an increased number of 44 ``ConflictErrors`` even on simple reads. 45 46 The advantage of both ZODB-geared approaches is, of course, complete 47 database consistency. ZODB will guarantee that your files are 48 available under some object name and can be handled as any other 49 Python object. 50 51 Another approach is to leave the ZODB behind and to store images and 52 other files in filesystem directly. This is faster (no ZEO contacts, 53 etc.), reduces probability of `ConflictErrors`, keeps the ZODB 54 smaller, and enables direct access (over filesystem) to the 55 files. Furthermore steps might be better understandable for 56 third-party developers. We opted for this last option. 57 58 **External File Store** 59 60 Our implementation for storing-files-API is defined in 61 :class:`ExtFileStore`. An instance of this file storage (which is also 62 able to store non-image files) is available at runtime as a global 63 utility implementing :class:`waeup.sirp.interfaces.IExtFileStore`. 64 65 The main task of this central component is to maintain a filesystem 66 root path for all files to be stored. It also provides methods to 67 store/get files under certain file ids which identify certain files 68 locally. 69 70 So, to store a file away, you can do something like this: 71 72 >>> from StringIO import StringIO 73 >>> from zope.component import getUtility 74 >>> from waeup.sirp.interfaces import IExtFileStore 75 >>> store = getUtility(IExtFileStore) 76 >>> store.createFile('myfile.txt', StringIO('some file content')) 77 78 All you need is a filename and the file-like object containing the 79 real file data. 80 81 This will store the file somewhere (you shouldn't make too much 82 assumptions about the real filesystem path here). 83 84 Later, we can get the file back like this: 85 86 >>> store.getFile('myfile.txt') 87 <open file ...> 88 89 What we get back is a file or file-like object already opened for 90 reading: 91 92 >>> store.getFile('myfile.txt').read() 93 'some file content' 94 95 **Handlers: Special Places for Special Files** 96 97 The file store supports special handling for certain files. For 98 example we want applicant images to be stored in a different directory 99 than student images, etc. Because the file store cannot know all 100 details about these special tratment of certain files, it looks up 101 helpers (handlers) to provide the information it needs for really 102 storing the files at the correct location. 103 104 That a file stored in filestore needs special handling can be 105 indicated by special filenames. These filenames start with a marker like 106 this:: 107 108 __<MARKER-STRING>__real-filename.jpg 109 110 Please note the double underscores before and after the marker 111 string. They indicate that all in between is a marker. 112 113 If you store a file in file store with such a filename (we call this a 114 `file_id` to distuingish it from real world filenames), the file store 115 will look up a handler for ``<MARKER-STRING>`` and pass it the file to 116 store. The handler then will return the internal path to store the 117 file and possibly do additional things as well like validating the 118 file or similar. 119 120 Examples for such a file store handler can be found in the 121 :mod:`waeup.sirp.applicants.applicant` module. Please see also the 122 :class:`DefaultFileStoreHandler` class below for more details. 123 124 The file store looks up handlers by utility lookups: it looks for a 125 named utiliy providing 126 :class:`waeup.sirp.interfaces.IFileStoreHandler` and named like the 127 marker string (without leading/trailing underscores) in lower 128 case. For example if the file id would be 129 130 ``__IMG_USER__manfred.jpg`` 131 132 then the looked up utility should be registered under name 133 134 ``img_user`` 135 136 and provide :class:`waeup.sirp.interfaces.IFileStoreHandler`. If no 137 such utility can be found, a default handler is used instead 138 (see :class:`DefaultFileStoreHandler`). 139 140 **Context Adapters: Knowing Your Family** 141 142 Often the internal filename or file id of a file depends on a 143 context. For example when we store passport photographs of applicants, 144 then each image belongs to a certain applicant instance. It is not 145 difficult to maintain such a connection manually: Say every applicant 146 had an id, then we could put this id into the filename as well and 147 would build the filename to store/get the connected file by using that 148 filename. You then would create filenames of a format like this:: 149 150 __<MARKER-STRING>__applicant0001.jpg 151 152 where ``applicant0001`` would tell exactly which applicant you can see 153 on the photograph. You notice that the internal file id might have 154 nothing to do with once uploaded filenames. The id above could have 155 been uploaded with filename ``manfred.jpg`` but with the new file id 156 we are able to find the file again later. 157 158 Unfortunately it might soon get boring or cumbersome to retype this 159 building of filenames for a certain type of context, especially if 160 your filenames take more of the context into account than only a 161 simple id. 162 163 Therefore you can define filename building for a context as an adapter 164 that then could be looked up by other components simply by doing 165 something like: 166 167 >>> from waeup.sirp.interfaces import IFileStoreNameChooser 168 >>> file_id = IFileStoreNameChooser(my_context_obj) 169 170 If you later want to change the way file ids are created from a 171 certain context, you only have to change the adapter implementation 172 accordingly. 173 174 Note, that this is only a convenience component. You don't have to 175 define context adapters but it makes things easier for others if you 176 do, as you don't have to remember the exact file id creation method 177 all the time and can change things quick and in only one location if 178 you need to do so. 179 180 Please see the :class:`FileStoreNameChooser` default implementation 181 below for details. 182 23 183 """ 24 184 import grok 25 import hashlib26 185 import os 27 import transaction 28 import warnings 29 from StringIO import StringIO 30 from ZODB.blob import Blob 31 from persistent import Persistent 186 import tempfile 187 from hurry.file import HurryFile 32 188 from hurry.file.interfaces import IFileRetrieval 33 from waeup.sirp.image import WAeUPImageFile 34 from waeup.sirp.utils.helpers import cmp_files 35 36 def md5digest(fd): 37 """Get an MD5 hexdigest for the file stored in `fd`. 38 39 `fd` 40 a file object open for reading. 41 189 from zope.component import queryUtility 190 from zope.interface import Interface 191 from waeup.sirp.interfaces import ( 192 IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,) 193 194 class FileStoreNameChooser(grok.Adapter): 195 """Default file store name chooser. 196 197 File store name choosers pick a file id, a string, for a certain 198 context object. They are normally registered as adapters for a 199 certain content type and know how to build the file id for this 200 special type of context. 201 202 Provides the :class:`waeup.sirp.interfaces.IFileStoreNameChooser` 203 interface. 204 205 This default file name chosser accepts almost every name as long 206 as it is a string or unicode object. 42 207 """ 43 return hashlib.md5(fd.read()).hexdigest() 44 45 class Basket(grok.Container): 46 """A basket holds a set of image files with same hash. 208 grok.context(Interface) 209 grok.implements(IFileStoreNameChooser) 210 211 def checkName(self, name): 212 """Check whether an object name is valid. 213 214 Raises a user error if the name is not valid. 215 216 For the default file store name chooser any name is valid. 217 """ 218 if isinstance(name, basestring): 219 return True 220 return False 221 222 def chooseName(self, name): 223 """Choose a unique valid name for the object. 224 225 The given name and object may be taken into account when 226 choosing the name. 227 228 chooseName is expected to always choose a valid name (that 229 would pass the checkName test) and never raise an error. 230 231 For this default name chooser we return the given name if it 232 is valid or ``unknown_file`` else. 233 """ 234 if self.checkName(name): 235 return name 236 return u'unknown_file' 237 238 class ExtFileStore(object): 239 """External file store. 240 241 External file stores are meant to store files 'externally' of the 242 ZODB, i.e. in filesystem. 243 244 Most important attribute of the external file store is the `root` 245 path which gives the path to the location where files will be 246 stored within. 247 248 By default `root` is a ``'media/'`` directory in the root of the 249 datacenter root of a site. 250 251 The `root` attribute is 'read-only' because you normally don't 252 want to change this path -- it is dynamic. That means, if you call 253 the file store from 'within' a site, the root path will be located 254 inside this site (a :class:`waeup.sirp.University` instance). If 255 you call it from 'outside' a site some temporary dir (always the 256 same during lifetime of the file store instance) will be used. The 257 term 'temporary' tells what you can expect from this path 258 persistence-wise. 259 260 If you insist, you can pass a root path on initialization to the 261 constructor but when calling from within a site afterwards, the 262 site will override your setting for security measures. This way 263 you can safely use one file store for different sites in a Zope 264 instance simultanously and files from one site won't show up in 265 another. 266 267 An ExtFileStore instance is available as a global utility 268 implementing :class:`waeup.sirp.interfaces.IExtFileStore`. 269 270 To add and retrieve files from the storage, use the appropriate 271 methods below. 47 272 """ 48 273 49 def _del(self): 50 """Remove temporary files associated with local blobs. 51 52 A basket holds files as Blob objects. Unfortunately, if a 53 basket was not committed (put into ZODB), those blobs linger 54 around as real files in some temporary directory and won't be 55 removed. 56 57 This is a helper function to remove all those uncommitted 58 blobs that has to be called explicitly, for instance in tests. 59 """ 60 key_list = list(self.keys()) 61 for key in key_list: 62 item = self[key] 63 if getattr(item, '_p_oid', None): 64 # Don't mess around with blobs in ZODB 65 continue 66 fd = item.open('r') 67 name = getattr(fd, 'name', None) 68 fd.close() 69 if name is not None and os.path.exists(name): 70 os.unlink(name) 71 del self[key] 274 grok.implements(IExtFileStore) 275 276 _root = None 277 278 @property 279 def root(self): 280 """Root dir of this storage. 281 282 The root dir is a readonly value determined dynamically. It 283 holds media files for sites or other components. 284 285 If a site is available we return a ``media/`` dir in the 286 datacenter storage dir. 287 288 Otherwise we create a temporary dir which will be remembered 289 on next call. 290 291 If a site exists and has a datacenter, it has always 292 precedence over temporary dirs, also after a temporary 293 directory was created. 294 295 Please note that retrieving `root` is expensive. You might 296 want to store a copy once retrieved in order to minimize the 297 number of calls to `root`. 298 299 """ 300 site = grok.getSite() 301 if site is not None: 302 root = os.path.join(site['datacenter'].storage, 'media') 303 return root 304 if self._root is None: 305 self._root = tempfile.mkdtemp() 306 return self._root 307 308 def __init__(self, root=None): 309 self._root = root 72 310 return 73 311 74 def getInternalId(self, fd): 75 """Get the basket-internal id for the file stored in `fd`. 76 77 `fd` must be a file open for reading. If an (byte-wise) equal 78 file can be found in the basket, its internal id (basket id) 79 is returned, ``None`` otherwise. 80 """ 81 fd.seek(0) 82 for key, val in self.items(): 83 fd_stored = val.open('r') 84 file_len = os.stat(fd_stored.name)[6] 85 if file_len == 0: 86 # Nasty workaround. Blobs seem to suffer from being emptied 87 # accidentally. 88 site = grok.getSite() 89 if site is not None: 90 site.logger.warn( 91 'Empty Blob detected: %s' % fd_stored.name) 92 warnings.warn("EMPTY BLOB DETECTED: %s" % fd_stored.name) 93 fd_stored.close() 94 val.open('w').write(fd.read()) 95 return key 96 fd_stored.seek(0) 97 if cmp_files(fd, fd_stored): 98 fd_stored.close() 99 return key 100 fd_stored.close() 101 return None 102 103 @property 104 def curr_id(self): 105 """The current basket id. 106 107 An integer number which is not yet in use. If there are 108 already `maxint` entries in the basket, a :exc:`ValueError` is 109 raised. The latter is _highly_ unlikely. It would mean to have 110 more than 2**32 hash collisions, i.e. so many files with the 111 same MD5 sum. 112 """ 113 num = 1 114 while True: 115 if str(num) not in self.keys(): 116 return str(num) 117 num += 1 118 if num <= 0: 119 name = getattr(self, '__name__', None) 120 raise ValueError('Basket full: %s' % name) 121 122 def storeFile(self, fd, filename): 123 """Store the file in `fd` into the basket. 124 125 The file will be stored in a Blob. 126 """ 127 fd.seek(0) 128 internal_id = self.getInternalId(fd) # Moves file pointer! 129 if internal_id is None: 130 internal_id = self.curr_id 131 fd.seek(0) 132 self[internal_id] = Blob() 133 transaction.commit() # Urgently needed to make the Blob 134 # persistent. Took me ages to find 135 # out that solution, which makes some 136 # design flaw in ZODB Blobs likely. 137 self[internal_id].open('w').write(fd.read()) 138 fd.seek(0) 139 self._p_changed = True 140 return internal_id 141 142 def retrieveFile(self, basket_id): 143 """Retrieve a file open for reading with basket id `basket_id`. 144 145 If there is no such id, ``None`` is returned. It is the 146 callers responsibility to close the open file. 147 """ 148 if basket_id in self.keys(): 149 return self[basket_id].open('r') 150 return None 151 152 class ImageStorage(grok.Container): 153 """A container for image files. 312 def getFile(self, file_id): 313 """Get a file stored under file ID `file_id`. 314 315 Returns a file already opened for reading. 316 317 If the file cannot be found ``None`` is returned. 318 319 This methods takes into account registered handlers for any 320 marker put into the file_id. 321 322 .. seealso:: :class:`DefaultFileStoreHandler` 323 """ 324 marker, filename, base, ext = self.extractMarker(file_id) 325 handler = queryUtility(IFileStoreHandler, name=marker, 326 default=DefaultFileStoreHandler()) 327 path = handler.pathFromFileID(self, self.root, file_id) 328 if not os.path.exists(path): 329 return None 330 fd = open(path, 'rb') 331 return fd 332 333 def getFileByContext(self, context): 334 """Get a file for given context. 335 336 Returns a file already opened for reading. 337 338 If the file cannot be found ``None`` is returned. 339 340 This method takes into account registered handlers and file 341 name choosers for context types. 342 343 This is a convenience method that internally calls 344 :meth:`getFile`. 345 346 .. seealso:: :class:`FileStoreNameChooser`, 347 :class:`DefaultFileStoreHandler`. 348 """ 349 file_id = IFileStoreNameChooser(context).chooseName() 350 return self.getFile(file_id) 351 352 def createFile(self, filename, f): 353 """Store a file. 354 """ 355 file_id = filename 356 root = self.root # Calls to self.root are expensive 357 marker, filename, base, ext = self.extractMarker(file_id) 358 handler = queryUtility(IFileStoreHandler, name=marker, 359 default=DefaultFileStoreHandler()) 360 f, path, file_obj = handler.createFile( 361 self, root, file_id, filename, f) 362 dirname = os.path.dirname(path) 363 if not os.path.exists(dirname): 364 os.makedirs(dirname, 0755) 365 open(path, 'wb').write(f.read()) 366 return file_obj 367 368 def extractMarker(self, file_id): 369 """split filename into marker, filename, basename, and extension. 370 371 A marker is a leading part of a string of form 372 ``__MARKERNAME__`` followed by the real filename. This way we 373 can put markers into a filename to request special processing. 374 375 Returns a quadruple 376 377 ``(marker, filename, basename, extension)`` 378 379 where ``marker`` is the marker in lowercase, filename is the 380 complete trailing real filename, ``basename`` is the basename 381 of the filename and ``extension`` the filename extension of 382 the trailing filename. See examples below. 383 384 Example: 385 386 >>> extractMarker('__MaRkEr__sample.jpg') 387 ('marker', 'sample.jpg', 'sample', '.jpg') 388 389 If no marker is contained, we assume the whole string to be a 390 real filename: 391 392 >>> extractMarker('no-marker.txt') 393 ('', 'no-marker.txt', 'no-marker', '.txt') 394 395 Filenames without extension give an empty extension string: 396 397 >>> extractMarker('no-marker') 398 ('', 'no-marker', 'no-marker', '') 399 400 """ 401 if not isinstance(file_id, basestring) or not file_id: 402 return ('', '', '', '') 403 parts = file_id.split('__', 2) 404 marker = '' 405 if len(parts) == 3 and parts[0] == '': 406 marker = parts[1].lower() 407 file_id = parts[2] 408 basename, ext = os.path.splitext(file_id) 409 return (marker, file_id, basename, ext) 410 411 grok.global_utility(ExtFileStore, provides=IExtFileStore) 412 413 class DefaultStorage(ExtFileStore): 414 """Default storage for files. 415 416 Registered globally as utility for 417 :class:`hurry.file.interfaces.IFileRetrieval`. 154 418 """ 155 def _del(self): 156 for basket in self.values(): 157 try: 158 basket._del() 159 except: 160 pass 161 162 def storeFile(self, fd, filename): 163 fd.seek(0) 164 digest = md5digest(fd) 165 fd.seek(0) 166 if not digest in self.keys(): 167 self[digest] = Basket() 168 basket_id = self[digest].storeFile(fd, filename) 169 full_id = "%s-%s" % (digest, basket_id) 170 return full_id 171 172 def retrieveFile(self, file_id): 173 if not '-' in file_id: 174 return None 175 full_id, basket_id = file_id.split('-', 1) 176 if not full_id in self.keys(): 177 return None 178 return self[full_id].retrieveFile(basket_id) 179 180 class ImageStorageFileRetrieval(Persistent): 181 grok.implements(IFileRetrieval) 182 183 def getImageStorage(self): 184 site = grok.getSite() 185 if site is None: 186 return None 187 return site.get('images', None) 188 189 def isImageStorageEnabled(self): 190 site = grok.getSite() 191 if site is None: 192 return False 193 if site.get('images', None) is None: 194 return False 195 return True 196 197 def getFile(self, data): 198 # ImageStorage is disabled, so give fall-back behaviour for 199 # testing without ImageStorage 200 if not self.isImageStorageEnabled(): 201 return StringIO(data) 202 storage = self.getImageStorage() 203 if storage is None: 204 raise ValueError('Cannot find an image storage') 205 result = storage.retrieveFile(data) 206 if result is None: 207 return StringIO(data) 208 return storage.retrieveFile(data) 209 210 def createFile(self, filename, f): 211 if not self.isImageStorageEnabled(): 212 return WAeUPImageFile(filename, f.read()) 213 storage = self.getImageStorage() 214 if storage is None: 215 raise ValueError('Cannot find an image storage') 216 file_id = storage.storeFile(f, filename) 217 return WAeUPImageFile(filename, file_id) 419 grok.provides(IFileRetrieval) 420 421 grok.global_utility(DefaultStorage, provides=IFileRetrieval) 422 423 class DefaultFileStoreHandler(grok.GlobalUtility): 424 """A default handler for external file store. 425 426 This handler is the fallback called by external file stores when 427 there is no or an unknown marker in the file id. 428 429 Registered globally as utility for 430 :class:`waeup.sirp.interfaces.IFileStoreHandler`. 431 """ 432 grok.implements(IFileStoreHandler) 433 434 def pathFromFileID(self, store, root, file_id): 435 """Return the root path of external file store appended by file id. 436 """ 437 return os.path.join(root, file_id) 438 439 def createFile(self, store, root, filename, file_id, f): 440 """Infos about what to store exactly and where. 441 442 When a file should be handled by an external file storage, it 443 looks up any handlers (like this one), passes runtime infos 444 like the storage object, root path, filename, file_id, and the 445 raw file object itself. 446 447 The handler can then change the file, raise exceptions or 448 whatever and return the result. 449 450 This handler returns the input file as-is, a path returned by 451 :meth:`pathFromFileID` and an instance of 452 :class:`hurry.file.HurryFile` for further operations. 453 454 Please note: although a handler has enough infos to store the 455 file itself, it should leave that task to the calling file 456 store. 457 """ 458 path = self.pathFromFileID(store, root, file_id) 459 return f, path, HurryFile(filename, file_id) -
main/waeup.sirp/trunk/src/waeup/sirp/interfaces.py
r7022 r7063 3 3 import os 4 4 from datetime import datetime 5 from hurry.file.interfaces import IFileRetrieval 5 6 from hurry.workflow.interfaces import IWorkflow, IWorkflowInfo 6 7 from zc.sourcefactory.basic import BasicSourceFactory … … 8 9 from zope.component import getUtility 9 10 from zope.component.interfaces import IObjectEvent 11 from zope.container.interfaces import INameChooser 10 12 from zope.interface import Interface, Attribute, implements 11 13 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm … … 591 593 """Unregister a logger. 592 594 """ 595 596 # 597 # External File Storage and relatives 598 # 599 class IFileStoreNameChooser(INameChooser): 600 """See zope.container.interfaces.INameChooser for base methods. 601 """ 602 def checkName(name): 603 """Check whether an object name is valid. 604 605 Raises a user error if the name is not valid. 606 """ 607 608 def chooseName(name): 609 """Choose a unique valid name for the object. 610 611 The given name and object may be taken into account when 612 choosing the name. 613 614 chooseName is expected to always choose a valid name (that would pass 615 the checkName test) and never raise an error. 616 """ 617 618 class IExtFileStore(IFileRetrieval): 619 """A file storage that stores files in filesystem (not as blobs). 620 """ 621 root = schema.TextLine( 622 title = u'Root path of file store.', 623 ) 624 625 def getFile(file_id): 626 """Get raw file data stored under file with `file_id`. 627 628 Returns a file descriptor open for reading or ``None`` if the 629 file cannot be found. 630 """ 631 632 def getFileByContext(context): 633 """Get raw file data stored for the given context. 634 635 Returns a file descriptor open for reading or ``None`` if no 636 such file can be found. 637 638 This is a convenience method. 639 """ 640 641 def createFile(filename, f): 642 """Create file given by f with filename `filename` 643 644 Returns a hurry.file.File-based object. 645 """ 646 647 class IFileStoreHandler(Interface): 648 """Filestore handlers handle specific files for file stores. 649 650 If a file to store/get provides a specific filename, a file store 651 looks up special handlers for that type of file. 652 653 """ 654 def pathFromFileID(store, root, filename): 655 """Turn file id into path to store. 656 657 Returned path should be absolute. 658 """ 659 660 def createFile(store, root, filename, file_id, file): 661 """Return some hurry.file based on `store` and `file_id`. 662 663 Some kind of callback method called by file stores to create 664 file objects from file_id. 665 666 Returns a tuple ``(raw_file, path, file_like_obj)`` where the 667 ``file_like_obj`` should be a HurryFile, a WAeUPImageFile or 668 similar. ``raw_file`` is the (maybe changed) input file and 669 ``path`` the relative internal path to store the file at. 670 671 Please make sure the ``raw_file`` is opened for reading and 672 the file descriptor set at position 0 when returned. 673 674 This method also gets the raw input file object that is about 675 to be stored and is expected to raise any exceptions if some 676 kind of validation or similar fails. 677 """ -
main/waeup.sirp/trunk/src/waeup/sirp/tests/test_app.py
r6593 r7063 6 6 from zope.interface.verify import verifyClass, verifyObject 7 7 from waeup.sirp.app import University 8 from waeup.sirp.imagestorage import ImageStorageFileRetrieval9 8 from waeup.sirp.interfaces import IUniversity 10 9 from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase … … 32 31 return 33 32 34 def test_images(self):35 # Make sure we have a image container in a university36 assert 'images' in self.app.keys()37 return38 39 33 def test_IFileRetrieval_utility(self): 40 34 # Make sure we can get a local IFileRetrieval utility … … 43 37 assert result is not None 44 38 assert IFileRetrieval.providedBy(result) 45 assert isinstance(result, ImageStorageFileRetrieval)46 39 return 47 40 -
main/waeup.sirp/trunk/src/waeup/sirp/tests/test_imagestorage.py
r6529 r7063 1 1 import os 2 import sys3 2 import tempfile 4 3 import shutil 5 4 import unittest 6 5 from StringIO import StringIO 6 from hurry.file import HurryFile 7 7 from hurry.file.interfaces import IFileRetrieval 8 from zope.component import ( 9 getUtility, provideUtility, queryUtility, provideAdapter) 8 10 from zope.component.hooks import setSite 9 11 from zope.interface.verify import verifyClass, verifyObject 10 12 from waeup.sirp.app import University 11 13 from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase 12 from waeup.sirp.image import createWAeUPImageFile13 14 from waeup.sirp.imagestorage import ( 14 md5digest, Basket, ImageStorage, ImageStorageFileRetrieval,) 15 FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler, 16 DefaultStorage) 17 from waeup.sirp.interfaces import ( 18 IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,) 15 19 16 20 class HelperFuncsTests(unittest.TestCase): … … 22 26 shutil.rmtree(self.workdir) 23 27 24 def test_md5digest(self): 25 samplefile = os.path.join(self.workdir, 'sample') 26 open(samplefile, 'wb').write('blah') 27 fp = open(samplefile, 'r') 28 digest = md5digest(fp) 29 self.assertEqual(digest, '6f1ed002ab5595859014ebf0951522d9') 30 31 class BasketTests(FunctionalTestCase): 28 class FileStoreNameChooserTests(FunctionalTestCase): 32 29 33 30 layer = FunctionalLayer 34 31 32 def test_iface(self): 33 # we provide the interfaces we promise to do 34 obj = FileStoreNameChooser(None) 35 verifyClass(IFileStoreNameChooser, FileStoreNameChooser) 36 verifyObject(IFileStoreNameChooser, obj) 37 return 38 39 def test_accessible_as_adapter(self): 40 # we can get a file name chooser via adapter 41 chooser = IFileStoreNameChooser(object()) 42 self.assertTrue( 43 isinstance(chooser, FileStoreNameChooser)) 44 return 45 46 def test_check_name(self): 47 # default file name choosers accept any string 48 chooser = FileStoreNameChooser(object()) 49 self.assertEqual(chooser.checkName('Hi there!'), True) 50 self.assertEqual(chooser.checkName(None), False) 51 return 52 53 def test_choose_name(self): 54 # we get a simple string if we do not pass in a valid string 55 chooser = FileStoreNameChooser(object()) 56 self.assertEqual(chooser.chooseName('myname'), 'myname') 57 self.assertEqual(chooser.chooseName(None), u'unknown_file') 58 return 59 60 class ExtFileStoreTests(unittest.TestCase): 61 # Test external file store (non-functional mode) 62 35 63 def setUp(self): 36 super(BasketTests, self).setUp() 64 self.workdir = tempfile.mkdtemp() 65 self.root = None 66 return 67 68 def tearDown(self): 69 shutil.rmtree(self.workdir) 70 if self.root is not None: 71 shutil.rmtree(self.root) 72 return 73 74 def test_iface(self): 75 obj = ExtFileStore(None) 76 verifyClass(IExtFileStore, ExtFileStore) 77 verifyObject(IExtFileStore, obj) 78 return 79 80 def test_root_setup_wo_site(self): 81 # if no site is available we can use a temporary root 82 fs = ExtFileStore() 83 self.root = fs.root 84 self.assertTrue(isinstance(self.root, basestring)) 85 self.assertTrue(os.path.exists(self.root)) 86 return 87 88 def test_create_instance(self): 89 storage1 = ExtFileStore() 90 storage2 = ExtFileStore(root=self.workdir) 91 self.root = storage1.root 92 self.assertTrue(storage1.root is not None) 93 self.assertTrue(storage1.root != storage2.root) 94 self.assertEqual(storage2.root, self.workdir) 95 return 96 97 def test_create_file(self): 98 # We can store files 99 storage = ExtFileStore(root=self.workdir) 100 dummy_file = StringIO('sample file') 101 image_file = storage.createFile('mysample', dummy_file) 102 self.assertTrue('mysample' in os.listdir(storage.root)) 103 self.assertEqual('mysample', image_file.data) 104 return 105 106 def test_get_file(self): 107 # We can get files after having them stored 108 storage = ExtFileStore(root=self.workdir) 109 dummy_file = StringIO('sample file') 110 image_file = storage.createFile('mysample', dummy_file) 111 result = storage.getFile(image_file.data) 112 self.assertEqual(result.read(), 'sample file') 113 return 114 115 def test_extract_marker(self): 116 # file stores support extracting markers from filenames 117 storage = ExtFileStore(root=self.workdir) 118 result1 = storage.extractMarker(None) 119 result2 = storage.extractMarker('') 120 result3 = storage.extractMarker('no-marker') 121 result4 = storage.extractMarker('no-marker.txt') 122 result5 = storage.extractMarker('__MARKER__foo.jpg') 123 result6 = storage.extractMarker('__MaRkEr__foo.jpg') 124 result7 = storage.extractMarker('__THE_MARKER__foo.jpg') 125 result8 = storage.extractMarker('__A_MARK__my__foo.jpg') 126 127 self.assertEqual(result1, ('', '', '', '')) 128 self.assertEqual(result2, ('', '', '', '')) 129 self.assertEqual(result3, ('', 'no-marker', 'no-marker', '')) 130 self.assertEqual(result4, ('', 'no-marker.txt', 'no-marker', '.txt')) 131 self.assertEqual(result5, ('marker', 'foo.jpg', 'foo', '.jpg')) 132 self.assertEqual(result6, ('marker', 'foo.jpg', 'foo', '.jpg')) 133 self.assertEqual(result7, ('the_marker', 'foo.jpg', 'foo', '.jpg')) 134 self.assertEqual(result8, ('a_mark', 'my__foo.jpg', 'my__foo', '.jpg')) 135 return 136 137 class DefaultFileStoreHandlerTests(unittest.TestCase): 138 139 def test_iface(self): 140 obj = DefaultFileStoreHandler() 141 verifyClass(IFileStoreHandler, DefaultFileStoreHandler) 142 verifyObject(IFileStoreHandler, obj) 143 return 144 145 class CustomizedFileHandler(object): 146 def pathFromFileID(self, store, root, file_id): 147 return os.path.join(root, file_id[12:]) 148 149 def createFile(self, store, root, file_id, filename, f): 150 path = self.pathFromFileID(store, root, file_id) 151 return f, path, HurryFile(filename, file_id) 152 153 class CustomContext(object): 154 pass 155 156 class CustomContextFileChooser(object): 157 def __init__(self, context): 158 self.context = context 159 160 def chooseName(self, name=None): 161 return '__mymarker__mysample.txt' 162 163 class FunctionalExtFileStoreTests(FunctionalTestCase): 164 165 layer = FunctionalLayer 166 167 def setUp(self): 168 super(FunctionalExtFileStoreTests, self).setUp() 37 169 self.workdir = tempfile.mkdtemp() 38 170 self.samplefile = os.path.join(self.workdir, 'sample') … … 40 172 open(self.samplefile, 'wb').write('Hi there!') 41 173 open(self.otherfile, 'wb').write('Hi from other!') 42 self.basket = Basket()43 174 self.fd = open(self.samplefile, 'r') 44 175 self.fd2 = open(self.otherfile, 'r') 45 self.stderr = StringIO() 46 self.old_stderr = sys.stderr 176 self.getRootFolder()['app'] = University() 177 self.app = self.getRootFolder()['app'] 178 self.app['datacenter'].setStoragePath(self.workdir) 179 # register a custom filename mangler 180 provideUtility( 181 CustomizedFileHandler(), IFileStoreHandler, name=u'mymarker') 182 # register a file chooser adapter for CustomContext 183 provideAdapter( 184 CustomContextFileChooser, 185 (CustomContext,), IFileStoreNameChooser) 186 return 187 47 188 48 189 def tearDown(self): 49 sys.stderr = self.old_stderr 50 super(BasketTests, self).tearDown() 51 self.fd.close() 52 shutil.rmtree(self.workdir) 53 self.basket._del() # Remove subojects explicitly 54 del self.basket 55 return 56 57 def test_ifaces(self): 58 pass 59 60 def test_curr_id_empty(self): 61 curr_id = self.basket.curr_id 62 self.assertEqual(curr_id, '1') 63 64 def test_getInternalId_empty(self): 65 basket_id = self.basket.getInternalId(self.fd) 66 self.assertTrue(basket_id is None) 67 68 def test_storeFile_single(self): 69 basket_id = self.basket.storeFile(self.fd, 'sample') 70 self.assertEqual(basket_id, '1') 71 contents = self.basket['1'].open('r').read() 72 self.assertEqual(contents, 'Hi there!') 73 74 def test_storeFile_double(self): 75 basket_id1 = self.basket.storeFile(self.fd, 'sample') 76 basket_id2 = self.basket.storeFile(self.fd, 'sample') 77 self.assertTrue(basket_id1 == basket_id2 == '1') 78 contents = self.basket['1'].open('r').read() 79 self.assertEqual(contents, 'Hi there!') 80 81 def test_storeFile_multiple(self): 82 basket_id1 = self.basket.storeFile(self.fd, 'sample') 83 basket_id2 = self.basket.storeFile(self.fd2, 'sample') 84 self.assertEqual(basket_id1, '1') 85 self.assertEqual(basket_id2, '2') 86 contents1 = self.basket['1'].open('r').read() 87 contents2 = self.basket['2'].open('r').read() 88 self.assertEqual(contents1, 'Hi there!') 89 self.assertEqual(contents2, 'Hi from other!') 90 91 def test_retrieveFile(self): 92 basket_id = self.basket.storeFile(self.fd, 'sample') 93 fd = self.basket.retrieveFile(basket_id) 94 result = fd.read() 95 self.assertEqual(result, 'Hi there!') 96 97 def test_retrieveFile_not_existent(self): 98 result = self.basket.retrieveFile('not-a-valid-basket-id') 99 self.assertTrue(result is None) 100 101 def test_detect_zero_length_blobs(self): 102 # Ensure we get a warning when an empty Blob is found 103 self.basket.storeFile(self.fd, 'sample') 104 self.basket['1'].open('w').write('') 105 self.fd.seek(0) 106 sys.stderr = self.stderr # Redirect stderr 107 self.basket.storeFile(self.fd, 'sample') 108 sys.stderr = self.old_stderr # Restore stderr 109 self.stderr.seek(0) 110 self.assertTrue( 111 "EMPTY BLOB DETECTED" in self.stderr.read()) 112 113 def test_refill_zero_length_blobs(self): 114 # When we detect an empty Blob, it will be reused 115 self.basket.storeFile(self.fd, 'sample') 116 self.basket['1'].open('w').write('') 117 self.fd.seek(0) 118 sys.stderr = self.stderr # Redirect stderr 119 self.basket.storeFile(self.fd, 'sample') 120 sys.stderr = self.old_stderr # Restore stderr 121 contents = self.basket['1'].open('r').read() 122 self.assertEqual(contents, 'Hi there!') 123 124 125 class ImageStorageTests(FunctionalTestCase): 126 127 layer = FunctionalLayer 128 129 def setUp(self): 130 super(ImageStorageTests, self).setUp() 131 self.workdir = tempfile.mkdtemp() 132 self.samplefile = os.path.join(self.workdir, 'sample') 133 self.otherfile = os.path.join(self.workdir, 'other') 134 open(self.samplefile, 'wb').write('Hi there!') 135 open(self.otherfile, 'wb').write('Hi from other!') 136 self.storage = ImageStorage() 137 self.fd = open(self.samplefile, 'r') 138 self.fd2 = open(self.otherfile, 'r') 139 140 def tearDown(self): 141 super(ImageStorageTests, self).tearDown() 190 super(FunctionalExtFileStoreTests, self).tearDown() 142 191 self.fd.close() 143 192 self.fd2.close() 144 193 shutil.rmtree(self.workdir) 145 self.storage._del() # Remove subojects explicitly 146 del self.storage 147 return 148 149 def test_ifaces(self): 150 pass 151 152 def test_storeFile(self): 153 full_id = self.storage.storeFile(self.fd, 'sample.txt') 154 self.assertEqual(full_id, '396199333edbf40ad43e62a1c1397793-1') 155 156 def test_storeFile_duplicate(self): 157 full_id1 = self.storage.storeFile(self.fd, 'sample1.txt') 158 full_id2 = self.storage.storeFile(self.fd, 'sample2.txt') 159 full_id3 = self.storage.storeFile(self.fd, 'sample1.txt') 160 self.assertEqual(full_id1, '396199333edbf40ad43e62a1c1397793-1') 161 self.assertEqual(full_id2, '396199333edbf40ad43e62a1c1397793-1') 162 self.assertEqual(full_id3, '396199333edbf40ad43e62a1c1397793-1') 163 contents = self.storage.retrieveFile( 164 '396199333edbf40ad43e62a1c1397793-1').read() 165 self.assertEqual(contents, 'Hi there!') 166 167 def test_storeFile_multiple(self): 168 full_id1 = self.storage.storeFile(self.fd, 'sample1.txt') 169 full_id2 = self.storage.storeFile(self.fd2, 'sample1.txt') 170 self.assertEqual(full_id1, '396199333edbf40ad43e62a1c1397793-1') 171 self.assertEqual(full_id2, '6936fcf8d564f1c5be5a017e650c5e8f-1') 172 173 def test_retrieveFile_not_existent(self): 174 result = self.storage.retrieveFile('not-existent') 175 self.assertTrue(result is None) 176 177 def test_retrieveFile_illegal_marker(self): 178 result1 = self.storage.retrieveFile('really-not-existent') 179 result2 = self.storage.retrieveFile('notexistent') 180 self.assertTrue(result1 is result2 is None) 181 182 def test_retrieveFile(self): 183 full_id = self.storage.storeFile(self.fd, 'sample.txt') 184 result = self.storage.retrieveFile(full_id) 185 content = result.read() 186 self.assertEqual(content, 'Hi there!') 187 188 def test_retrieveFile_multiple(self): 189 full_id1 = self.storage.storeFile(self.fd, 'sample.txt') 190 full_id2 = self.storage.storeFile(self.fd2, 'other.txt') 191 result1 = self.storage.retrieveFile(full_id1) 192 result2 = self.storage.retrieveFile(full_id2) 193 content1 = result1.read() 194 content2 = result2.read() 195 self.assertEqual(content1, 'Hi there!') 196 self.assertEqual(content2, 'Hi from other!') 197 198 class ImageStorageFileRetrievalTests(FunctionalTestCase): 199 200 layer = FunctionalLayer 201 202 def setUp(self): 203 super(ImageStorageFileRetrievalTests, self).setUp() 204 self.workdir = tempfile.mkdtemp() 205 self.samplefile = os.path.join(self.workdir, 'sample') 206 self.otherfile = os.path.join(self.workdir, 'other') 207 open(self.samplefile, 'wb').write('Hi there!') 208 open(self.otherfile, 'wb').write('Hi from other!') 209 self.storage = ImageStorage() 210 self.fd = open(self.samplefile, 'r') 211 self.fd2 = open(self.otherfile, 'r') 212 # Set up a single image storage in a site 213 self.getRootFolder()['app'] = University() 214 self.app = self.getRootFolder()['app'] 215 if not 'images' in self.app.keys(): 216 self.app['images'] = ImageStorage() 217 self.storage = self.app['images'] 218 return 219 220 221 def tearDown(self): 222 super(ImageStorageFileRetrievalTests, self).tearDown() 223 self.fd.close() 224 self.fd2.close() 225 shutil.rmtree(self.workdir) 226 self.storage._del() # Remove subojects explicitly 227 return 228 229 def test_ifaces(self): 230 retrieval = ImageStorageFileRetrieval() 231 assert verifyClass(IFileRetrieval, ImageStorageFileRetrieval) 232 assert verifyObject(IFileRetrieval, retrieval) 233 return 234 235 def test_getImageStorage_nosite(self): 236 retrieval = ImageStorageFileRetrieval() 237 storage = retrieval.getImageStorage() 238 self.assertTrue(storage is None) 239 return 240 241 def test_getImageStorage(self): 194 return 195 196 def test_root_setup_w_site(self): 197 # if a site is available we use it to determine the root dir 198 fs = ExtFileStore() 242 199 setSite(self.app) 243 retrieval = ImageStorageFileRetrieval() 244 storage = retrieval.getImageStorage() 245 self.assertTrue(storage is self.storage) 246 return 247 248 def test_isImageStorageEnabled_nosite(self): 249 retrieval = ImageStorageFileRetrieval() 250 self.assertTrue(retrieval.isImageStorageEnabled() is False) 251 return 252 253 def test_isImageStorageEnabled(self): 254 setSite(self.app) 255 retrieval = ImageStorageFileRetrieval() 256 self.assertTrue(retrieval.isImageStorageEnabled() is True) 257 return 258 259 def test_getFile_nosite(self): 260 retrieval = ImageStorageFileRetrieval() 261 f = retrieval.getFile('Hi there!') 262 self.assertEqual(f.read(), 'Hi there!') 263 return 264 265 def test_getFile(self): 266 setSite(self.app) 267 retrieval = ImageStorageFileRetrieval() 268 waeup_image = retrieval.createFile('sample.txt', self.fd) 269 full_id = waeup_image.data 270 result = retrieval.getFile(full_id) 271 self.assertEqual(result.read(), 'Hi there!') 272 return 273 274 def test_createFile_nosite(self): 275 retrieval = ImageStorageFileRetrieval() 276 waeup_image = retrieval.createFile('sample.txt', self.fd) 277 self.assertEqual(waeup_image.data, 'Hi there!') 278 return 279 280 def test_createFile(self): 281 # Ensure we can create WAeUPImageFiles when in site 282 setSite(self.app) 283 retrieval = ImageStorageFileRetrieval() 284 waeup_image = retrieval.createFile('sample.txt', self.fd) 285 full_id = waeup_image.data 286 self.assertEqual(full_id, '396199333edbf40ad43e62a1c1397793-1') 287 return 288 289 def test_waeupimagefile(self): 290 # Make sure WAeUPImageFile can use our file retrieval 291 setSite(self.app) 292 myfile = createWAeUPImageFile('sample.jpg', self.fd) 293 contents = myfile.file.read() 294 self.assertEqual(contents, 'Hi there!') 295 296 def test_waeupimagefile_raw(self): 297 # Make sure we can retrieve a file also if it was initialized 298 # with no image storage available 299 myfile = createWAeUPImageFile('sample.jpg', self.fd) 300 setSite(self.app) 301 contents = myfile.file.read() 302 self.assertEqual(contents, 'Hi there!') 200 self.root = fs.root 201 expected_root = os.path.join( 202 self.app['datacenter'].storage, 'media') 203 self.assertTrue(isinstance(self.root, basestring)) 204 self.assertEqual(self.root, expected_root) 205 return 206 207 def test_get_utility(self): 208 # we can get an ExtFileStore by global utility lookup 209 fs1 = getUtility(IExtFileStore) 210 fs2 = getUtility(IExtFileStore) 211 self.assertTrue(isinstance(fs1, ExtFileStore)) 212 self.assertTrue(fs1 is fs2) 213 return 214 215 def test_default_handler_create_file(self): 216 # we can use the default handler to store files 217 fs = ExtFileStore() 218 result = fs.createFile('sample.txt', StringIO('sample text')) 219 self.assertEqual(result.data, 'sample.txt') 220 self.assertTrue('sample.txt' in os.listdir(fs.root)) 221 return 222 223 def test_default_handler_get_file(self): 224 # we can get files stored by the default handler 225 fs = ExtFileStore() 226 fs.createFile('sample.txt', StringIO('sample text')) 227 result1 = fs.getFile('sample.txt') 228 result2 = fs.getFile('not-existent') 229 self.assertEqual(result1.read(), 'sample text') 230 self.assertTrue(result2 is None) 231 return 232 233 def test_customized_handler_create_file(self): 234 # we can use registered filename handlers 235 fs = ExtFileStore() 236 result = fs.createFile( 237 '__MYMARKER__sample.txt', StringIO('sample text')) 238 self.assertEqual(result.data, '__MYMARKER__sample.txt') 239 self.assertTrue('sample.txt' in os.listdir(fs.root)) 240 return 241 242 def test_customized_handler_get_file(self): 243 # we consider registered filename handlers when asking for 244 # stored files. 245 fs = ExtFileStore() 246 fs.createFile('__MYMARKER__sample.txt', StringIO('sample text')) 247 result1 = fs.getFile('__MYMARKER__sample.txt') 248 result2 = fs.getFile('__MYMARKER__not-existent') 249 result3 = fs.getFile('not-existent') 250 self.assertEqual(result1.read(), 'sample text') 251 self.assertTrue(result2 is None) 252 self.assertTrue(result3 is None) 253 return 254 255 def test_get_file_by_context(self): 256 # if we register a file name chooser, we can also get a file 257 # by context 258 fs = ExtFileStore() 259 context = CustomContext() 260 file_id = IFileStoreNameChooser(context).chooseName() 261 fs = ExtFileStore() 262 fs.createFile(file_id, StringIO('my sample')) 263 result = fs.getFileByContext(context) 264 self.assertEqual( 265 file_id, '__mymarker__mysample.txt') 266 self.assertEqual( 267 result.read(), 'my sample') 268 return 269 270 def test_get_default_handler(self): 271 # we can get a default handler 272 result = queryUtility(IFileStoreHandler) 273 self.assertTrue( 274 isinstance(result, DefaultFileStoreHandler)) 275 return 276 277 def test_get_default_file_retrieval(self): 278 # we get a file store when requesting a file retrieval 279 result = queryUtility(IFileRetrieval) 280 self.assertTrue( 281 isinstance(result, DefaultStorage)) -
main/waeup.sirp/trunk/src/waeup/sirp/university/certificatecontainer.py
r6243 r7063 26 26 27 27 Each :class:`CertificateContainer` provides 28 : iface:`ICertificateContainer`.28 :class:`ICertificateContainer`. 29 29 """ 30 30 grok.implements(ICertificateContainer) … … 35 35 36 36 The `certificate` must be an object implementing 37 : iface:`waeup.sirp.university.interfaces.ICertificate`. If37 :class:`waeup.sirp.university.interfaces.ICertificate`. If 38 38 not, a :exc:`TypeError` is raised. 39 39 … … 75 75 76 76 The certificate must be an object implementing 77 : iface:`waeup.sirp.university.interfaces.ICertificate`. If77 :class:`waeup.sirp.university.interfaces.ICertificate`. If 78 78 not, a :exc:`TypeError` is raised. 79 79
Note: See TracChangeset for help on using the changeset viewer.