Mixing in new-style classes in Zope 2.7
April 9, 2008
0 comments Zope
Don't ask why I'm developing products for Zope 2.7 but I had to and I should have been more careful with these oldtimers.
I kept getting this error:
TypeError: expected 2 arguments, got 1
(notice the strange double space after the : colon)
This is different from the standard python TypeError when you get the parameters wrong which looks like this TypeError: __init__() takes exactly 2 arguments (1 given)
.
The line it complained this happened looked like this:
class MyTool(SimpleItem, UniqueObject, OtherClass):
id = 'some_tool'
meta_type = 'some meta type'
def __init__(self, id='some_tool'):
self.id = id # <--- THIS WAS THE CULPRIT LINE APPARENTLY!!
I couldn't understand what the hell was wrong on that line! Clearly it wasn't a normal Python error. Here's the explaination: That OtherClass was a new-style class inheriting from object. It looked like this:
class OtherClass(object):
...
When I changed that to:
class OtherClass:
...
The whole thing started to work. Long lesson learnt, don't use new-style classes mixed in into Zope 2.7.
Tip: Printer friendly pages with Page Templates in Zope
March 24, 2008
0 comments Zope
Since I've seen so many poor solutions to this problem I thought I'd share mine. Here's how I make printer friendly pages.
1. Add a method in your base class that looks like this:
def getMainTemplate(self):
""" return the suitable METAL header object """
# assuming zpt/main_template.zpt
template_obj = self.main_template
# assuming the "first" line of main_template.zpt to
# look like this:
# <metal:block metal:define-macro="master">
return template_obj.macros['master']
2. Change all your Page Templates to refer to a method rather than a macro directly so that pages like index_html.zpt start like this:
<html metal:use-macro="here/getMainTemplate">
I've seen the hard coded way too many times where people do something like this <html metal:use-macro="here/main_template/macros/master">
which gives you no flexibility.
3. Now make a copy of main_template.zpt
called print_main_template.zpt
and the most important change is to make print.css
load by default. Here's what it should look like this somewhere inside the <head>
tag:
<link rel="stylesheet" type="text/css" href="/screen.css" media="screen" />
<link rel="stylesheet" type="text/css" href="/print.css" />
Note how the print.css link
tag is now not conditional. Before in main_template.zpt it should have looked like this:
<link rel="stylesheet" type="text/css" href="/print.css" media="print" />
<link rel="stylesheet" type="text/css" href="/screen.css" media="screen" />
And note how the order is stacked just to be extra safe to weird browsers that don't understand the media
condition.
As a last optional feature you should add is to add these lines at the bottom of the template 'print_main_template.zpt':
<script type="text/javascript">
window.print();
</script>
</body>
</html>
Another tip is to add something like this to the footer because it becomes useful when you look at a printed copy:
<div id="footer">
Printed from <span tal:replace="here/absolute_url"></span> on
<span tal:replace="python:here.ZopeTime().strftime('%Y/%m/%d')"></span>
4. Now rewrite the method getMainTemplate()
to become usefully intelligent:
def getMainTemplate(self):
""" return the suitable METAL header object """
if self.REQUEST.get('print-version'):
# assuming zpt/print_main_template.zpt
template_obj = self.print_main_template
else:
# assuming zpt/main_template.zpt
template_obj = self.main_template
# assuming the "first" line of main_template.zpt to
# look like this:
# <metal:block metal:define-macro="master">
return template_obj.macros['master']
5. Prepare the interface now for the printer friendly page. This can be done in two different ways. One way is to put a link in the footer or byline like this:
<a href="?print-version=1">Print this page</a>
Or if you want to force a particular page to always be printer friendly, for example print_invoice.zpt
then write it like this:
<tal:item define="dummy python:request.set('print-version',1)"
replace="nothing"
/><html metal:use-macro="here/getMainTemplate">
As a final point; how you solve your web design with screen.css
and print.css
varies. One way is to define multiple css files each suitable for individual things like this example shows:
<link rel="stylesheet" type="text/css" href="/typography.css" />
<link rel="stylesheet" type="text/css" href="/print.css" media="print" />
<link rel="stylesheet" type="text/css" href="/screen.css" media="screen" />
An alternative solution is to don't expect print.css
to stand on its own two legs but only be a supplement of the general css file. When doing this you're probably just going to want to override some things and hide some other things like this example from a 'print.css':
body {
width:100% !important
}
form#login, #navigation, .also-online {
display:none
}
To conclude
This gives you a robust framework for enabling printer friendly pages that are quite different from the main template and doing it like this means that you don't have add conditional hacks to your main template that displays certain things if in printer friendly mode or not.
Most importantly, this gives you the framework for adding other versions of main template. For example these:
mobile_main_template.zpt
(guess what for)minimal_main_template.zpt
(for things like Help page popups)
A healthy and fair use of METAL macros is also key to asserting that you don't have to repeat yourself too much in the copies of main_template.pt
.
Good luck!
Mocking a Python standard library
March 14, 2008
2 comments Zope
Here's one of many things I've learnt today at PyCon. Inspired by code that Grig Gheorghiu showed in his slides on automated testing, you can monkey patch a standard library that your application is using in your unit tests to, in my case, mock a remote service without having to run a server. I've done lots of monkey-patching in Zope but then I've only been monkey patching individual methods or attributes of imported classes. This is very similar to that. Here's what my application does:
from poplib import POP3
class MyZopeApp(...):
def check4mail(self, hostname, port, user, pwd):
connection = POP3(hostname, port=port)
...download emails and process them...
Adjacent to this I have a unit/integration test that looks like this:
class TestCase(ZopeTestCase):
def test_check4mail(self):
# monkey patch!
# note that this imports a module, not a class
from Products.IssueTrackerProduct import IssueTracker
FakePOP3.files = ('test1.email',)
IssueTracker.POP3 = FakePOP3
# now check what happens when check4mail() is run
result = self.folder.tracker.check4mail()
assert ...
Now for the mock. The mock is a fake POP3 class that instead of getting its data from the network reads local filesystem files. Here's what the code for FakePOP3
is:
from poplib import POP3, error_proto
class FakePOP3(POP3):
username = 'test'
password = 'test'
files = []
def __init__(self, hostname, port=110):
self.hostname = hostname
self.port = port
def getwelcome(self):
return "Welcome to fake account"
def user(self, user):
if user != self.username:
raise error_proto("Wrong username.")
def pass_(self, pswd):
if pswd != self.password:
raise error_proto("Wrong password.")
def list(self, which=None):
# eg. ('+OK 4 messages:', ['1 71017', '2 2201', '3 7723', '4 44152'], 34)
files = self.files
responses = []
for i, f in enumerate(files):
responses.append('%s %s' % (i+1, os.stat(f)[stat.ST_SIZE]))
return ('+OK %s messages:' % len(files), responses, None)
def retr(self, which):
# ['response', ['line', ...], octets]
filename = self.files[which-1]
return ('response', open(filename, 'r').xreadlines(), None)
def quit(self):
pass
That's it! That's how you fake a POP3 server without having to run an actual mock server which could have been a solution.
logrotating all my Zope event logs
February 6, 2008
0 comments Zope, Linux
I've installed a lot of Zope instances on my laptop since version 2.7.3 and out of curiosity and desperate need for more hard drive space I thought I'd log rotate them all with the standard Linux logrotate
program.
Before doing the log rotate, the total size of all my event.log
files came to about 290Mb! After running logrotate
(twice of course to go from event.log.1 to event.log.2.gz) the total size become 20Mb. Not a huge significance in the world of gigabyte hard drives but at least something.
DateIndex in Zope doesn't have indexed attributes
October 28, 2007
0 comments Zope
This took me a while to grok and perhaps by mentioning it here, it'll prevent other people from making the same mistake as I did and perhaps preventing myself from doing the same mistake again.
In the ZCatalog, when you set up indexes you can give them a name and an index attribute. If you omit the index attribute, it'll try to hook into the objects by the name of the index. For example, if you set the index to be title
with no indexed attribute it'll fetch the title
attribute of the objects it catalogs. But if you set the indexed attribute to be something like idx_getTitle
you can do something like this in your class:
def idx_getTitle(self):
""" return title as we want it to be available in the ZCatalog """
return re.sub('<*.?>','', self.title)
The same can not be done with indexes of type DateIndex
.
I don't know why it is so but perhaps there's a good explanation that I don't understand. If you use the ZMI it's clear that you can't add an indexed attribute but it doesn't stop you from adding one in pure python like you do with all other indexes:
from ZPublisher.HTTPRequest import record
zcatalog = self.MyCatalog
indexes = zcatalog._catalog.indexes
# this works
extra = record()
extra.indexed_attrs = 'getSearchableCountry'
zcatalog.addIndex('country', 'FieldIndex', extra)
# this does NOT work!
extra = record()
extra.indexed_attrs = 'getPublishDateHourless'
zcatalog.addIndex('publish_date', 'DateIndex', extra)
If you run this code, it'll even pick up the method getPublishDateHourless
in the ZMI view of the catalog's indexes. But it's never run!
By the way, what I wanted to achieve was to index the publish date of certain objects but only index them without the hour/minute/second bit. Because I couldn't have such a "proxy method" I instead searched on the index publish_date
with a range like this:
zcatalog = self.MyCatalog
v = DateTime('2007/12/13')
query = {'query':[v, v+1], 'range':'min:max'}
return zcatalog.searchResults(publish_date=query)
Nasty human error in Zope ZEO setup
September 14, 2007
0 comments Zope
Together with my colleague Jan we today tried to fix a problem with Zope ZEO server that wouldn't start. Very strange we thought since another ZEO server on the same machine was working fine. We compared zope.conf
files and found nothing obvious. Here was the error you get with 'bin/runzeo':
root@da-ovz-vm99182:/var/lib/zope-2.8.9/aragdb-zeo1# ./bin/runzeo
Error: error opening file //var/lib/zope-2.8.9/aragdb-zeo1/etc/zeo.conf:
[Errno ftp error] no host given
What?? ftp error
??
Long story short, the error was this line in 'runzeo':
CONFIG_FILE="/${INSTANCE_HOME}/etc/zeo.conf"
which should have been this:
CONFIG_FILE="${INSTANCE_HOME}/etc/zeo.conf"
Not so easy to spot because the CONFIG_FILE
line is something you rarely look closely at.