├── 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] == "":
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'\n'\
139 | 'a \n'\
140 | 'b \n'\
141 | ' '
142 | self.assertEqual(html, expect)
143 |
144 | html = func('name', 'b', options=['a','b'])
145 | expect = u'\n'\
146 | 'a \n'\
147 | 'b \n'\
148 | ' '
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'\n'\
154 | 'a \n'\
155 | 'b \n'\
156 | 'c \n'\
157 | ' '
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'a b '
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'a b '
178 | self.assertEqual(html, expect)
179 |
180 |
181 | html = func('name', 2, options=[1,2,3])
182 | expect = u'\n'\
183 | '1 \n'\
184 | '2 \n'\
185 | '3 \n'\
186 | ' '
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'\n'\
192 | '1 \n'\
193 | '2 \n'\
194 | '3 \n'\
195 | ' '
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'\n'\
201 | '1 \n'\
202 | '2 \n'\
203 | '3 \n'\
204 | ' '
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'\n'\
211 | '1 \n'\
212 | '2 \n'\
213 | '3 \n'\
214 | ' '
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'1 '\
222 | u'2 '
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('')]
77 | tmp_t = t
78 | for bad in regex.findall(t):
79 | if bad and bad not in alsook:
80 | t = t.replace(bad,'')
81 | each = t+each[each.find(''):]
82 | parts.append(each)
83 | return '>'.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 | $label:
45 |
46 |
47 | """
48 | template = u'\n\t%(label)s:%(mandot)s%(subsup)s \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 | $label:
64 |
65 | """
66 | template = u'%(label)s:%(mandot)s%(optional)s%(subsup)s \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 += '%(label)s ' %\
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'%s \n'%\
398 | (option, label)
399 | else:
400 | html += u'%s \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 |
--------------------------------------------------------------------------------