├── TODO.txt ├── refresh.txt ├── tests ├── __init__.py ├── test-min.css ├── test.css ├── test.js ├── test.min.js ├── image.jpg ├── large.jpg ├── testUtils.py ├── containsimages.css ├── testUKPostcode.py ├── classes.py ├── testBases.py ├── framework.py ├── testInput.py └── testZope.py ├── version.txt ├── Constants.py ├── .gitignore ├── Bases.py ├── unaccent.py ├── __init__.py ├── README.txt ├── fileupload.py ├── Decorators.py ├── split_search.py ├── CHANGES.txt ├── feedcreator.py ├── html2plaintext.py ├── addhrefs.py ├── TemplateAdder.py ├── ukpostcode.py ├── postcodeeverywhere2.py ├── Input.py └── Zope.py /TODO.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /refresh.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 0.22 -------------------------------------------------------------------------------- /Constants.py: -------------------------------------------------------------------------------- 1 | ## 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | .svn 4 | 5 | -------------------------------------------------------------------------------- /tests/test-min.css: -------------------------------------------------------------------------------- 1 | body{font-family: Fun;margin:0px;} 2 | -------------------------------------------------------------------------------- /tests/test.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Fun; 3 | margin: 0px; 4 | } -------------------------------------------------------------------------------- /Bases.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/FriedZopeBase/master/Bases.py -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | function foo (x, y) { 2 | var z = x + y; 3 | return z; 4 | } -------------------------------------------------------------------------------- /unaccent.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/FriedZopeBase/master/unaccent.py -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Not a Zope instanciatiable product so we don't register any classes 2 | -------------------------------------------------------------------------------- /tests/test.min.js: -------------------------------------------------------------------------------- 1 | function add(number1,number2) 2 | {return number1 + number2;} 3 | 4 | -------------------------------------------------------------------------------- /tests/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/FriedZopeBase/master/tests/image.jpg -------------------------------------------------------------------------------- /tests/large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/FriedZopeBase/master/tests/large.jpg -------------------------------------------------------------------------------- /tests/testUtils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/FriedZopeBase/master/tests/testUtils.py -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | FriedZopeBase 2 | ============= 3 | 4 | Overview 5 | -------- 6 | 7 | License: ZPL 8 | Copyright, Peter Bengtsson, peter@fry-it.com, Fry-IT, 2007 9 | 10 | A set of base classes with useful methods attached for Zope2 Product 11 | development. 12 | 13 | 14 | Note 15 | ---- 16 | 17 | This package was developed initially many years ago by Peter 18 | Bengtsson where he put various conventions and useful functions and 19 | scripts into one place. -------------------------------------------------------------------------------- /tests/containsimages.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Fun; 3 | margin: 0px; 4 | background: url("/misc_/FriedZopeBase.tests.testZope.MyProduct/large.jpg"); /* not registered */ 5 | } 6 | 7 | apostrophes { 8 | background: url('/misc_/FriedZopeBase.tests.testZope.MyProduct/image.jpg'); 9 | } 10 | 11 | quotes { 12 | background: url("/misc_/FriedZopeBase.tests.testZope.MyProduct/image.jpg"); 13 | } 14 | 15 | lazy { 16 | background: url(/misc_/FriedZopeBase.tests.testZope.MyProduct/image.jpg); 17 | } -------------------------------------------------------------------------------- /fileupload.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-1 -* 2 | ## 3 | ## FriedZopeBase 4 | ## (c) Fry-IT, www.fry-it.com 5 | ## 6 | ## 7 | import os 8 | 9 | 10 | class NewFileUpload: 11 | """ 12 | This class makes it easy and possible to 'fake' uploading 13 | files from code that doesn't use the ZPublisher. 14 | """ 15 | 16 | def __init__(self, file_path, selfdestruct=True): 17 | self.file = open(file_path, 'rb') 18 | self.filename = os.path.basename(file_path) 19 | self.file_path = file_path 20 | self.selfdestruct = selfdestruct 21 | 22 | def read(self, bytes=None): 23 | import stat 24 | print os.stat(self.file_path)[stat.ST_SIZE] 25 | if bytes: 26 | return self.file.read(bytes) 27 | else: 28 | return self.file.read() 29 | 30 | def seek(self, offset, whence=0): 31 | self.file.seek(offset, whence) 32 | 33 | def __del__(self): 34 | if self.selfdestruct: 35 | if os.path.isfile(self.file_path): 36 | os.remove(self.file_path) 37 | 38 | def tell(self): 39 | return self.file.tell() 40 | 41 | -------------------------------------------------------------------------------- /Decorators.py: -------------------------------------------------------------------------------- 1 | import os 2 | from Products.PageTemplates.PageTemplateFile import PageTemplateFile as ZPageTemplateFile 3 | class PageTemplateFile(ZPageTemplateFile): 4 | 5 | def _exec(self, bound_names, args, kw): 6 | extra_kw = self.view_wrapper_function(self, self.REQUEST) 7 | if isinstance(extra_kw, dict): 8 | extra_kw.pop('self') 9 | extra_kw.pop('REQUEST') 10 | kw.update(extra_kw) 11 | return super(PageTemplateFile, self)._exec(bound_names, args, kw) 12 | 13 | 14 | 15 | class zpt_function: 16 | """ 17 | Use like this: 18 | @zpt_function(MyClass, 'foo.zpt', globals()) 19 | def my_dashboard(self, REQUEST): 20 | variable = 123 21 | return locals() 22 | """ 23 | def __init__(self, host_object, template, globals, name=None, **kwargs): 24 | self.host_object = host_object 25 | self.template = template 26 | self.name = name 27 | self.globals = globals 28 | 29 | def __call__(self, func, *a, **kw): 30 | if self.name: 31 | name = self.name 32 | else: 33 | name = func.func_name 34 | pt = PageTemplateFile(self.template, self.globals, 35 | __name__=name) 36 | pt.view_wrapper_function = func 37 | setattr(self.host_object, name, pt) 38 | -------------------------------------------------------------------------------- /tests/testUKPostcode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-1 -* 2 | 3 | """FriedZopeBase Utils ZopeTestCase 4 | """ 5 | import os, re, sys 6 | if __name__ == '__main__': 7 | execfile(os.path.join(sys.path[0], 'framework.py')) 8 | 9 | from classes import TestBase 10 | 11 | from Products.FriedZopeBase import ukpostcode 12 | 13 | class TestUKPostcode(TestBase): 14 | """ 15 | the file ukpostcode.py has its own unit tests but including them 16 | here is a good idea so that it's tested with the rest of FriedZopeBase 17 | which will make sure it imports properly and continues to work. 18 | """ 19 | 20 | def test_valid_uk_postcode(self): 21 | yes = ('se19pg','e1 6jx', ) 22 | no = ('se1', '', '123123123123','se1 kkk') 23 | for each in yes: 24 | self.assertTrue(ukpostcode.valid_uk_postcode(each)) 25 | for each in no: 26 | self.assertTrue(not ukpostcode.valid_uk_postcode(each)) 27 | 28 | def test_format_uk_postcode(self): 29 | func = ukpostcode.format_uk_postcode 30 | self.assertEqual(func('e16jx'), 'E1 6JX') 31 | self.assertEqual(func('s e 1 9 p g'), 'SE1 9PG') 32 | 33 | 34 | 35 | def test_suite(): 36 | from unittest import TestSuite, makeSuite 37 | suite = TestSuite() 38 | suite.addTest(makeSuite(TestUKPostcode)) 39 | return suite 40 | 41 | if __name__ == '__main__': 42 | framework() 43 | 44 | -------------------------------------------------------------------------------- /tests/classes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-1 -* 2 | ## 3 | ## unittest Bases 4 | ## (c) Fry-IT, www.fry-it.com 5 | ## 6 | ## 7 | 8 | from Globals import SOFTWARE_HOME 9 | from Testing import ZopeTestCase 10 | 11 | 12 | 13 | ZopeTestCase.installProduct('FriedZopeBase') 14 | 15 | 16 | #------------------------------------------------------------------------------ 17 | # 18 | # Some constants 19 | # 20 | 21 | #------------------------------------------------------------------------------ 22 | 23 | 24 | # Open ZODB connection 25 | app = ZopeTestCase.app() 26 | 27 | # Set up sessioning objects 28 | ZopeTestCase.utils.setupCoreSessions(app) 29 | 30 | # Set up example applications 31 | #if not hasattr(app, 'Examples'): 32 | # ZopeTestCase.utils.importObjectFromFile(app, examples_path) 33 | 34 | # Close ZODB connection 35 | ZopeTestCase.close(app) 36 | 37 | 38 | #------------------------------------------------------------------------------ 39 | 40 | 41 | class TestBase(ZopeTestCase.ZopeTestCase): 42 | 43 | def dummy_redirect(self, *a, **kw): 44 | self.has_redirected = a[0] 45 | if kw: 46 | print "*** Redirecting to %r + (%s)" % (a[0], kw) 47 | else: 48 | print "*** Redirecting to %r" % a[0] 49 | 50 | def afterSetUp(self): 51 | #dispatcher = self.folder.manage_addProduct['MExpenses'] 52 | #dispatcher.manage_addHomepage('mexpenses') 53 | #self.mexpenses = self.folder['mexpenses'] 54 | #self.mexpenses.http_redirect = self.dummy_redirect 55 | self.has_redirected = False 56 | 57 | def _prepareSessionManager(): 58 | request = self.app.REQUEST 59 | sdm = self.app.session_data_manager 60 | request.set('SESSION', sdm.getSessionData()) 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/testBases.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-1 -* 2 | ## 3 | ## unittest Bases 4 | ## (c) Fry-IT, www.fry-it.com 5 | ## 6 | ## 7 | 8 | import cgi 9 | from sets import Set 10 | import cStringIO 11 | import os, sys 12 | if __name__ == '__main__': 13 | execfile(os.path.join(sys.path[0], 'framework.py')) 14 | 15 | 16 | from classes import TestBase 17 | 18 | from Products.FriedZopeBase.Bases import HomepageBase 19 | 20 | class TestBases(TestBase): 21 | 22 | def test_getDomainCookie(self): 23 | x = HomepageBase() 24 | x.REQUEST = self.app.REQUEST 25 | 26 | x.REQUEST.URL = 'http://www.mobilexpenses.com' 27 | self.assertEqual(x._getCookieDomain(), '.mobilexpenses.com') 28 | 29 | x.REQUEST.URL = 'http://www.mobilexpenses.com' 30 | self.assertEqual(x._getCookieDomain(), '.mobilexpenses.com') 31 | 32 | x.REQUEST.URL = 'http://127.0.0.1:8080/foo/bar.html' 33 | self.assertEqual(x._getCookieDomain(), None) 34 | 35 | x.REQUEST.URL = 'http://www.mobilexpenses.com/foo/bar.html' 36 | self.assertEqual(x._getCookieDomain(), '.mobilexpenses.com') 37 | 38 | x.REQUEST.URL = 'http://www.mobilexpenses.co.uk/foo/bar.html' 39 | self.assertEqual(x._getCookieDomain(), '.mobilexpenses.co.uk') 40 | 41 | x.REQUEST.URL = 'http://www.mobile.expenses.com/foo/bar.html' 42 | self.assertEqual(x._getCookieDomain(), '.mobile.expenses.com') 43 | 44 | x.REQUEST.URL = 'http://www.mobile.expenses.co.uk/foo/bar.html' 45 | self.assertEqual(x._getCookieDomain(), '.mobile.expenses.co.uk') 46 | 47 | x.REQUEST.URL = 'http://mobilexpenses.co.uk/foo/bar.html' 48 | self.assertEqual(x._getCookieDomain(), None) 49 | 50 | x.REQUEST.URL = 'http://mobilexpenses.com/foo/bar.html' 51 | self.assertEqual(x._getCookieDomain(), None) 52 | 53 | 54 | def dummy_redirect(self, *a, **kw): 55 | self.last_redirect = a[0] 56 | #if kw: 57 | # print "*** Redirecting to %r + (%s)" % (a[0], kw) 58 | #else: 59 | # print "*** Redirecting to %r" % a[0] 60 | 61 | def test_http_redirect(self): 62 | """ test http_redirect() """ 63 | hb = HomepageBase() 64 | hb.REQUEST = self.app.REQUEST 65 | hb.REQUEST.RESPONSE.redirect = self.dummy_redirect 66 | func = hb.http_redirect 67 | 68 | 69 | 70 | func('bajs', **{'balle':'fjong'}) 71 | self.assertEqual(self.last_redirect.split('/')[-1], 72 | 'bajs?balle=fjong') 73 | 74 | 75 | func('bajs#anchor', **{'balle':'fjong'}) 76 | self.assertEqual(self.last_redirect.split('/')[-1], 77 | 'bajs?balle=fjong#anchor') 78 | 79 | func('%s/#anchor' % self.app.absolute_url(), **{'balle':'fjong'}) 80 | self.assertEqual(self.last_redirect, 81 | '%s/?balle=fjong#anchor' % self.app.absolute_url()) 82 | 83 | 84 | func('/GKM/Tavlor-se/MAGIK#fm-12', **{'success':'balle fjong'}) 85 | expect = 'http://nohost/GKM/Tavlor-se/MAGIK?success=balle+fjong#fm-12' 86 | self.assertEqual(self.last_redirect, expect) 87 | 88 | 89 | def test_suite(): 90 | from unittest import TestSuite, makeSuite 91 | suite = TestSuite() 92 | suite.addTest(makeSuite(TestBases)) 93 | return suite 94 | 95 | if __name__ == '__main__': 96 | framework() 97 | 98 | -------------------------------------------------------------------------------- /split_search.py: -------------------------------------------------------------------------------- 1 | #-*- coding: iso-8859-1 -* 2 | ## 3 | ## Split search string - Useful when building advanced search application 4 | ## By: Peter Bengtsson, mail@peterbe.com 5 | ## May 2008 6 | ## ZPL 7 | ## 8 | 9 | __version__='1.0' 10 | 11 | """ 12 | 13 | split_search(searchstring [str or unicode], 14 | keywords [list or tuple]) 15 | 16 | Splits the search string into a free text part and a dictionary of keyword 17 | pairs. For example, if you search for 'Something from: Peter to: Lukasz' 18 | this function will return 19 | 'Something', {'from':'Peter', 'to':'Lukasz'} 20 | 21 | It works equally well with unicode strings. 22 | 23 | Any keywords in the search string that isn't recognized is considered text. 24 | 25 | """ 26 | 27 | import re 28 | 29 | 30 | def split_search(q, keywords): 31 | params = {} 32 | s = [] 33 | 34 | regex = re.compile(r'\b(%s):' % '|'.join(keywords), re.I) 35 | bits = regex.split(q) 36 | 37 | skip_next = False 38 | for i, bit in enumerate(bits): 39 | if skip_next: 40 | skip_next = False 41 | else: 42 | if bit in keywords: 43 | params[bit.lower()] = bits[i+1].strip() 44 | skip_next = True 45 | elif bit.strip(): 46 | s.append(bit.strip()) 47 | 48 | return ' '.join(s), params 49 | 50 | if __name__=='__main__': 51 | import unittest 52 | class Test(unittest.TestCase): 53 | 54 | def test_basic(self): 55 | """ one free text part, two keywords """ 56 | keywords = ('to','from') 57 | q = "Peter something to:AAa aa from:Foo bar" 58 | s, params = split_search(q, keywords) 59 | self.assertEqual(s, 'Peter something') 60 | self.assertEqual(params, {'to': 'AAa aa', 'from': 'Foo bar'}) 61 | 62 | def test_unrecognized_keywords(self): 63 | """ free text and keywords we don't support """ 64 | keywords = ('something','else') 65 | q = "Free text junk: Aaa aaa foo:bar" 66 | s, params = split_search(q, keywords) 67 | self.assertEqual(s, q) 68 | self.assertEqual(params, {}) 69 | 70 | def test_unrecognized_and_recognized_keywords(self): 71 | """ free text and keywords we don't support """ 72 | keywords = ('something','else','name') 73 | q = "Free text junk: something else name: peter" 74 | s, params = split_search(q, keywords) 75 | self.assertEqual(s, 'Free text junk: something else') 76 | self.assertEqual(params, {'name': 'peter'}) 77 | 78 | def test_empty_keyword_value(self): 79 | """ free text and an empty keyword """ 80 | keywords = ('to',) 81 | q = "Naughty parameter to:" 82 | s, params = split_search(q, keywords) 83 | self.assertEqual(s, "Naughty parameter") 84 | self.assertEqual(params, {'to':''}) 85 | 86 | def test_unicode_string(self): 87 | """ test with unicode string input """ 88 | keywords = ('from','to') 89 | q = u"\xa1 to:\xa2 from:\xa3" 90 | s, params = split_search(q, keywords) 91 | self.assertEqual(s, u'\xa1') 92 | self.assertEqual(params, {u'to': u'\xa2', u'from': u'\xa3'}) 93 | 94 | def suite(): 95 | return unittest.makeSuite(Test) 96 | 97 | unittest.main() 98 | 99 | 100 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | - 0.22 (peter) 2 | 3 | Fixed all broken tests in testZope with and without slimmer 4 | installed. 5 | Vastly improved Zope.py and it's registerXXXFile functions. 6 | 7 | - 0.21 (peter) 8 | 9 | Fixed important bug in sendEmailNG() that would try to send to and 10 | from non-ascii emails (e.g. u'test@fry-it.com') 11 | 12 | - 0.20 (peter) 13 | 14 | Added ukpostcode which exposes two useful methods: 15 | valid_uk_postcode, format_uk_postcode 16 | 17 | - 0.19 (peter) 18 | 19 | Improvements to sendEmailNG() with support for charset parameter 20 | 21 | Improvements to html2plaintext when there is no body tag in the 22 | email. 23 | 24 | Fix for getRedirectURL() where parameter 'params' wasn't reset 25 | between usage. Always vary of using empty dicts or lists as default 26 | parameter values. 27 | 28 | - 0.18 (lukasz) 29 | 30 | Functions that register files are now return number of files 31 | registered 32 | 33 | - 0.17 34 | 35 | New feature: Made it possible to set environment variable 36 | ABSOLUTE_MISC_URLS to get http://.../misc_/... instead of just 37 | /misc_/... 38 | 39 | - 0.16 40 | 41 | New feature: made sendEmailNG() smarter. If the 'debug' parameter 42 | is a valid directory or a valid file it'll write to that instead 43 | of stdout. 44 | 45 | - 0.15 46 | 47 | Bug fixed: GzippedImageFile.index_html() had an error on 48 | self.original_content_size 49 | 50 | - 0.14 51 | 52 | New feature: set_cookie() accepts 'across_domain_cookie_' which sets 53 | the cookie acros multiple subdomains like 'm.peterbe.com' AND 54 | 'www.peterbe.com' 55 | 56 | New feature: attachImage() and attachImages() sets imagefiles into 57 | classes as class attributes. 58 | 59 | New feature: Utils.iuniqify() does what uniqify does but case 60 | insensitively if possible. 61 | 62 | New feature: http_redirect() can sniff for getRootURL() method. 63 | 64 | - 0.13 65 | 66 | Bug fixed: doCache() rounded off hours if a float < 24 wrongly. 67 | 68 | - 0.12 69 | 70 | Bug fixed: Zope.registerIcons() can now accept a list of strings 71 | of imagenames. 72 | 73 | New feature: registerIcon() prevents paths with '//' in them. 74 | 75 | - 0.11 76 | 77 | New feature: Added stopCache() and doCache() for HTTP header caching 78 | 79 | - 0.10 80 | 81 | Bug fixed: sendEmail() sends the email differetly if the found 82 | Mailhost is a SecureMailHost or regular MailHost object so that the 83 | headers look right. 84 | 85 | - 0.9 86 | 87 | New feature: sendEmail() takes a new parameter 'debug' which can 88 | be a boolean or an object that has a 'write()' method eg. StringIO 89 | instances or sys.stderr 90 | 91 | - 0.8 92 | 93 | Bug fixed: registerIcons() accepts Globals=globals() parameter. 94 | 95 | - 0.7 96 | 97 | Bug fixed: _deployImages() skips '.svn' and 'CVS' folders. 98 | 99 | New feature: sendEmail() uses secureSend() if mailhost is 100 | SecureMailHost 101 | 102 | - 0.6 103 | 104 | New feature: sendEmail(msg, to, from, subject, swallowerrors=False) 105 | 106 | New feature: Added expire_cookie() method to Bases 107 | 108 | - 0.5 109 | 110 | New feature: http_redirect() can take a dict as second param or 111 | applied keyword arguments and these are added to the URL. 112 | 113 | - 0.4 114 | 115 | New feature: Base http_redirect() 116 | 117 | - 0.3 118 | 119 | New feature: Added HomepageBTreeBase and 120 | HomepageBTreeBaseCatalogAware classes. 121 | 122 | - 0.2 123 | 124 | New feature: set_cookie() makes sure DateTime objects are 125 | converted to RFC822 format. 126 | 127 | - 0.1 128 | 129 | Project started. -------------------------------------------------------------------------------- /tests/framework.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2005 Zope Corporation and Contributors. All Rights Reserved. 4 | # 5 | # This software is subject to the provisions of the Zope Public License, 6 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 7 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 8 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 9 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 10 | # FOR A PARTICULAR PURPOSE. 11 | # 12 | ############################################################################## 13 | """ZopeTestCase framework 14 | 15 | COPY THIS FILE TO YOUR 'tests' DIRECTORY. 16 | 17 | This version of framework.py will use the SOFTWARE_HOME 18 | environment variable to locate Zope and the Testing package. 19 | 20 | If the tests are run in an INSTANCE_HOME installation of Zope, 21 | Products.__path__ and sys.path with be adjusted to include the 22 | instance's Products and lib/python directories respectively. 23 | 24 | If you explicitly set INSTANCE_HOME prior to running the tests, 25 | auto-detection is disabled and the specified path will be used 26 | instead. 27 | 28 | If the 'tests' directory contains a custom_zodb.py file, INSTANCE_HOME 29 | will be adjusted to use it. 30 | 31 | If you set the ZEO_INSTANCE_HOME environment variable a ZEO setup 32 | is assumed, and you can attach to a running ZEO server (via the 33 | instance's custom_zodb.py). 34 | 35 | The following code should be at the top of every test module: 36 | 37 | import os, sys 38 | if __name__ == '__main__': 39 | execfile(os.path.join(sys.path[0], 'framework.py')) 40 | 41 | ...and the following at the bottom: 42 | 43 | if __name__ == '__main__': 44 | framework() 45 | 46 | $Id: framework.py 10921 2006-10-17 07:41:02Z peterbe $ 47 | """ 48 | 49 | __version__ = '0.2.4' 50 | 51 | # Save start state 52 | # 53 | __SOFTWARE_HOME = os.environ.get('SOFTWARE_HOME', '') 54 | __INSTANCE_HOME = os.environ.get('INSTANCE_HOME', '') 55 | 56 | if __SOFTWARE_HOME.endswith(os.sep): 57 | __SOFTWARE_HOME = os.path.dirname(__SOFTWARE_HOME) 58 | 59 | if __INSTANCE_HOME.endswith(os.sep): 60 | __INSTANCE_HOME = os.path.dirname(__INSTANCE_HOME) 61 | 62 | # Find and import the Testing package 63 | # 64 | if not sys.modules.has_key('Testing'): 65 | p0 = sys.path[0] 66 | if p0 and __name__ == '__main__': 67 | os.chdir(p0) 68 | p0 = '' 69 | s = __SOFTWARE_HOME 70 | p = d = s and s or os.getcwd() 71 | while d: 72 | if os.path.isdir(os.path.join(p, 'Testing')): 73 | zope_home = os.path.dirname(os.path.dirname(p)) 74 | sys.path[:1] = [p0, p, zope_home] 75 | break 76 | p, d = s and ('','') or os.path.split(p) 77 | else: 78 | print 'Unable to locate Testing package.', 79 | print 'You might need to set SOFTWARE_HOME.' 80 | sys.exit(1) 81 | 82 | import Testing, unittest 83 | execfile(os.path.join(os.path.dirname(Testing.__file__), 'common.py')) 84 | 85 | # Include ZopeTestCase support 86 | # 87 | if 1: # Create a new scope 88 | 89 | p = os.path.join(os.path.dirname(Testing.__file__), 'ZopeTestCase') 90 | 91 | if not os.path.isdir(p): 92 | print 'Unable to locate ZopeTestCase package.', 93 | print 'You might need to install ZopeTestCase.' 94 | sys.exit(1) 95 | 96 | ztc_common = 'ztc_common.py' 97 | ztc_common_global = os.path.join(p, ztc_common) 98 | 99 | f = 0 100 | if os.path.exists(ztc_common_global): 101 | execfile(ztc_common_global) 102 | f = 1 103 | if os.path.exists(ztc_common): 104 | execfile(ztc_common) 105 | f = 1 106 | 107 | if not f: 108 | print 'Unable to locate %s.' % ztc_common 109 | sys.exit(1) 110 | 111 | # Debug 112 | # 113 | print 'SOFTWARE_HOME: %s' % os.environ.get('SOFTWARE_HOME', 'Not set') 114 | print 'INSTANCE_HOME: %s' % os.environ.get('INSTANCE_HOME', 'Not set') 115 | sys.stdout.flush() 116 | 117 | -------------------------------------------------------------------------------- /feedcreator.py: -------------------------------------------------------------------------------- 1 | from types import InstanceType 2 | import time 3 | 4 | RSS_START=""" 5 | 6 | 12 | 13 | %(title)s 14 | %(webpage)s 15 | %(description)s 16 | %(language)s 17 | %(webmaster)s 18 | """ 19 | #" bug in jed editor (peter) 20 | 21 | RSS_END='' 22 | RDF_LI_ITEM = ' ' 23 | RDF_ITEM = """ 24 | 25 | %(title)s 26 | %(description)s 27 | %(link)s 28 | %(subject)s 29 | %(date)s 30 | %(extras)s 31 | 32 | """ 33 | 34 | 35 | class Item: 36 | def __init__(self, title, link, description='', subject='', 37 | abouturl=None, date=None, timestamp=None, **extras): 38 | 39 | if not date: 40 | date = time.strftime("%Y-%m-%dT%H:%M:%S+00:00") 41 | elif hasattr(date, 'strftime'): 42 | date = date.strftime("%Y-%m-%dT%H:%M:%S+00:00") 43 | if not abouturl: 44 | abouturl = link 45 | self.title = title 46 | self.link = link 47 | self.description = description 48 | self.subject = subject 49 | self.abouturl = abouturl 50 | self.date = date 51 | if not timestamp: 52 | timestamp = time.time() 53 | self.timestamp = timestamp 54 | self._extras = extras 55 | 56 | def out(self): 57 | info = {'extras':''} 58 | if self._extras: 59 | for k, v in self._extras.items(): 60 | info['extras'] += "%s\n"%(k,v,k) 61 | for each in ('title','link','description','subject', 62 | 'abouturl','date'): 63 | value = getattr(self, each) 64 | if value.find('<') > -1 or value.find('>') > -1 or value.find('&') > -1: 65 | value = '' % value 66 | info[each] = value 67 | return (RDF_ITEM%(info)).strip() 68 | __str__=__call__=out 69 | 70 | 71 | class Feed: 72 | def __init__(self, URL, 73 | title='Intranet', 74 | webpage=None, description='', language='en-uk', 75 | webmaster='root@localhost', 76 | abouturl=None 77 | ): 78 | self.URL = URL 79 | self.title = title 80 | if not abouturl: 81 | abouturl = URL 82 | self.abouturl = abouturl 83 | if not webpage: 84 | webpage = '/'.join(URL.split('/')[:-1]) 85 | self.webpage = webpage 86 | self.description = description 87 | self.language = language 88 | self.webmaster = webmaster 89 | self.items = [] 90 | 91 | def append(self, itemobject): 92 | """ add one more item object """ 93 | assert type(itemobject)==InstanceType 94 | assert itemobject.__class__.__name__=='Item' 95 | items = self.items 96 | items.append(itemobject) 97 | self.items = items 98 | 99 | 100 | def out(self): 101 | """ return the who XML string """ 102 | head_info = {} 103 | for each in ('abouturl','title','webpage','description', 104 | 'language','webmaster'): 105 | head_info[each] = getattr(self, each) 106 | header = RSS_START%(head_info) 107 | items_list = ['',''] 108 | all_items = [] 109 | for item in self.items: 110 | items_list.append(RDF_LI_ITEM%{'url':item.link}) 111 | all_items.append(str(item)) 112 | 113 | all_items = '\n\n'.join(all_items) 114 | 115 | items_list.extend(['','']) 116 | items_list = '\n'.join(items_list) 117 | 118 | return '\n'.join([header, items_list, '', all_items, RSS_END]) 119 | 120 | __str__=__call__=out 121 | 122 | def save(self, filename='intranet.xml'): 123 | """ save to file """ 124 | open(filename, 'w').write(self.__str__().strip()+'\n') 125 | 126 | 127 | def test_test1(): 128 | feed = Feed("http://www.stuff.com/rss.xml", 129 | "Stuff", description="Bla bla bla", 130 | webmaster="peter@fry-it.com") 131 | 132 | item1 = Item("Gozilla", "http://news.com/gozilla", 133 | "Gozilla is in town rampaging and destroying!", 134 | "News & Entertainment") 135 | feed.append(item1) 136 | item2 = Item("Blondes", "http://sexyblondes.org/page3", 137 | "Beautiful tall and volumtious blondes", 138 | date="2004-12-13T15:45") 139 | feed.append(item2) 140 | 141 | 142 | print feed 143 | 144 | if __name__=='__main__': 145 | test_test1() 146 | -------------------------------------------------------------------------------- /html2plaintext.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-1 -* 2 | ## 3 | ## (c) Fry-IT, www.fry-it.com, 2007 4 | ## 5 | ## 6 | 7 | """ 8 | A very spartan attempt of a script that converts HTML to 9 | plaintext. 10 | 11 | The original use for this little script was when I send HTML emails out I also 12 | wanted to send a plaintext version of the HTML email as multipart. Instead of 13 | having two methods for generating the text I decided to focus on the HTML part 14 | first and foremost (considering that a large majority of people don't have a 15 | problem with HTML emails) and make the fallback (plaintext) created on the fly. 16 | 17 | This little script takes a chunk of HTML and strips out everything except the 18 | (or an elemeny ID) and inside that chunk it makes certain conversions 19 | such as replacing all hyperlinks with footnotes where the URL is shown at the 20 | bottom of the text instead. words are converted to *words* 21 | and it does a fair attempt of getting the linebreaks right. 22 | 23 | As a last resort, it strips away all other tags left that couldn't be gracefully 24 | replaced with a plaintext equivalent. 25 | Thanks for Fredrik Lundh's unescape() function things like: 26 | 'Terms & Conditions' is converted to 27 | 'Termss & Conditions' 28 | 29 | It's far from perfect but a good start. It works for me for now. 30 | 31 | 32 | TODO: 33 | * proper unit tests 34 | * understand some basic style commands such as font-weight:bold 35 | 36 | 37 | Announcement here: 38 | http://www.peterbe.com/plog/html2plaintext 39 | 40 | Thanks to: 41 | Philipp (http://www.peterbe.com/plog/html2plaintext#c0708102y47) 42 | """ 43 | 44 | __version__='0.2' 45 | 46 | 47 | import re, sys 48 | from BeautifulSoup import BeautifulSoup, SoupStrainer, Comment 49 | 50 | def html2plaintext(html, body_id=None, encoding='ascii'): 51 | """ from an HTML text, convert the HTML to plain text. 52 | If @body_id is provided then this is the tag where the 53 | body (not necessarily ) starts. 54 | """ 55 | urls = [] 56 | if body_id is not None: 57 | strainer = SoupStrainer(id=body_id) 58 | else: 59 | # if the html doesn't contain a tag it doesn't make 60 | # sense to use a strainer 61 | if html.count('','*').replace('','*') 93 | html = html.replace('','*').replace('','*') 94 | html = html.replace('

','*').replace('

','*') 95 | html = html.replace('

','**').replace('

','**') 96 | html = html.replace('

','**').replace('

','**') 97 | html = html.replace('','/').replace('','/') 98 | 99 | 100 | # the only line breaks we respect is those of ending tags and 101 | # breaks 102 | 103 | html = html.replace('\n',' ') 104 | html = html.replace('
', '\n') 105 | html = html.replace(' ', ' ') 106 | html = html.replace('

', '\n\n') 107 | html = re.sub('', '\n', html) 108 | html = html.replace(' ' * 2, ' ') 109 | 110 | 111 | # for all other tags we failed to clean up, just remove then and 112 | # complain about them on the stderr 113 | def desperate_fixer(g): 114 | #print >>sys.stderr, "failed to clean up %s" % str(g.group()) 115 | return ' ' 116 | 117 | html = re.sub('<.*?>', desperate_fixer, html) 118 | 119 | # lstrip all lines 120 | html = u'\n'.join([x.lstrip() for x in html.splitlines()]) 121 | 122 | for i, url in enumerate(url_index): 123 | if i == 0: 124 | html += u'\n\n' 125 | html += u'[%s] %s\n' % (i+1, url) 126 | 127 | html = unescape(html) 128 | 129 | return html 130 | 131 | import htmlentitydefs 132 | # from http://effbot.org/zone/re-sub.htm#strip-html 133 | ## 134 | # Removes HTML or XML character references and entities from a text string. 135 | # 136 | # @param text The HTML (or XML) source text. 137 | # @return The plain text, as a Unicode string, if necessary. 138 | def unescape(text): 139 | def fixup(m): 140 | text = m.group(0) 141 | if text[:2] == "&#": 142 | # character reference 143 | try: 144 | if text[:3] == "&#x": 145 | return unichr(int(text[3:-1], 16)) 146 | else: 147 | return unichr(int(text[2:-1])) 148 | except ValueError: 149 | pass 150 | else: 151 | # named entity 152 | try: 153 | text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) 154 | except KeyError: 155 | pass 156 | return text # leave as is 157 | return re.sub("&#?\w+;", fixup, text) 158 | 159 | 160 | 161 | def test_html2plaintest(): 162 | html = ''' 163 | 164 | 165 | 166 | 167 |
168 |

This is a paragraph.

169 | 170 |

Foobar 171 |
172 | 173 | two.com 174 | 175 |

176 |

Visit www.google.com.

177 |
178 | Text elsewhere. 179 | 180 | Elsewhere 181 | 182 |
183 | 184 | 185 | ''' 186 | print html2plaintext(html, body_id='main') 187 | 188 | 189 | if __name__=='__main__': 190 | test_html2plaintest() -------------------------------------------------------------------------------- /addhrefs.py: -------------------------------------------------------------------------------- 1 | ## 2 | ## addhrefs.py 3 | ## by Peter Bengtsson, 2004-2005, mail@peterbe.com 4 | ## 5 | ## License: ZPL (http://www.zope.org/Resources/ZPL) 6 | ## 7 | __doc__='''A little function that puts HTML links into text.''' 8 | __version__='0.7' 9 | 10 | 11 | __changes__ = ''' 12 | 0.7 addhrefs() checks that emaillinkfunction and urllinkfunction if passed 13 | are callable. 14 | 15 | 0.6 three new parameters: 16 | return_everything=0 - returns (text, urlinfo, emailinfo) 17 | where urlinfo and emailinfo are lists 18 | emaillinkfunction=None - callback function for making an email 19 | into a HTML link the way you want it 20 | urllinkfunction=None - callback function for making an URL into 21 | a HTML link the way you want it 22 | 23 | 0.5 "foo https bar" created one link 24 | 25 | 0.4 Python2.1 compatible with custom True,False 26 | 27 | 0.3 Only one replace which solved the problem with 28 | "www.p.com bla bla http://www.p.com" 29 | 30 | 0.2 Lots of fine tuning. Created unit tests. 31 | 32 | 0.1 Project started. First working draft 33 | ''' 34 | 35 | __credits__=''' 36 | David Otton, 37 | "flump cakes" 38 | ''' 39 | 40 | True = not not 1 41 | False = not True 42 | 43 | import re, sys 44 | 45 | 46 | _end_dropouts = list(')>.;:,"') 47 | _start_dropouts = list('(<') 48 | def _massageURL(url): 49 | while url[-1] in _end_dropouts: 50 | url = url[:-1] 51 | if url[0] in _start_dropouts: 52 | url = url[1:] 53 | 54 | return url 55 | 56 | def _improveURL(url): 57 | if url.startswith('www.'): 58 | return 'http://'+url 59 | return url 60 | 61 | 62 | def _makeLink(url): 63 | return '%s'%(_improveURL(url), url) 64 | 65 | def _makeMailLink(url): 66 | return '%s'%(_improveURL(url), url) 67 | 68 | def _rejectEmail(email, start): 69 | if email.find(':') > -1: 70 | return True 71 | return False 72 | 73 | _bad_in_url = list('!()<>') 74 | _dont_start_url = list('@') 75 | def _rejectURL(url, start): 76 | """ return true if the URL can't be a URL """ 77 | if url.lower()=='https': 78 | return True 79 | for each in _bad_in_url: 80 | if url.find(each) > -1: 81 | return True 82 | whereat = url.find('@') 83 | if whereat > -1: 84 | url = url.replace('http://','').replace('ftp://','') 85 | if not -1 < url.find(':') < whereat: 86 | return True 87 | if start in _dont_start_url: 88 | return True 89 | return False 90 | 91 | 92 | _mailto_regex = re.compile('((^|\(|<|\s|)(\S+@\S+\.\S+)(\)|>|\s|$))') 93 | _url_regex = re.compile('((^|\(|<|@|\s|)(ftp\S+|http\S+|www\.\S+)(\)|>|\s|$))') 94 | def addhrefs(text, return_everything=0, 95 | emaillinkfunction=_makeMailLink, 96 | urllinkfunction=_makeLink): 97 | 98 | if not callable(emaillinkfunction): 99 | if emaillinkfunction is not None: 100 | print >>sys.stderr, "%r is not callable email link function"%emaillinkfunction 101 | emaillinkfunction = _makeMailLink 102 | 103 | if not callable(urllinkfunction): 104 | if urllinkfunction is not None: 105 | print >>sys.stderr, "%r is not callable URL link function"%urllinkfunction 106 | urllinkfunction = _makeLink 107 | 108 | info_emails = [] 109 | info_urls = [] 110 | 111 | urls = _url_regex.findall(text) 112 | for each in urls: 113 | whole, start, url, end = each 114 | 115 | url = _massageURL(url) 116 | if _rejectURL(url, start): 117 | continue 118 | link = urllinkfunction(url) 119 | if return_everything: 120 | info_urls.append((url, link)) 121 | better = whole.replace(url, link) 122 | text = text.replace(whole, better, 1) 123 | 124 | 125 | for each in _mailto_regex.findall(text): 126 | whole, start, url, end = each 127 | url = _massageURL(url) 128 | if _rejectEmail(url, start): 129 | continue 130 | if url.find(':') > -1: 131 | link = urllinkfunction(url) 132 | if return_everything: 133 | info_urls.append((url, link)) 134 | better = whole.replace(url, link) 135 | else: 136 | link = emaillinkfunction(url) 137 | info_emails.append((url, link)) 138 | better = whole.replace(url, link) 139 | text = text.replace(whole, better) 140 | 141 | if return_everything: 142 | return text, info_urls, info_emails 143 | else: 144 | return text 145 | 146 | 147 | def test(): 148 | t="this some text http://www.peterbe.com/ with links www.peterbe.com in it" 149 | t='''this some text http://www.peterbe.com/ 150 | with links www.peterbe.com in it Example''' 151 | 152 | 153 | 154 | 155 | t2='this some text http://www.peterbe.com/ '\ 156 | 'with links www.peterbe.com in it '\ 157 | 'Example' 158 | 159 | t3='''this some text http://www.peterbe.com/ 160 | with links www.peterbe.com in it Example 161 | www,peterbe.com and www.peterbe.com 162 | ''' 163 | 164 | t4='''https://www.imdb.com (www.peterbe.com/?a=e) asd tra la www.google.com''' 165 | 166 | t = 'word (www.peterbe.com) word' 167 | 168 | t = 'word and so on' 169 | 170 | t = 'Go to: http://www.peterbe.com. There youll find' 171 | 172 | t = 'Go to: http://www.peterbe.com:' 173 | t = '''https://www.imdb.com.''' 174 | t = 'Hello mail@peterbe.com to you' 175 | t = 'Hello and to you' 176 | #t = open('sample-htmlfree.txt').read() 177 | t = 'Link1 link www.2.com' 178 | t = "Link1 link www.2.com" 179 | t = '''1. http://www.peterbe.com 180 | 2. www.peterbe.com 181 | 3. 182 | 4. mail@foobar.com 183 | 5. "Name "''' 184 | t = 'xxx mail@peterbe.com peter@grenna.net' 185 | t += ' xxx www.peterbe.com www.google.com xxx' 186 | 187 | t = 'mail@peterbe.com 123@a.com or www2.ibm.com or www.ibm.com?asda=ewr&gr:int=34.' 188 | 189 | t = 'peter@grenna.net 123@a.com ftp://ftp.uk.linux.org/' 190 | t = 'http://david:otton@www.something.com david:otton@www.something.com' 191 | t = ''' xxx 192 | abc ''' 193 | t='''www.msn.co.uk 194 | 195 | http://msn.co.uk 196 | http://www.msn.co.uk 197 | 198 | ftp:/google.com 199 | ''' 200 | t = 'At http://localhost/ I have apache and at http://localhost:8080 '\ 201 | ' I have Zope David used http://enchanter or http://enchanter/' 202 | t = 'See http://www.something.com/page?this=that#001' 203 | t = 'Bla bla https bla bla and http bla' 204 | print addhrefs(t, emaillinkfunction=None) 205 | 206 | 207 | 208 | 209 | 210 | 211 | if __name__=='__main__': 212 | test() 213 | -------------------------------------------------------------------------------- /TemplateAdder.py: -------------------------------------------------------------------------------- 1 | __doc__="""Generic Template adder with CheckoutableTemplates 2 | 3 | Use like this:: 4 | 5 | from TemplateAdder import addTemplates2Class as aTC 6 | 7 | ----- 8 | class MyProduct(...): 9 | def foo(...): 10 | 11 | zpts = ('zpt/viewpage', 12 | ('zpt/view_index', 'index_html'), 13 | {'f':'zpt/dodgex', 'n':'do_give', 'd':'Some description'}, 14 | {'f':'zpt/view_page', 'n':'view', 'o':'HTML'}, 15 | # same thing different name on the optimize keyword 16 | {'f':'zpt/view_page2', 'n':'view2', 'optimize':'HTML'}, 17 | ) 18 | aTC(MyProduct, zpts) 19 | dtmls = ('manage_delete', 20 | ('dtml/cool_style.css','stylesheet.css','CSS'), 21 | ) 22 | aTC(MyProduct, dtmls) 23 | dtmls_opt_all = ('dtml/page1','dtml/page2') 24 | aTC(MyProduct, dtmls_opt_all, optimize='HTML') 25 | 26 | ---- 27 | 28 | The second parameter (the list of files to add) can be notated 29 | in these different ways: 30 | 31 | 1) 'zpt/name_of_file' 32 | Expect to find a file called 'name_of_file.zpt' 33 | 34 | 2) ('zpt/name_of_file','name_of_attr') 35 | Expect to find a file called 'name_of_file.zpt', and once 36 | loaded the attribute will be called 'name_of_attr'. 37 | This is useful if you have dedicated 'index_html' templates 38 | all sitting in the same directory to be used for different 39 | classes. 40 | 41 | 3) ('zpt/name_of_file','name_of_attr', 'optimzesyntax') 42 | Same as (2) except the last item in the tuple (or list) is the 43 | optimization syntax to use. 44 | 45 | 46 | 4) {'f':'zpt/name_of_file'} 47 | Exactly the same effect as (1) 48 | 49 | 5) {'f':'zpt/name_of_file', 'n':'name_of_attr'} 50 | Exactly the same effect as (2) 51 | 52 | 6) {'f':'zpt/name_of_file', 'd':'Some description'} 53 | Exactly the same effect as (1) but CT template is flagged 54 | with 'Some description' for the description. 55 | 56 | 7) {'f':'zpt/name_of_file', 'n':'name_of_attr', 'd':'Some description'} 57 | Exactly the same as (4) but with the description set. 58 | 59 | 8) {'f':'zpt/name_of_file', 'o':'optimizesyntax'} 60 | Same as (4) but with the optmization syntax variable set. 61 | 62 | 63 | Note: 64 | 65 | 1) Use of dict-style items, always requires the 'f' key. 66 | 67 | 2) The addTemplates2Class() constructor accepts a keyword argument 68 | like optimize='CSS' that sets the optimization argument on all 69 | templates defined in that tuple/list. Exceptions withing are 70 | taken into account. 71 | 72 | 73 | Changelog: 74 | 75 | 0.1.11 Parameter 'debug__' introduced to addTemplates2Class() 76 | 77 | 0.1.10 Parameter 'globals_' instead of 'Globals' in addTemplates2Class() 78 | 79 | 0.1.9 Possible to override the usage of checkoutable templates even if installed 80 | 81 | 0.1.8 Ability to set TEMPLATEADDER_LOG_USAGE environment variable to debug which 82 | files get instanciated. 83 | 84 | 0.1.7 Changed so that it can work with Zope 2.8.0 85 | 86 | 0.1.6 Removed the need to pass what extension it is 87 | 88 | 0.1.5 Fixed bug that if one template in 'templates' does optimize, 89 | then the rest had to suffer from that accept that too. 90 | 91 | 0.1.4 If CheckoutableTemplates is not installed, the default template 92 | handlers are used.. 93 | 94 | 0.1.3 Added support of 'optimize' parameter in CheckoutableTemplates. 95 | 96 | 0.1.2 Fixed bug in use of variable name 'template' 97 | 98 | 0.1.1 Added support for description parameter in dict- 99 | style. 100 | 101 | 0.1.0 Started 102 | 103 | """ 104 | 105 | __version__='0.1.11' 106 | 107 | 108 | import os 109 | import time 110 | 111 | import logging 112 | logger = logging.getLogger('FriedZopeBase.TemplateAdder') 113 | 114 | from Globals import DTMLFile 115 | from App.Common import package_home 116 | from Products.PageTemplates.PageTemplateFile import PageTemplateFile 117 | 118 | try: 119 | from Products.CheckoutableTemplates import CTDTMLFile as CTD 120 | from Products.CheckoutableTemplates import CTPageTemplateFile as CTP 121 | except ImportError: 122 | CTD = DTMLFile 123 | CTP = PageTemplateFile 124 | 125 | #------------------------------------------------------------------------------ 126 | 127 | # if you set this to a filepath instead of None or False it will write down 128 | # each template that gets used in a tab separated fashion 129 | LOG_USAGE = os.environ.get('TEMPLATEADDER_LOG_USAGE', None) 130 | 131 | #------------------------------------------------------------------------------ 132 | 133 | 134 | def addTemplates2Class(Class, templates, extension=None, optimize=None, 135 | globals_=globals(), use_checkoutable_templates=True, 136 | Globals=None, 137 | dtml_template_adder=None, 138 | zpt_template_adder=None, 139 | debug__=False): 140 | 141 | if Globals is not None: 142 | import warnings 143 | warnings.warn("Use 'globals_' parameter instead of 'Globals' when using"\ 144 | " addTemplates2Class()", DeprecationWarning, 2) 145 | globals_ = Globals 146 | 147 | 148 | 149 | if use_checkoutable_templates: 150 | dtml_adder = CTD 151 | 152 | if zpt_template_adder: 153 | zpt_adder = zpt_template_adder 154 | else: 155 | zpt_adder = CTP 156 | else: 157 | # If you don't want to use checkoutable templates, the reassign 158 | 159 | if dtml_template_adder is not None: 160 | assert callable(dtml_template_adder) 161 | dtml_adder = dtml_template_adder 162 | else: 163 | dtml_adder = DTMLFile 164 | 165 | if zpt_template_adder is not None: 166 | assert callable(zpt_template_adder) 167 | zpt_adder = zpt_template_adder 168 | else: 169 | zpt_adder = PageTemplateFile 170 | 171 | if isinstance(templates, basestring): 172 | # is it the name of a directory? 173 | if os.path.isdir(templates): 174 | if debug__: 175 | print "%r was a directory" % templates 176 | templates = [x for x in os.listdir(templates) 177 | if x.endswith('.zpt')] 178 | elif os.path.isdir(os.path.join(package_home(globals_), templates)): 179 | if debug__: 180 | print "%r was a directory" % os.path.join(package_home(globals_), templates) 181 | templates = [os.path.join(templates, x) for x 182 | in os.listdir(os.path.join(package_home(globals_), templates)) 183 | if x.endswith('.zpt')] 184 | if debug__: 185 | print "containing:" 186 | print templates 187 | 188 | elif debug__: 189 | print "%r was not directory" % templates 190 | 191 | 192 | root = '' 193 | optimize_orgin = optimize 194 | 195 | for template in templates: 196 | optimize = optimize_orgin 197 | description = '' 198 | if isinstance(template, (tuple, list)): 199 | if len(template)==3: 200 | template, dname, optimize = template 201 | else: 202 | template, dname = template 203 | 204 | elif isinstance(template, dict): 205 | dname = template.get('n', template['f'].split('/')[-1]) 206 | description = template.get('d','') 207 | optimize = template.get('o', template.get('optimize', optimize)) 208 | template = template['f'] 209 | 210 | else: 211 | # can't set 'optimize' this way 212 | dname = template.split('/')[-1] 213 | 214 | if dname.endswith('.dtml'): 215 | dname = dname[:-len('.dtml')] 216 | elif dname.endswith('.zpt'): 217 | dname = dname[:-len('.zpt')] 218 | 219 | if template.count('..'): 220 | root = template 221 | else: 222 | # why do we need to do this? 223 | root = apply(os.path.join, template.split('/')) 224 | f = root 225 | 226 | # now we need to figure out what extension this file is 227 | if template.startswith('dtml/') or template.endswith('.dtml'): 228 | extension = 'dtml' 229 | elif template.startswith('zpt/') or template.endswith('.zpt'): 230 | extension = 'zpt' 231 | else: 232 | # guess work 233 | if os.path.isfile(template + '.dtml'): 234 | extension = 'dtml' 235 | elif os.path.isfile(template + '.zpt'): 236 | extension = 'zpt' 237 | 238 | 239 | if LOG_USAGE: 240 | tmpl = '%s\t%s\t%s\t%s\n' 241 | open(LOG_USAGE.strip(),'a').write(tmpl % (Class.__name__, extension, f, description)) 242 | 243 | 244 | if extension == 'zpt': 245 | setattr(Class, dname, zpt_adder(f, globals_, d=description, 246 | __name__=dname, 247 | optimize=optimize)) 248 | elif extension == 'dtml': 249 | setattr(Class, dname, dtml_adder(f, globals_, d=description, 250 | optimize=optimize)) 251 | 252 | else: 253 | raise "UnrecognizedExtension", \ 254 | "Unrecognized template extension %r" % extension 255 | 256 | -------------------------------------------------------------------------------- /tests/testInput.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-1 -* 2 | 3 | """FriedZopeBase Utils ZopeTestCase 4 | """ 5 | import os, re, sys 6 | if __name__ == '__main__': 7 | execfile(os.path.join(sys.path[0], 'framework.py')) 8 | 9 | from classes import TestBase 10 | 11 | from Products.FriedZopeBase.Input import InputWidgets 12 | from Products.FriedZopeBase.Input import InputwidgetTypeError, InputwidgetValueError, InputwidgetNameError 13 | 14 | 15 | 16 | class TestInput(TestBase): 17 | 18 | def assertEqualLongString(self, a, b): 19 | NOT, POINT = '-', '*' 20 | if a != b: 21 | print a 22 | o = '' 23 | for i, e in enumerate(a): 24 | try: 25 | if e != b[i]: 26 | o += POINT 27 | else: 28 | o += NOT 29 | except IndexError: 30 | o += '*' 31 | 32 | o += NOT * (len(a)-len(o)) 33 | if len(b) > len(a): 34 | o += POINT* (len(b)-len(a)) 35 | 36 | print o 37 | print b 38 | 39 | raise AssertionError, '(see string comparison above)' 40 | 41 | 42 | def test_inputwidget(self): 43 | """ test the basic inputwidget() method that returns a piece of HTML 44 | of the input thing. """ 45 | 46 | iw = InputWidgets() 47 | iw.REQUEST = self.app.REQUEST 48 | func = iw.inputwidget 49 | 50 | html = func('name') 51 | expect = '' 52 | self.assertEqual(html, expect) 53 | 54 | html = func('name', 'value') 55 | expect = '' 56 | self.assertEqual(html, expect) 57 | 58 | # try setting the value in the REQUEST 59 | self.app.REQUEST.set('name','value') 60 | html = func('name') 61 | expect = '' 62 | self.assertEqual(html, expect) 63 | 64 | html = func('name:latin1:ustring', u'Sm\xa3') 65 | expect = u'' 66 | self.assertEqual(html, expect) 67 | 68 | # let's complicate things with different type_ 69 | html = func('name', 'value', type_='hidden') 70 | expect = '' 71 | self.assertEqual(html, expect) 72 | 73 | html = func('name', 'value', type_='password') 74 | expect = '' 75 | self.assertEqual(html, expect) 76 | 77 | html = func('name', 'value', type_='file') 78 | expect = '' 79 | self.assertEqual(html, expect) 80 | 81 | html = func('name', 'value', type_='textarea') 82 | expect = '' 83 | self.assertEqual(html, expect) 84 | 85 | # try setting an unrecognized type_ 86 | self.assertRaises(InputwidgetTypeError, func, 'name', type_='junk') 87 | 88 | # the name can't be padded with whitespace 89 | self.assertRaises(InputwidgetNameError, func, 'name ',) 90 | # and it can't obviously be blank 91 | self.assertRaises(InputwidgetNameError, func, '',) 92 | 93 | # use the type_ to guess the value from the REQUEST 94 | self.app.REQUEST.set('name','value') 95 | html = func('name', self.app.REQUEST ) 96 | expect = '' 97 | self.assertEqual(html, expect) 98 | self.app.REQUEST.set('name', None) 99 | 100 | # if you want to use the radio type, make sure the value isn't a list 101 | self.assertRaises(InputwidgetValueError, func, 'name', [1,2], type_='radio') 102 | 103 | # and you can't have multiple for radio inputs 104 | self.assertRaises(InputwidgetTypeError, func, 'name', 'value', type_='radio', multiple='multiple') 105 | 106 | # a luxury, if the value is an integer and as a string little, 107 | # then it automatically sets the size=5 108 | html = func('name', 123,) 109 | expect = '' 110 | self.assertEqual(html, expect) 111 | 112 | # if the type is checkbox, convert it to value 1 and use checked or not 113 | html = func('name', True, type_='checkbox') 114 | expect = '' 115 | self.assertEqual(html, expect) 116 | 117 | # suppose there is a 'submiterrors' in REQUEST with this name 118 | submiterrors={'name':u"Bad input!", 'other':'junk'} 119 | self.app.REQUEST.set('submiterrors', submiterrors) 120 | html = func('name', 'value') 121 | expect = u''\ 122 | 'Bad input!
' 123 | self.assertEqual(html, expect) 124 | self.app.REQUEST.set('submiterrors', {}) 125 | 126 | # set the class_ 127 | html = func('name', class_='koko') 128 | expect = '' 129 | self.assertEqual(html, expect) 130 | 131 | html = func('name', class_=['ko','ku']) 132 | expect = '' 133 | self.assertEqual(html, expect) 134 | 135 | # if you provide a list of options and don't explicitly set 136 | # the type to be radio, then make it a select input. 137 | html = func('name', options=['a','b']) 138 | expect = u'' 142 | self.assertEqual(html, expect) 143 | 144 | html = func('name', 'b', options=['a','b']) 145 | expect = u'' 149 | #self.assertEqual(html, expect) 150 | self.assertEqualLongString(html, expect) 151 | 152 | html = func('name', ['b','a'], options=['a','b','c'], multiple=1) 153 | expect = u'' 158 | self.assertEqual(html, expect) 159 | 160 | # test the posttext thing 161 | html = func('name', 'value', posttext=u'\xa3') 162 | expect = u''\ 163 | u'\xa3' 164 | self.assertEqual(html, expect) 165 | 166 | # a radio input 167 | html = func('name', 'b', options=['a','b'], type_='radio') 168 | expect = u''\ 169 | u'

' 171 | self.assertEqualLongString(html, expect) 172 | 173 | # a radio input with extra keywords 174 | html = func('name', 'b', options=['a','b'], type_='radio', class_='fmradio') 175 | expect = u''\ 176 | u'

' 178 | self.assertEqual(html, expect) 179 | 180 | 181 | html = func('name', 2, options=[1,2,3]) 182 | expect = u'' 187 | self.assertEqual(html, expect) 188 | 189 | # now the value is actually different from the options 190 | html = func('name', '2', options=[1,2,3]) 191 | expect = u'' 196 | self.assertEqual(html, expect) 197 | 198 | # with the special keyword 'careful_int_match__' it can work 199 | html = func('name', '2', options=[1,2,3], careful_int_match__=True) 200 | expect = u'' 205 | self.assertEqual(html, expect) 206 | 207 | 208 | # this time, the value can't even be converted to an int 209 | html = func('name', 'x', options=[1,2,3], careful_int_match__=True) 210 | expect = u'' 215 | self.assertEqual(html, expect) 216 | 217 | # the same but for radio 218 | html = func('name', '2', options=[1,2], type_='radio', 219 | careful_int_match__=True) 220 | expect = u''\ 221 | u'
'\ 222 | u'
' 225 | self.assertEqualLongString(html, expect) 226 | 227 | 228 | 229 | 230 | def test_suite(): 231 | from unittest import TestSuite, makeSuite 232 | suite = TestSuite() 233 | suite.addTest(makeSuite(TestInput)) 234 | return suite 235 | 236 | if __name__ == '__main__': 237 | framework() 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /ukpostcode.py: -------------------------------------------------------------------------------- 1 | # Module : postcode.py 2 | # Synopsis : UK postcode parser 3 | # Programmer : Simon Brunning - simon@brunningonline.net 4 | # Date : 14 April 2004 5 | # Version : 1.0 6 | # Copyright : Released to the public domain. Provided as-is, with no warranty. 7 | # Notes : 8 | '''UK postcode parser 9 | 10 | Provides the parse_uk_postcode function for parsing UK postcodes.''' 11 | 12 | import re 13 | 14 | # Build up the regex patterns piece by piece 15 | POSTAL_ZONES = ['AB', 'AL', 'B' , 'BA', 'BB', 'BD', 'BH', 'BL', 'BN', 'BR', 16 | 'BS', 'BT', 'CA', 'CB', 'CF', 'CH', 'CM', 'CO', 'CR', 'CT', 17 | 'CV', 'CW', 'DA', 'DD', 'DE', 'DG', 'DH', 'DL', 'DN', 'DT', 18 | 'DY', 'E' , 'EC', 'EH', 'EN', 'EX', 'FK', 'FY', 'G' , 'GL', 19 | 'GY', 'GU', 'HA', 'HD', 'HG', 'HP', 'HR', 'HS', 'HU', 'HX', 20 | 'IG', 'IM', 'IP', 'IV', 'JE', 'KA', 'KT', 'KW', 'KY', 'L' , 21 | 'LA', 'LD', 'LE', 'LL', 'LN', 'LS', 'LU', 'M' , 'ME', 'MK', 22 | 'ML', 'N' , 'NE', 'NG', 'NN', 'NP', 'NR', 'NW', 'OL', 'OX', 23 | 'PA', 'PE', 'PH', 'PL', 'PO', 'PR', 'RG', 'RH', 'RM', 'S' , 24 | 'SA', 'SE', 'SG', 'SK', 'SL', 'SM', 'SN', 'SO', 'SP', 'SR', 25 | 'SS', 'ST', 'SW', 'SY', 'TA', 'TD', 'TF', 'TN', 'TQ', 'TR', 26 | 'TS', 'TW', 'UB', 'W' , 'WA', 'WC', 'WD', 'WF', 'WN', 'WR', 27 | 'WS', 'WV', 'YO', 'ZE'] 28 | POSTAL_ZONES_ONE_CHAR = [zone for zone in POSTAL_ZONES if len(zone) == 1] 29 | POSTAL_ZONES_TWO_CHARS = [zone for zone in POSTAL_ZONES if len(zone) == 2] 30 | THIRD_POS_CHARS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'S', 31 | 'T', 'U', 'W'] 32 | FOURTH_POS_CHARS = ['A', 'B', 'E', 'H', 'M', 'N', 'P', 'R', 'V', 'W', 'X', 33 | 'Y'] 34 | INCODE_CHARS = ['A', 'B', 'D', 'E', 'F', 'G', 'H', 'J', 'L', 'N', 'P', 'Q', 35 | 'R', 'S', 'T', 'U', 'W', 'X', 'Y', 'Z'] 36 | OUTCODE_PATTERN = (r'(' + 37 | r'(?:(?:' + 38 | '|'.join(POSTAL_ZONES_ONE_CHAR) + 39 | r')(?:\d[' + 40 | ''.join(THIRD_POS_CHARS) + 41 | r']|\d{1,2}))' + 42 | r'|' + 43 | r'(?:(?:' + 44 | '|'.join(POSTAL_ZONES_TWO_CHARS) + 45 | r')(?:\d[' + 46 | ''.join(FOURTH_POS_CHARS) + 47 | r']|\d{1,2}))' + 48 | r')') 49 | INCODE_PATTERN = (r'(\d[' + 50 | ''.join(INCODE_CHARS) + 51 | r'][' + 52 | ''.join(INCODE_CHARS) + 53 | r'])') 54 | POSTCODE_PATTERN = OUTCODE_PATTERN + INCODE_PATTERN 55 | STANDALONE_OUTCODE_PATTERN = OUTCODE_PATTERN + r'\s*$' 56 | 57 | # Compile regexs 58 | POSTCODE_REGEX = re.compile(POSTCODE_PATTERN) 59 | STANDALONE_OUTCODE_REGEX = re.compile(STANDALONE_OUTCODE_PATTERN) 60 | 61 | def parse_uk_postcode(postcode, strict=True, incode_mandatory=True): 62 | '''Split UK postcode into outcode and incode portions. 63 | 64 | Arguments: 65 | postcode The postcode to be split. 66 | strict If true, the postcode will be validated according to 67 | the rules as specified at the Universal Postal Union[1] 68 | and The UK Government Data Standards Catalogue[2]. If 69 | the supplied postcode doesn't adhere to these rules a 70 | ValueError will be thrown. 71 | incode_mandatory If true, and only an outcode has been supplied, the 72 | function will throw a ValueError. 73 | 74 | Returns: outcode, incode 75 | 76 | Raises: ValueError, if postcode is longer than seven 77 | characters, or if 'strict' or 'incode_mandatory' 78 | conditions are broken - see above. 79 | 80 | Usage example: >>> from postcode import parse_uk_postcode 81 | >>> parse_uk_postcode('cr0 2yr') 82 | ('CR0', '2YR') 83 | >>> parse_uk_postcode('cr0') 84 | Traceback (most recent call last): 85 | File "", line 1, in ? 86 | File "postcode.py", line 101, in parse_uk_postcode 87 | raise ValueError('Incode mandatory') 88 | ValueError: Incode mandatory 89 | >>> parse_uk_postcode('cr0', False, False) 90 | ('CR0', '') 91 | 92 | [1] http://www.upu.int/post_code/en/countries/GBR.pdf 93 | [2] http://www.govtalk.gov.uk/gdsc/html/noframes/PostCode-2-1-Release.htm 94 | ''' 95 | 96 | postcode = postcode.replace(' ', '').upper() # Normalize 97 | 98 | if len(postcode) > 7: 99 | raise ValueError('Incode mandatory') 100 | 101 | # Validate postcode 102 | if strict: 103 | 104 | # Try for full postcode match 105 | postcode_match = POSTCODE_REGEX.match(postcode) 106 | if postcode_match: 107 | return postcode_match.group(1, 2) 108 | 109 | # Try for outcode only match 110 | outcode_match = STANDALONE_OUTCODE_REGEX.match(postcode) 111 | if outcode_match: 112 | if incode_mandatory: 113 | raise ValueError('Incode mandatory') 114 | else: 115 | return outcode_match.group(1), '' 116 | 117 | # Try Girobank special case 118 | if postcode == 'GIR0AA': 119 | return 'GIR', '0AA' 120 | elif postcode == 'GIR': 121 | if incode_mandatory: 122 | raise ValueError('Incode mandatory') 123 | else: 124 | return 'GIR', '' 125 | 126 | # None of the above 127 | raise ValueError('Invalid postcode') 128 | 129 | # Just chop up whatever we've been given. 130 | else: 131 | # Outcode only 132 | if len(postcode) <= 4: 133 | if incode_mandatory: 134 | raise ValueError('Incode mandatory') 135 | else: 136 | return postcode, '' 137 | # Full postcode 138 | else: 139 | return postcode[:-3], postcode[-3:] 140 | 141 | if __name__ == '__main__': 142 | print 'Self test:' 143 | test_data = [ 144 | ('cr0 2yr' , False, False, ('CR0' , '2YR')), 145 | ('CR0 2YR' , False, False, ('CR0' , '2YR')), 146 | ('cr02yr' , False, False, ('CR0' , '2YR')), 147 | ('dn16 9aa', False, False, ('DN16', '9AA')), 148 | ('dn169aa' , False, False, ('DN16', '9AA')), 149 | ('ec1a 1hq', False, False, ('EC1A', '1HQ')), 150 | ('ec1a1hq' , False, False, ('EC1A', '1HQ')), 151 | ('m2 5bq' , False, False, ('M2' , '5BQ')), 152 | ('m25bq' , False, False, ('M2' , '5BQ')), 153 | ('m34 4ab' , False, False, ('M34' , '4AB')), 154 | ('m344ab' , False, False, ('M34' , '4AB')), 155 | ('sw19 2et', False, False, ('SW19', '2ET')), 156 | ('sw192et' , False, False, ('SW19', '2ET')), 157 | ('w1a 4zz' , False, False, ('W1A' , '4ZZ')), 158 | ('w1a4zz' , False, False, ('W1A' , '4ZZ')), 159 | ('cr0' , False, False, ('CR0' , '' )), 160 | ('sw19' , False, False, ('SW19', '' )), 161 | ('xx0 2yr' , False, False, ('XX0' , '2YR')), 162 | ('3r0 2yr' , False, False, ('3R0' , '2YR')), 163 | ('20 2yr' , False, False, ('20' , '2YR')), 164 | ('3r0 ayr' , False, False, ('3R0' , 'AYR')), 165 | ('3r0 22r' , False, False, ('3R0' , '22R')), 166 | ('w1m 4zz' , False, False, ('W1M' , '4ZZ')), 167 | ('3r0' , False, False, ('3R0' , '' )), 168 | ('ec1c 1hq', False, False, ('EC1C', '1HQ')), 169 | ('m344cb' , False, False, ('M34' , '4CB')), 170 | ('gir 0aa' , False, False, ('GIR' , '0AA')), 171 | ('gir' , False, False, ('GIR' , '' )), 172 | ('w1m 4zz' , False, False, ('W1M' , '4ZZ')), 173 | ('w1m' , False, False, ('W1M' , '' )), 174 | ('dn169aaA', False, False, 'ValueError' ), 175 | 176 | ('cr0 2yr' , False, True , ('CR0', '2YR')), 177 | ('CR0 2YR' , False, True , ('CR0' , '2YR')), 178 | ('cr02yr' , False, True , ('CR0', '2YR')), 179 | ('dn16 9aa', False, True , ('DN16', '9AA')), 180 | ('dn169aa' , False, True , ('DN16', '9AA')), 181 | ('ec1a 1hq', False, True , ('EC1A', '1HQ')), 182 | ('ec1a1hq' , False, True , ('EC1A', '1HQ')), 183 | ('m2 5bq' , False, True , ('M2' , '5BQ')), 184 | ('m25bq' , False, True , ('M2' , '5BQ')), 185 | ('m34 4ab' , False, True , ('M34' , '4AB')), 186 | ('m344ab' , False, True , ('M34' , '4AB')), 187 | ('sw19 2et', False, True , ('SW19', '2ET')), 188 | ('sw192et' , False, True , ('SW19', '2ET')), 189 | ('w1a 4zz' , False, True , ('W1A' , '4ZZ')), 190 | ('w1a4zz' , False, True , ('W1A' , '4ZZ')), 191 | ('cr0' , False, True , 'ValueError' ), 192 | ('sw19' , False, True , 'ValueError' ), 193 | ('xx0 2yr' , False, True , ('XX0' , '2YR')), 194 | ('3r0 2yr' , False, True , ('3R0' , '2YR')), 195 | ('20 2yr' , False, True , ('20' , '2YR')), 196 | ('3r0 ayr' , False, True , ('3R0' , 'AYR')), 197 | ('3r0 22r' , False, True , ('3R0' , '22R')), 198 | ('w1m 4zz' , False, True , ('W1M' , '4ZZ')), 199 | ('3r0' , False, True , 'ValueError' ), 200 | ('ec1c 1hq', False, True , ('EC1C', '1HQ')), 201 | ('m344cb' , False, True , ('M34' , '4CB')), 202 | ('gir 0aa' , False, True , ('GIR' , '0AA')), 203 | ('gir' , False, True , 'ValueError' ), 204 | ('w1m 4zz' , False, True , ('W1M' , '4ZZ')), 205 | ('w1m' , False, True , 'ValueError' ), 206 | ('dn169aaA', False, True , 'ValueError' ), 207 | 208 | ('cr0 2yr' , True , False, ('CR0' , '2YR')), 209 | ('CR0 2YR' , True , False, ('CR0' , '2YR')), 210 | ('cr02yr' , True , False, ('CR0' , '2YR')), 211 | ('dn16 9aa', True , False, ('DN16', '9AA')), 212 | ('dn169aa' , True , False, ('DN16', '9AA')), 213 | ('ec1a 1hq', True , False, ('EC1A', '1HQ')), 214 | ('ec1a1hq' , True , False, ('EC1A', '1HQ')), 215 | ('m2 5bq' , True , False, ('M2' , '5BQ')), 216 | ('m25bq' , True , False, ('M2' , '5BQ')), 217 | ('m34 4ab' , True , False, ('M34' , '4AB')), 218 | ('m344ab' , True , False, ('M34' , '4AB')), 219 | ('sw19 2et', True , False, ('SW19', '2ET')), 220 | ('sw192et' , True , False, ('SW19', '2ET')), 221 | ('w1a 4zz' , True , False, ('W1A' , '4ZZ')), 222 | ('w1a4zz' , True , False, ('W1A' , '4ZZ')), 223 | ('cr0' , True , False, ('CR0' , '' )), 224 | ('sw19' , True , False, ('SW19', '' )), 225 | ('xx0 2yr' , True , False, 'ValueError' ), 226 | ('3r0 2yr' , True , False, 'ValueError' ), 227 | ('20 2yr' , True , False, 'ValueError' ), 228 | ('3r0 ayr' , True , False, 'ValueError' ), 229 | ('3r0 22r' , True , False, 'ValueError' ), 230 | ('w1m 4zz' , True , False, 'ValueError' ), 231 | ('3r0' , True , False, 'ValueError' ), 232 | ('ec1c 1hq', True , False, 'ValueError' ), 233 | ('m344cb' , True , False, 'ValueError' ), 234 | ('gir 0aa' , True , False, ('GIR' , '0AA')), 235 | ('gir' , True , False, ('GIR' , '' )), 236 | ('w1m 4zz' , True , False, 'ValueError' ), 237 | ('w1m' , True , False, 'ValueError' ), 238 | ('dn169aaA', True , False, 'ValueError' ), 239 | 240 | ('cr0 2yr' , True , True , ('CR0', '2YR')), 241 | ('CR0 2YR' , True , True , ('CR0' , '2YR')), 242 | ('cr02yr' , True , True , ('CR0', '2YR')), 243 | ('dn16 9aa', True , True , ('DN16', '9AA')), 244 | ('dn169aa' , True , True , ('DN16', '9AA')), 245 | ('ec1a 1hq', True , True , ('EC1A', '1HQ')), 246 | ('ec1a1hq' , True , True , ('EC1A', '1HQ')), 247 | ('m2 5bq' , True , True , ('M2' , '5BQ')), 248 | ('m25bq' , True , True , ('M2' , '5BQ')), 249 | ('m34 4ab' , True , True , ('M34' , '4AB')), 250 | ('m344ab' , True , True , ('M34' , '4AB')), 251 | ('sw19 2et', True , True , ('SW19', '2ET')), 252 | ('sw192et' , True , True , ('SW19', '2ET')), 253 | ('w1a 4zz' , True , True , ('W1A' , '4ZZ')), 254 | ('w1a4zz' , True , True , ('W1A' , '4ZZ')), 255 | ('cr0' , True , True , 'ValueError' ), 256 | ('sw19' , True , True , 'ValueError' ), 257 | ('xx0 2yr' , True , True , 'ValueError' ), 258 | ('3r0 2yr' , True , True , 'ValueError' ), 259 | ('20 2yr' , True , True , 'ValueError' ), 260 | ('3r0 ayr' , True , True , 'ValueError' ), 261 | ('3r0 22r' , True , True , 'ValueError' ), 262 | ('w1m 4zz' , True , True , 'ValueError' ), 263 | ('3r0' , True , True , 'ValueError' ), 264 | ('ec1c 1hq', True , True , 'ValueError' ), 265 | ('m344cb' , True , True , 'ValueError' ), 266 | ('gir 0aa' , True , True , ('GIR' , '0AA')), 267 | ('gir' , True , True , 'ValueError' ), 268 | ('w1m 4zz' , True , True , 'ValueError' ), 269 | ('w1m' , True , True , 'ValueError' ), 270 | ('dn169aaA', True , True , 'ValueError' ), 271 | 272 | #('WC2H 8DN', True 273 | ] 274 | passes, failures = 0, 0 275 | for postcode, strict, incode_mandatory, required_result in test_data: 276 | try: 277 | actual_result = parse_uk_postcode(postcode, strict, incode_mandatory) 278 | except ValueError: 279 | actual_result = 'ValueError' 280 | if actual_result != required_result: 281 | failures += 1 282 | print 'Failed:', repr(actual_result), '!=', repr(required_result), \ 283 | 'for input postcode =', repr(postcode) + \ 284 | ', strict =', repr(strict) + \ 285 | ', incode_mandatory =', repr(incode_mandatory) 286 | else: 287 | passes += 1 288 | if failures: 289 | print failures, "failures. :-(" 290 | print passes, "passed." 291 | else: 292 | print passes, "passed! ;-)" 293 | 294 | 295 | def valid_uk_postcode(postcode): 296 | try: 297 | parse_uk_postcode(postcode) 298 | return True 299 | except ValueError: 300 | return False 301 | 302 | def format_uk_postcode(postcode): 303 | partA, partB = parse_uk_postcode(postcode.strip()) 304 | return '%s %s' % (partA.upper(), partB.upper()) 305 | 306 | 307 | if __name__=='__main__': 308 | print valid_uk_postcode('WC2H 8DN') -------------------------------------------------------------------------------- /postcodeeverywhere2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | postcodeeverywhere.py 4 | Fry-IT Ltd. (c) 2007 5 | by Peter Bengtsson 6 | 7 | This module lets you do lookups on AFD's postcodeeverywhere XML 8 | server. 9 | 10 | The Postcodefinder class has these important methods: 11 | - AddressLookup 12 | 13 | """ 14 | import re, sys 15 | from urllib import urlopen, urlencode 16 | 17 | from cStringIO import StringIO 18 | 19 | try: 20 | import cElementTree as ElementTree 21 | except ImportError: 22 | print >>sys.stderr, "cElementTree not installed" 23 | from elementtree import ElementTree 24 | 25 | try: 26 | from ukpostcode import parse_uk_postcode 27 | except ImportError: 28 | def parse_uk_postcode(s): 29 | if len(s.split()) != 2: 30 | raise ValueError, "use ukpostcode instead!" 31 | return s.split() 32 | 33 | 34 | COUNTY_EQUIVS = { 35 | 'Bath and North East Somerset':'Somerset', 36 | 'County of Herefordshire':'Herefordshire', 37 | 38 | } 39 | 40 | 41 | def getCountyEquivalent(county): 42 | if COUNTY_EQUIVS.has_key(county): 43 | return COUNTY_EQUIVS.get(county) 44 | elif county.lower() in [x.lower() for x in COUNTY_EQUIVS.keys()]: 45 | county = county.lower() 46 | for k,v in COUNTY_EQUIVS.items(): 47 | if k.lower()==county: 48 | return v 49 | else: 50 | return county 51 | 52 | SERVER = "pce.afd.co.uk" 53 | SERIAL = "811771" 54 | PASSWORD = "test123" 55 | USERID = "FRYIT01" 56 | COUNTYTYPE = "0" 57 | #0 Omit Counties 58 | #1 Postal Counties 59 | #2 Abbreviated Postal Counties 60 | #3 Postal Including Optional Counties 61 | #4 Abbreviated Postal Including Optional Counties 62 | #5 Traditional Counties 63 | #6 Administrative Counties 64 | 65 | 66 | 67 | 68 | def _clearUpXMLString(s): 69 | """ make sure everything between >...< tags are things we recognize """ 70 | ok=r'\W' # [^a-zA-Z0-9_] 71 | alsook = [',','.',"'",'"',' ','\n','\r','&',';'] 72 | regex = re.compile(ok, re.I) 73 | parts =[] 74 | for each in s.split('>'): 75 | if each.rfind(' 0: 76 | t = each[:each.rfind(''.join(parts) 84 | 85 | 86 | 87 | class ParameterError(Exception): 88 | """ when some parameter is wrong """ 89 | pass 90 | 91 | 92 | #------------------------------------------------------------------------------------ 93 | 94 | class Postcodefinder: 95 | def __init__(self, server=SERVER, serial=SERIAL, password=PASSWORD, 96 | userid=USERID, countytype=COUNTYTYPE): 97 | self.server = server 98 | self.serial = serial 99 | self.password = password 100 | self.userid = userid 101 | self.countytype = countytype 102 | 103 | def _getQS(self, **kwd): 104 | autoinclude = {'serial':self.serial, 105 | 'password':self.password, 106 | 'userid':self.userid, 107 | 'countytype':self.countytype} 108 | 109 | for k,v in autoinclude.items(): 110 | if not kwd.has_key(k): 111 | kwd[k] = v 112 | 113 | return urlencode(kwd) 114 | 115 | def getURL(self, page, **kwd): 116 | """ return full URL """ 117 | u = 'http://%s/%s?'%(self.server, page) 118 | u += apply(self._getQS, (), kwd) 119 | return u 120 | 121 | def AddressLookup(self, postcode=None, postkey=None, 122 | property=None, countytype=None): 123 | """ Takes either a postcode and optional property or a postkey and 124 | returns a single address record. 125 | """ 126 | if countytype is None: 127 | countytype = self.countytype 128 | 129 | if postcode is not None: 130 | kw = {'postcode':postcode} 131 | elif postkey is not None: 132 | kw = {'postkey':postkey} 133 | else: 134 | raise ParameterError, "Neither postcode or postkey :(" 135 | 136 | kw['countytype'] = countytype 137 | if property: 138 | kw['property'] = property 139 | 140 | url = self.getURL('addresslookup.pce', **kw) 141 | 142 | page = urlopen(url) 143 | xmlstring = page.read() 144 | xmlfile = StringIO(xmlstring) 145 | try: 146 | doc = ElementTree.ElementTree(file=xmlfile) 147 | except: 148 | print >>sys.stderr, "Failed to parse XML file" 149 | return {} 150 | 151 | root = doc.getroot() 152 | record = {} 153 | conversions = {'GridEast':'grid_ref_x', 154 | 'GridNorth':'grid_ref_y', 155 | 'Street':'street', 156 | 'Town':'town', 157 | 'County':'county', 158 | 'Postcode':'postcode', 159 | } 160 | for element in doc.getiterator(): 161 | if element.tag in conversions: 162 | text = element.text 163 | if text is not None and text.isdigit(): 164 | try: 165 | text = int(text) 166 | except ValueError: 167 | pass 168 | record[conversions.get(element.tag)] = text 169 | 170 | if not [x for x in record.values() if x is not None]: 171 | return {} 172 | 173 | if record.get('postcode') == 'Error: Postcode Not Found': 174 | return {} 175 | 176 | return record 177 | 178 | 179 | def AddressFastFind(self, searchterm, countytype=None, 180 | max_sub_lookups=3, approximate=False): 181 | """ return a list of records like this basically 182 | [AddressLookup(), AddressLookup()] 183 | """ 184 | if countytype is None: 185 | countytype = self.countytype 186 | 187 | kw = {'fastfind': searchterm} 188 | kw['countytype'] = countytype 189 | 190 | url = self.getURL('addressfastfind.pce', **kw) 191 | 192 | page = urlopen(url) 193 | xmlstring = page.read() 194 | xmlfile = StringIO(xmlstring) 195 | try: 196 | doc = ElementTree.ElementTree(file=xmlfile) 197 | except: 198 | print >>sys.stderr, "Failed to parse XML file" 199 | return [] 200 | 201 | root = doc.getroot() 202 | postkeys = [] 203 | for node in root.findall('AddressListItem'): 204 | for node2 in node.findall('PostKey'): 205 | if node2.text is not None: 206 | postkeys.append(node2.text) 207 | 208 | records = [] 209 | records_hash_x = {} 210 | records_hash_y = {} 211 | if approximate: 212 | for i in range(0, len(postkeys), max(len(postkeys)/max_sub_lookups, 1)): 213 | record = self.AddressLookup(postkey=postkeys[i]) 214 | records.append(record) 215 | else: 216 | for postkey in postkeys: 217 | record = self.AddressLookup(postkey=postkey) 218 | records.append(record) 219 | 220 | if approximate: 221 | # insert one record in the begining whose grid refs is the 222 | # average of all other grid refs 223 | 224 | all_x = [x['grid_ref_x'] for x in records] 225 | all_y = [x['grid_ref_y'] for x in records] 226 | average_x = int(sum(all_x)/float(len(all_x))) 227 | average_y = int(sum(all_y)/float(len(all_y))) 228 | 229 | r = records[0] 230 | 231 | postcode = r['postcode'] 232 | try: 233 | postcode, __ = parse_uk_postcode(postcode) 234 | except ValueError: 235 | pass 236 | 237 | records.insert(0, dict(postcode=postcode, 238 | grid_ref_x=average_x, 239 | grid_ref_y=average_y, 240 | county=r['county'], 241 | town=r['town'])) 242 | 243 | 244 | keep = [] 245 | for record in records: 246 | if [x for x in record.values() if x is not None]: 247 | if 'street' not in record: 248 | record['street'] = None 249 | keep.append(record) 250 | return keep 251 | 252 | def Postcode2Addresses(self, postcode): 253 | """ 254 | Return list of addresses for postcode 255 | """ 256 | kw = {'fastfind': postcode} 257 | kw['countytype'] = self.countytype 258 | 259 | url = self.getURL('addressfastfind.pce', **kw) 260 | 261 | page = urlopen(url) 262 | xmlstring = page.read() 263 | xmlfile = StringIO(xmlstring) 264 | try: 265 | doc = ElementTree.ElementTree(file=xmlfile) 266 | except: 267 | print >>sys.stderr, "Failed to parse XML file" 268 | return [] 269 | 270 | root = doc.getroot() 271 | addresses = [] 272 | for node in root.findall('AddressListItem'): 273 | for node2 in node.findall('Address'): 274 | if node2.text is not None: 275 | parts = node2.text.split('\t') 276 | if len(parts) > 1: 277 | addresses.append(parts[1]) 278 | return addresses 279 | 280 | #------------------------------------------------------------------------------------ 281 | 282 | 283 | def get_gridref(term, alternatives=1, onlyfullpostcode=0, accurate=0): 284 | """ wrap class for external scripts """ 285 | finder = Postcodefinder() 286 | get_g = finder.get_gridref 287 | if type(term)==type([]): 288 | results = [] 289 | for t in term: 290 | result = get_g(term, alternatives=alternatives, 291 | onlyfullpostcode=onlyfullpostcode, 292 | accurate=accurate) 293 | results.append(result) 294 | return results 295 | else: 296 | return get_g(term, alternatives=alternatives, 297 | onlyfullpostcode=onlyfullpostcode, 298 | accurate=accurate) 299 | 300 | 301 | def address_search_postkey(postkey, ret=None): 302 | """ wrap class for external scripts """ 303 | finder = Postcodefinder() 304 | return finder.address_search_postkey(postkey, ret=ret) 305 | 306 | 307 | #------------------------------------------------------------------------------------ 308 | 309 | def test(): 310 | # Run some tests 311 | 312 | ps = [#'ec1v8dd','nonexist', 313 | #'ox9','ox9 3wh','ox93ep', 314 | 'sw5', 315 | #'ec1v 8dd','ec1v8dd','ec1v', 316 | #'sw7','sw7 4ub','sw74ub', 317 | 'ox8 4js', 'ox8', 318 | ] 319 | 320 | finder = Postcodefinder() 321 | for p in ps: 322 | t0 = time.time() 323 | t='\t' 324 | if len(p)<7: 325 | t+='\t' 326 | print p, "%s-->"%t, finder.get_gridref(p), " (%s seconds)"%str(round(time.time()-t0, 3)) 327 | 328 | def test2(): 329 | # Run some tests 330 | 331 | ls = ['beaconsfield', 332 | 'gerrards cross', 333 | 'worcester', 334 | 'coombe', 335 | ] 336 | finder = Postcodefinder() 337 | for l in ls: 338 | t0 = time.time() 339 | t='\t' 340 | if len(l)<7: 341 | t+='\t' 342 | print l, "%s-->"%t, finder.get_gridref(l), " (%s seconds)"%str(round(time.time()-t0, 3)) 343 | 344 | 345 | def test_postcodes(postcodelist, geocodes=False): 346 | finder = Postcodefinder() 347 | lookup = finder.AddressLookup 348 | search = finder.AddressFastFind 349 | for arg in postcodelist: 350 | r = lookup(postcode=arg) 351 | if not r: 352 | r = search(arg) 353 | yield r 354 | 355 | 356 | def test_postcode(postcode, geocodes=False): 357 | finder = Postcodefinder() 358 | if geocodes: 359 | return finder.get_geocode(postcode) 360 | else: 361 | return finder.AddressLookup(postcode) 362 | 363 | 364 | 365 | def run(): 366 | """ where the arguments to this script is a comma 367 | separated list of postcodes """ 368 | args = ' '.join(sys.argv[1:]) 369 | 370 | _get_geocodes = False 371 | if args.find('--geo') > -1: 372 | _get_geocodes = True 373 | args = args.replace('--geo','') 374 | 375 | 376 | args = args.split(',') 377 | args = [x.strip() for x in args if x.strip()] 378 | if args: 379 | for each in test_postcodes(args): 380 | print each 381 | 382 | def run2(): 383 | args = sys.argv[1:] 384 | 385 | exactly = False 386 | if '--exactly' in args: 387 | exactly = True 388 | args.remove('--exactly') 389 | elif '-e' in args: 390 | exactly = True 391 | args.remove('-e') 392 | 393 | if ' '.join(args).find(',') > -1: 394 | args = ' '.join(args).split(',') 395 | 396 | args = [x.strip() for x in args if x.strip()] 397 | 398 | finder = Postcodefinder() 399 | search = finder.AddressFastFind 400 | skip_next = False 401 | for i, arg in enumerate(args): 402 | if skip_next: 403 | skip_next = False 404 | continue 405 | 406 | results = search(arg) 407 | grep_house_number = None 408 | try: 409 | if args[i+1].isdigit(): 410 | grep_house_number = args[i+1] 411 | skip_next = True 412 | except IndexError: 413 | pass 414 | 415 | for result in results: 416 | if result['street'] is None: 417 | continue 418 | if grep_house_number and not result['street'].startswith('%s ' % grep_house_number): 419 | continue 420 | print '\t'.join([result['street'], result['town'], result['postcode']]) 421 | 422 | 423 | #if not exactly and not r: 424 | # r = search(arg) 425 | 426 | return 0 427 | 428 | def run3(): 429 | postcode = "" 430 | number = "" 431 | args = ' '.join(sys.argv[1:]) 432 | pcf = re.compile('^.*postcodefrom *= *\"*(.*?)\"*(postcodeto.*$|property.*$|$)') 433 | pct = re.compile('^.*postcodeto *= *\"*(.*?)\"*(postcodefrom.*$|property.*$|$)') 434 | prp = re.compile('^.*property *= *\"*(.*?)\"*(postcodefrom.*$|postcodeto.*$|$)') 435 | m1 = pcf.match(args) 436 | m2 = pct.match(args) 437 | m3 = prp.match(args) 438 | if m1: postcode=m1.group(1) 439 | if m2: postcode=m2.group(1) 440 | if m3: number=m3.group(1) 441 | if postcode <> "": 442 | finder = Postcodefinder() 443 | if number <> "": 444 | result = finder.AddressLookup(postcode=postcode,property=number) 445 | if result['street'] is not None: 446 | print '\t'.join([result['street'], result['town'], result['postcode']]) 447 | else : 448 | results = finder.AddressFastFind(postcode) 449 | for result in results: 450 | if result['street'] is None: continue 451 | print '\t'.join([result['street'], result['town'], result['postcode']]) 452 | return 0 453 | 454 | if __name__ == '__main__': 455 | import time 456 | #test() 457 | #test2() 458 | #run() 459 | sys.exit(run3()) 460 | 461 | 462 | 463 | 464 | COUNTIES = ['Aberdeenshire', 465 | 'Anglesey', 466 | 'Angus', 467 | 'Argyll & Bute', 468 | 'Ayrshire', 469 | 'Bedfordshire', 470 | 'Berkshire', 471 | 'Blaenau Gwent', 472 | 'Bridgend', 473 | 'Bristol', 474 | 'Buckinghamshire', 475 | 'Caerphilly', 476 | 'Cambridgeshire', 477 | 'Cardiff', 478 | 'Carmarthenshire', 479 | 'Ceredigion', 480 | 'Channel Islands', 481 | 'Cheshire', 482 | 'Clackmannanshire', 483 | 'Conwy', 484 | 'Cornwall', 485 | 'County Antrim', 486 | 'County Armagh', 487 | 'County Down', 488 | 'County Fermanagh', 489 | 'County Londonderry', 490 | 'County Tyrone', 491 | 'Cumbria', 492 | 'Denbighshire', 493 | 'Derbyshire', 494 | 'Devon', 495 | 'Dorset', 496 | 'Dumfries & Galloway', 497 | 'Dunbartonshire', 498 | 'Dundee', 499 | 'Durham', 500 | 'East Lothian', 501 | 'Edinburgh', 502 | 'Essex', 503 | 'Falkirk', 504 | 'Fife', 505 | 'Flintshire', 506 | 'Glasgow', 507 | 'Gloucestershire', 508 | 'Greater London', 509 | 'Greater Manchester', 510 | 'Gwynedd', 511 | 'Hampshire', 512 | 'Herefordshire', 513 | 'Hertfordshire', 514 | 'Highland', 515 | 'Inverclyde', 516 | 'Isle of Man', 517 | 'Isle of Wight', 518 | 'Isles of Scilly', 519 | 'Kent', 520 | 'Lanarkshire', 521 | 'Lancashire', 522 | 'Leicestershire', 523 | 'Lincolnshire', 524 | 'London', 525 | 'Merseyside', 526 | 'Merthyr Tydfil', 527 | 'Midlothian', 528 | 'Monmouthshire', 529 | 'Moray', 530 | 'Neath & Port Talbot', 531 | 'Norfolk', 532 | 'Northamptonshire', 533 | 'Northumberland', 534 | 'Nottinghamshire', 535 | 'Oxfordshire', 536 | 'Pembrokeshire', 537 | 'Perth & Kinross', 538 | 'Powys', 539 | 'Renfrewshire', 540 | 'Rhondda Cynon Taff', 541 | 'Scottish Borders', 542 | 'Scottish Islands', 543 | 'Shropshire', 544 | 'Somerset', 545 | 'Staffordshire', 546 | 'Stirling', 547 | 'Suffolk', 548 | 'Surrey', 549 | 'Sussex', 550 | 'Swansea', 551 | 'Torfaen', 552 | 'Tyne & Wear', 553 | 'Vale of Glamorgan', 554 | 'Warwickshire', 555 | 'West Lothian', 556 | 'West Midlands', 557 | 'Wiltshire', 558 | 'Worcestershire', 559 | 'Wrexham', 560 | 'Yorkshire'] 561 | -------------------------------------------------------------------------------- /Input.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-1 -* 2 | ## 3 | ## (c) Fry-IT, www.fry-it.com 4 | ## 5 | ## 6 | 7 | # python 8 | import os, sys, re 9 | 10 | 11 | # Other Zope products 12 | from Utils import html_quote, niceboolean, unicodify 13 | 14 | __version__='1.4' 15 | 16 | #------------------------------------------------------------------------------ 17 | 18 | class InputwidgetTypeError(Exception): 19 | """ happens when the type_ passed to the inputwidgry functions doesn't 20 | add up. """ 21 | pass 22 | 23 | class InputwidgetValueError(Exception): 24 | """ happens when the value passed to the inputwidgry functions doesn't 25 | add up. """ 26 | pass 27 | 28 | class InputwidgetNameError(Exception): 29 | """ happens when the value passed to the inputwidgry functions doesn't 30 | add up. """ 31 | pass 32 | 33 | #------------------------------------------------------------------------------ 34 | class InputWidgets: 35 | 36 | 37 | def inputwidgetTR(self, name, value=None, label=None, 38 | mandatory=False, optional=False, 39 | sup=None, sub=None, 40 | type_=None, class_=None, **kw): 41 | """ 42 | Return a chunk of HTML like this: 43 | 44 | 45 | 46 | 47 | """ 48 | template = u'\n\t\n'\ 49 | u'\t%(input_part)s\n' 50 | return self._inputwidget_by_template(template, name, value=value, 51 | label=label, mandatory=mandatory, 52 | optional=optional, 53 | sub=sub, sup=sup, 54 | type_=type_, class_=class_, **kw) 55 | 56 | 57 | def inputwidgetDT(self, name, value=None, label=None, 58 | mandatory=False, optional=False, 59 | sub=None, sup=None, 60 | type_=None, class_=None, **kw): 61 | """ 62 | Return a chunk of HTML like this: 63 |
64 |
65 | """ 66 | template = u'
\n'\ 67 | u'
%(input_part)s
' 68 | return self._inputwidget_by_template(template, name, value=value, 69 | label=label, mandatory=mandatory, 70 | optional=optional, 71 | type_=type_, class_=class_, **kw) 72 | 73 | 74 | def _inputwidget_by_template(self, template, name, 75 | value=None, label=None, 76 | mandatory=False, optional=False, 77 | sub=None, sup=None, 78 | type_=None, class_=None, **kw): 79 | 80 | unicode_encoding = kw.get('unicode_encoding', 81 | getattr(self, 'UNICODE_ENCODING', 'UTF-8')) 82 | 83 | # If the name passed to this function is 'title:latin1:ustring' 84 | # or 'price:float', create a new variable called name_full and change 85 | # the original variable name. 86 | name_full = name 87 | if name.find(':') > -1: 88 | name_full = name 89 | name = name_full.split(':')[0] 90 | 91 | if len(name_full.split(':')) == 2 and name_full.split(':')[1] in ('ustring','utext'): 92 | # lazy! You didn't include the encoding 93 | name_full = name_full.split(':')[0] + ':%s:' % unicode_encoding + name_full.split(':')[1] 94 | 95 | input_part = self.inputwidget(name_full, value, type_=type_, class_=class_, 96 | **kw) 97 | 98 | if label is None: 99 | label = self._name2label(name) 100 | 101 | def isBiggerThan1Int(x): 102 | try: 103 | return int(x) > 1 104 | except ValueError: 105 | return False 106 | if isBiggerThan1Int(mandatory): 107 | tmpl = u' %s' 108 | note = u'*' * int(mandatory) 109 | #if int(mandatory) == 4: 110 | # note = u'†' 111 | #elif int(mandatory) == 5: 112 | # note = u'‡' 113 | mandot = tmpl % (mandatory, note) 114 | elif mandatory: 115 | mandot = u' *' 116 | else: 117 | mandot = u'' 118 | 119 | if sub: 120 | subsup = '%s' % sub 121 | elif sup: 122 | subsup = '%s' % sup 123 | else: 124 | subsup = '' 125 | 126 | if isinstance(optional, basestring): 127 | # keep 'optional' the way it is 128 | optional = unicodify(optional) 129 | if optional.startswith('(') and optional.startswith(')'): 130 | optional = optional[1:-1] 131 | optional = u' (%s)' % unicodify(optional) 132 | elif bool(optional): 133 | optional = u' (optional)' 134 | else: 135 | optional = u'' 136 | 137 | nameid = self._name2nameID(name) 138 | data = dict(nameid=nameid, input_part=input_part, label=label, 139 | mandot=mandot, optional=optional, subsup=subsup) 140 | return template % data 141 | 142 | 143 | def inputwidget(self, name, value=None, type_=None, class_=None, 144 | options=None, **kw): 145 | """ Return the input part as a chunk of HTML. Most cases it's just an 146 | tag but this can be different depending on the value of 147 | type_. For example, if type_=='textarea' return a ' 351 | elif type_ in ('radio','checkbox') and options and isinstance(options, (list, tuple)): 352 | c = 0 353 | for option in options: 354 | valueid = self._name2nameID(name)+'_%s' % c 355 | c += 1 356 | if isinstance(option, (tuple, list)): 357 | option, label = option 358 | elif isinstance(option, dict): 359 | option, label = option['key'], option['value'] 360 | else: 361 | option, label = option, option 362 | 363 | if isEqual(option, value): 364 | template = u'' 377 | html += '' %\ 378 | dict(valueid=valueid, label=label) 379 | html += kw.get('radio_label_delimiter__','
') 380 | html = html.strip() 381 | 382 | elif options and isinstance(options, (list, tuple)): 383 | html = html.strip() + u'>\n' 384 | preval = value 385 | if isinstance(preval, (tuple, list)) and not kw.get(u'multiple'): 386 | raise InputwidgetValueError, "Pass multiple='multiple' if the value is an iterable" 387 | for option in options: 388 | 389 | if isinstance(option, (tuple, list)): 390 | option, label = option 391 | elif isinstance(option, dict): 392 | option, label = option['key'], option['value'] 393 | else: 394 | option, label = option, option 395 | 396 | if isEqual(option, preval) or (kw.get(u'multiple') and option in preval): 397 | html += u'\n'%\ 398 | (option, label) 399 | else: 400 | html += u'\n' % (option, label) 401 | 402 | html += u'' 403 | elif type_ == 'select': 404 | # The type was 'select' but there weren't any options! 405 | raise ValueError, "Type 'select' but no options provided" 406 | else: 407 | if type_ != 'radio': 408 | html += u'/>' 409 | 410 | if kw.get(u'posttext'): 411 | html += '%s' % kw.get(u'posttext') 412 | 413 | 414 | # error_message is in most cases a empty string 415 | html += error_message 416 | 417 | return html 418 | 419 | 420 | def _name2nameID(self, name): 421 | """ return a suitable id based on the name """ 422 | 423 | return u'id_%s' % name 424 | 425 | def _name2label(self, name): 426 | """ if name is 'first_name' return 'First name' """ 427 | return name.replace('_',' ').capitalize() 428 | -------------------------------------------------------------------------------- /Zope.py: -------------------------------------------------------------------------------- 1 | #-*- coding: iso-8859-1 -* 2 | ## 3 | ## Fry-IT Zope Bases 4 | ## Peter Bengtsson, peter@fry-it.com, (c) 2005-2007 5 | ## 6 | import re 7 | import os 8 | import gzip 9 | import stat 10 | from tempfile import gettempdir 11 | from time import time 12 | try: 13 | from slimmer import js_slimmer, css_slimmer 14 | except ImportError: 15 | css_slimmer = js_slimmer = None 16 | 17 | 18 | # Zope 19 | import App 20 | from Globals import package_home, DevelopmentMode 21 | from ZPublisher.Iterators import filestream_iterator 22 | from Acquisition import aq_inner, aq_parent 23 | 24 | 25 | try: 26 | from zope.app.content_types import guess_content_type 27 | 28 | except ImportError: 29 | # hmm, must be zope < 2.10 30 | from OFS.content_types import guess_content_type 31 | 32 | # For GzippedImageFile 33 | from App.Common import rfc1123_date 34 | from Utils import anyTrue, base_hasattr, getEnvBool 35 | 36 | 37 | 38 | 39 | FILESTREAM_ITERATOR_THRESHOLD = 2 << 16 # 128 Kb (from LocalFS StreamingFile.py) 40 | 41 | EXPIRY_INFINITY = 60*60*24*365*5 # 5 years 42 | 43 | DATA64_REGEX = re.compile('(DATA64\(([\w/\.]+\.(gif|png|jpg))\))') 44 | referred_css_images_regex = re.compile('url\(([^\)]+)\)') 45 | 46 | from base64 import encodestring 47 | 48 | def _find_related_context(context, path): 49 | """Return a new context relative to this path. 50 | For example if context os and path is something like 51 | ../images/foo then the new path will be: 52 | aq_parent(aq_inner(context)).images.foo 53 | If the path starts with / then we have to hope that acquisition will do its 54 | magic. 55 | """ 56 | for bit in [x for x in path.split('/') if x]: 57 | if bit == '..': 58 | context = aq_parent(aq_inner(context)) 59 | else: 60 | context = getattr(context, bit) 61 | return context 62 | 63 | def my_guess_content_type(path, data): 64 | content_type, enc = guess_content_type(path, data) 65 | if content_type in ('text/plain', 'text/html','text/x-unknown-content-type'): 66 | if os.path.basename(path).endswith('.js-slimmed.js'): 67 | content_type = 'application/x-javascript' 68 | elif os.path.basename(path).find('.css-slimmed.css') > -1: 69 | # the find() covers both 'foo.css-slimmed' and 70 | # 'foo.css-slimmed-data64expanded' 71 | content_type = 'text/css' 72 | elif os.path.basename(path).find('.css-aliased.css') > -1: 73 | content_type = 'text/css' 74 | return content_type, enc 75 | 76 | 77 | def _getAutogeneratedFilepath(filepath, directoryname='.autogenerated'): 78 | """ given a filepath, return (and make sure the directory exists) the filepath 79 | as landed in the filpath. 80 | Eg. If filepath='/home/peterbe/css/screen.css' then return 81 | '/home/peterbe/css/.autogenerated/screen.css' 82 | """ 83 | dirname = os.path.dirname(filepath) 84 | basename = os.path.basename(filepath) 85 | if directoryname in dirname.split(os.path.sep): 86 | return filepath 87 | dirname = os.path.join(dirname, directoryname) 88 | if not os.path.isdir(dirname): 89 | os.mkdir(dirname) 90 | return os.path.join(dirname, basename) 91 | 92 | 93 | class BetterImageFile(App.ImageFile.ImageFile): # that name needs to improve 94 | 95 | def __init__(self, path, _prefix=None, max_age_development=60, max_age_production=3600, 96 | content_type=None, set_expiry_header=True): 97 | if _prefix is None: 98 | _prefix = getConfiguration().softwarehome 99 | elif type(_prefix) is not type(''): 100 | _prefix = package_home(_prefix) 101 | path = os.path.join(_prefix, path) 102 | self.path = self.original_path = path 103 | self.set_expiry_header = set_expiry_header 104 | 105 | if DevelopmentMode: 106 | # In development mode, a shorter time is handy 107 | max_age = max_age_development 108 | else: 109 | # A longer time reduces latency in production mode 110 | max_age = max_age_production 111 | self.max_age = max_age 112 | self.cch = 'public,max-age=%d' % max_age 113 | 114 | data = open(path, 'rb').read() 115 | if content_type is None: 116 | content_type, __ = my_guess_content_type(path, data) 117 | 118 | if content_type: 119 | self.content_type=content_type 120 | else: 121 | raise ValueError, "content_type not set or couldn't be guessed" 122 | #self.content_type='text/plain' 123 | 124 | self.__name__=path[path.rfind('/')+1:] 125 | self.lmt=float(os.stat(path)[8]) or time.time() 126 | self.lmh=rfc1123_date(self.lmt) 127 | self.content_size = os.stat(path)[stat.ST_SIZE] 128 | 129 | 130 | def index_html(self, REQUEST, RESPONSE): 131 | """Default document""" 132 | # HTTP If-Modified-Since header handling. This is duplicated 133 | # from OFS.Image.Image - it really should be consolidated 134 | # somewhere... 135 | RESPONSE.setHeader('Content-Type', self.content_type) 136 | RESPONSE.setHeader('Last-Modified', self.lmh) 137 | RESPONSE.setHeader('Cache-Control', self.cch) 138 | RESPONSE.setHeader('Content-Length', self.content_size) 139 | if self.set_expiry_header: 140 | RESPONSE.setHeader('Expires', self._expires()) 141 | 142 | header=REQUEST.get_header('If-Modified-Since', None) 143 | if header is not None: 144 | header=header.split(';')[0] 145 | # Some proxies seem to send invalid date strings for this 146 | # header. If the date string is not valid, we ignore it 147 | # rather than raise an error to be generally consistent 148 | # with common servers such as Apache (which can usually 149 | # understand the screwy date string as a lucky side effect 150 | # of the way they parse it). 151 | try: mod_since=long(DateTime(header).timeTime()) 152 | except: mod_since=None 153 | if mod_since is not None: 154 | if getattr(self, 'lmt', None): 155 | last_mod = long(self.lmt) 156 | else: 157 | last_mod = long(0) 158 | if last_mod > 0 and last_mod <= mod_since: 159 | RESPONSE.setStatus(304) 160 | return '' 161 | 162 | if self.content_size > FILESTREAM_ITERATOR_THRESHOLD: 163 | return filestream_iterator(self.path, 'rb') 164 | else: 165 | return open(self.path,'rb').read() 166 | 167 | HEAD__roles__=None 168 | def HEAD(self, REQUEST, RESPONSE): 169 | """ """ 170 | RESPONSE.setHeader('Content-Type', self.content_type) 171 | RESPONSE.setHeader('Last-Modified', self.lmh) 172 | RESPONSE.setHeader('Content-Length', self.content_size) 173 | 174 | return '' 175 | 176 | def _expires(self): 177 | return rfc1123_date(time()+self.max_age) 178 | 179 | def __str__(self): 180 | """ return the content of the file """ 181 | return open(self.path,'rb').read() 182 | 183 | 184 | 185 | class GzippedFile(BetterImageFile): 186 | def __init__(self, path, _prefix=None, max_age_development=60, max_age_production=3600, 187 | set_expiry_header=False): 188 | BetterImageFile.__init__(self, path, _prefix=_prefix, 189 | set_expiry_header=set_expiry_header, 190 | max_age_development=max_age_development, 191 | max_age_production=max_age_production) 192 | self._prepareGZippedContent() 193 | 194 | def _prepareGZippedContent(self): 195 | # Make a gzip copy and 196 | if not hasattr(self, 'original_path'): 197 | self.original_path = self.path 198 | 199 | if hasattr(self, 'original_path') and not hasattr(self, 'original_content_size'): 200 | self.original_content_size = os.stat(self.original_path)[stat.ST_SIZE] 201 | 202 | data = open(self.original_path, 'rb').read() 203 | gz_path = _getAutogeneratedFilepath(self.original_path + '.gz') 204 | gzip.open(gz_path, 'wb').write(data) 205 | self.path = gz_path 206 | self.content_size = os.stat(self.path)[stat.ST_SIZE] 207 | 208 | def index_html(self, REQUEST, RESPONSE): 209 | """Default document""" 210 | # HTTP If-Modified-Since header handling. This is duplicated 211 | # from OFS.Image.Image - it really should be consolidated 212 | # somewhere... 213 | RESPONSE.setHeader('Content-Type', self.content_type) 214 | RESPONSE.setHeader('Last-Modified', self.lmh) 215 | RESPONSE.setHeader('Cache-Control', self.cch) 216 | if self.set_expiry_header: 217 | RESPONSE.setHeader('Expires', self._expires()) 218 | 219 | header=REQUEST.get_header('If-Modified-Since', None) 220 | if header is not None: 221 | header=header.split(';')[0] 222 | # Some proxies seem to send invalid date strings for this 223 | # header. If the date string is not valid, we ignore it 224 | # rather than raise an error to be generally consistent 225 | # with common servers such as Apache (which can usually 226 | # understand the screwy date string as a lucky side effect 227 | # of the way they parse it). 228 | try: mod_since=long(DateTime(header).timeTime()) 229 | except: mod_since=None 230 | if mod_since is not None: 231 | if getattr(self, 'lmt', None): 232 | last_mod = long(self.lmt) 233 | else: 234 | last_mod = long(0) 235 | if last_mod > 0 and last_mod <= mod_since: 236 | RESPONSE.setStatus(304) 237 | return '' 238 | 239 | if not os.path.isfile(self.path): 240 | self._prepareGZippedContent() 241 | 242 | if REQUEST['HTTP_ACCEPT_ENCODING'].find('gzip')>-1: 243 | # HTTP/1.1 'gzip', HTTP/1.0 'x-gzip' 244 | RESPONSE.setHeader("Content-encoding", "gzip") 245 | RESPONSE.setHeader("Content-Length", self.content_size) 246 | if self.content_size > FILESTREAM_ITERATOR_THRESHOLD: 247 | return filestream_iterator(self.path,'rb') 248 | else: 249 | return open(self.path,'rb').read() 250 | else: 251 | RESPONSE.setHeader("Content-Length", self.original_content_size) 252 | if self.original_content_size > FILESTREAM_ITERATOR_THRESHOLD: 253 | return filestream_iterator(self.original_path, 'rb') 254 | else: 255 | return open(self.original_path,'rb').read() 256 | 257 | def __str__(self): 258 | return open(self.original_path,'rb').read() 259 | 260 | 261 | def attachImages(class_, files, globals_): 262 | """ do attachImage() multiple times """ 263 | for file in files: 264 | attachImage(class_, file, globals_) 265 | 266 | def attachImage(class_, file, globals_): 267 | """ attach an BetterImageFile instance into a class. 268 | The file parameter must be a path to an image like 269 | 'images/foo.jpg' and then any instance of class_ 270 | (eg. localhost:8080/Bar), the image will be available as 271 | localhost:8080/Bar/foo.jpg. 272 | """ 273 | import warnings 274 | warnings.warn("Don't use this. Use registerImage() or registerImages() instead", 275 | DeprecationWarning, 2) 276 | if not os.path.isfile(file) and os.path.isfile(os.path.join('images',file)): 277 | file = os.path.join('images', file) 278 | filename = os.path.basename(file) 279 | setattr(class_, filename, App.ImageFile.ImageFile(file, globals_)) 280 | 281 | def registerJSFiles(product, files, Globals=globals(), rel_path='js', 282 | slim_if_possible=True, gzip_if_possible=False, 283 | max_age_development=60, max_age_production=3600, 284 | set_expiry_header=True): 285 | """ register all js files """ 286 | result = _registerFiles(registerJSFile, product, files, Globals, 287 | rel_path=rel_path, 288 | slim_if_possible=slim_if_possible, 289 | gzip_if_possible=gzip_if_possible, 290 | max_age_development=max_age_development, 291 | max_age_production=max_age_production, 292 | set_expiry_header=set_expiry_header) 293 | return result 294 | 295 | def registerJSFile(product, filename, epath=None, Globals=globals(), 296 | rel_path='js', 297 | slim_if_possible=True, 298 | gzip_if_possible=False, 299 | set_expiry_header=False, 300 | max_age_development=60, max_age_production=3600): 301 | p_home = package_home(Globals) # product home 302 | 303 | if filename.count('-slimmed.js'): 304 | raise SystemError, "Again!??!" 305 | 306 | objectid = filename 307 | path = "%s/" % rel_path 308 | if epath: 309 | path = "%s/%s/" % (rel_path, epath) 310 | 311 | filepath = '%s%s' % (path, filename) 312 | 313 | if len(filename.split(',')) > 1: 314 | # it's a combo name!! 315 | real_filepath = _getAutogeneratedFilepath(os.path.join(p_home, filepath)) 316 | out = open(real_filepath, 'w') 317 | mtimes = [] 318 | for filepath_ in [os.path.join(p_home, '%s%s' % (path, x)) 319 | for x in filename.split(',')]: 320 | content = open(filepath_).read() 321 | if slim_if_possible and js_slimmer and not dont_slim_file(filepath_): 322 | content = js_slimmer(content) 323 | out.write(content + '\n') 324 | mtimes.append(os.stat(filepath_)[stat.ST_MTIME]) 325 | out.close() 326 | filepath = real_filepath 327 | mtime = max(mtimes) 328 | 329 | # since we've taken the slimming thought into account already 330 | # here in the loop there is no need to consider slimming the 331 | # content further down. 332 | slim_if_possible = False 333 | 334 | else: 335 | mtime = os.stat(os.path.join(p_home, filepath))[stat.ST_MTIME] 336 | 337 | setattr(product, 338 | objectid, 339 | BetterImageFile(filepath, Globals, 340 | max_age_development=max_age_development, 341 | max_age_production=max_age_production, 342 | set_expiry_header=set_expiry_header) 343 | ) 344 | obj = getattr(product, objectid) 345 | 346 | if slim_if_possible and dont_slim_file(os.path.join(p_home, filepath)): 347 | slim_if_possible = False 348 | 349 | if slim_if_possible and \ 350 | os.path.isfile(os.path.join(p_home, filepath)): 351 | if os.path.isfile(os.path.join(p_home, filepath+'.nogzip')): 352 | gzip_if_possible = False 353 | 354 | if js_slimmer is not None and slim_if_possible: 355 | obj = getattr(product, objectid) 356 | slimmed = js_slimmer(open(obj.path,'rb').read(), hardcore=False) 357 | filepath = _getAutogeneratedFilepath(obj.path + '-slimmed.js') 358 | open(filepath, 'wb').write(slimmed) 359 | setattr(obj, 'path', filepath) 360 | 361 | 362 | # set up an alias too with near infinite max_age 363 | a, b = os.path.splitext(filename) 364 | objectid_alias = a + '.%s' % mtime + b 365 | setattr(product, 366 | objectid_alias, 367 | BetterImageFile(obj.path, Globals, 368 | set_expiry_header=set_expiry_header, 369 | max_age_development=EXPIRY_INFINITY, # 5 years 370 | max_age_production=EXPIRY_INFINITY # 5 years 371 | ) 372 | ) 373 | # make a note of the alias 374 | if hasattr(product, 'misc_infinite_aliases'): 375 | aliases = product.misc_infinite_aliases 376 | else: 377 | aliases = {} 378 | aliases[objectid] = objectid_alias 379 | setattr(product, 'misc_infinite_aliases', aliases) 380 | 381 | if gzip_if_possible: 382 | setattr(product, objectid, 383 | GzippedFile(filepath, Globals, 384 | max_age_development=max_age_development, 385 | max_age_production=max_age_production, 386 | set_expiry_header=set_expiry_header) 387 | ) 388 | 389 | # then set up an alias (overwrite the previous alias) 390 | # for the gzipped version too. 391 | setattr(product, objectid_alias, 392 | GzippedFile(filepath, Globals, 393 | max_age_development=EXPIRY_INFINITY, 394 | max_age_production=EXPIRY_INFINITY, 395 | set_expiry_header=set_expiry_header) 396 | ) 397 | 398 | def registerCSSFiles(product, files, Globals=globals(), 399 | rel_path='css', slim_if_possible=True, gzip_if_possible=False, 400 | max_age_development=60, max_age_production=3600, 401 | set_expiry_header=False, 402 | expand_data64=False, 403 | replace_images_with_aliases=False): 404 | """ register all js files """ 405 | result = _registerFiles(registerCSSFile, product, files, Globals, 406 | rel_path=rel_path, 407 | slim_if_possible=slim_if_possible, 408 | gzip_if_possible=gzip_if_possible, 409 | max_age_development=max_age_development, 410 | max_age_production=max_age_production, 411 | expand_data64=expand_data64, 412 | set_expiry_header=set_expiry_header, 413 | replace_images_with_aliases=replace_images_with_aliases) 414 | return result 415 | 416 | def registerCSSFile(product, filename, epath=None, Globals=globals(), 417 | rel_path='css', 418 | slim_if_possible=True, gzip_if_possible=False, 419 | set_expiry_header=False, 420 | max_age_development=60, max_age_production=3600, 421 | expand_data64=False, 422 | replace_images_with_aliases=False): 423 | 424 | p_home = package_home(Globals) # product home 425 | 426 | if filename.count('-slimmed.css'): 427 | raise SystemError, "Again!??!" 428 | 429 | objectid = filename 430 | path = "%s/" % rel_path 431 | if epath: 432 | path = "%s/%s/" % (rel_path, epath) 433 | 434 | filepath = '%s%s' % (path, filename) 435 | 436 | 437 | if len(filename.split(',')) > 1: 438 | # it's a combo name!! 439 | real_filepath = _getAutogeneratedFilepath(os.path.join(p_home, filepath)) 440 | out = open(real_filepath, 'w') 441 | mtimes = [] 442 | for filepath_ in [os.path.join(p_home, '%s%s' % (path, x)) 443 | for x in filename.split(',')]: 444 | content = open(filepath_).read() 445 | if slim_if_possible and css_slimmer and not dont_slim_file(filepath_): 446 | content = css_slimmer(content) 447 | out.write(content+'\n') 448 | mtimes.append(os.stat(filepath_)[stat.ST_MTIME]) 449 | out.close() 450 | filepath = real_filepath 451 | mtime = max(mtimes) 452 | 453 | # since we've taken the slimming thought into account already 454 | # here in the loop there is no need to consider slimming the 455 | # content further down. 456 | slim_if_possible = False 457 | 458 | else: 459 | mtime = os.stat(os.path.join(p_home, filepath))[stat.ST_MTIME] 460 | 461 | setattr(product, 462 | objectid, 463 | BetterImageFile(filepath, Globals, 464 | set_expiry_header=set_expiry_header, 465 | max_age_development=max_age_development, 466 | max_age_production=max_age_production) 467 | ) 468 | obj = getattr(product, objectid) 469 | 470 | if slim_if_possible and dont_slim_file(os.path.join(p_home, filepath)): 471 | slim_if_possible = False 472 | 473 | if slim_if_possible and \ 474 | os.path.isfile(os.path.join(p_home, filepath)): 475 | if os.path.isfile(os.path.join(p_home, filepath+'.nogzip')): 476 | gzip_if_possible = False 477 | 478 | if css_slimmer is not None and slim_if_possible: 479 | slimmed = css_slimmer(open(obj.path,'rb').read()) 480 | filepath = _getAutogeneratedFilepath(obj.path + '-slimmed.css') 481 | open(filepath, 'wb').write(slimmed) 482 | setattr(obj, 'path', filepath) 483 | 484 | if expand_data64: 485 | # If this is true, then try to convert things like 486 | # 'DATA64(www/img.gif)' into 487 | # 'data:image/gif;base64,ASIDJADIJAIW00100.... ...\n' 488 | # This feature is highly experimental and has proved problematic 489 | # since it sometimes just doesn't work and it's really hard to 490 | # debug why certain images aren't working. Use carefully. 491 | # 492 | # The spec is here: http://tools.ietf.org/html/rfc2397 493 | # The inspiration came from here: 494 | # http://developer.yahoo.com/performance/rules.html#num_http 495 | new_content = False 496 | content = file(obj.path).read() 497 | for whole_tag, path, extension in DATA64_REGEX.findall(content): 498 | fp = os.path.join(p_home, path) 499 | content = content.replace(whole_tag, 500 | 'data:image/%s;base64,%s' % \ 501 | (extension, 502 | encodestring(file(fp,'rb').read()).replace('\n','\\n'))) 503 | 504 | new_content = content 505 | 506 | if new_content: 507 | filepath = _getAutogeneratedFilepath(obj.path + '-data64expanded') 508 | open(filepath, 'wb').write(new_content) 509 | setattr(obj, 'path', filepath) 510 | 511 | if replace_images_with_aliases: 512 | # This feature opens the CSS content and looks for things like 513 | # url(/misc_/MyProduct/close.png) and replaces that with 514 | # url(/misc_/MyProduct/close.1223555.png) if possible. 515 | new_content = False 516 | if getattr(product, 'misc_infinite_aliases', {}): 517 | content = file(obj.path).read() 518 | images = [] 519 | 520 | if content.count('/misc_/'): 521 | filenames = [] 522 | for org, alias in product.misc_infinite_aliases.items(): 523 | if anyTrue(org.endswith, ('.gif','.jpg','.png')): 524 | filenames.append(org) 525 | regex = 'url\(["\']*/misc_/%s/(' % product + \ 526 | '|'.join([re.escape(x) for x in filenames]) + \ 527 | ')["\']*\)' 528 | regex = re.compile(regex) 529 | def replacer(g): 530 | whole = g.group() 531 | better_filename = product.misc_infinite_aliases.get(g.groups()[0], g.groups()[0]) 532 | whole = whole.replace(g.groups()[0], better_filename) 533 | return whole 534 | new_content = regex.sub(replacer, content) 535 | else: 536 | def replacer(match): 537 | filepath, filename = match.groups()[0].rsplit('/', 1) 538 | 539 | try: 540 | image_product = _find_related_context(product, filepath) 541 | except AttributeError: 542 | #import warnings 543 | import logging 544 | logging.warn("Unable to find image product of %s from %r" % \ 545 | (product, filepath)) 546 | return match.group() 547 | aliased = getattr(image_product, 'misc_infinite_aliases', 548 | {}).get(filename) 549 | if aliased: 550 | return match.group().replace(filename, aliased) 551 | else: 552 | return match.group() 553 | #new_content = content 554 | new_content = referred_css_images_regex.sub(replacer, content) 555 | 556 | if new_content: 557 | filepath = _getAutogeneratedFilepath(obj.path + '-aliased.css') 558 | open(filepath, 'wb').write(new_content) 559 | setattr(obj, 'path', filepath) 560 | 561 | 562 | # set up an alias too with near infinit max_age 563 | a, b = os.path.splitext(filename) 564 | objectid_alias = a + '.%s' % mtime + b 565 | setattr(product, 566 | objectid_alias, 567 | BetterImageFile(obj.path, Globals, 568 | set_expiry_header=set_expiry_header, 569 | max_age_development=EXPIRY_INFINITY, # 5 years 570 | max_age_production=EXPIRY_INFINITY # 5 years 571 | ) 572 | ) 573 | # make a note of the alias 574 | if hasattr(product, 'misc_infinite_aliases'): 575 | aliases = product.misc_infinite_aliases 576 | else: 577 | aliases = {} 578 | 579 | if objectid in aliases: 580 | # this is not supposed to happen in the same sense that you can't have 581 | # two files by the same name in the same directory 582 | 583 | if getEnvBool('PREVENT_DUPLICATE_STATIC_STORAGE', False): 584 | # by default we don't worry about that much 585 | raise ValueError, "%r is already defined in %s.misc_infinite_aliases" %\ 586 | (objectid, product) 587 | 588 | aliases[objectid] = objectid_alias 589 | setattr(product, 'misc_infinite_aliases', aliases) 590 | 591 | if gzip_if_possible: 592 | setattr(product, objectid, 593 | GzippedFile(filepath, Globals, 594 | set_expiry_header=set_expiry_header, 595 | max_age_development=max_age_development, 596 | max_age_production=max_age_production) 597 | ) 598 | 599 | # also set up the alias which overwrites the previously 600 | # set up alias. 601 | setattr(product, objectid_alias, 602 | GzippedFile(filepath, Globals, 603 | set_expiry_header=set_expiry_header, 604 | max_age_development=EXPIRY_INFINITY, 605 | max_age_production=EXPIRY_INFINITY) 606 | ) 607 | 608 | 609 | 610 | def registerImages(product, images, Globals=globals(), rel_path='www', 611 | max_age_development=60, max_age_production=3600, 612 | set_expiry_header=False, 613 | use_rel_path_in_alias=False): 614 | """ register all images """ 615 | result = _registerFiles(registerImage, product, images, Globals, rel_path=rel_path, 616 | max_age_development=max_age_development, 617 | max_age_production=max_age_production, 618 | set_expiry_header=set_expiry_header, 619 | use_rel_path_in_alias=use_rel_path_in_alias) 620 | return result 621 | 622 | def registerIcons(product, images, Globals=globals()): 623 | """ legacy name of function """ 624 | import warnings 625 | warnings.warn("Please use registerImages() instead", 626 | DeprecationWarning, 2) 627 | 628 | registerImages(product, images, Globals) 629 | 630 | 631 | def registerImage(product, filename, idreplacer={}, epath=None, 632 | Globals=globals(), rel_path='www', 633 | set_expiry_header=False, 634 | max_age_development=60, max_age_production=3600, 635 | use_rel_path_in_alias=False): 636 | # A helper function that takes an image filename (assumed 637 | # to live in a 'www' subdirectory of this package). It 638 | # creates an ImageFile instance and adds it as an attribute 639 | # of misc_.MyPackage of the zope application object (note 640 | # that misc_.MyPackage has already been created by the product 641 | # initialization machinery by the time registerIcon is called). 642 | p_home = package_home(Globals) # product home 643 | 644 | if use_rel_path_in_alias: 645 | objectid = filename 646 | else: 647 | objectid = os.path.basename(filename) 648 | 649 | if epath: 650 | path = "%s/%s/" % (rel_path, epath) 651 | else: 652 | path = "%s/" % (rel_path) 653 | 654 | 655 | #fullpath = '%s%s' % (path, filename) 656 | if os.path.isfile(os.path.join(path, filename)): 657 | fullpath = os.path.join(path, filename) 658 | else: 659 | # let's try to manually guess it 660 | fullpath = os.path.join(p_home, path, filename) 661 | fullpath = fullpath.replace('//','/') 662 | 663 | for k,v in idreplacer.items(): 664 | objectid = objectid.replace(k,v) 665 | 666 | mtime = os.stat(fullpath)[stat.ST_MTIME] 667 | a, b = os.path.splitext(objectid) 668 | 669 | objectid_alias = a + '.%s' % mtime + b 670 | 671 | setattr(product, 672 | objectid, 673 | BetterImageFile(fullpath, Globals, 674 | set_expiry_header=set_expiry_header, 675 | max_age_development=max_age_development, 676 | max_age_production=max_age_production) 677 | ) 678 | # set up an alias with near infinite max_age 679 | setattr(product, 680 | objectid_alias, 681 | BetterImageFile(fullpath, Globals, 682 | set_expiry_header=set_expiry_header, 683 | max_age_development=EXPIRY_INFINITY, 684 | max_age_production=EXPIRY_INFINITY) 685 | ) 686 | 687 | # make a non-persistent note of this added available alias 688 | if base_hasattr(product, 'misc_infinite_aliases'): 689 | aliases = product.misc_infinite_aliases 690 | else: 691 | aliases = {} 692 | aliases[objectid] = objectid_alias 693 | setattr(product, 'misc_infinite_aliases', aliases) 694 | 695 | 696 | def registerIcon(product, filename, idreplacer={}, epath=None, 697 | Globals=globals()): 698 | import warnings 699 | warnings.warn("Please use registerImage() instead", 700 | DeprecationWarning, 2) 701 | return registerImage(product, filename, 702 | idreplacer=idreplacer, 703 | epath=epath, 704 | Globals=Globals) 705 | 706 | 707 | def _registerFiles(handler, product, files, Globals, rel_path='www', **kw): 708 | """ I really shold right a comment here """ 709 | all = {} 710 | number = 0 711 | for each in files: 712 | if isinstance(each, basestring): 713 | names = each 714 | dirname = '' 715 | elif isinstance(each, (list, tuple)): 716 | dirname = '' 717 | names = ','.join(each) 718 | else: 719 | names = each['n'] 720 | dirname = each.get('d','') 721 | 722 | 723 | if isinstance(names, str) and len(names.split(':')) > 1: 724 | names = names.split(':') 725 | names = [x.strip() for x in names] 726 | elif type(names)==type('s'): 727 | names = [names] 728 | 729 | prev = all.get(dirname, []) 730 | prev.extend(names) 731 | all[dirname] = prev 732 | for dirname, names in all.items(): 733 | for name in names: 734 | handler(product, name, epath=dirname, Globals=Globals, 735 | rel_path=rel_path, **kw) 736 | number += 1 737 | return number 738 | 739 | 740 | def dont_slim_file(filepath): 741 | """ return true if there's enough evidence that the file shouldn't 742 | be slimmed. Note that this has nothing to do with manual configuration 743 | just other factors such as filename and presence of marker files. 744 | 745 | The evidence for NOT slimming is: 746 | 1. Is there a .noslim ? 747 | 2. Does the filename contain .packed. or something similar 748 | """ 749 | assert os.path.isfile(filepath), \ 750 | "Can't test for reason not to slim because %s doesn't exist" % filepath 751 | 752 | if os.path.isfile(filepath+'.noslim'): 753 | return True 754 | 755 | for part in ('-min.', '.min.', '-packed.','.packed'): 756 | if os.path.basename(filepath).find(part) > -1: 757 | return True 758 | 759 | # default is False, i.e. there's NO reason not to slim 760 | return False 761 | 762 | -------------------------------------------------------------------------------- /tests/testZope.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-1 -* 2 | 3 | """FriedZopeBase Utils ZopeTestCase 4 | """ 5 | import os, re, sys 6 | import stat 7 | import StringIO 8 | import gzip 9 | from time import time 10 | if __name__ == '__main__': 11 | execfile(os.path.join(sys.path[0], 'framework.py')) 12 | 13 | from classes import TestBase 14 | 15 | try: 16 | from slimmer import js_slimmer, css_slimmer 17 | except ImportError: 18 | print >>sys.stderr, "slimmer not installed" 19 | js_slimmer = css_slimmer = None 20 | 21 | from App.Common import rfc1123_date 22 | from Products.FriedZopeBase.Zope import EXPIRY_INFINITY 23 | from Products.FriedZopeBase.Zope import registerImage, registerCSSFile, registerJSFile 24 | from Products.FriedZopeBase.Zope import GzippedFile, BetterImageFile 25 | from Products.FriedZopeBase.Zope import registerJSFiles, registerCSSFiles 26 | 27 | right_here = lambda x: os.path.join(os.path.dirname(__file__), x) 28 | 29 | def _gzip2ascii(gzipped_content): 30 | compressedstream = StringIO.StringIO(gzipped_content) 31 | gzipper = gzip.GzipFile(fileobj=compressedstream) 32 | return gzipper.read() 33 | 34 | def _get_autogenerated_dir(): 35 | # XXX Perhaps use an environment variable here instead 36 | # that puts the files in /var/www/cache/myproject/ 37 | return os.path.join(right_here('.'), '.autogenerated') 38 | 39 | class TestZope(TestBase): 40 | 41 | def tearDown(self): 42 | autogenerated_dir = _get_autogenerated_dir() 43 | if os.path.isdir(autogenerated_dir): 44 | for filename in os.listdir(autogenerated_dir): 45 | os.remove(os.path.join(autogenerated_dir, filename)) 46 | os.rmdir(autogenerated_dir) 47 | TestBase.tearDown(self) 48 | 49 | def test_basic_BetterImageFile(self): 50 | """ test the BetterImageFile class """ 51 | class MyProduct: 52 | pass 53 | mtime = os.stat(right_here('image.jpg'))[stat.ST_MTIME] 54 | registerImage(MyProduct, right_here('image.jpg'), rel_path='tests') 55 | instance = MyProduct() 56 | 57 | img = getattr(instance, 'image.jpg', None) 58 | self.assertNotEqual(img, None) 59 | 60 | # expect also that it creates a file called 61 | # image..jpg 62 | img_infinite = getattr(instance, 'image.%s.jpg' % mtime, None) 63 | self.assertNotEqual(img_infinite, None) 64 | 65 | # expect this to be a BetterImageFile instance 66 | self.assertEqual(img.__class__, BetterImageFile) 67 | self.assertTrue(isinstance(img, BetterImageFile)) 68 | 69 | # rendering it should return a big binary content load 70 | self.assertEqual(len(str(img)), os.stat(right_here('image.jpg'))[stat.ST_SIZE]) 71 | self.assertEqual(len(str(img_infinite)), os.stat(right_here('image.jpg'))[stat.ST_SIZE]) 72 | 73 | # there should now be a dict set in the instance called misc_infinite_aliases 74 | # which links 'image.jpg' to 'image..jpg' 75 | self.assertTrue(bool(getattr(instance, 'misc_infinite_aliases'))) 76 | aliases = instance.misc_infinite_aliases 77 | self.assertTrue(isinstance(aliases, dict)) 78 | 79 | self.assertTrue('image.jpg' in aliases) 80 | 81 | self.assertEqual(aliases.get('image.jpg'), 'image.%s.jpg' % mtime) 82 | 83 | # if we render it with img.index_html() expect certain headers to be set 84 | REQUEST = self.app.REQUEST 85 | RESPONSE = REQUEST.RESPONSE 86 | bin_content = img.index_html(REQUEST, RESPONSE) 87 | 88 | self.assertEqual(RESPONSE.getHeader('last-modified'), 89 | rfc1123_date(os.stat(right_here('image.jpg'))[stat.ST_MTIME])) 90 | self.assertEqual(RESPONSE.getHeader('cache-control'), 'public,max-age=3600') # default 91 | self.assertEqual(RESPONSE.getHeader('content-type'), 'image/jpeg') 92 | self.assertEqual(int(RESPONSE.getHeader('content-length')), 93 | os.stat(right_here('image.jpg'))[stat.ST_SIZE]) 94 | 95 | 96 | # if we render the infinitely cached one we can expect different headers 97 | bin_content = img_infinite.index_html(REQUEST, RESPONSE) 98 | 99 | self.assertEqual(RESPONSE.getHeader('last-modified'), 100 | rfc1123_date(os.stat(right_here('image.jpg'))[stat.ST_MTIME])) 101 | self.assertEqual(RESPONSE.getHeader('cache-control'), 'public,max-age=%s' % EXPIRY_INFINITY) # 102 | self.assertEqual(RESPONSE.getHeader('content-type'), 'image/jpeg') 103 | self.assertEqual(int(RESPONSE.getHeader('content-length')), 104 | os.stat(right_here('image.jpg'))[stat.ST_SIZE]) 105 | 106 | 107 | def test_set_expiry_header(self): 108 | """ if you register the image with @set_expiry_header=True, expect another header 109 | called 'Expires' 110 | """ 111 | class MyProduct: 112 | pass 113 | 114 | registerImage(MyProduct, 'image.jpg', rel_path='tests', set_expiry_header=True) 115 | instance = MyProduct() 116 | img = getattr(instance, 'image.jpg') 117 | 118 | REQUEST = self.app.REQUEST 119 | RESPONSE = REQUEST.RESPONSE 120 | bin_content = img.index_html(REQUEST, RESPONSE) 121 | 122 | self.assertEqual(RESPONSE.getHeader('last-modified'), 123 | rfc1123_date(os.stat(right_here('image.jpg'))[stat.ST_MTIME])) 124 | self.assertEqual(RESPONSE.getHeader('cache-control'), 'public,max-age=3600') # default 125 | self.assertEqual(RESPONSE.getHeader('content-type'), 'image/jpeg') 126 | self.assertEqual(int(RESPONSE.getHeader('content-length')), 127 | os.stat(right_here('image.jpg'))[stat.ST_SIZE]) 128 | 129 | self.assertEqual(RESPONSE.getHeader('expires'), 130 | rfc1123_date(time()+img.max_age)) 131 | 132 | # expect a expires header for the infinitely cached alias 133 | img_infinite_name = instance.misc_infinite_aliases['image.jpg'] 134 | img_infinite = getattr(instance, img_infinite_name) 135 | bin_content = img_infinite.index_html(REQUEST, RESPONSE) 136 | self.assertEqual(RESPONSE.getHeader('expires'), 137 | rfc1123_date(time()+EXPIRY_INFINITY)) 138 | 139 | 140 | 141 | 142 | def test_large_BetterImageFile(self): 143 | """ test the BetterImageFile() class but with larger images """ 144 | class MyProduct: 145 | pass 146 | 147 | registerImage(MyProduct, 'large.jpg', rel_path='tests', set_expiry_header=True) 148 | instance = MyProduct() 149 | img = getattr(instance, 'large.jpg') 150 | 151 | REQUEST = self.app.REQUEST 152 | RESPONSE = REQUEST.RESPONSE 153 | bin_content = img.index_html(REQUEST, RESPONSE) 154 | 155 | # make sure it's an iterator we received 156 | self.assertTrue(isinstance(bin_content, open)) 157 | 158 | _content = bin_content.read() 159 | self.assertEqual(len(_content), os.stat(right_here('large.jpg'))[stat.ST_SIZE]) 160 | 161 | 162 | def test_registerCSSFile(self): 163 | class MyProduct: 164 | pass 165 | 166 | registerCSSFile(MyProduct, 'test.css', rel_path='tests', 167 | set_expiry_header=True, 168 | slim_if_possible=False 169 | ) 170 | instance = MyProduct() 171 | static = getattr(instance, 'test.css') 172 | 173 | test_css_stat = os.stat(right_here('test.css')) 174 | 175 | # run through the same tests as above but with this 176 | mtime = test_css_stat[stat.ST_MTIME] 177 | static_infinite = getattr(instance, 'test.%s.css' % mtime, None) 178 | self.assertNotEqual(static_infinite, None) 179 | 180 | self.assertEqual(len(str(static)), test_css_stat[stat.ST_SIZE]) 181 | self.assertEqual(len(str(static_infinite)), test_css_stat[stat.ST_SIZE]) 182 | 183 | # if we render it with img.index_html() expect certain headers to be set 184 | REQUEST = self.app.REQUEST 185 | RESPONSE = REQUEST.RESPONSE 186 | bin_content = static.index_html(REQUEST, RESPONSE) 187 | 188 | self.assertEqual(RESPONSE.getHeader('last-modified'), 189 | rfc1123_date(os.stat(right_here('test.css'))[stat.ST_MTIME])) 190 | self.assertEqual(RESPONSE.getHeader('cache-control'), 'public,max-age=3600') # default 191 | self.assertEqual(RESPONSE.getHeader('content-type'), 'text/css') 192 | self.assertEqual(int(RESPONSE.getHeader('content-length')), 193 | os.stat(right_here('test.css'))[stat.ST_SIZE]) 194 | 195 | 196 | # if we render the infinitely cached one we can expect different headers 197 | bin_content = static_infinite.index_html(REQUEST, RESPONSE) 198 | 199 | self.assertEqual(RESPONSE.getHeader('last-modified'), 200 | rfc1123_date(os.stat(right_here('test.css'))[stat.ST_MTIME])) 201 | self.assertEqual(RESPONSE.getHeader('cache-control'), 'public,max-age=%s' % EXPIRY_INFINITY) # 202 | self.assertEqual(RESPONSE.getHeader('content-type'), 'text/css') 203 | self.assertEqual(int(RESPONSE.getHeader('content-length')), 204 | os.stat(right_here('test.css'))[stat.ST_SIZE]) 205 | 206 | 207 | def test_registerJSFile(self): 208 | class MyProduct: 209 | pass 210 | 211 | registerJSFile(MyProduct, 'test.js', rel_path='tests', 212 | set_expiry_header=True, 213 | slim_if_possible=False) 214 | instance = MyProduct() 215 | static = getattr(instance, 'test.js') 216 | 217 | # run through the same tests as above but with this 218 | mtime = os.stat(right_here('test.js'))[stat.ST_MTIME] 219 | static_infinite = getattr(instance, 'test.%s.js' % mtime, None) 220 | self.assertNotEqual(static_infinite, None) 221 | 222 | self.assertEqual(len(str(static)), os.stat(right_here('test.js'))[stat.ST_SIZE]) 223 | self.assertEqual(len(str(static_infinite)), os.stat(right_here('test.js'))[stat.ST_SIZE]) 224 | 225 | # if we render it with img.index_html() expect certain headers to be set 226 | REQUEST = self.app.REQUEST 227 | RESPONSE = REQUEST.RESPONSE 228 | bin_content = static.index_html(REQUEST, RESPONSE) 229 | 230 | self.assertEqual(RESPONSE.getHeader('last-modified'), 231 | rfc1123_date(os.stat(right_here('test.js'))[stat.ST_MTIME])) 232 | self.assertEqual(RESPONSE.getHeader('cache-control'), 'public,max-age=3600') # default 233 | self.assertEqual(RESPONSE.getHeader('content-type'), 'application/x-javascript') 234 | self.assertEqual(int(RESPONSE.getHeader('content-length')), 235 | os.stat(right_here('test.js'))[stat.ST_SIZE]) 236 | 237 | 238 | # if we render the infinitely cached one we can expect different headers 239 | bin_content = static_infinite.index_html(REQUEST, RESPONSE) 240 | 241 | self.assertEqual(RESPONSE.getHeader('last-modified'), 242 | rfc1123_date(os.stat(right_here('test.js'))[stat.ST_MTIME])) 243 | self.assertEqual(RESPONSE.getHeader('cache-control'), 'public,max-age=%s' % EXPIRY_INFINITY) # 244 | self.assertEqual(RESPONSE.getHeader('content-type'), 'application/x-javascript') 245 | self.assertEqual(int(RESPONSE.getHeader('content-length')), 246 | os.stat(right_here('test.js'))[stat.ST_SIZE]) 247 | 248 | 249 | def test_registering_with_slimming_basic(self): 250 | try: 251 | from slimmer import js_slimmer, css_slimmer 252 | except ImportError: 253 | # not possible to test this 254 | return 255 | 256 | class MyProduct: 257 | pass 258 | 259 | 260 | instance = MyProduct() 261 | 262 | registerJSFile(MyProduct, 'test.js', rel_path='tests', 263 | set_expiry_header=True, 264 | slim_if_possible=True) 265 | 266 | static = getattr(instance, 'test.js') 267 | self.assertEqual(str(static), js_slimmer(open(right_here('test.js')).read())) 268 | 269 | # this will have created a file called 'test.js-slimmed.js' whose content 270 | # is the same as str(static) 271 | copy_test_js = os.path.join(_get_autogenerated_dir(), 'test.js-slimmed.js') 272 | self.assertTrue(os.path.isfile(copy_test_js)) 273 | self.assertEqual(open(copy_test_js).read(), str(static)) 274 | # and it that directory there should not be any other files 275 | for f in os.listdir(os.path.dirname(copy_test_js)): 276 | self.assertEqual(f, os.path.basename(copy_test_js)) 277 | 278 | registerCSSFile(MyProduct, 'test.css', rel_path='tests', 279 | set_expiry_header=True, 280 | slim_if_possible=True 281 | ) 282 | static = getattr(instance, 'test.css') 283 | self.assertEqual(str(static), css_slimmer(open(right_here('test.css')).read())) 284 | 285 | # this will have created a file called 'test.css-slimmed.css' whose content 286 | # is the same as str(static) 287 | copy_test_css = os.path.join(_get_autogenerated_dir(), 'test.css-slimmed.css') 288 | self.assertTrue(os.path.isfile(copy_test_css)) 289 | self.assertEqual(open(copy_test_css).read(), str(static)) 290 | # and it that directory there should not be any other files other 291 | # than the one we made before called test.js-slimmed.js 292 | for f in os.listdir(os.path.dirname(copy_test_css)): 293 | self.assertTrue(f == os.path.basename(copy_test_js) or \ 294 | f == os.path.basename(copy_test_css)) 295 | 296 | 297 | # if you don it again it should just overwrite the old one 298 | registerCSSFile(MyProduct, 'test.css', rel_path='tests', 299 | set_expiry_header=True, 300 | slim_if_possible=True, 301 | 302 | ) 303 | 304 | static = getattr(instance, 'test.css') 305 | # there should still only be two files in the autogenerated directory 306 | self.assertEqual(len(os.listdir(_get_autogenerated_dir())), 2) 307 | 308 | 309 | def test_registering_with_slimming_and_gzip(self): 310 | try: 311 | from slimmer import js_slimmer, css_slimmer 312 | except ImportError: 313 | # not possible to test this 314 | return 315 | 316 | class MyProduct: 317 | pass 318 | 319 | 320 | instance = MyProduct() 321 | 322 | registerJSFile(MyProduct, 'test.js', rel_path='tests', 323 | set_expiry_header=True, 324 | slim_if_possible=True, 325 | gzip_if_possible=True) 326 | 327 | static = getattr(instance, 'test.js') 328 | # Note, using str(static) means it does not send the Accept-Encoding: gzip 329 | # header. 330 | self.assertEqual(str(static), 331 | js_slimmer(open(right_here('test.js')).read())) 332 | 333 | 334 | # this will have created a file called 'test.js-slimmed.js' whose content 335 | # is the same as str(static) 336 | copy_test_js = os.path.join(_get_autogenerated_dir(), 'test.js-slimmed.js') 337 | self.assertTrue(os.path.isfile(copy_test_js)) 338 | self.assertEqual(open(copy_test_js).read(), str(static)) 339 | # it would also have generated another copy called 340 | # test.js-slimmed.js.gz 341 | copy_test_js_gz = os.path.join(_get_autogenerated_dir(), 'test.js-slimmed.js.gz') 342 | self.assertTrue(os.path.isfile(copy_test_js_gz)) 343 | 344 | self.assertEqual(gzip.open(copy_test_js_gz).read(), 345 | str(static)) 346 | 347 | 348 | registerCSSFile(MyProduct, 'test.css', rel_path='tests', 349 | set_expiry_header=True, 350 | slim_if_possible=True, 351 | gzip_if_possible=True) 352 | static = getattr(instance, 'test.css') 353 | self.assertEqual(str(static), css_slimmer(open(right_here('test.css')).read())) 354 | 355 | # this will have created a file called 'test.css-slimmed.css' whose content 356 | # is the same as str(static) 357 | copy_test_css = os.path.join(_get_autogenerated_dir(), 'test.css-slimmed.css') 358 | self.assertTrue(os.path.isfile(copy_test_css)) 359 | self.assertEqual(open(copy_test_css).read(), str(static)) 360 | # it would also have generated another copy called 361 | # test.css-slimmed.css.gz 362 | copy_test_css_gz = os.path.join(_get_autogenerated_dir(), 'test.css-slimmed.css.gz') 363 | self.assertTrue(os.path.isfile(copy_test_css_gz)) 364 | 365 | self.assertEqual(gzip.open(copy_test_css_gz).read(), 366 | str(static)) 367 | 368 | # if you don it AGAIN it should just overwrite the old one 369 | registerCSSFile(MyProduct, 'test.css', rel_path='tests', 370 | set_expiry_header=True, 371 | slim_if_possible=True, 372 | gzip_if_possible=True) 373 | 374 | static = getattr(instance, 'test.css') 375 | self.assertEqual(str(static), 376 | css_slimmer(open(right_here('test.css')).read())) 377 | 378 | 379 | def test_registering_with_slimming_and_images_in_css(self): 380 | try: 381 | from slimmer import js_slimmer, css_slimmer 382 | except ImportError: 383 | # not possible to test this 384 | return 385 | 386 | class MyProduct: 387 | pass 388 | 389 | 390 | instance = MyProduct() 391 | 392 | # it will only fix images that have otherwise been registered 393 | registerImage(MyProduct, right_here('image.jpg'), rel_path='tests') 394 | 395 | registerCSSFile(MyProduct, 'containsimages.css', rel_path='tests', 396 | set_expiry_header=True, 397 | slim_if_possible=True, 398 | replace_images_with_aliases=True) 399 | static = getattr(instance, 'containsimages.css') 400 | 401 | # Not the same... 402 | self.assertNotEqual(str(static), 403 | css_slimmer(open(right_here('containsimages.css') 404 | ).read())) 405 | # unless you remove all '.\d+.' 406 | self.assertEqual(re.sub('\.\d{10,11}\.', '.', str(static)), 407 | css_slimmer(open(right_here('containsimages.css') 408 | ).read())) 409 | 410 | # because we haven't registered large.jpg it won't be aliased 411 | self.assertTrue('large.jpg' in str(static)) 412 | self.assertTrue('image.jpg' not in str(static)) 413 | 414 | self.assertEqual(sorted(['containsimages.css-slimmed.css-aliased.css', 415 | 'containsimages.css-slimmed.css']), 416 | sorted(os.listdir(_get_autogenerated_dir()))) 417 | 418 | # if you don it again it should just overwrite the old one 419 | registerCSSFile(MyProduct, 'containsimages.css', rel_path='tests', 420 | set_expiry_header=True, 421 | slim_if_possible=True, 422 | replace_images_with_aliases=True 423 | ) 424 | 425 | self.assertEqual(sorted(['containsimages.css-slimmed.css-aliased.css', 426 | 'containsimages.css-slimmed.css']), 427 | sorted(os.listdir(_get_autogenerated_dir()))) 428 | 429 | 430 | def test_registering_with_slimming_and_gzip_and_images_in_css(self): 431 | try: 432 | from slimmer import js_slimmer, css_slimmer 433 | except ImportError: 434 | # not possible to test this 435 | return 436 | 437 | class MyProduct: 438 | pass 439 | 440 | 441 | instance = MyProduct() 442 | 443 | registerJSFile(MyProduct, 'test.js', rel_path='tests', 444 | set_expiry_header=True, 445 | slim_if_possible=True) 446 | 447 | static = getattr(instance, 'test.js') 448 | self.assertEqual(str(static), js_slimmer(open(right_here('test.js')).read())) 449 | 450 | registerCSSFile(MyProduct, 'test.css', rel_path='tests', 451 | set_expiry_header=True, 452 | slim_if_possible=True, 453 | gzip_if_possible=True) 454 | static = getattr(instance, 'test.css') 455 | self.assertEqual(str(static), css_slimmer(open(right_here('test.css')).read())) 456 | 457 | # if you don it again it should just overwrite the old one 458 | registerCSSFile(MyProduct, 'test.css', rel_path='tests', 459 | set_expiry_header=True, 460 | slim_if_possible=True, 461 | gzip_if_possible=True) 462 | 463 | static = getattr(instance, 'test.css') 464 | self.assertEqual(str(static), css_slimmer(open(right_here('test.css')).read())) 465 | 466 | 467 | 468 | 469 | 470 | 471 | def test_basic_GzippedImageFile(self): 472 | """ test registering a css file or a js file with gzip_if_possible=True """ 473 | # first without also slimming 474 | class MyProduct: 475 | pass 476 | 477 | registerJSFile(MyProduct, 'test.js', rel_path='tests', 478 | set_expiry_header=True, 479 | slim_if_possible=False, 480 | gzip_if_possible=True) 481 | instance = MyProduct() 482 | static = getattr(instance, 'test.js') 483 | 484 | REQUEST = self.app.REQUEST 485 | RESPONSE = REQUEST.RESPONSE 486 | bin_content = static.index_html(REQUEST, RESPONSE) 487 | 488 | self.assertEqual(RESPONSE.getHeader('last-modified'), 489 | rfc1123_date(os.stat(right_here('test.js'))[stat.ST_MTIME])) 490 | self.assertEqual(RESPONSE.getHeader('cache-control'), 'public,max-age=3600') # default 491 | self.assertEqual(RESPONSE.getHeader('content-type'), 'application/x-javascript') 492 | self.assertEqual(int(RESPONSE.getHeader('content-length')), 493 | os.stat(right_here('test.js'))[stat.ST_SIZE]) 494 | 495 | # since our REQUEST does't have gzip in HTTP_ACCEPT_ENCODING 496 | # we're not going to get the gzipped version 497 | self.assertEqual(len(bin_content), 498 | os.stat(right_here('test.js'))[stat.ST_SIZE]) 499 | 500 | # now change HTTP_ACCEPT_ENCODING 501 | REQUEST.set('HTTP_ACCEPT_ENCODING','gzip') 502 | bin_content = static.index_html(REQUEST, RESPONSE) 503 | 504 | self.assertNotEqual(len(bin_content), 505 | os.stat(right_here('test.js'))[stat.ST_SIZE]) 506 | 507 | data = _gzip2ascii(bin_content) 508 | self.assertEqual(data, 509 | open(right_here('test.js')).read()) 510 | 511 | 512 | def test_large_GzippedImageFile(self): 513 | """ test registering a css file or a js file with gzip_if_possible=True """ 514 | class MyProduct: 515 | pass 516 | 517 | # first without also slimming 518 | registerJSFile(MyProduct, 'large.js', rel_path='tests', 519 | set_expiry_header=True, 520 | slim_if_possible=False, 521 | gzip_if_possible=True) 522 | instance = MyProduct() 523 | static = getattr(instance, 'large.js') 524 | 525 | REQUEST = self.app.REQUEST 526 | RESPONSE = REQUEST.RESPONSE 527 | bin_content = static.index_html(REQUEST, RESPONSE) 528 | 529 | self.assertEqual(RESPONSE.getHeader('last-modified'), 530 | rfc1123_date(os.stat(right_here('large.js'))[stat.ST_MTIME])) 531 | self.assertEqual(RESPONSE.getHeader('cache-control'), 'public,max-age=3600') # default 532 | self.assertEqual(RESPONSE.getHeader('content-type'), 'application/x-javascript') 533 | self.assertEqual(int(RESPONSE.getHeader('content-length')), 534 | os.stat(right_here('large.js'))[stat.ST_SIZE]) 535 | 536 | # since our REQUEST does't have gzip in HTTP_ACCEPT_ENCODING 537 | # we're not going to get the gzipped version 538 | self.assertEqual(len(bin_content), 539 | os.stat(right_here('large.js'))[stat.ST_SIZE]) 540 | 541 | # now change HTTP_ACCEPT_ENCODING 542 | REQUEST.set('HTTP_ACCEPT_ENCODING','gzip') 543 | bin_content = static.index_html(REQUEST, RESPONSE) 544 | self.assertTrue(isinstance(bin_content, open)) 545 | _content = bin_content.read() 546 | 547 | self.assertNotEqual(len(_content), 548 | os.stat(right_here('large.js'))[stat.ST_SIZE]) 549 | 550 | data = _gzip2ascii(_content) 551 | self.assertEqual(data, 552 | open(right_here('large.js')).read()) 553 | 554 | 555 | def test_registerJSFiles(self): 556 | """ test the registerJSFiles() """ 557 | class MyProduct: 558 | pass 559 | 560 | # test setting a bunch of files 561 | files = ['large.js','test.js'] 562 | files.append(tuple(files)) 563 | registerJSFiles(MyProduct, files, rel_path='tests', 564 | set_expiry_header=True, 565 | slim_if_possible=False, 566 | gzip_if_possible=False) 567 | 568 | instance = MyProduct() 569 | for filename in files: 570 | if isinstance(filename, tuple): 571 | filename = ','.join(filename) 572 | static = getattr(instance, filename) 573 | self.assertTrue(isinstance(static, BetterImageFile)) 574 | 575 | 576 | def test_registerJSFiles__both_slimmed(self): 577 | """ test the registerJSFiles() with slim_if_possible=True """ 578 | if js_slimmer is None: 579 | return 580 | 581 | class MyProduct: 582 | pass 583 | 584 | # test setting a bunch of files 585 | files = ['large.js','test.js'] 586 | files.append(tuple(files)) 587 | registerJSFiles(MyProduct, files, rel_path='tests', 588 | set_expiry_header=True, 589 | slim_if_possible=True, 590 | gzip_if_possible=False) 591 | 592 | instance = MyProduct() 593 | for filename in files: 594 | if isinstance(filename, tuple): 595 | filename = ','.join(filename) 596 | static = getattr(instance, filename) 597 | self.assertTrue(isinstance(static, BetterImageFile)) 598 | rendered = str(static) 599 | # expect this to be slimmed 600 | if len(filename.split(',')) > 1: 601 | content_parts = [open(right_here(x)).read() for x in filename.split(',')] 602 | expected_content = js_slimmer('\n'.join(content_parts)) 603 | else: 604 | expected_content = js_slimmer(open(right_here(filename)).read()) 605 | self.assertEqual(rendered.strip(), expected_content.strip()) 606 | 607 | 608 | def test_registerJSFiles__one_slimmed(self): 609 | """ test the registerJSFiles() with slim_if_possible=True but one 610 | of the files shouldn't be slimmed because its filename indicates 611 | that it's already been slimmed/packed/minified. 612 | 613 | In this test, the big challange is when two js files are combined 614 | and one of them should be slimmed, the other one not slimmed. 615 | """ 616 | 617 | class MyProduct: 618 | pass 619 | 620 | files = ['test.min.js','test.js'] 621 | files.append(tuple(files)) 622 | registerJSFiles(MyProduct, files, rel_path='tests', 623 | set_expiry_header=True, 624 | slim_if_possible=True, 625 | gzip_if_possible=False) 626 | 627 | instance = MyProduct() 628 | for filename in files: 629 | if isinstance(filename, tuple): 630 | filename = ','.join(filename) 631 | static = getattr(instance, filename) 632 | self.assertTrue(isinstance(static, BetterImageFile)) 633 | rendered = str(static).strip() 634 | # expect this to be slimmed 635 | if len(filename.split(',')) > 1: 636 | 637 | content_parts = [] 638 | for filename in filename.split(','): 639 | content = open(right_here(filename)).read() 640 | if filename.find('min') > -1 or js_slimmer is None: 641 | content_parts.append(content) 642 | else: 643 | content_parts.append(js_slimmer(content)) 644 | 645 | expected_content = '\n'.join(content_parts) 646 | expected_content = expected_content.strip() 647 | 648 | else: 649 | content = open(right_here(filename)).read() 650 | if filename.find('min') > -1 or js_slimmer is None: 651 | expected_content = content 652 | else: 653 | expected_content = js_slimmer(content) 654 | expected_content = expected_content.strip() 655 | 656 | self.assertEqual(rendered, expected_content) 657 | 658 | 659 | 660 | def test_registerJSFiles__both_slimmed_and_gzipped(self): 661 | """ test the registerJSFiles() with slim_if_possible=True """ 662 | if js_slimmer is None: 663 | return 664 | 665 | class MyProduct: 666 | pass 667 | 668 | # test setting a bunch of files 669 | files = ['large.js','test.js'] 670 | files.append(tuple(files)) 671 | registerJSFiles(MyProduct, files, rel_path='tests', 672 | set_expiry_header=True, 673 | slim_if_possible=True, 674 | gzip_if_possible=True) 675 | 676 | instance = MyProduct() 677 | 678 | REQUEST = self.app.REQUEST 679 | RESPONSE = REQUEST.RESPONSE 680 | 681 | for filename in files: 682 | if isinstance(filename, tuple): 683 | filename = ','.join(filename) 684 | static = getattr(instance, filename) 685 | self.assertTrue(isinstance(static, BetterImageFile)) 686 | 687 | # if you just call static.__str__() you're not calling it 688 | # with a REQUEST that accepts gzip encoding 689 | REQUEST.set('HTTP_ACCEPT_ENCODING','gzip') 690 | bin_rendered = static.index_html(REQUEST, RESPONSE) 691 | # expect this to be slimmed 692 | if len(filename.split(',')) > 1: 693 | content_parts = [open(right_here(x)).read() for x in filename.split(',')] 694 | expected_content = js_slimmer('\n'.join(content_parts)) 695 | else: 696 | content = open(right_here(filename)).read() 697 | expected_content = js_slimmer(content) 698 | 699 | rendered = _gzip2ascii(bin_rendered) 700 | 701 | self.assertEqual(rendered.strip(), expected_content.strip()) 702 | 703 | 704 | def test_registerJSFiles__one_slimmed_and_gzipped(self): 705 | """ test the registerJSFiles() with slim_if_possible=True but one 706 | of the files shouldn't be slimmed because its filename indicates 707 | that it's already been slimmed/packed/minified. 708 | 709 | In this test, the big challange is when two js files are combined 710 | and one of them should be slimmed, the other one not slimmed. 711 | """ 712 | if js_slimmer is None: 713 | return 714 | 715 | class MyProduct: 716 | pass 717 | 718 | files = ['test.min.js','test.js'] 719 | files.append(tuple(files)) 720 | registerJSFiles(MyProduct, files, rel_path='tests', 721 | set_expiry_header=True, 722 | slim_if_possible=True, 723 | gzip_if_possible=True) 724 | 725 | REQUEST = self.app.REQUEST 726 | RESPONSE = REQUEST.RESPONSE 727 | 728 | instance = MyProduct() 729 | for filename in files: 730 | if isinstance(filename, tuple): 731 | filename = ','.join(filename) 732 | static = getattr(instance, filename) 733 | self.assertTrue(isinstance(static, BetterImageFile)) 734 | 735 | # if you just call static.__str__() you're not calling it 736 | # with a REQUEST that accepts gzip encoding 737 | REQUEST.set('HTTP_ACCEPT_ENCODING','gzip') 738 | bin_rendered = static.index_html(REQUEST, RESPONSE) 739 | 740 | # expect this to be slimmed 741 | if len(filename.split(',')) > 1: 742 | 743 | content_parts = [] 744 | for filename in filename.split(','): 745 | content = open(right_here(filename)).read() 746 | if filename.find('min') > -1: 747 | content_parts.append(content) 748 | else: 749 | if js_slimmer is None: 750 | content_parts.append(content) 751 | else: 752 | content_parts.append(js_slimmer(content)) 753 | 754 | expected_content = '\n'.join(content_parts) 755 | expected_content = expected_content.strip() 756 | 757 | else: 758 | if filename.find('min') > -1: 759 | expected_content = open(right_here(filename)).read() 760 | else: 761 | content = open(right_here(filename)).read() 762 | if js_slimmer is None: 763 | expected_content = content 764 | else: 765 | expected_content = js_slimmer(content) 766 | expected_content = expected_content.strip() 767 | 768 | 769 | rendered = _gzip2ascii(bin_rendered) 770 | 771 | self.assertEqual(rendered.strip(), expected_content) 772 | 773 | 774 | 775 | 776 | 777 | # CSSFiles 778 | 779 | def test_registerCSSFiles_basic(self): 780 | """ test the registerCSSFiles() """ 781 | 782 | class MyProduct: 783 | pass 784 | 785 | # test setting a bunch of files 786 | files = ['large.css','test.css'] 787 | files.append(tuple(files)) 788 | registerCSSFiles(MyProduct, files, rel_path='tests', 789 | set_expiry_header=True, 790 | slim_if_possible=False, 791 | gzip_if_possible=False) 792 | 793 | instance = MyProduct() 794 | for filename in files: 795 | if isinstance(filename, tuple): 796 | filename = ','.join(filename) 797 | static = getattr(instance, filename) 798 | self.assertTrue(isinstance(static, BetterImageFile)) 799 | 800 | 801 | def test_registerCSSFiles__both_slimmed(self): 802 | """ test the registerCSSFiles() with slim_if_possible=True """ 803 | if css_slimmer is None: 804 | return 805 | try: 806 | 807 | class MyProduct: 808 | pass 809 | 810 | # test setting a bunch of files 811 | files = ['large.css','test.css'] 812 | files.append(tuple(files)) 813 | registerCSSFiles(MyProduct, files, rel_path='tests', 814 | set_expiry_header=True, 815 | slim_if_possible=True, 816 | gzip_if_possible=False) 817 | 818 | instance = MyProduct() 819 | for filename in files: 820 | if isinstance(filename, tuple): 821 | filename = ','.join(filename) 822 | static = getattr(instance, filename) 823 | self.assertTrue(isinstance(static, BetterImageFile)) 824 | rendered = str(static) 825 | # expect this to be slimmed 826 | if len(filename.split(',')) > 1: 827 | content_parts = [open(right_here(x)).read() for x in filename.split(',')] 828 | expected_content = '\n'.join([css_slimmer(x) for x in content_parts]) 829 | else: 830 | expected_content = css_slimmer(open(right_here(filename)).read()) 831 | self.assertEqual(rendered.strip(), expected_content.strip()) 832 | except ImportError: 833 | pass 834 | 835 | 836 | def test_registerCSSFiles__one_slimmed(self): 837 | """ test the registerCSSFiles() with slim_if_possible=True but one 838 | of the files shouldn't be slimmed because its filename indicates 839 | that it's already been slimmed/packed/minified. 840 | 841 | In this test, the big challange is when two js files are combined 842 | and one of them should be slimmed, the other one not slimmed. 843 | """ 844 | if css_slimmer is None: 845 | return 846 | 847 | class MyProduct: 848 | pass 849 | 850 | files = ['test-min.css','test.css'] 851 | files.append(tuple(files)) 852 | registerCSSFiles(MyProduct, files, rel_path='tests', 853 | set_expiry_header=True, 854 | slim_if_possible=True, 855 | gzip_if_possible=False) 856 | 857 | instance = MyProduct() 858 | for filename in files: 859 | if isinstance(filename, tuple): 860 | filename = ','.join(filename) 861 | static = getattr(instance, filename) 862 | self.assertTrue(isinstance(static, BetterImageFile)) 863 | rendered = str(static).strip() 864 | # expect this to be slimmed partially 865 | if len(filename.split(',')) > 1: 866 | 867 | content_parts = [] 868 | for filename in filename.split(','): 869 | content = open(right_here(filename)).read() 870 | if filename.find('min') > -1: 871 | content_parts.append(content) 872 | else: 873 | content_parts.append(css_slimmer(content)) 874 | 875 | expected_content = '\n'.join(content_parts) 876 | expected_content = expected_content.strip() 877 | else: 878 | if filename.find('min') > -1: 879 | expected_content = open(right_here(filename)).read() 880 | else: 881 | expected_content = css_slimmer(open(right_here(filename)).read()) 882 | expected_content = expected_content.strip() 883 | 884 | self.assertEqual(rendered.strip(), expected_content) 885 | 886 | 887 | 888 | def test_registerCSSFiles__both_slimmed_and_gzipped(self): 889 | """ test the registerCSSFiles() with slim_if_possible=True """ 890 | if css_slimmer is None: 891 | return 892 | 893 | class MyProduct: 894 | pass 895 | 896 | # test setting a bunch of files 897 | files = ['large.css','test.css'] 898 | files.append(tuple(files)) 899 | registerCSSFiles(MyProduct, files, rel_path='tests', 900 | set_expiry_header=True, 901 | slim_if_possible=True, 902 | gzip_if_possible=True) 903 | 904 | instance = MyProduct() 905 | 906 | REQUEST = self.app.REQUEST 907 | RESPONSE = REQUEST.RESPONSE 908 | 909 | for filename in files: 910 | if isinstance(filename, tuple): 911 | filename = ','.join(filename) 912 | static = getattr(instance, filename) 913 | self.assertTrue(isinstance(static, BetterImageFile)) 914 | 915 | # if you just call static.__str__() you're not calling it 916 | # with a REQUEST that accepts gzip encoding 917 | REQUEST.set('HTTP_ACCEPT_ENCODING','gzip') 918 | bin_rendered = static.index_html(REQUEST, RESPONSE) 919 | rendered = _gzip2ascii(bin_rendered).strip() 920 | 921 | # expect this to be slimmed 922 | if len(filename.split(',')) > 1: 923 | content_parts = [open(right_here(x)).read() for x in filename.split(',')] 924 | 925 | if css_slimmer is None: 926 | expected_content = '\n'.join(content_parts) 927 | else: 928 | expected_content = '\n'.join([css_slimmer(x) for x in content_parts]) 929 | else: 930 | content = open(right_here(filename)).read() 931 | if css_slimmer is None: 932 | expected_content = content 933 | else: 934 | expected_content = css_slimmer(content) 935 | 936 | self.assertEqual(rendered.strip(), expected_content.strip()) 937 | 938 | 939 | def test_registerCSSFiles__one_slimmed_and_gzipped(self): 940 | """ test the registerCSSFiles() with slim_if_possible=True but one 941 | of the files shouldn't be slimmed because its filename indicates 942 | that it's already been slimmed/packed/minified. 943 | 944 | In this test, the big challange is when two js files are combined 945 | and one of them should be slimmed, the other one not slimmed. 946 | """ 947 | if css_slimmer is None: 948 | return 949 | 950 | class MyProduct: 951 | pass 952 | 953 | files = ['test-min.css','test.css'] 954 | files.append(tuple(files)) 955 | registerCSSFiles(MyProduct, files, rel_path='tests', 956 | set_expiry_header=True, 957 | slim_if_possible=True, 958 | gzip_if_possible=True) 959 | 960 | REQUEST = self.app.REQUEST 961 | RESPONSE = REQUEST.RESPONSE 962 | 963 | instance = MyProduct() 964 | for filename in files: 965 | if isinstance(filename, tuple): 966 | filename = ','.join(filename) 967 | static = getattr(instance, filename) 968 | self.assertTrue(isinstance(static, BetterImageFile)) 969 | 970 | # if you just call static.__str__() you're not calling it 971 | # with a REQUEST that accepts gzip encoding 972 | REQUEST.set('HTTP_ACCEPT_ENCODING','gzip') 973 | bin_rendered = static.index_html(REQUEST, RESPONSE) 974 | 975 | # expect this to be slimmed 976 | if len(filename.split(',')) > 1: 977 | 978 | content_parts = [] 979 | for filename in filename.split(','): 980 | content = open(right_here(filename)).read() 981 | if filename.find('min') > -1: 982 | content_parts.append(content) 983 | else: 984 | content_parts.append(css_slimmer(content)) 985 | 986 | expected_content = '\n'.join(content_parts) 987 | expected_content = expected_content.strip() 988 | 989 | else: 990 | if filename.find('min') > -1: 991 | expected_content = open(right_here(filename)).read() 992 | else: 993 | expected_content = css_slimmer(open(right_here(filename)).read()) 994 | expected_content = expected_content.strip() 995 | 996 | rendered = _gzip2ascii(bin_rendered) 997 | 998 | self.assertEqual(rendered.strip(), expected_content) 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | def test_suite(): 1007 | from unittest import TestSuite, makeSuite 1008 | suite = TestSuite() 1009 | suite.addTest(makeSuite(TestZope)) 1010 | return suite 1011 | 1012 | if __name__ == '__main__': 1013 | framework() 1014 | 1015 | 1016 | 1017 | --------------------------------------------------------------------------------