<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-4871829230797738545</id><updated>2024-09-02T02:36:30.076-06:00</updated><category term="plone"/><category term="howto"/><category term="problem solving"/><category term="open source"/><category term="la jornada"/><category term="nitf"/><category term="testing"/><category term="zope"/><category term="buildout"/><category term="performance"/><category term="python"/><category term="community"/><category term="git"/><category term="github"/><category term="haproxy"/><category term="media"/><category term="monitoring"/><category term="webservice"/><category term="activism"/><category term="api"/><category term="code-analysis"/><category term="compositepack"/><category term="css"/><category term="eff"/><category term="grok"/><category term="haystack"/><category term="jquery"/><category term="latinoware"/><category term="literature"/><category term="load balancing"/><category term="newscodes"/><category term="newsml"/><category term="quality-assurance"/><category term="resource registries"/><category term="schemaextender"/><category term="science fiction"/><category term="tagcloud"/><category term="templates"/><category term="transmogrifier"/><category term="travis ci"/><category term="zodb"/><title type='text'>hvelarde</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>30</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-7574892615574221539</id><published>2018-09-12T15:25:00.001-05:00</published><updated>2018-09-12T15:29:45.671-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="open source"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="resource registries"/><title type='text'>Look Ma! No Resource Registries</title><content type='html'>Over the past years I have spent a lot of time trying to make our add-ons compatible with Plone 4.3 and Plone 5. This has been difficult for many reasons including differences in the API of default content types frameworks and registration of static resources, just to mention some.&lt;br /&gt;
&lt;br /&gt;
On the latest topic I&#39;ve been very critic of the approach taken and even tried to propose a different one to avoid some well known problems on &lt;a href=&quot;https://github.com/plone/Products.CMFPlone/issues/1896&quot;&gt;PLIP 1896&lt;/a&gt;. Unfortunately this was not understood at the time and I just gave up because we didn&#39;t had use cases to justify the migration to Plone 5.&lt;br /&gt;
&lt;br /&gt;
This has started to change in latest months and we can&#39;t continue kicking the can down the road any longer: &lt;a href=&quot;https://mail.python.org/pipermail/python-dev/2018-March/152348.html&quot;&gt;Python 2.7 will be EOL in 2020&lt;/a&gt;, &lt;a href=&quot;https://community.plone.org/t/6519&quot;&gt;Zope 4 has been released&lt;/a&gt;, and there has been a lot of work lately to &lt;a href=&quot;https://plone.org/news/2017/porting-plone-and-zope-to-python-3-successful-alpine-city-sprint&quot;&gt;make Plone compatible with Python 3&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
IMO, Plone 5 resource registries are complex and must be avoided for many reasons as stated in the PLIP aforementioned:&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;the revolutionary approach chosen (rewrite everything) has proven buggy and difficult to maintain, as shown by the &lt;a href=&quot;https://github.com/plone/Products.CMFPlone/labels/01%20type%3A%20bug&quot;&gt;many issues reported&lt;/a&gt; over the last years (&lt;a href=&quot;https://github.com/plone/Products.CMFPlone/issues/2512&quot;&gt;the latest&lt;/a&gt; opened just 2 days ago)&lt;/li&gt;
&lt;li&gt;some of the JavaScript tools used at the time (Bower, Grunt, RequireJS…) are less attractive nowadays than more modern options like &lt;a href=&quot;https://www.npmjs.com/&quot;&gt;npm&lt;/a&gt;, &lt;a href=&quot;https://yarnpkg.com/&quot;&gt;Yarn&lt;/a&gt; and &lt;a href=&quot;https://webpack.js.org/&quot; rel=&quot;nofollow&quot;&gt;webpack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;resource bundling was a workaround for a limitation of the HTTP/1.1 protocol and not a CMS feature; &lt;a href=&quot;http://stackoverflow.com/a/30864259/644075&quot; rel=&quot;nofollow&quot;&gt;HTTP/2 made this unnecessary and even undesirable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;LESS is now way less popular than &lt;a href=&quot;https://sass-lang.com/&quot;&gt;SASS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
I still think that every add-on author must be able to use the tools of their choice, and I still think Plone should not have to worry about resource bundling.&lt;br /&gt;
&lt;br /&gt;
In early 2016 we selected webpack for our projects and in early 2017 we developed a Buildout recipe (called &lt;a href=&quot;https://pypi.python.org/pypi/sc.recipe.staticresources/&quot;&gt;sc.recipe.staticresources&lt;/a&gt;) to make the inclusion of it into the Plone ecosystem easier. Over the past 2 years we have been developing, testing, and enhancing this approach in many add-ons and projects. But the resource registries were still there…&lt;br /&gt;
&lt;br /&gt;
Not anymore.&lt;br /&gt;
&lt;br /&gt;
I&#39;m pleased to announce the release of &lt;a href=&quot;https://pypi.org/project/collective.lazysizes/4.1.1.1/&quot;&gt;collective.lazysizes 4.1.1.1&lt;/a&gt;, the first add-on that uses a new and very opinionated approach on how to handle static resources in Plone: we just deprecated resource registries in favor of a viewlet registered in &lt;tt&gt;plone.htmlhead&lt;/tt&gt;.
This simplifies maintenance among multiple Plone versions and avoids bundling of unrelated resources.&lt;br /&gt;
&lt;br /&gt;
How does it work?&lt;br /&gt;
&lt;br /&gt;
We use webpack to generate all static resources (in our case, a JavaScript file, a page template and an icon; check the &lt;a href=&quot;https://github.com/collective/collective.lazysizes/blob/4.1.1.1/webpack/webpack.config.js&quot;&gt;webpack.config.js&lt;/a&gt; file). The page template is used for the viewlet and includes&amp;nbsp; the following code on it:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&amp;lt;script async=&quot;&quot; src=&quot;https://www.example.com/++resource++collective.lazysizes/lazysizes-43c36fc.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;br /&gt;
As you can see, the JavaScript id already includes a hash (&lt;tt&gt;lazysizes-43c36fc.js&lt;/tt&gt;), so you don&#39;t have to worry about cooking resources, neither caching the wrong file. The hash will only change if the related code changes.&lt;br /&gt;
&lt;br /&gt;
(To understand better the work done, check the following pull requests: &lt;a href=&quot;https://github.com/collective/collective.lazysizes/pull/57/files&quot;&gt;Deprecate resource registries &lt;/a&gt; and 
&lt;a href=&quot;https://github.com/collective/collective.lazysizes/pull/62/files&quot;&gt;Create JS resource with unique ID&lt;/a&gt;.)
&lt;br /&gt;
&lt;br /&gt;
What are the benefits of this for developers?&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;you can work on your project using latest state-of-the-art JavaScript technologies like &lt;a href=&quot;http://www.ecma-international.org/ecma-262/6.0/&quot;&gt;ES2015&lt;/a&gt; and &lt;a href=&quot;https://vuejs.org/&quot;&gt;Vue.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;you don&#39;t have to worry about resource registries anymore as this approach works out of the box in all Plone versions: no more duplicated registrations, cleaner and simpler ZCML and XML files&lt;/li&gt;
&lt;li&gt;you don&#39;t have to worry about upgrade steps anymore (at least the ones related with resource registry cooking/bundling)&lt;/li&gt;
&lt;li&gt;you end up with better static resources: all CSS and JS files minimized, all images optimized&lt;/li&gt;
&lt;li&gt;your code will be faster, as rendering a viewlet is less expensive than rendering a resource in the registry (at least, in Plone 4.3)&lt;/li&gt;
&lt;/ul&gt;
&amp;nbsp;What are the benefits of this for system administrators?&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;less steps to upgrade your sites&lt;/li&gt;
&lt;li&gt;less unneeded cache invalidations&lt;/li&gt;
&lt;li&gt;faster sites as resources can be loaded in parallel &lt;/li&gt;
&lt;/ul&gt;
See how easy is to add a new feature to our resources: &lt;a href=&quot;https://github.com/collective/collective.lazysizes/pull/59/files&quot;&gt;Add lazysizes print plugin&lt;/a&gt;; and how easy is to update the version of the JavaScript library we&#39;re using: &lt;a href=&quot;https://github.com/collective/collective.lazysizes/pull/60/files&quot;&gt;Upgrade lazysizes to 4.1.1&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
Now you can concentrate in the things that matter and stop worrying about the ugly parts; isn&#39;t that fun? &lt;br /&gt;
&lt;br /&gt;
Share and enjoy! </content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/7574892615574221539/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2018/09/look-ma-no-resource-registries.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/7574892615574221539'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/7574892615574221539'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2018/09/look-ma-no-resource-registries.html' title='Look Ma! No Resource Registries'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-864406555139323287</id><published>2017-12-18T15:53:00.003-06:00</published><updated>2017-12-20T12:32:08.226-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="haproxy"/><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="load balancing"/><category scheme="http://www.blogger.com/atom/ns#" term="monitoring"/><category scheme="http://www.blogger.com/atom/ns#" term="open source"/><category scheme="http://www.blogger.com/atom/ns#" term="performance"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="zope"/><title type='text'>Configuring better load balancing and health checks for Plone with HAProxy</title><content type='html'>In the &lt;a href=&quot;http://hvelarde.blogspot.com/2017/12/we-have-been-doing-health-checks-wrong.html&quot;&gt;previous post of this blog&lt;/a&gt; I enumerated some problems we face when using normal load balancer health checks on Plone instances and I described a possible solution using &lt;a href=&quot;https://pypi.python.org/pypi/five.z2monitor&quot;&gt;five.z2monitor&lt;/a&gt;. In this post I&#39;m going to show you how to configure HAProxy and other components of the stack in order to get the most of it.&lt;br /&gt;
&lt;br /&gt;
As mentioned previously, we can do load balancing using nginx, Varnish or HAProxy. Both nginx and Varnish provide more features on their commercial versions, but I&#39;m going to focus only on the community versions. In my opinion, if you ever need more features in your load balancer you should try HAProxy before buying one of those subscriptions.&lt;br /&gt;
&lt;br /&gt;
On nginx the load balancer is handled by the &lt;a href=&quot;http://nginx.org/en/docs/http/ngx_http_upstream_module.html&quot;&gt;ngx_http_upstream_module&lt;/a&gt;, it&#39;s very easy to enable and configure and it has some nice features (like backup servers and the &lt;a href=&quot;http://nginx.org/en/docs/http/ngx_http_upstream_module.html#least_conn&quot;&gt;least connections&lt;/a&gt; method), but also many limitations (like basic health checks and no queue control). This works very well on small sites.&lt;br /&gt;
&lt;br /&gt;
On Varnish the load balancer is handled by the &lt;a href=&quot;https://varnish-cache.org/docs/4.1/reference/vmod_directors.generated.html&quot;&gt;directors module&lt;/a&gt;, and I have to admit that I&#39;m not impressed by its features as it lacks support for least connections which is the chief method with Plone instances. On the other side, &lt;a href=&quot;https://varnish-cache.org/docs/4.1/reference/vcl.html#backend-definition&quot;&gt;backend definitions&lt;/a&gt; and &lt;a href=&quot;https://varnish-cache.org/docs/4.1/reference/vcl.html#reference-vcl-probes&quot;&gt;health checks&lt;/a&gt; are more configurable, but support HTTP requests  only. You may consider Varnish as a load balancer if you already use it on high traffic sites and is going to do a good and honest work.&lt;br /&gt;
&lt;br /&gt;
(Previously, I was arguing in favor of the hash method, but later tests showed that the standard round robin is less prone to instance overloads.)&lt;br /&gt;
&lt;br /&gt;
As you can see both nginx and Varnish can only handle HTTP requests for health checks and we have very limited control of request queues, something that in my experience has prove to be an issue.&lt;br /&gt;
&lt;br /&gt;
(I never care about the ngnix queue, but in Varnish I always configure a &lt;code&gt;max_connections&lt;/code&gt; directive to avoid flooding instances with requests that could make them unreachable for a long time.)&lt;br /&gt;
&lt;br /&gt;
Let&#39;s go back on track: &lt;a href=&quot;https://www.haproxy.org/&quot;&gt;HAProxy&lt;/a&gt; is a very complex piece of software; it&#39;s so complex and &lt;a href=&quot;http://cbonte.github.io/haproxy-dconv/1.8/intro.html&quot;&gt;its documentation&lt;/a&gt; so huge that I preferred just to removed it from the equation when I first had to take care of the infrastructure in our company some years ago.&lt;br /&gt;
&lt;br /&gt;
That choice proved to be right at the moment, but I always like to investigate and act as my own devil&#39;s advocate from time to time. So, when one of our main sites started receiving some very nasty DoS attacks I started playing with it again.&lt;br /&gt;
&lt;br /&gt;
It took me a whole week to prepare, test and review the configuration I&#39;m going to share with you, but first I&#39;m going to talk a little bit about our infrastructure and rationale.&lt;br /&gt;
&lt;br /&gt;
The sites I&#39;m talking about use (at least) 2 servers with a full stack: nginx, Varnish, HAProxy and Zope/Plone; both servers are configured using &lt;a href=&quot;https://github.com/zc/zrs&quot;&gt;ZODB Replicated Storage&lt;/a&gt; (ZRS), one acting as the master ZEO server and the other as the slave. All Plone instances point to both ZEO servers. When the master ZEO server fails, all instances connect automatically to the slave in read-only mode. When all instances in one server fail, Varnish connects to the HAProxy in the other server as a backup. When Varnish fails, nginx tries to use the Varnish on the other server. That&#39;s more or less how it works.&lt;br /&gt;
&lt;br /&gt;
This is our working &lt;code&gt;haproxy.cfg&lt;/code&gt; and I going to explain some choices I made:&lt;br /&gt;
&lt;br /&gt;
&lt;script src=&quot;https://gist.github.com/hvelarde/628976fcdcbb14382d200d5389242e23.js&quot;&gt;&lt;/script&gt;&lt;br /&gt;
First the standard &lt;code&gt;listen&lt;/code&gt; section: we use &lt;a href=&quot;http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4-option%20tcp-check&quot;&gt;&lt;code&gt;option tcp-check&lt;/code&gt;&lt;/a&gt; (layer 4) to make the health checks as is way faster that doing a HTTP (layer 7) check: we ask Zope on the alternate binded port for the &lt;code&gt;ok&lt;/code&gt; command and expect the &lt;code&gt;OK&lt;/code&gt; string as a result.&lt;br /&gt;
&lt;br /&gt;
In my tests layer 4 checks took just a couple of milliseconds to finish, 2 orders of magnitude less than layer 7 checks. Faster, non-blocking responses mean you can check more frequently and you&#39;re going to detect sooner any failure on the instance.&lt;br /&gt;
&lt;br /&gt;
Health checks are done every 2 seconds and, in case of failure, the &lt;a href=&quot;http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#5.2-slowstart&quot;&gt;&lt;code&gt;slowstart&lt;/code&gt;&lt;/a&gt; parameter avoids flooding an instance during it&#39;s defined warming up period of one minute.&lt;br /&gt;
&lt;br /&gt;
We inform the status of the backend upstream to Varnish using the &lt;a href=&quot;http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#monitor-uri&quot;&gt;&lt;code&gt;monitor-uri&lt;/code&gt;&lt;/a&gt; directive: if we have no backends alive, HAProxy&#39;s monitor will also fail.&lt;br /&gt;
&lt;br /&gt;
In Varnish we have have configured the backend like this:&lt;br /&gt;
&lt;br /&gt;
&lt;script src=&quot;https://gist.github.com/hvelarde/df595cb92e1928bdfaa8b046d104939f.js&quot;&gt;&lt;/script&gt;&lt;br /&gt;
Varnish will detect any backend failure in 2 seconds, at worst; Varnish will then try to use the HAProxy backend on the other server as a backup.&lt;br /&gt;
&lt;br /&gt;
Back into HAProxy&#39;s configuration, we set &lt;code&gt;&lt;a href=&quot;http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#maxconn%20%28Server%20and%20default-server%20options%29&quot;&gt;maxconn&lt;/a&gt; 4&lt;/code&gt; on backend servers to avoid sending more that 4 request to any instance; similarly, we set &lt;code&gt;&lt;a href=&quot;http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#timeout%20queue&quot;&gt;timeout queue&lt;/a&gt; 1m&lt;/code&gt; to keep the queue size under control.&lt;br /&gt;
&lt;br /&gt;
Finally, the &lt;code&gt;global&lt;/code&gt; section: you&#39;ll see we&#39;re using &lt;code&gt;&lt;a href=&quot;http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#maxconn&quot;&gt;maxconn&lt;/a&gt; 256&lt;/code&gt;; in my opinion there is no reason to use a bigger value: if you have more that 256 requests on queue, you obviously have a problem and you need more instances to serve the traffic.&lt;br /&gt;
&lt;br /&gt;
The typical stats screen of HAProxy with this configuration looks like this:&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiX32JpodKUwd008jLLlB4QMQK4DVrh9xeeoChYYKiYwsCY8lQMbD9BtmjPW5OMr8dKT4uyD-DOe80GDLIHmpqxHR5WYEHo3Z6R2AXHE0Gd2b1NzOJN0qTSl1CotziUD916x6zvIprXEdE/s1600/haproxy-stats.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;480&quot; data-original-width=&quot;1280&quot; height=&quot;120&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiX32JpodKUwd008jLLlB4QMQK4DVrh9xeeoChYYKiYwsCY8lQMbD9BtmjPW5OMr8dKT4uyD-DOe80GDLIHmpqxHR5WYEHo3Z6R2AXHE0Gd2b1NzOJN0qTSl1CotziUD916x6zvIprXEdE/
/haproxy-stats.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
As you can see, over a period of almost 25 days we have no backend failures in this server, even with a large number of individual instance failures.&lt;br /&gt;
&lt;br /&gt;
Last, but not least: if you want to cut the start up time of your instances you have to include the following directives on your buildout configuration (more about that on this &lt;a href=&quot;https://community.plone.org/t/how-to-cut-plone-instances-restart-time-by-over-90/4688?u=hvelarde&quot;&gt;Plone forum thread&lt;/a&gt;):&lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;
[instance]&lt;br /&gt;
…&lt;br /&gt;
# play with this value depending on the number of objects in your ZODB&lt;br /&gt;
zeo-client-cache-size = 256MB&lt;br /&gt;
zeo-client-client = zeoclient&lt;br /&gt;
&lt;/code&gt;
&lt;br /&gt;
Share and enjoy!</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/864406555139323287/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2017/12/configuring-better-load-balancing-and.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/864406555139323287'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/864406555139323287'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2017/12/configuring-better-load-balancing-and.html' title='Configuring better load balancing and health checks for Plone with HAProxy'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-6295532087894938256</id><published>2017-12-11T20:16:00.002-06:00</published><updated>2017-12-18T16:01:06.525-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="haproxy"/><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="monitoring"/><category scheme="http://www.blogger.com/atom/ns#" term="open source"/><category scheme="http://www.blogger.com/atom/ns#" term="performance"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="problem solving"/><category scheme="http://www.blogger.com/atom/ns#" term="python"/><category scheme="http://www.blogger.com/atom/ns#" term="zope"/><title type='text'>We have been doing health checks wrong in Plone</title><content type='html'>In the &lt;a href=&quot;http://hvelarde.blogspot.com/2016/12/plone-site-performance-threads-or.html&quot;&gt;previous post of this blog&lt;/a&gt; I was arguing on how to increase the performance of high-traffic Plone sites. As mentioned there, we have different ways to do so: increasing the number of server threads, increasing the number of instances, and increasing both.&lt;br /&gt;
&lt;br /&gt;
Increasing the number of threads is easier and consumes less memory but, as mentioned, is less resilient and can be affected by the Python GIL on multicore servers. Increasing the number of instances, on the other side, will increase the complexity of our stack as we will need to install a load balancer, a piece of hardware or software that distributes the incoming traffic across all the backend instances.&lt;br /&gt;
&lt;br /&gt;
Over the years we have tried different software load balancers depending on the traffic of a site: we use &lt;a href=&quot;http://nginx.org/en/docs/http/load_balancing.html&quot;&gt;nginx alone as web server and load balancer&lt;/a&gt; on smaller sites and we add &lt;a href=&quot;http://book.varnish-software.com/4.0/chapters/Saving_a_Request.html#directors&quot;&gt;Varnish as web accelerator and load balancer&lt;/a&gt; when the load increases; lately we started using &lt;a href=&quot;http://www.haproxy.org/&quot;&gt;HAProxy&lt;/a&gt; again to solve extreme problems on sites being continuously attacked (I&#39;ll write about on a different post).&lt;br /&gt;
&lt;br /&gt;
When you use a load balancer you have to do health checking, as the load balancer needs to know when one of the backend instances has become unavailable because is quite busy to answer further requests, has been restarted, is out for maintenance, or is simply dead. And, in my opinion, we have been doing it wrong. &lt;br /&gt;
&lt;br /&gt;
The typical configuration for health checking is to send requests to the same port used by the Zope HTTP server and this has some fundamental problems: First, the Zope HTTP server is slow to answer requests: it can take hundreds of milliseconds to answer even the most simple HEAD request.&lt;br /&gt;
&lt;br /&gt;
To make things worst, the requests that are answered by the Zope HTTP server are the slower ones (content not in ZODB cache, search results, you name it…), as the most common requests are already being served by the intermediate caches. In the tests I made, I found that even a well configured server running a mature code base can take as many as 10 seconds to answer this kind of requests. This is a huge problem as health check requests start queuing and timing out taking perfectly functional instances out of the pool, and making things just worst.&lt;br /&gt;
&lt;br /&gt;
To avoid this problem we normally configure health checks with long intervals (typically 10 seconds) and windows of 3 failures for every 5 checks. And this, of course, creates another problem as the load balancer takes, in our case, up to 30 seconds to discover that an instance has been restarted when using things like &lt;a href=&quot;http://supervisord.org/&quot;&gt;Supervisor&lt;/a&gt; and its memmon plugin, leading to a lot of 503 Service Unavailable errors in the mean time.&lt;br /&gt;
&lt;br /&gt;
So, it&#39;s a complete mess no mater how you analyze it and I needed to find a way to solve it: enters &lt;a href=&quot;https://pypi.python.org/pypi/five.z2monitor&quot;&gt;five.z2monitor&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
five.z2monitor plugs &lt;a href=&quot;http://pypi.python.org/pypi/zc.monitor&quot; rel=&quot;nofollow&quot;&gt;zc.monitor&lt;/a&gt; and &lt;a href=&quot;http://pypi.python.org/pypi/zc.z3monitor&quot;&gt;zc.z3monitor&lt;/a&gt; into Zope 2, enabling another thread and port to handle 
monitoring. To install it you just need to add something like this into your buildout configuration:&lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;
[buildout]&lt;br /&gt;
eggs =&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; …&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; five.z2monitor&lt;br /&gt;zcml =&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; …&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; five.z2monitor&lt;br /&gt;&lt;br /&gt;
[instance]&lt;br /&gt;
…&lt;br /&gt;zope-conf-additional =&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;product-config five.z2monitor&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; bind 127.0.0.1:8881&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/product-config&amp;gt;&lt;br /&gt;
&lt;/code&gt;
&lt;br /&gt;
&lt;br /&gt;
After running buildout and restarting your instance you can communicate with your Zope server over the new port using different commands, called probes:&lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;
$ bin/instance monitor help&lt;br /&gt;
Supported commands:&lt;br /&gt;
&amp;nbsp; dbinfo -- Get database statistics&lt;br /&gt;
&amp;nbsp; help -- Get help about server commands&lt;br /&gt;
&amp;nbsp; interactive -- Turn on monitor&#39;s interactive mode&lt;br /&gt;
&amp;nbsp; monitor -- Get general process info&lt;br /&gt;
&amp;nbsp; ok -- Return the string &#39;OK&#39;.&lt;br /&gt;
&amp;nbsp; quit -- Quit the monitor&lt;br /&gt;
&amp;nbsp; zeocache -- Get ZEO client cache statistics&lt;br /&gt;
&amp;nbsp; zeostatus -- Get ZEO client status information&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
Suppose you want to get the ZEO client cache statistics for this instance; all you have to do is use the following command:&lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;$ bin/instance monitor zeocache main&lt;br /&gt;417554 895451465 435095 900622900 35429160&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
You can also use the &lt;a href=&quot;https://linux.die.net/man/1/nc&quot;&gt;Netcat&lt;/a&gt; utility  to get the same information:&lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;$ echo &#39;zeocache main&#39; | nc -i 1 127.0.0.1 8881&lt;br /&gt;417753 896710955 435422 901905068 35467686&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
It&#39;s easy to extend the list of supported commands by writing you own probes; in the list above I have added to the default command set one that I create to know if the server is running or not; it&#39;s called &quot;ok&quot;, and here is its source code:&lt;br /&gt;
&lt;br /&gt;
&lt;script src=&quot;https://gist.github.com/hvelarde/fa7b6da93bc65db99c4e1204c3172abe.js&quot;&gt;&lt;/script&gt;&lt;br /&gt;
We have now a dedicate port and thread that can be used for health checking: &lt;br /&gt;
&lt;br /&gt;
&lt;code&gt;$ echo &#39;ok&#39; | nc -i 1 127.0.0.1 8881&lt;br /&gt;OK&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
With this we solved most of the problems I mentioned above: we have faster response time and no queuing; we can decrease the health check interval to a couple of seconds and we are almost sure that a failure is a failure and not just a timeout.&lt;br /&gt;
&lt;br /&gt;
Note we can&#39;t use this with nginx, nor Varnish, as their health checks are limited and expect the same port used for HTTP requests; only HAProxy supports this configuration.&lt;br /&gt;
&lt;br /&gt;
So, in the next post I&#39;ll show you how to configure HAProxy health checks to use this probe and how to reduce the latency to a couple of milliseconds.</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/6295532087894938256/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2017/12/we-have-been-doing-health-checks-wrong.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/6295532087894938256'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/6295532087894938256'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2017/12/we-have-been-doing-health-checks-wrong.html' title='We have been doing health checks wrong in Plone'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-3518502191446957017</id><published>2016-12-23T11:28:00.003-06:00</published><updated>2016-12-23T12:55:29.647-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="performance"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="zope"/><title type='text'>Plone performance: threads or instances?</title><content type='html'>Recently we had a discussion on the Plone community forum on &lt;a href=&quot;https://community.plone.org/t/advice-on-caching/3138?u=hvelarde&quot;&gt;how to increase performance&lt;/a&gt; of Plone-based sites.&lt;br /&gt;
&lt;br /&gt;
I was arguing in favor of instances, because some time ago I read a &lt;a href=&quot;http://glicksoftware.com/blog/on-zope-multiple-cores-and-the-gil&quot;&gt;blog post&lt;/a&gt; by davisagli taking about the impact of the Python GIL on performance of multicore servers. Others, like jensens and djay, were skeptical on this argument and told me not to overestimate that.&lt;br /&gt;
&lt;br /&gt;
So, I decide to test this using the production servers of one of our customers.&lt;br /&gt;
&lt;br /&gt;
The site is currently running on 2 different DigitalOcean servers with 4 processors and 8GB RAM; we are using Cloudflare in front of it, and round-robin, DNS-based load balancing.&lt;br /&gt;
&lt;br /&gt;
Prior to my changes, both servers were running with the same configuration: &lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;nginx, doing caching of static files and proxy rewrites&lt;/li&gt;
&lt;li&gt;Varnish, doing caching and URL-based load balancing &lt;/li&gt;
&lt;li&gt;4 Plone instances running on ZEO client mode, with 1 thread and 100.000 objects in cache&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
Both servers where running also a ZEO server on ZRS configuration, one as a master and the other as a slave, doing blob storage replication.&lt;br /&gt;
&lt;br /&gt;
First, here we have some information from this morning, before I made the changes. Here are some graphics I obtained using New Relic on the master:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhosdWvDtXJmaJ0OnL0C02U6ShD4qgxP6EM7Itw0sFY16AhXQaQv-G2yI83fS0AuV4sfcNsv9yTp07A9XhMSJ7rA0V3VcaYF-9Q557HW1EoHni2fw3F1Elx0UBHsq3OLrm1ya6uzZtypEo/s1600/caf11-before.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;290&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhosdWvDtXJmaJ0OnL0C02U6ShD4qgxP6EM7Itw0sFY16AhXQaQv-G2yI83fS0AuV4sfcNsv9yTp07A9XhMSJ7rA0V3VcaYF-9Q557HW1EoHni2fw3F1Elx0UBHsq3OLrm1ya6uzZtypEo/s320/caf11-before.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDJgVpeEkpFQ3_PHwM4RZ9wefZfuNjQpkK3HtO082y3M7h1AHcBmMloLMuO-gI2B2bx5MaeXnWLtCYSQFQe6sAvXyykM8Nb1hW0J1_b2KNJ0-zdDmtIIKCuauQey71JtXSjvXmBKbe-Lg/s1600/caf12-before.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;290&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDJgVpeEkpFQ3_PHwM4RZ9wefZfuNjQpkK3HtO082y3M7h1AHcBmMloLMuO-gI2B2bx5MaeXnWLtCYSQFQe6sAvXyykM8Nb1hW0J1_b2KNJ0-zdDmtIIKCuauQey71JtXSjvXmBKbe-Lg/s320/caf12-before.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;span id=&quot;goog_1186714128&quot;&gt;&lt;/span&gt;&lt;span id=&quot;goog_1186714129&quot;&gt;&lt;/span&gt;&lt;br /&gt;
&lt;span id=&quot;goog_1186714128&quot;&gt;Here is the same information from the slave:&lt;/span&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrOG7Fg5p5DPZhCEmtt-PXiB5W5879ZU0-ba1wssnOcuPby8wROCPIJ8hcUKFfV9t_T59n2vNAxX8yUdkj9Qv5ZvN1w4oHuvEcc7kuvASdbrPa1EhXI8Vp9-rNPC5ZeFu_fxFgat2ynOU/s1600/caf21-before.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;290&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrOG7Fg5p5DPZhCEmtt-PXiB5W5879ZU0-ba1wssnOcuPby8wROCPIJ8hcUKFfV9t_T59n2vNAxX8yUdkj9Qv5ZvN1w4oHuvEcc7kuvASdbrPa1EhXI8Vp9-rNPC5ZeFu_fxFgat2ynOU/s320/caf21-before.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhy3AmTqZoLzWN8fPKTebEvj_6gdP5GSqwWPmPg3MSNy2d4VpHKz917rF1s76GnmKQLcM_2ul_2qfriK5ccJf4bWQXok7hhrlQu8PUWASBYuY4YbP4I3Df-GhMSoxqLfVxTj3O1t0NkKiU/s1600/caf22-before.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;290&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhy3AmTqZoLzWN8fPKTebEvj_6gdP5GSqwWPmPg3MSNy2d4VpHKz917rF1s76GnmKQLcM_2ul_2qfriK5ccJf4bWQXok7hhrlQu8PUWASBYuY4YbP4I3Df-GhMSoxqLfVxTj3O1t0NkKiU/s320/caf22-before.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;span id=&quot;goog_1186714128&quot;&gt;As you can see, everything is running smoothly: CPU consumption is low and memory consumption is high&lt;/span&gt; and… yes, we have an issue with some PhamtomJS processes left behind.&lt;br /&gt;
&lt;br /&gt;
This is what I did later:&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;on the master server, I restarted the 4 instances&lt;/li&gt;
&lt;li&gt;on the slave server, I changed the configuration of instance1 to use 4 threads and restarted it; I stopped the other 3 instances&lt;/li&gt;
&lt;/ul&gt;
I also stopped the memmon Supervisor plugin (just because I had no idea on how much memory the slave server instance will be consuming after the changes), and killed all PhamtomJS processes.&lt;br /&gt;
&lt;br /&gt;
The servers have been running for a couple of hours now and I can share the results. This is the master server:&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEglzPDUXPjGKEHcwJctyiaSz6dZUR7ZR1u3t4LVByOUrKV4kpPzQJUVKDsNyamRHWskG8FC-pKB21jrX15Vd7owUTznZv4lbO14M0UdBBeWB6ztfwnWlldan-YbUuuszxQ-G3OYvqcSUEA/s1600/caf11-after.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;290&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEglzPDUXPjGKEHcwJctyiaSz6dZUR7ZR1u3t4LVByOUrKV4kpPzQJUVKDsNyamRHWskG8FC-pKB21jrX15Vd7owUTznZv4lbO14M0UdBBeWB6ztfwnWlldan-YbUuuszxQ-G3OYvqcSUEA/s320/caf11-after.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8GF2QYwHsN_OA2Nd7xQmuwOOUQNbpfFcOJiGKWugdK2QuiMojMHndEywZOn5-ZwiSkf2pLfq9isq6bOarvqmdNDbNzXWFkTi-DRMvnCuXjjAhjC6H5jgu2k9WkMk4VAj01NHiSE9p8GU/s1600/caf12-after.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;290&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8GF2QYwHsN_OA2Nd7xQmuwOOUQNbpfFcOJiGKWugdK2QuiMojMHndEywZOn5-ZwiSkf2pLfq9isq6bOarvqmdNDbNzXWFkTi-DRMvnCuXjjAhjC6H5jgu2k9WkMk4VAj01NHiSE9p8GU/s320/caf12-after.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
And this is now the slave:&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiH1JOj6xpLHrEhPIcBWDL_06TnVpY16q3V_ig9xK9-NZgCNiJ6OxPlknPf5w_vWbk7AAjgm0aCpFWRM25QIeQT3q5VH7Zm-RARQ8KNh8iaZOZQNGeHSx6gCqj7NC4s71TVi6neZ18Doo/s1600/caf21-after.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;290&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiH1JOj6xpLHrEhPIcBWDL_06TnVpY16q3V_ig9xK9-NZgCNiJ6OxPlknPf5w_vWbk7AAjgm0aCpFWRM25QIeQT3q5VH7Zm-RARQ8KNh8iaZOZQNGeHSx6gCqj7NC4s71TVi6neZ18Doo/s320/caf21-after.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjY-0x1499uVdnEo_oLH04pk7c6XmSdI77_jOx4_wPP_IeBwBSgN9KVkDvf80Xlm0GtqK2tUQW7Klwr4SoZLoawjpVPlGVX9UhyFiRuw-y58dglWPtZQqkMNhTS-c1u78fUQf0N-3KuHDA/s1600/caf22-after.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;290&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjY-0x1499uVdnEo_oLH04pk7c6XmSdI77_jOx4_wPP_IeBwBSgN9KVkDvf80Xlm0GtqK2tUQW7Klwr4SoZLoawjpVPlGVX9UhyFiRuw-y58dglWPtZQqkMNhTS-c1u78fUQf0N-3KuHDA/s320/caf22-after.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
The only obvious change here is in memory consumption: wow! the sole instance on the slave server is consuming 1GB less than the 4 instances in the master server!&lt;br /&gt;
&lt;br /&gt;
Let&#39;s do a little bit more research now. Here we have some information on database activity on the master server (just one instance for the sake of simplicity):&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrF56kzN_ieYrq92d27-z5kxRGO9YRU35At4JcZaPpWOlYpS91DQhdrlCCT8MUMArPOWUSgeEUD_CzpkIQy0QnLmeO-QwT2B65dUv5T0qtcyCT7Vrh5p4vosUXQ7c5fZ2JyjiUem3eJYA/s1600/Screen+Shot+2016-12-23+at+14.52.14.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;250&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrF56kzN_ieYrq92d27-z5kxRGO9YRU35At4JcZaPpWOlYpS91DQhdrlCCT8MUMArPOWUSgeEUD_CzpkIQy0QnLmeO-QwT2B65dUv5T0qtcyCT7Vrh5p4vosUXQ7c5fZ2JyjiUem3eJYA/s320/Screen+Shot+2016-12-23+at+14.52.14.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUaobER6p4A_c9G85sXXhM-elD2qJnStf00TvTneTrWwl_0gapuRBnQPMLIks5PVEeDMW-EVztUn3PlAJwo2qRIK9kLkhwJ6XY8g0PoiRqhpUXWNZDEsjcxGJnF_VVNms_GMGbqdr1giQ/s1600/Screen+Shot+2016-12-23+at+14.52.16.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;250&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUaobER6p4A_c9G85sXXhM-elD2qJnStf00TvTneTrWwl_0gapuRBnQPMLIks5PVEeDMW-EVztUn3PlAJwo2qRIK9kLkhwJ6XY8g0PoiRqhpUXWNZDEsjcxGJnF_VVNms_GMGbqdr1giQ/s320/Screen+Shot+2016-12-23+at+14.52.16.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
Now here is some similar information for the slave server:&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrjbarZPmCTD3f6R-Sm8lc2vg2cCQX15oEcmOTNiEnxkQczH-KgPVtWRxUQbnUyU1l_4SuS4eFkEtSfKU5TiVNwh9-zvy6cV1jK5ZnOtEZHDQC_hJk2TAUW-eZeekb16H72_v8J1lb07Q/s1600/Screen+Shot+2016-12-23+at+14.54.20.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;250&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrjbarZPmCTD3f6R-Sm8lc2vg2cCQX15oEcmOTNiEnxkQczH-KgPVtWRxUQbnUyU1l_4SuS4eFkEtSfKU5TiVNwh9-zvy6cV1jK5ZnOtEZHDQC_hJk2TAUW-eZeekb16H72_v8J1lb07Q/s320/Screen+Shot+2016-12-23+at+14.54.20.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEis-ihW_xi1lAHBXpJs4EQ4vjUnVSrr-T1YuTVXPd7zfa5ZCexzkRT9grtcTBv54QufZuvpmD-hYzuQ1sGALZsAiz8kI5yfGpfgs0pWkJGqPWyvdp8P93EJMcEKMWe7K-RuBzKvMa5vAQU/s1600/Screen+Shot+2016-12-23+at+14.54.23.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;250&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEis-ihW_xi1lAHBXpJs4EQ4vjUnVSrr-T1YuTVXPd7zfa5ZCexzkRT9grtcTBv54QufZuvpmD-hYzuQ1sGALZsAiz8kI5yfGpfgs0pWkJGqPWyvdp8P93EJMcEKMWe7K-RuBzKvMa5vAQU/s320/Screen+Shot+2016-12-23+at+14.54.23.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
I can say that I was expecting this: there&#39;s a lot more activity and the caching is not very well utilized on the slave server (see, smcmahon, that&#39;s the beauty of the URL-based load balancing on Varnish demonstrated).&lt;br /&gt;
&lt;br /&gt;
Let&#39;s try a different look, now using the &lt;a href=&quot;http://www.linuxcommand.org/man_pages/vmstat8.html&quot;&gt;vmstat&lt;/a&gt; command:&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEim7NU3TIxrLCs3YqIthDvlVPEoCA5E6lARcYPKgoMGg7hIta2XkRIkpUGXpl8EUDc4BQOkut5-AMcfePQDY84nWHDwqz2zbMBZXfMc8WkVzCMbMzYoW_Ft-BHaWPAlqZlNyDzQELpbSs8/s1600/Selection_002.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;177&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEim7NU3TIxrLCs3YqIthDvlVPEoCA5E6lARcYPKgoMGg7hIta2XkRIkpUGXpl8EUDc4BQOkut5-AMcfePQDY84nWHDwqz2zbMBZXfMc8WkVzCMbMzYoW_Ft-BHaWPAlqZlNyDzQELpbSs8/s320/Selection_002.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiHHIJdBz4nI6g-lDqiwM5YI5Zc5PyBJ5w38w6VYolYH3yGXyki-NMwIslsHsrIffb4gboJOop-tlh5dKgbT4_c6n5geX_33j-W-HvFDYgYGfqIKkHm4TdrV-nexTElvMeZ5JK8SDcOcq8/s1600/Selection_001.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;177&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiHHIJdBz4nI6g-lDqiwM5YI5Zc5PyBJ5w38w6VYolYH3yGXyki-NMwIslsHsrIffb4gboJOop-tlh5dKgbT4_c6n5geX_33j-W-HvFDYgYGfqIKkHm4TdrV-nexTElvMeZ5JK8SDcOcq8/s320/Selection_001.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
Not many differences here: the CPU is idle most of the time and the interrupts and context switching are almost the same.&lt;br /&gt;
&lt;br /&gt;
Now let&#39;s see how much our instances are being used, with the &lt;a href=&quot;https://www.varnish-cache.org/docs/trunk/reference/varnishstat.html&quot;&gt;varnishstat&lt;/a&gt; command:&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjpCW3_YqS9B1yRq4rR4H8vGHtmgPgj9xziw0Jjy4_BXdG2aBAy-9REFSQkQ2ygOA6ovJHOgESco5j3YZuwgO-STcQOGo28NwXTwHcfgc16bAk134jGhLbwzgBzsJ9WrLMlW964Nnl8hHo/s1600/Selection_004.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;244&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjpCW3_YqS9B1yRq4rR4H8vGHtmgPgj9xziw0Jjy4_BXdG2aBAy-9REFSQkQ2ygOA6ovJHOgESco5j3YZuwgO-STcQOGo28NwXTwHcfgc16bAk134jGhLbwzgBzsJ9WrLMlW964Nnl8hHo/s320/Selection_004.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjkcFQ39pfMd_orES4hxov-c-aXV4dh3pCeYhV3uhTPm6i3-u4GP2rOKlxk2RjnsUYkelsfIMWQJAqO_Pb1f7jakUr6-Jd1TdSEXjjnkoMs8HRnhSSR5DgXB0pjKlyavoZbjOKmDxVKoRQ/s1600/Selection_003.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;244&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjkcFQ39pfMd_orES4hxov-c-aXV4dh3pCeYhV3uhTPm6i3-u4GP2rOKlxk2RjnsUYkelsfIMWQJAqO_Pb1f7jakUr6-Jd1TdSEXjjnkoMs8HRnhSSR5DgXB0pjKlyavoZbjOKmDxVKoRQ/s320/Selection_003.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;
Here you can see why there&#39;s not too much difference: in fact Varnish is taking care of nearly 90% of the requests and we have only around 3 requests/second hitting the servers.&lt;br /&gt;
&lt;br /&gt;
Let&#39;s make another test to see how quickly we are responding the requests using the &lt;a href=&quot;https://www.varnish-cache.org/docs/trunk/reference/varnishhist.html&quot;&gt;varnishhist&lt;/a&gt; command:&lt;br /&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYOu9yFZ5ElyWUcHVpuJ_OaK0ZMc1pIq5uTnzXQOtclp-86yZ0FY-04nA1B3AUfbjDiNuK90jCxtJq_C3VTGklSO4AjfnqgnjUl7kZo0jONbpzdg1XeyBGSjlAy-otXfUhKZKtvXS_s44/s1600/Selection_005.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;244&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYOu9yFZ5ElyWUcHVpuJ_OaK0ZMc1pIq5uTnzXQOtclp-86yZ0FY-04nA1B3AUfbjDiNuK90jCxtJq_C3VTGklSO4AjfnqgnjUl7kZo0jONbpzdg1XeyBGSjlAy-otXfUhKZKtvXS_s44/s320/Selection_005.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZkZkJrZMYBXcbKFV-t0aqad2QlIBM8LjTbhEHEMrYtm1v-co0dhlurKQIidQsVNfzvgK_uM3aHkOBOwCF5OM2xlPb0JknMku-kuBSP-mEDC6JldsOx3jp_xW-2dwV3nJuuH8aYeplCz4/s1600/Selection_006.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;244&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZkZkJrZMYBXcbKFV-t0aqad2QlIBM8LjTbhEHEMrYtm1v-co0dhlurKQIidQsVNfzvgK_uM3aHkOBOwCF5OM2xlPb0JknMku-kuBSP-mEDC6JldsOx3jp_xW-2dwV3nJuuH8aYeplCz4/s320/Selection_006.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
Again, there&#39;s almost no difference here.&lt;br /&gt;
&lt;br /&gt;
Conclusion: for our particular case, using threads seems not to affect too much the performance and has a positive impact on memory consumption.&lt;br /&gt;
&lt;br /&gt;
What I&#39;m going to do now is to change the configuration used in production to have 2 instances and 2 threads… why? because restarting a single instance on a server for maintenance purposes would let us without backends during the process if we were using only one instance.&lt;br /&gt;
&lt;br /&gt;
Share and enjoy!</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/3518502191446957017/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2016/12/plone-site-performance-threads-or.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/3518502191446957017'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/3518502191446957017'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2016/12/plone-site-performance-threads-or.html' title='Plone performance: threads or instances?'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhosdWvDtXJmaJ0OnL0C02U6ShD4qgxP6EM7Itw0sFY16AhXQaQv-G2yI83fS0AuV4sfcNsv9yTp07A9XhMSJ7rA0V3VcaYF-9Q557HW1EoHni2fw3F1Elx0UBHsq3OLrm1ya6uzZtypEo/s72-c/caf11-before.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-903645611089502</id><published>2014-04-30T19:52:00.000-05:00</published><updated>2014-05-01T10:04:29.549-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="buildout"/><category scheme="http://www.blogger.com/atom/ns#" term="code-analysis"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="python"/><category scheme="http://www.blogger.com/atom/ns#" term="quality-assurance"/><title type='text'>Using pep257, a Python docstring style checker, with Buildout</title><content type='html'>&lt;a href=&quot;http://www.python.org/dev/peps/pep-0257&quot;&gt;PEP 257&lt;/a&gt; documents the semantics and conventions associated with
Python docstrings. According to it:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote class=&quot;tr_bq&quot;&gt;
&lt;i&gt;A docstring is a string literal that occurs as the first statement in
a module, function, class, or method definition.  Such a docstring
becomes the &lt;code&gt;__doc__&lt;/code&gt; special attribute of that object.&lt;/i&gt;&lt;/blockquote&gt;
&lt;br /&gt;
This is specially useful, for instance, when you are introspecting code and you want to know what a specific method or function does.&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;https://pypi.python.org/pypi/pep257&quot;&gt;pep257&lt;/a&gt; is a Python docstring style checker.&lt;br /&gt;
&lt;br /&gt;
I already filled a feature request on &lt;a href=&quot;https://github.com/plone/plone.recipe.codeanalysis/issues/76&quot;&gt;plone.recipe.codeanalysis&lt;/a&gt; to add support for it; meanwhile, if you want to use it in your &lt;a href=&quot;http://www.buildout.org/&quot;&gt;Buildout&lt;/a&gt;-based projects, you can add the following to your buildout configuration:&lt;br /&gt;
&lt;br /&gt;
&lt;script src=&quot;https://gist.github.com/hvelarde/f967e7e961e8b76d6948.js&quot;&gt;&lt;/script&gt;

After running &lt;code&gt;bin/buildout&lt;/code&gt; you will find a &lt;code&gt;bin/pep257&lt;/code&gt; script.&lt;br /&gt;
&lt;br /&gt;
A typical output will look like this:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;(python-2.7)# hvelarde@nanovac (master * u+1) ~/collective/polls 
# bin/pep257 src/
src/collective/__init__.py:1 at module level:
        D100: Docstring missing
src/collective/polls/js_i18n_helper.py:1 at module level:
        D100: Docstring missing
src/collective/polls/js_i18n_helper.py:8 in public class `LegendOthersTranslation`:
        D101: Docstring missing
src/collective/polls/js_i18n_helper.py:14 in public method `render`:
        D102: Docstring missing
src/collective/polls/testing.py:1 at module level:
        D100: Docstring missing
src/collective/polls/testing.py:10 in public class `Fixture`:
        D101: Docstring missing
src/collective/polls/testing.py:14 in public method `setUpZope`:
        D102: Docstring missing
src/collective/polls/testing.py:19 in public method `setUpPloneSite`:
        D102: Docstring missing
src/collective/polls/config.py:1 at module level:
        D100: Docstring missing
src/collective/polls/polls.py:1 at module level:
        D100: Docstring missing
src/collective/polls/polls.py:21 in public class `IPolls`:
        D101: Docstring missing
src/collective/polls/polls.py:24 in public method `recent_polls`:
        D400: First line should end with &#39;.&#39;, not &#39;s&#39;
src/collective/polls/polls.py:24 in public method `recent_polls`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:27 in public method `uid_for_poll`:
        D400: First line should end with &#39;.&#39;, not &#39;l&#39;
src/collective/polls/polls.py:27 in public method `uid_for_poll`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:30 in public method `poll_by_uid`:
        D400: First line should end with &#39;.&#39;, not &#39;d&#39;
src/collective/polls/polls.py:30 in public method `poll_by_uid`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:33 in public method `voters_in_a_poll`:
        D400: First line should end with &#39;.&#39;, not &#39;l&#39;
src/collective/polls/polls.py:33 in public method `voters_in_a_poll`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:36 in public method `voted_in_a_poll`:
        D400: First line should end with &#39;.&#39;, not &#39;d&#39;
src/collective/polls/polls.py:36 in public method `voted_in_a_poll`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:39 in public method `allowed_to_edit`:
        D401: First line should be imperative: &#39;i&#39;, not &#39;is&#39;
src/collective/polls/polls.py:39 in public method `allowed_to_edit`:
        D400: First line should end with &#39;.&#39;, not &#39;l&#39;
src/collective/polls/polls.py:39 in public method `allowed_to_edit`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:42 in public method `allowed_to_view`:
        D401: First line should be imperative: &#39;I&#39;, not &#39;Is&#39;
src/collective/polls/polls.py:42 in public method `allowed_to_view`:
        D400: First line should end with &#39;.&#39;, not &#39;l&#39;
src/collective/polls/polls.py:42 in public method `allowed_to_view`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:45 in public method `allowed_to_vote`:
        D400: First line should end with &#39;.&#39;, not &#39;l&#39;
src/collective/polls/polls.py:45 in public method `allowed_to_vote`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:48 in public method `anonymous_vote_id`:
        D400: First line should end with &#39;.&#39;, not &#39;d&#39;
src/collective/polls/polls.py:48 in public method `anonymous_vote_id`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:52 in public class `Polls`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/polls.py:52 in public class `Polls`:
        D400: First line should end with &#39;.&#39;, not &#39;s&#39;
src/collective/polls/polls.py:52 in public class `Polls`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:60 in public method `ct`:
        D102: Docstring missing
src/collective/polls/polls.py:64 in public method `mt`:
        D102: Docstring missing
src/collective/polls/polls.py:68 in public method `wt`:
        D102: Docstring missing
src/collective/polls/polls.py:72 in public method `member`:
        D102: Docstring missing
src/collective/polls/polls.py:75 in private method `_query_for_polls`:
        D400: First line should end with &#39;.&#39;, not &#39;s&#39;
src/collective/polls/polls.py:75 in private method `_query_for_polls`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:81 in public method `uid_for_poll`:
        D400: First line should end with &#39;.&#39;, not &#39;l&#39;
src/collective/polls/polls.py:81 in public method `uid_for_poll`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:85 in public method `recent_polls`:
        D400: First line should end with &#39;.&#39;, not &#39;s&#39;
src/collective/polls/polls.py:85 in public method `recent_polls`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:98 in public method `poll_by_uid`:
        D400: First line should end with &#39;.&#39;, not &#39;d&#39;
src/collective/polls/polls.py:98 in public method `poll_by_uid`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:109 in public method `voted_in_a_poll`:
        D400: First line should end with &#39;.&#39;, not &#39;d&#39;
src/collective/polls/polls.py:109 in public method `voted_in_a_poll`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:128 in public method `allowed_to_edit`:
        D401: First line should be imperative: &#39;I&#39;, not &#39;Is&#39;
src/collective/polls/polls.py:128 in public method `allowed_to_edit`:
        D400: First line should end with &#39;.&#39;, not &#39;l&#39;
src/collective/polls/polls.py:128 in public method `allowed_to_edit`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:133 in public method `allowed_to_view`:
        D401: First line should be imperative: &#39;I&#39;, not &#39;Is&#39;
src/collective/polls/polls.py:133 in public method `allowed_to_view`:
        D400: First line should end with &#39;.&#39;, not &#39;l&#39;
src/collective/polls/polls.py:133 in public method `allowed_to_view`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:137 in public method `allowed_to_vote`:
        D401: First line should be imperative: &#39;i&#39;, not &#39;is&#39;
src/collective/polls/polls.py:137 in public method `allowed_to_vote`:
        D400: First line should end with &#39;.&#39;, not &#39;?&#39;
src/collective/polls/polls.py:137 in public method `allowed_to_vote`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/polls.py:156 in public class `PollPortletRender`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/polls.py:156 in public class `PollPortletRender`:
        D204: Expected 1 blank line *after* class docstring, found 0
src/collective/polls/polls.py:156 in public class `PollPortletRender`:
        D400: First line should end with &#39;.&#39;, not &#39;w&#39;
src/collective/polls/polls.py:181 in public method `render_portlet`:
        D202: No blank lines allowed *after* method docstring, found 1
src/collective/polls/polls.py:251 in public method `render`:
        D400: First line should end with &#39;.&#39;, not &#39;e&#39;
src/collective/polls/__init__.py:1 at module level:
        D100: Docstring missing
src/collective/polls/subscribers.py:1 at module level:
        D100: Docstring missing
src/collective/polls/subscribers.py:17 in public function `fix_permissions`:
        D401: First line should be imperative: &#39;Thi&#39;, not &#39;This&#39;
src/collective/polls/subscribers.py:17 in public function `fix_permissions`:
        D400: First line should end with &#39;.&#39;, not &#39;f&#39;
src/collective/polls/subscribers.py:17 in public function `fix_permissions`:
        D205: Blank line missing between one-line summary and description
src/collective/polls/subscribers.py:17 in public function `fix_permissions`:
        D208: Docstring is over-indented
src/collective/polls/subscribers.py:17 in public function `fix_permissions`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/subscribers.py:37 in public function `remove_votes`:
        D401: First line should be imperative: &#39;Thi&#39;, not &#39;This&#39;
src/collective/polls/subscribers.py:37 in public function `remove_votes`:
        D400: First line should end with &#39;.&#39;, not &#39;t&#39;
src/collective/polls/subscribers.py:37 in public function `remove_votes`:
        D205: Blank line missing between one-line summary and description
src/collective/polls/subscribers.py:37 in public function `remove_votes`:
        D208: Docstring is over-indented
src/collective/polls/subscribers.py:37 in public function `remove_votes`:
        D300: Expected &quot;&quot;&quot;-quotes, got &#39;&#39;&#39;-quotes
src/collective/polls/setuphandlers.py:1 at module level:
        D100: Docstring missing
src/collective/polls/setuphandlers.py:12 in public class `HiddenProfiles`:
        D101: Docstring missing
src/collective/polls/setuphandlers.py:18 in public method `getNonInstallableProfiles`:
        D102: Docstring missing
src/collective/polls/setuphandlers.py:23 in public function `updateWorkflowDefinitions`:
        D103: Docstring missing
src/collective/polls/setuphandlers.py:29 in public function `setupVarious`:
        D103: Docstring missing
src/collective/polls/portlet/voteportlet.py:1 at module level:
        D100: Docstring missing
src/collective/polls/portlet/voteportlet.py:24 in public function `PossiblePolls`:
        D103: Docstring missing
src/collective/polls/portlet/voteportlet.py:46 in public class `IVotePortlet`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/portlet/voteportlet.py:46 in public class `IVotePortlet`:
        D400: First line should end with &#39;.&#39;, not &#39;t&#39;
src/collective/polls/portlet/voteportlet.py:46 in public class `IVotePortlet`:
        D205: Blank line missing between one-line summary and description
src/collective/polls/portlet/voteportlet.py:87 in public class `Assignment`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/portlet/voteportlet.py:102 in public method `__init__`:
        D102: Docstring missing
src/collective/polls/portlet/voteportlet.py:111 in public method `title`:
        D401: First line should be imperative: &#39;Thi&#39;, not &#39;This&#39;
src/collective/polls/portlet/voteportlet.py:111 in public method `title`:
        D400: First line should end with &#39;.&#39;, not &#39;e&#39;
src/collective/polls/portlet/voteportlet.py:111 in public method `title`:
        D205: Blank line missing between one-line summary and description
src/collective/polls/portlet/voteportlet.py:118 in public class `Renderer`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/portlet/voteportlet.py:129 in public method `utility`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/portlet/voteportlet.py:135 in public method `portlet_manager_name`:
        D102: Docstring missing
src/collective/polls/portlet/voteportlet.py:149 in public method `poll`:
        D102: Docstring missing
src/collective/polls/portlet/voteportlet.py:170 in public method `poll_uid`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/portlet/voteportlet.py:176 in public method `getVotingResults`:
        D102: Docstring missing
src/collective/polls/portlet/voteportlet.py:184 in public method `can_vote`:
        D102: Docstring missing
src/collective/polls/portlet/voteportlet.py:193 in public method `available`:
        D102: Docstring missing
src/collective/polls/portlet/voteportlet.py:202 in public method `is_closed`:
        D102: Docstring missing
src/collective/polls/portlet/voteportlet.py:208 in public class `AddForm`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/portlet/voteportlet.py:208 in public class `AddForm`:
        D204: Expected 1 blank line *after* class docstring, found 0
src/collective/polls/portlet/voteportlet.py:217 in public method `create`:
        D102: Docstring missing
src/collective/polls/portlet/voteportlet.py:221 in public class `EditForm`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/portlet/voteportlet.py:221 in public class `EditForm`:
        D204: Expected 1 blank line *after* class docstring, found 0
src/collective/polls/portlet/__init__.py:1 at module level:
        D100: Docstring missing
src/collective/polls/Extensions/Install.py:1 at module level:
        D100: Docstring missing
src/collective/polls/Extensions/Install.py:7 in public function `uninstall`:
        D103: Docstring missing
src/collective/polls/Extensions/__init__.py:1 at module level:
        D100: Docstring missing
src/collective/polls/tests/__init__.py:1 at module level:
        D100: Docstring missing
src/collective/polls/content/__init__.py:1 at module level:
        D100: Docstring missing
src/collective/polls/content/poll.py:1 at module level:
        D100: Docstring missing
src/collective/polls/content/poll.py:34 in public class `InsuficientOptions`:
        D101: Docstring missing
src/collective/polls/content/poll.py:38 in public class `IPoll`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/content/poll.py:38 in public class `IPoll`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:80 in public method `validate_options`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:90 in public class `Poll`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/content/poll.py:90 in public class `Poll`:
        D400: First line should end with &#39;.&#39;, not &#39;e&#39;
src/collective/polls/content/poll.py:90 in public class `Poll`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:101 in public method `annotations`:
        D102: Docstring missing
src/collective/polls/content/poll.py:105 in public method `utility`:
        D102: Docstring missing
src/collective/polls/content/poll.py:109 in public method `getOptions`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:115 in private method `_getVotes`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:133 in public method `getResults`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:144 in private method `_validateVote`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:155 in private method `_setVoter`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:180 in public method `voters`:
        D102: Docstring missing
src/collective/polls/content/poll.py:186 in public method `total_votes`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:192 in public method `setVote`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:217 in public class `PollAddForm`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/content/poll.py:217 in public class `PollAddForm`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:223 in public method `create`:
        D102: Docstring missing
src/collective/polls/content/poll.py:235 in public class `PollEditForm`:
        D203: Expected 1 blank line *before* class docstring, found 0
src/collective/polls/content/poll.py:235 in public class `PollEditForm`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:241 in public method `updateWidgets`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:258 in public method `applyChanges`:
        D102: Docstring missing
src/collective/polls/content/poll.py:270 in public class `View`:
        D101: Docstring missing
src/collective/polls/content/poll.py:277 in public method `update`:
        D102: Docstring missing
src/collective/polls/content/poll.py:346 in public method `can_vote`:
        D102: Docstring missing
src/collective/polls/content/poll.py:357 in public method `can_edit`:
        D102: Docstring missing
src/collective/polls/content/poll.py:362 in public method `has_voted`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:371 in public method `poll_uid`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:377 in public method `getOptions`:
        D200: One-line docstring should not occupy 2 lines
src/collective/polls/content/poll.py:382 in public method `getResults`:
        D200: One-line docstring should not occupy 2 lines
&lt;/pre&gt;
</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/903645611089502/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2014/04/using-pep257-python-docstring-style.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/903645611089502'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/903645611089502'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2014/04/using-pep257-python-docstring-style.html' title='Using pep257, a Python docstring style checker, with Buildout'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-6192001412818418534</id><published>2014-01-08T14:34:00.000-06:00</published><updated>2014-01-15T08:35:28.879-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="api"/><category scheme="http://www.blogger.com/atom/ns#" term="community"/><category scheme="http://www.blogger.com/atom/ns#" term="git"/><category scheme="http://www.blogger.com/atom/ns#" term="github"/><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="open source"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="python"/><category scheme="http://www.blogger.com/atom/ns#" term="webservice"/><title type='text'>How to get statistics about your contributions on a GitHub organization</title><content type='html'>&lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt;, the  web-based hosting service for software development projects, lets you check statistics on your contributions on specific projects easily.&lt;br /&gt;
&lt;br /&gt;
You can check, for instance,  the statistics on &lt;a href=&quot;https://github.com/collective/collective.cover&quot;&gt;collective.cover&lt;/a&gt; just by visiting the following page:&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;https://github.com/collective/collective.cover/graphs/contributors&quot;&gt;https://github.com/collective/collective.cover/graphs/contributors&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
GitHub gives you a count of commits and a visual representation of them among time.&lt;br /&gt;
&lt;br /&gt;
If you want to check only the number of commits by author, you could use the &lt;b&gt;git-shortlog&lt;/b&gt; command:&lt;br /&gt;
&lt;br /&gt;
&lt;script src=&quot;https://gist.github.com/hvelarde/8321897.js&quot;&gt;&lt;/script&gt;

The &lt;b&gt;-s&lt;/b&gt; option gives you a summary output and the &lt;b&gt;sort&lt;/b&gt; command shows the information sorted in reverse order (&lt;b&gt;-nr&lt;/b&gt;).&lt;br /&gt;
&lt;br /&gt;
You could improve this listing by using the &lt;b&gt;.mailmap&lt;/b&gt; feature to add commits belonging to the same author using two different email addresses.&lt;br /&gt;
&lt;br /&gt;
Image now that you want to get a list of all your contributions on a specific organization, lets say, the &lt;a href=&quot;https://github.com/collective&quot;&gt;Plone collective&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
Enter the &lt;a href=&quot;http://developer.github.com/v3/&quot;&gt;GitHub API&lt;/a&gt; and &lt;a href=&quot;https://pypi.python.org/pypi/github3.py&quot;&gt;github3.py&lt;/a&gt;, a Python wrapper for it.&lt;br /&gt;
&lt;br /&gt;
Until today, the Plone collective has more that 1.1k repositories so we need to use the authenticated access  to the API to avoid depletion of requests (rate limit&amp;nbsp;allow us to
make up to 60 requests per hour for unauthenticated requests and 5,000 for authenticated requests). In this specific example we used 1,171 requests to the API.&lt;br /&gt;
&lt;br /&gt;
&lt;script src=&quot;https://gist.github.com/hvelarde/8322843.js&quot;&gt;&lt;/script&gt;

&lt;br /&gt;
Using github3.py we iterate over all of the organization repositories and get information of each one only if my user name is listed among the contributors. We get the results as a list of tuples &lt;b&gt;(repo, total)&lt;/b&gt;.&lt;br /&gt;
&lt;br /&gt;
After that we sort the list in reverse order using the total commits as the key and make the sum of all commits in general.&lt;br /&gt;
&lt;br /&gt;
As you can see I have contributed to 74 repositories on the Plone collective making 2,975 commits in total.&lt;br /&gt;
&lt;br /&gt;
Not bad, isn&#39;t it? :-) &lt;br /&gt;
&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/6192001412818418534/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2014/01/how-to-get-statistics-about-your.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/6192001412818418534'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/6192001412818418534'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2014/01/how-to-get-statistics-about-your.html' title='How to get statistics about your contributions on a GitHub organization'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-5868787792788968554</id><published>2012-08-08T12:50:00.001-05:00</published><updated>2012-09-15T15:09:32.113-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="buildout"/><category scheme="http://www.blogger.com/atom/ns#" term="github"/><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="open source"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="problem solving"/><category scheme="http://www.blogger.com/atom/ns#" term="testing"/><category scheme="http://www.blogger.com/atom/ns#" term="travis ci"/><title type='text'>Integrating Travis CI with your Plone add-ons hosted on GitHub</title><content type='html'>&lt;i&gt;Update 15/9/2012: Kudos for Asko Soukka who has developed &lt;a href=&quot;http://datakurre.pandala.org/2012/09/speed-up-your-plone-add-on-tests-on.html&quot;&gt;an alternative method of installing Plone using the old good universal installer&lt;/a&gt; that reduces the amount of time needed by half. So go and read his post instead of loosing your time with mine.&lt;/i&gt;&lt;br /&gt;
&lt;br /&gt;
I took me a little bit but, with the help of &lt;a href=&quot;https://github.com/miohtama&quot;&gt;Mikko&lt;/a&gt; and &lt;a href=&quot;https://github.com/optilude&quot;&gt;Martin&lt;/a&gt;, I&#39;ve got a couple of add-ons running tests with &lt;a href=&quot;http://travis-ci.org/&quot;&gt;Travis CI&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
Before setting up &lt;b&gt;Travis CI&lt;/b&gt;, you have to make some changes to the &lt;a href=&quot;http://docs.python.org/dev/packaging/setupscript.html&quot;&gt;Setup Script&lt;/a&gt; of your package.&lt;br /&gt;
&lt;br /&gt;
In my case, my add-on package only works for Plone versions 4.1 and later, so I have &lt;a href=&quot;https://plone.org/documentation/manual/upgrade-guide/version/upgrading-plone-4.0-to-4.1/updating-add-on-products-for-plone-4.1/changing-dependencies-from-plone-to-products.cmfplone&quot;&gt;added Products.CMFPlone as a dependency&lt;/a&gt;:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;    …
    install_requires=[
        &#39;setuptools&#39;,
        &lt;b&gt;&#39;Products.CMFPlone&amp;gt;=4.1&#39;,&lt;/b&gt;
        ],
    extras_require={
        &#39;test&#39;: [&#39;plone.app.testing&#39;],
        },
    …
&lt;/pre&gt;
&lt;br /&gt;
&lt;b&gt;Products.CMFPlone&lt;/b&gt; contains a cut down feature set: just the things I need to run my tests.&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;http://about.travis-ci.org/docs/user/getting-started/&quot;&gt;Setting up Travis CI&lt;/a&gt; is pretty easy: just &lt;a href=&quot;http://travis-ci.org/users/auth/github&quot;&gt;sign in&lt;/a&gt; and activate your &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; Service Hook&lt;br /&gt;
&lt;br /&gt;
Now, you need to &lt;a href=&quot;http://about.travis-ci.org/docs/user/build-configuration/&quot;&gt;configure your Travis CI build with a .travis.yml file&lt;/a&gt; in the root of your repo:&lt;br /&gt;
&lt;br /&gt;
&lt;script src=&quot;https://gist.github.com/3296482.js?file=.travis.yml&quot;&gt;&lt;/script&gt;

In my case I&#39;m running tests for Plone&#39;s latest stable release (4.2 at I write this post) on top of Python 2.7.&lt;br /&gt;
&lt;br /&gt;
Let&#39;s take a look at the &lt;b&gt;travis.cfg&lt;/b&gt; &lt;a href=&quot;http://buildout.org/&quot;&gt;buildout&lt;/a&gt; configuration file:&lt;br /&gt;
&lt;br /&gt;
&lt;script src=&quot;https://gist.github.com/3296522.js?file=travis.cfg&quot;&gt;&lt;/script&gt;

The main issue I experimented on my first tests was timeouts, so I have a couple of tricks here for you: first, we are extending the standard Plone testing buildout configuration that includes most declarations for us and takes care of always running the latest stable version; we are only going to use the &lt;b&gt;test&lt;/b&gt; part. You need to add the &lt;b&gt;package-extras&lt;/b&gt; just if your add-on is using &lt;a href=&quot;http://pypi.python.org/pypi/plone.app.testing&quot;&gt;plone.app.testing&lt;/a&gt; on the &lt;b&gt;test&lt;/b&gt; option in &lt;b&gt;extras_require&lt;/b&gt; of your package declaration as mentioned above.&lt;br /&gt;
&lt;br /&gt;
&lt;strike&gt;&lt;b&gt;zope.globalrequest&lt;/b&gt; is needed to run the tests and it was not included on &lt;a href=&quot;http://pypi.python.org/pypi/Products.CMFPlone&quot;&gt;Products.CMFPlone&lt;/a&gt;&lt;/strike&gt; (this is already fixed on Plone&#39;s branches for versions 4.2 and 4.3). You may also need to include &lt;a href=&quot;http://pypi.python.org/pypi/Pillow&quot;&gt;Pillow&lt;/a&gt; in &lt;b&gt;test-eggs&lt;/b&gt;; just uncomment it the line.&lt;br /&gt;
&lt;br /&gt;
We also need to add a &lt;b&gt;socket-timeout&lt;/b&gt; of 3 seconds (only available on zc.buildout &amp;gt;= 1.5.0) and a list of &lt;b&gt;allow-hosts&lt;/b&gt; to download the dependencies. This is pretty important and will avoid timeouts as &lt;b&gt;Travis CI&lt;/b&gt; has hard time limits and timeouts are between 10 and 15 minutes for test suite runs (&lt;a href=&quot;http://about.travis-ci.org/docs/user/build-configuration/&quot;&gt;1&lt;/a&gt;).&lt;br /&gt;
&lt;br /&gt;
Last, we have to replace the &lt;b&gt;eggs&lt;/b&gt; option on the &lt;b&gt;test&lt;/b&gt; part; we need to do this because we don&#39;t want to include neither Plone or &lt;b&gt;plone.app.upgrade&lt;/b&gt; on the tests.&lt;br /&gt;
&lt;br /&gt;
Finally, you can add a &lt;a href=&quot;http://about.travis-ci.org/docs/user/status-images/&quot;&gt;Status Image&lt;/a&gt; with a link back to the result of your last build on your &lt;b&gt;README.txt&lt;/b&gt; file:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;.. image:: https://secure.travis-ci.org/collective/&lt;b&gt;your.package&lt;/b&gt;.png
    :target: http://travis-ci.org/collective/&lt;b&gt;your.package&lt;/b&gt;
&lt;/pre&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://secure.travis-ci.org/collective/collective.prettydate.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://secure.travis-ci.org/collective/collective.prettydate.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
To run the tests you only need to make a push to your &lt;b&gt;GitHub&lt;/b&gt; repo. Easy, isn&#39;t it?&lt;br /&gt;
&lt;br /&gt;
For a live example of all I mentioned above, take a look at the &lt;a href=&quot;https://github.com/collective/collective.prettydate&quot;&gt;collective.prettydate&lt;/a&gt; package.&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;http://travis-ci.org/&quot;&gt;Travis CI&lt;/a&gt; is really easy to set up and fun to use; I strongly recommend it and, if you like it, please &lt;a href=&quot;https://love.travis-ci.org/&quot;&gt;show your love donating&lt;/a&gt;.</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/5868787792788968554/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2012/08/integrating-travis-ci-with-your-plone.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/5868787792788968554'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/5868787792788968554'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2012/08/integrating-travis-ci-with-your-plone.html' title='Integrating Travis CI with your Plone add-ons hosted on GitHub'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-893770317264619187</id><published>2012-06-07T13:05:00.002-05:00</published><updated>2012-06-07T15:09:34.650-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="literature"/><category scheme="http://www.blogger.com/atom/ns#" term="science fiction"/><title type='text'>Ray Bradbury in memoriam</title><content type='html'>&lt;div style=&quot;text-align: right;&quot;&gt;
&lt;i&gt;The Illustrated Man turned in the moonlight. He turned again… and again… and again…&lt;/i&gt;&lt;br /&gt;
Ray Bradbury (&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Illustrated_Man&quot;&gt;The Illustrated Man&lt;/a&gt;, 1951)&lt;/div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiAjxX6Zb0ZrZeBmMJlpJI4xEXCB1sLTJvg7HdakA3iakyvkIz5JnxQzc4kKqdYygZUYIfdcCMMNrz4PwnLPTvqq7OMnXvM15W2QyONHfaOXUttU0MAMO_isbB7zSOMLrRASbAMAVP8dtE/s1600/ripbradbury.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;240&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiAjxX6Zb0ZrZeBmMJlpJI4xEXCB1sLTJvg7HdakA3iakyvkIz5JnxQzc4kKqdYygZUYIfdcCMMNrz4PwnLPTvqq7OMnXvM15W2QyONHfaOXUttU0MAMO_isbB7zSOMLrRASbAMAVP8dtE/s400/ripbradbury.jpg&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;a href=&quot;https://en.wikipedia.org/wiki/Ray_Bradbury&quot;&gt;Ray Bradbury&lt;/a&gt;, author of &lt;a href=&quot;https://en.wikipedia.org/wiki/Fahrenheit_451&quot;&gt;Fahrenheit 451&lt;/a&gt; and one of the greatest science fiction writers of the 20th century, died at 91 two days ago.&lt;br /&gt;
&lt;br /&gt;
I leave with you, as an homage, a fragment of one the most beautiful stories written by him.&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;h2&gt;

&lt;i&gt;Kaleidoscope (fragment)&lt;/i&gt;&lt;/h2&gt;
&lt;i&gt;[…]&lt;/i&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;i&gt;The many good-bys. The short farewells. And now the great loose brain was disintegrating. The components of the brain which had worked so beautifully and efficiently in the skull case of the rocket ship firing through space were dying one by one; the meaning of their life together was falling apart. And as a body dies when the brain ceases functioning, so the spirit of the ship and their long time together and what they meant to one another was dying. Applegate was now no more than a finger blown from the parent body, no longer to be despised and worked against. The brain was exploded, and the senseless, useless fragments of it were far scattered. The voices faded and now all of space was silent. Hollis was alone, falling.
&lt;br /&gt;&lt;br /&gt;
They were all alone. Their voices had died like echoes of the words of God spoken and vibrating in the starred deep. There went the captain to the Moon; there Stone with the meteor swarm; there Stimson; there Applegate toward Pluto; there Smith and Turner and Underwood and all the rest, the shards of the kaleidoscope that had formed a thinking pattern for so long, hurled apart.
&lt;br /&gt;&lt;br /&gt;
And I? thought Hollis. What can I do? Is there anything I can do now to make up for a terrible and empty life? If only I could do one good thing to make up for the meanness I collected all these years and didn’t even know was in me! But there’s no one here but myself, and how can you do good all alone? You can’t. Tomorrow night I’ll hit Earth s atmosphere.
&lt;br /&gt;&lt;br /&gt;
I’ll burn, he thought, and be scattered in ashes all over the continental lands. I’ll be put to use. Just a little bit, but ashes are ashes and they’ll add to the land.
&lt;br /&gt;&lt;br /&gt;
He fell swiftly, like a bullet, like a pebble, like an iron weight, objective, objective all of the time now, not sad or happy or anything, but only wishing he could do a good thing now that everything was gone, a good thing for just himself to know about.
&lt;br /&gt;&lt;br /&gt;
When I hit the atmosphere, I’ll burn like a meteor.
&lt;br /&gt;&lt;br /&gt;
“I wonder,” he said, “if anyone’ll see me?”
&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;
The small boy on the country road looked up and screamed. “Look, Mom, look! A falling star!”
&lt;br /&gt;&lt;br /&gt;
The blazing white star fell down the sky of dusk in Illinois. “Make a wish,” said his mother. “Make a wish.”&lt;/i&gt;&lt;br /&gt;
&lt;br /&gt;
(1949)&lt;/blockquote&gt;
&lt;br /&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgH81hhpsaLQWN958Rtt18w4a_P2JY0IORjiuXjWWergpZwjLolGbbI4i8DypExl_9L6-soT9RqNjKzrD7jT95VRYVVB4eb9T3UKUoScUnkPIDeKPvMQz0OQCyttNuKellIzBxwFmEcXRc/s1600/3247053188_f9c6c12dfe_b.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgH81hhpsaLQWN958Rtt18w4a_P2JY0IORjiuXjWWergpZwjLolGbbI4i8DypExl_9L6-soT9RqNjKzrD7jT95VRYVVB4eb9T3UKUoScUnkPIDeKPvMQz0OQCyttNuKellIzBxwFmEcXRc/s400/3247053188_f9c6c12dfe_b.jpg&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Photo: &lt;a href=&quot;https://secure.flickr.com/photos/eneas/3247053188/&quot;&gt;Eneas&lt;/a&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/893770317264619187/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2012/06/ray-bradbury-in-memoriam.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/893770317264619187'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/893770317264619187'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2012/06/ray-bradbury-in-memoriam.html' title='Ray Bradbury in memoriam'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiAjxX6Zb0ZrZeBmMJlpJI4xEXCB1sLTJvg7HdakA3iakyvkIz5JnxQzc4kKqdYygZUYIfdcCMMNrz4PwnLPTvqq7OMnXvM15W2QyONHfaOXUttU0MAMO_isbB7zSOMLrRASbAMAVP8dtE/s72-c/ripbradbury.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-4085141932931233450</id><published>2012-06-05T17:56:00.001-05:00</published><updated>2012-06-06T07:20:25.208-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="git"/><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="problem solving"/><category scheme="http://www.blogger.com/atom/ns#" term="python"/><title type='text'>Running pep8 before any commit on Git</title><content type='html'>&lt;div style=&quot;text-align: right;&quot;&gt;
&lt;i&gt;Readability counts.&lt;/i&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: right;&quot;&gt;
&lt;a href=&quot;http://www.python.org/dev/peps/pep-0020/&quot;&gt;The Zen of Python&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
One of the things that made me choose &lt;a href=&quot;http://www.python.org/&quot;&gt;Python&lt;/a&gt; as a programming language in the first place was its readability.&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;http://www.python.org/dev/peps/pep-0008/&quot;&gt;PEP 8&lt;/a&gt; —the Style Guide for Python Code— gives coding conventions for the Python code and &lt;a href=&quot;http://pypi.python.org/pypi/pep8&quot;&gt;pep8&lt;/a&gt; is a tool to check your code against these conventions.&lt;br /&gt;
&lt;br /&gt;
I use &lt;a href=&quot;http://projects.gnome.org/gedit/&quot;&gt;gedit&lt;/a&gt; as my editor and I have installed the &lt;a href=&quot;https://launchpad.net/gdp&quot;&gt;developer plugins&lt;/a&gt; to check my code against PEP 8 every time I save a file but, as not everyone does this, &lt;a href=&quot;https://github.com/ericof&quot;&gt;Érico&lt;/a&gt; asked me today about a way to enforce this practice.&lt;br /&gt;
&lt;br /&gt;
I started searching the web and I have compiled  (from &lt;a href=&quot;http://tech.myemma.com/python-pep8-git-hooks/&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://gist.github.com/810399&quot;&gt;2&lt;/a&gt; and &lt;a href=&quot;http://stackoverflow.com/questions/2293498/git-commit-hooks-global-settings&quot;&gt;3&lt;/a&gt;) a nice solution using a &lt;a href=&quot;http://git-scm.com/book/en/Customizing-Git-Git-Hooks&quot;&gt;pre-commit hook&lt;/a&gt; with &lt;a href=&quot;http://git-scm.com/&quot;&gt;Git&lt;/a&gt; (this is possible also with &lt;a href=&quot;https://subversion.apache.org/&quot;&gt;Subversion&lt;/a&gt;, but I&#39;m not pretty interested on it right now):&lt;br /&gt;
&lt;br /&gt;
First you need to be sure you are running Git version 1.7.1 or later, and that you have pep8 installed in your system (check the package documentation).&lt;br /&gt;
&lt;br /&gt;
Create a directory to store the hooks globally:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;b&gt;mkdir -p ~/.git_template/hooks&lt;/b&gt;
&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
Tell Git all new repositories you create or clone will use this directory for templates:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;b&gt;git config --global init.templatedir &#39;~/.git_template&#39;
&lt;/b&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
&lt;br /&gt;
Put the following &lt;a href=&quot;https://raw.github.com/gist/2878450/876625330b6b50d58788b8426a0cf513ed6cf77e/pre-commit&quot;&gt;script&lt;/a&gt; in the &lt;b&gt;~/.git_template/hooks&lt;/b&gt; directory:&lt;br /&gt;
&lt;br /&gt;
&lt;script src=&quot;https://gist.github.com/2878450.js?file=pre-commit&quot;&gt;
&lt;/script&gt;
&lt;br /&gt;
Make the file executable:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;b&gt;chmod +x ~/.git_template/hooks/pre-commit
&lt;/b&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
If you want to use this hook on an existing repository all you have to do is reinitialize it:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;b&gt;git init
&lt;/b&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
Now the &lt;b&gt;pre-commit&lt;/b&gt; hook script lives in the &lt;b&gt;.git/hooks&lt;/b&gt; directory of your repository.&lt;br /&gt;
&lt;br /&gt;
Test it trying to commit some changes: if the files you are trying to commit comply with PEP 8 (excepting the list of  errors or warnings to ignore), the commit will be done as usual; if there are any issues, the commit will be aborted until you fix them.&lt;br /&gt;
&lt;br /&gt;
Feel free to modify the list of errors and warnings to ignore, globally or from project to project, to fit your personal needs.&lt;br /&gt;
&lt;br /&gt;
Remember PEP 8:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;i&gt;A style guide is about consistency.  Consistency with this style guide
is important.  Consistency within a project is more important.
Consistency within one module or function is most important.&lt;/i&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;i&gt;But most importantly: know when to be inconsistent -- sometimes the
style guide just doesn&#39;t apply.  When in doubt, use your best
judgment.  Look at other examples and decide what looks best.  And
don&#39;t hesitate to ask!&lt;/i&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;i&gt;Two good reasons to break a particular rule:&lt;/i&gt;&lt;br /&gt;
&lt;ol class=&quot;arabic simple&quot;&gt;
&lt;li&gt;&lt;i&gt;When applying the rule would make the code less readable, even for
someone who is used to reading code that follows the rules.&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;To be consistent with surrounding code that also breaks it (maybe
for historic reasons)&lt;/i&gt; —&lt;i&gt; although this is also an opportunity to
clean up someone else&#39;s mess (in true &lt;a href=&quot;https://en.wikipedia.org/wiki/Extreme_programming&quot;&gt;XP&lt;/a&gt; style).&lt;/i&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/4085141932931233450/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2012/06/running-pep8-before-any-commit-on-git.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/4085141932931233450'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/4085141932931233450'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2012/06/running-pep8-before-any-commit-on-git.html' title='Running pep8 before any commit on Git'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-5898781102127620237</id><published>2012-05-30T19:36:00.001-05:00</published><updated>2012-06-05T11:13:30.200-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="buildout"/><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="open source"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="problem solving"/><category scheme="http://www.blogger.com/atom/ns#" term="testing"/><title type='text'>Robot Framework and SeleniumLibrary for Plone developers</title><content type='html'>&lt;i&gt;Update 05/06/2012: Added a tip for handling overlays and a reference to datakurre&#39;s work.&lt;/i&gt;&lt;br /&gt;
&lt;br /&gt;
Let&#39;s face it: &lt;a href=&quot;http://pypi.python.org/pypi/zope.testbrowser&quot;&gt;zope.testbrowser&lt;/a&gt; sucks for functional and acceptance tests: tests are boring to write, hard to debug when they fail, and there&#39;s no support for JavaScript.&lt;br /&gt;
&lt;br /&gt;
The latest is becoming more important every day, as Plone codebase now includes almost 30% of JavaScript code according to &lt;a href=&quot;http://weblion.psu.edu/symposium/talks/ui-testing-with-selenium-and-robot-framework&quot;&gt;Ed Manlove&lt;/a&gt; (and &lt;a href=&quot;https://www.ohloh.net/p/plone/analyses/latest&quot;&gt;Ohloh&lt;/a&gt;).&lt;br /&gt;
&lt;br /&gt;
There are some alternatives that help on solving these issues and &lt;a href=&quot;http://seleniumhq.org/&quot;&gt;Selenium&lt;/a&gt; —a portable software testing framework for web applications— stands among the best.&lt;br /&gt;
&lt;br /&gt;
Some years ago I started using Selenium to make some functional tests, but I abandoned the task because it was a little bit slow and boring.&lt;br /&gt;
&lt;br /&gt;
During the sprints held after the Plone Conference 2011 in San Francisco, &lt;a href=&quot;http://bubblenet.be/Members/gotcha&quot;&gt;Godefroid Chapelle&lt;/a&gt; introduced us to &lt;a href=&quot;https://code.google.com/p/robotframework/&quot;&gt;Robot Framework&lt;/a&gt; —a generic test automation framework for acceptance testing and acceptance test-driven development (ATDD)— and &lt;a href=&quot;https://code.google.com/p/robotframework-seleniumlibrary/&quot;&gt;SeleniumLibrary&lt;/a&gt; —a Robot Framework test library that uses the Selenium web testing tool internally.&lt;br /&gt;
&lt;br /&gt;
Writing tests with Robot Framework is pretty easy: you can create tests by using &lt;a href=&quot;https://github.com/robotframework/RIDE&quot;&gt;RIDE&lt;/a&gt; —a light-weight and intuitive editor for Robot Framework test case
files— or just by using your favorite text editor.&lt;br /&gt;
&lt;br /&gt;
A test case is made with a list of keywords and you can create more keywords using the same syntax used for creating the test cases; you can use natural language and you can reuse your test code. This way you start pretty slow but you become more and more productive with every new keyword you add to your test suites.&lt;br /&gt;
&lt;br /&gt;
After coming back home I started adding some very basic functional tests to some of the packages we have developed lately. &lt;br /&gt;
&lt;br /&gt;
I&#39;m not interested on writing a Robot Framework tutorial; there are many on the web (&lt;a href=&quot;http://www.wallix.org/2011/07/26/how-to-use-robotframework-with-the-selenium-library/&quot;&gt;How to use Robot Framework with the Selenium Library&lt;/a&gt; is a pretty good one; &lt;a href=&quot;http://www.wallix.org/2011/09/06/how-to-use-robotframework-part-2/&quot;&gt;part 2&lt;/a&gt; and &lt;a href=&quot;http://www.wallix.org/2011/10/11/extending-robotframework-to-check-emails/&quot;&gt;Extending Robot Framework to check emails&lt;/a&gt;, are also available).&lt;br /&gt;
&lt;br /&gt;
The purpose of this post is to help other Plone developers on getting Robot Framework up and running and to share some tricks I&#39;ve learned over the last few months.
&lt;br /&gt;
&lt;h2&gt;












Don&#39;t panic&lt;/h2&gt;
&lt;br /&gt;
Basically all you need to start playing with Robot Framework and 
SeleniumLibrary in Plone is a buildout configuration, a small resource 
file including some basic keywords for Plone, a couple of helper files 
and some test suites.&lt;br /&gt;
&lt;br /&gt;
You can take a look at the test suites written during the sprint in the &lt;a href=&quot;https://github.com/plone/buildout.coredev/tree/4.1-robot&quot;&gt;4.1-robot branch&lt;/a&gt; of Plone&#39;s &lt;a href=&quot;https://github.com/plone/buildout.coredev&quot;&gt;buildout.coredev&lt;/a&gt; on &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt;, and you can grab the basic files from there (&lt;a href=&quot;https://github.com/plone/buildout.coredev/blob/4.1-robot/pybot.cfg&quot;&gt;pybot.cfg&lt;/a&gt; and the whole &lt;a href=&quot;https://github.com/plone/buildout.coredev/tree/4.1-robot/acceptance-tests&quot;&gt;acceptance-tests&lt;/a&gt; and &lt;a href=&quot;https://github.com/plone/buildout.coredev/tree/4.1-robot/templates&quot;&gt;templates&lt;/a&gt; directories); we are going to modify them a little bit in a minute.&lt;br /&gt;
&lt;br /&gt;
You will also need to download and install the  &lt;a href=&quot;http://seleniumhq.org/projects/ide/&quot;&gt;Selenium IDE Plugins&lt;/a&gt; —an integrated development environment for Selenium scripts implemented as a Firefox extension— in your Firefox browser (sorry, but I don&#39;t know if there are other browser options available at the moment).&lt;br /&gt;
&lt;br /&gt;
First, here is the simplified buildout configuration I&#39;ve been working on based on the one created by Godefroid:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;b&gt;[buildout]&lt;/b&gt;
extends = buildout.cfg
parts += plonesite robot selenium library-settings

&lt;b&gt;[versions]&lt;/b&gt;
selenium-server = 2.22.0

&lt;b&gt;[plonesite]&lt;/b&gt;
recipe = collective.recipe.plonesite
profiles = &lt;i&gt;my.package:default

&lt;/i&gt;&lt;b&gt;[robot]&lt;/b&gt;
recipe = zc.recipe.egg
eggs =
    robotframework
    robotframework-seleniumlibrary
entry-points = pybot=robot:run_cli rebot=robot:rebot_cli
arguments = sys.argv[1:]

&lt;b&gt;[selenium]&lt;/b&gt;
recipe = hexagonit.recipe.download
download-only = true
url = http://selenium.googlecode.com/files/selenium-server-standalone-&lt;i&gt;${versions:selenium-server}&lt;/i&gt;.jar
filename = selenium-server.jar

&lt;b&gt;[library-settings]&lt;/b&gt;
recipe = collective.recipe.template
input = templates/library-settings.txt.in
output = ${buildout:directory}/acceptance-tests/library-settings.txt
&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
We extend our standard development configuration by adding four parts: &lt;b&gt;plonesite&lt;/b&gt;, that creates a new Plone site, if there is none, and runs the profiles listed (&lt;i&gt;my.package:default&lt;/i&gt;); &lt;b&gt;robot&lt;/b&gt;, that downloads the &lt;a href=&quot;http://pypi.python.org/pypi/robotframework&quot;&gt;robotframework&lt;/a&gt; and &lt;a href=&quot;http://pypi.python.org/pypi/robotframework-seleniumlibrary&quot;&gt;robotframework-seleniumlibrary&lt;/a&gt; eggs, and creates the commands needed to run the tests (yes, this is 2012 and they are still unaware of egg entry points); &lt;b&gt;selenium&lt;/b&gt;, that downloads the Selenium standalone server; and, finally, &lt;b&gt;library-settings&lt;/b&gt;, that is used to initialize some variables used by the tests, it takes a template file (templates/library-settings.txt.in) and replaces buildout variables with their values creating a new file (acceptance-tests/library-settings.txt).&lt;br /&gt;
&lt;br /&gt;
Please note the use of a &lt;i&gt;${versions:selenium-server}&lt;/i&gt; variable in the &lt;b&gt;selenium&lt;/b&gt; part of the configuration; this helps us on keeping it up to date just like with any other egg.&lt;br /&gt;
&lt;br /&gt;
Let&#39;s get started: add the files you downloaded to the buildout you want to test. Run &lt;i&gt;bin/buildout -c pybot.cfg&lt;/i&gt; and you will find a couple of new scripts inside your bin directory: pybot and rebot.&lt;br /&gt;
&lt;br /&gt;
Start your instance as usual using &lt;i&gt;bin/instance fg&lt;/i&gt; and, on another terminal window, run &lt;i&gt;bin/pybot acceptance-tests&lt;/i&gt;. Sit comfortably; let the show begin…&lt;br /&gt;
&lt;br /&gt;
After a few seconds a couple of Firefox windows will appear: the first one is the RemoteRunner which is required to control the Plone site running in the second window.&lt;br /&gt;
&lt;br /&gt;
The tests will run and you will get a complete report of their results as a couple of HTML files: &lt;b&gt;report.html&lt;/b&gt; includes summary information, test statistics and details; &lt;b&gt;log.html&lt;/b&gt; includes a complete log of all tests executed.&lt;br /&gt;
&lt;br /&gt;
In case of error, &lt;b&gt;log.html&lt;/b&gt; includes detailed information about it, the source code of the page where the error ocurred and a screenshot of it.&lt;br /&gt;
&lt;br /&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghJUtO8pFmeCUuloBE4gcUPrWWY7dLT6rALNqsdjwN9MF6z4Ao_TXZee44_OwI7KSwHbOj_wz8A8LRH_YjP5La-YSfXBURPBx0ieA7VYqGPIz23fxOyLJDksYZ8eBCkDdF1z9fy_MVfmI/s1600/report.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;180&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghJUtO8pFmeCUuloBE4gcUPrWWY7dLT6rALNqsdjwN9MF6z4Ao_TXZee44_OwI7KSwHbOj_wz8A8LRH_YjP5La-YSfXBURPBx0ieA7VYqGPIz23fxOyLJDksYZ8eBCkDdF1z9fy_MVfmI/s320/report.png&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Test Report&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br /&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJW81ySeaJ9AfWct8R-PmEgxJ3bWh_7h11_EhNQbdppJGa4zng4CedJ8vJt8TdKN0cgaB3L0nr1MFZDlbgtSAp5z0Pr6H-MwAnyE45ErL71gfHrBogx0MZfpfk6NYEnVQ_b0GYnWeuj5c/s1600/log.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;400&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJW81ySeaJ9AfWct8R-PmEgxJ3bWh_7h11_EhNQbdppJGa4zng4CedJ8vJt8TdKN0cgaB3L0nr1MFZDlbgtSAp5z0Pr6H-MwAnyE45ErL71gfHrBogx0MZfpfk6NYEnVQ_b0GYnWeuj5c/s400/log.png&quot; width=&quot;281&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Test Log&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;












Share and enjoy&lt;/h2&gt;
And now for something completely different… the tips and tricks.&lt;br /&gt;
&lt;h3&gt;












What to include in your .gitignore file&lt;/h3&gt;
Most of our packages live on GitHub and I like include a list of objects to be ignored from version control using a &lt;a href=&quot;http://help.github.com/ignore-files/&quot;&gt;.gitignore&lt;/a&gt; file; the following is what I use to add in packages using Robot Framework and SeleniumLibrary tests:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;library-settings.txt
log.html
output.xml
report.html
selenium* &lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;












Running a single test suite&lt;/h3&gt;
If you want to run only one test suite you can do something like this:&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;bin/pybot -s &lt;i&gt;test_suite&lt;/i&gt; acceptance-tests&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;

Handling overlays &lt;/h3&gt;
Suppose you have to wait for an overlay window to show up in the screen. By default, a page load is expected to happen whenever a link
    or image is clicked, or a form submitted. In this case we pass the &lt;b&gt;don&#39;t wait&lt;/b&gt; argument to the keyword.&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;Delete Item
    [Arguments]  ${title}

    Click Link  ${title}
    &lt;b&gt;Click Link  Delete  don&#39;t wait&lt;/b&gt;
    Wait Until Page Contains  Do you really want to delete this item?&lt;b&gt;
&lt;/b&gt;    Click Button  Delete&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
As there is no &lt;i&gt;Wait Until Page Does Not Contains&lt;/i&gt; nor &lt;i&gt;Wait Until Page Does Not Contains Element &lt;/i&gt;keywords, you will have to use something like this in case you want to know if an overlay was closed:&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;    &lt;b&gt;Wait Until Keyword Succeeds&lt;/b&gt;  1  5  Page Should Not Contain  ${text}&lt;/pre&gt;
&lt;/blockquote&gt;
The trick here is the &lt;b&gt;Wait Until Keyword Succeeds&lt;/b&gt; keyword.
If the specified keyword does not succeed within &lt;span class=&quot;name&quot;&gt;timeout&lt;/span&gt;, this keyword fails and waits lapse of time before trying to run the keyword again.

&lt;br /&gt;
&lt;h3&gt;


Uploading files and images&lt;/h3&gt;
This is a typical test case: let&#39;s say you developed a new content type that includes a file or image field and you want to test it. A basic test case could be something similar to this one:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;*** Settings ***

Resource  plone.txt
Suite Setup  Setup

*** Variables ***

&lt;b&gt;${link_locator}&lt;/b&gt; =  a#file
&lt;b&gt;${input_identifier}&lt;/b&gt; =  input#file_file

*** Test cases ***

Test Add Audio File
    Goto Homepage
    Add File  ${PATH_TO_TEST_FILES}/test.mp3

*** Keywords ***

Setup
    Log In  admin  admin

Add File
    [arguments]  ${file}

    Open Add New Menu
    Click Link  css=&lt;b&gt;${link_locator}&lt;/b&gt;
    Page Should Contain  Add File
    Choose File  css=&lt;b&gt;${input_identifier}&lt;/b&gt;  ${file}
    Click Button  Save
    Page Should Contain  Changes saved
&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
The variable ${PATH_TO_TEST_FILES} could be declared in your library-settings.txt.in file as something like this (please note theres are 2 spaces after the equal sign):&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;*** Variables ***

&lt;b&gt;${dollar}&lt;/b&gt;{PATH_TO_TEST_FILES} =  ${buildout:directory}/src/my/package/tests&lt;/pre&gt;
&lt;/blockquote&gt;
The trick here is the use of a &lt;b&gt;${dollar}&lt;/b&gt; variable that is defined in the &lt;br /&gt;
library-settings part of our pybot.cfg buildout configuration as:&lt;br /&gt;
&lt;blockquote&gt;
&lt;pre&gt;[library-settings]
…
&lt;b&gt;dollar = $&lt;/b&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
In case you need to upload an image you will replace the CSS selectors &lt;b&gt;${link_locator}&lt;/b&gt; and &lt;b&gt;${input_identifier}&lt;/b&gt; with &lt;i&gt;a#image&lt;/i&gt; and &lt;i&gt;input#image_file&lt;/i&gt; respectively.&lt;br /&gt;
&lt;h2&gt;












So long, and thanks for all the fish&lt;/h2&gt;
Robot Framework and SeleniumLibrary makes your life 
much easier and fun when writing functional and acceptance tests: you become 
more productive with the addition of every new keyword and debugging 
failures is child&#39;s play with the help of page source code and screenshots.&lt;br /&gt;
&lt;br /&gt;
The
 main drawback at the moment is that we lack a good resource file 
including more keywords covering other Plone features, but that can be 
solved easily as soon as more people start using Robot Framework and we 
find a way to collaborate on this.&lt;br /&gt;
&lt;br /&gt;
Another important point that I had forgotten to mention, until &lt;a href=&quot;https://github.com/datakurre&quot;&gt;Asko Soukka&lt;/a&gt; remembered it to me, is that we don&#39;t have a concept of layers to set up fixtures and a way to clean up global state of the Plone site. This is particularly useful if you want to return your site to its original state without having to remove all changes made by some test case. &lt;a href=&quot;https://github.com/datakurre/robotsuite&quot;&gt;Asko has made some advances on that lately&lt;/a&gt;.</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/5898781102127620237/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2012/05/robot-framework-and-seleniumlibrary-for.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/5898781102127620237'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/5898781102127620237'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2012/05/robot-framework-and-seleniumlibrary-for.html' title='Robot Framework and SeleniumLibrary for Plone developers'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghJUtO8pFmeCUuloBE4gcUPrWWY7dLT6rALNqsdjwN9MF6z4Ao_TXZee44_OwI7KSwHbOj_wz8A8LRH_YjP5La-YSfXBURPBx0ieA7VYqGPIz23fxOyLJDksYZ8eBCkDdF1z9fy_MVfmI/s72-c/report.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-2615866520643397657</id><published>2012-01-23T23:45:00.000-06:00</published><updated>2012-01-23T23:45:03.361-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="buildout"/><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="problem solving"/><category scheme="http://www.blogger.com/atom/ns#" term="testing"/><title type='text'>Adding test users programmatically using collective.recipe.plonesite</title><content type='html'>I&#39;m writing some functional tests for a Plone project and I need to add a group of test users every time I create a test site.&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;http://pypi.python.org/pypi/collective.recipe.plonesite&quot;&gt;collective.recipe.plonesite&lt;/a&gt; is a cool Buildout recipe that enables you to create and update a Plone site as part of a buildout run.&lt;br /&gt;
&lt;br /&gt;
I added the following lines to my buildout configuration:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;parts =
    …
    plonesite

[plonesite]
recipe = collective.recipe.plonesite
profiles = my.project:default
post-extras = ${buildout:directory}/acceptance-tests/add_test_users.py&lt;/pre&gt;&lt;br /&gt;
The code of the &lt;b&gt;add_test_users.py&lt;/b&gt; script is pretty straightforward:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&quot;&quot;&quot;This script will add a number of test users to a Plone site.

You can used it in post-extras option of collective.recipe.plonesite. It will
be evaluated after running QuickInstaller and GenericSetup profiles.

@param portal: The Plone site as defined by the site-id option
&quot;&quot;&quot;

import logging
logger = logging.getLogger(&#39;collective.recipe.plonesite&#39;)

test_users = [
    # (username, password, group),
    (&#39;username1&#39;, &#39;password1&#39;, &#39;group1&#39;),
    (&#39;username2&#39;, &#39;password2&#39;, &#39;group2&#39;),
    (&#39;username3&#39;, &#39;password3&#39;, &#39;group3&#39;),
    ]

for username, password, group in test_users:
    if username not in portal.acl_users.getUserIds():
        try:
            portal.portal_registration.addMember(username, password)
            portal.portal_groups.addPrincipalToGroup(username, group)
        except ValueError:
            logger.warn(&#39;The login name &quot;%s&quot; is not valid.&#39; % username)
        except KeyError:
            logger.warn(&#39;The group &quot;%s&quot; is not valid.&#39; % group)
&lt;/pre&gt;&lt;br /&gt;
Enjoy!&lt;br /&gt;
&lt;br /&gt;
(Next time I&#39;ll give you my first impressions on SeleniumLibrary, a web testing library for Robot Framework, I&#39;m using to write the tests.)</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/2615866520643397657/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2012/01/adding-test-users-programmatically.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/2615866520643397657'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/2615866520643397657'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2012/01/adding-test-users-programmatically.html' title='Adding test users programmatically using collective.recipe.plonesite'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-4978048172423629334</id><published>2012-01-13T19:08:00.001-06:00</published><updated>2012-01-13T20:23:22.910-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="problem solving"/><category scheme="http://www.blogger.com/atom/ns#" term="zodb"/><category scheme="http://www.blogger.com/atom/ns#" term="zope"/><title type='text'>Setting the right permissions on your blobstorage directory</title><content type='html'>Have you ever been annoyed by a message saying that the blobstorage directory of your instance has an insecure mode setting?&lt;br /&gt;
&lt;br /&gt;
That happens to me all the time, so today I spent a couple of minutes trying to figure out how to fix it.&lt;br /&gt;
&lt;br /&gt;
In &lt;b&gt;ZODB/blob.py&lt;/b&gt; we have the following:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;class &lt;b&gt;FilesystemHelper&lt;/b&gt;:
    # Storages that implement IBlobStorage can choose to use this
    # helper class to generate and parse blob filenames.  This is not
    # a set-in-stone interface for all filesystem operations dealing
    # with blobs and storages needn&#39;t indirect through this if they
    # want to perform blob storage differently.

    …

    def &lt;b&gt;create&lt;/b&gt;(self):
        if not os.path.exists(self.base_dir):
            os.makedirs(self.base_dir, 0700)
            log(&quot;Blob directory &#39;%s&#39; does not exist. &quot;
                &quot;Created new directory.&quot; % self.base_dir)
        if not os.path.exists(self.temp_dir):
            os.makedirs(self.temp_dir, 0700)
            log(&quot;Blob temporary directory &#39;%s&#39; does not exist. &quot;
                &quot;Created new directory.&quot; % self.temp_dir)

        if not os.path.exists(os.path.join(self.base_dir, LAYOUT_MARKER)):
            layout_marker = open(
                os.path.join(self.base_dir, LAYOUT_MARKER), &#39;wb&#39;)
            layout_marker.write(self.layout_name)
        else:
            layout = open(os.path.join(self.base_dir, LAYOUT_MARKER), &#39;rb&#39;
                          ).read().strip()
            if layout != self.layout_name:
                raise ValueError(
                    &quot;Directory layout `%s` selected for blob directory %s, but &quot;
                    &quot;marker found for layout `%s`&quot; %
                    (self.layout_name, self.base_dir, layout))

    def &lt;b&gt;isSecure&lt;/b&gt;(self, path):
        &quot;&quot;&quot;Ensure that (POSIX) path mode bits are 0700.&quot;&quot;&quot;
        &lt;b&gt;return (os.stat(path).st_mode &amp;amp; 077) == 0&lt;/b&gt;

    def &lt;b&gt;checkSecure&lt;/b&gt;(self):
        if not self.isSecure(self.base_dir):
            &lt;b&gt;log(&#39;Blob dir %s has insecure mode setting&#39; % self.base_dir,
                level=logging.WARNING)
&lt;/b&gt;&lt;/pre&gt;&lt;br /&gt;
Then, the only thing you need to do is run &lt;b&gt;chmod 700 var/blobstorage&lt;/b&gt; (&lt;i&gt;owner can read, write and execute&lt;/i&gt;) in your installation directory.&lt;br /&gt;
&lt;br /&gt;
Why this directory is created with a different setting (&lt;b&gt;755&lt;/b&gt;) is a mystery to solve another day.</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/4978048172423629334/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2012/01/setting-right-permissions-on-your.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/4978048172423629334'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/4978048172423629334'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2012/01/setting-right-permissions-on-your.html' title='Setting the right permissions on your blobstorage directory'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-4296049146725462856</id><published>2011-12-20T14:34:00.000-06:00</published><updated>2011-12-20T14:34:18.166-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="grok"/><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><title type='text'>Hiding uninstall profiles using Grok</title><content type='html'>Tired of seeing your uninstall profile listed even when your package has not being installed yet?&lt;br /&gt;
&lt;br /&gt;
If you have &lt;a href=&quot;http://pypi.python.org/pypi/five.grok&quot;&gt;five.grok&lt;/a&gt; among the dependencies  of your package in &lt;b&gt;setup.py&lt;/b&gt;, you can put the following code in a &lt;b&gt;setuphandlers.py&lt;/b&gt; file:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;from five import grok

from Products.CMFPlone.interfaces import INonInstallable


class HiddenProfiles(grok.GlobalUtility):

    grok.implements(INonInstallable)
    grok.provides(INonInstallable)
    grok.name(&#39;&lt;b&gt;your.package&lt;/b&gt;&#39;)

    def getNonInstallableProfiles(self):
        profiles = [&#39;&lt;b&gt;your.package&lt;/b&gt;:uninstall&#39;]
        return profiles
&lt;/pre&gt;&lt;br /&gt;
Remember that you need to Grok your package using the following line in your &lt;b&gt;configure.zcml&lt;/b&gt;:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&amp;lt;grok:grok package=&quot;.&quot; /&amp;gt;&lt;/pre&gt;&lt;br /&gt;
This cool tip has been brought to you by &lt;a href=&quot;http://www.erico.com.br/&quot;&gt;Érico Andrei&lt;/a&gt;; enjoy it!</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/4296049146725462856/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2011/12/hiding-uninstall-profiles-using-grok.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/4296049146725462856'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/4296049146725462856'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2011/12/hiding-uninstall-profiles-using-grok.html' title='Hiding uninstall profiles using Grok'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-8544649643914287473</id><published>2011-11-18T20:04:00.003-06:00</published><updated>2011-11-18T20:39:27.434-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="activism"/><category scheme="http://www.blogger.com/atom/ns#" term="eff"/><category scheme="http://www.blogger.com/atom/ns#" term="open source"/><title type='text'>Max Keiser interviews Grateful Dead&#39;s John Perry Barlow about activism and open source</title><content type='html'>&lt;a href=&quot;http://en.wikipedia.org/wiki/John_Perry_Barlow&quot;&gt;John Perry Barlow&lt;/a&gt; is a poet, essayist, retired rancher, political activist, former lyricist for the &lt;a href=&quot;http://en.wikipedia.org/wiki/Grateful_Dead&quot;&gt;Grateful Dead&lt;/a&gt; and founding member of the &lt;a href=&quot;http://en.wikipedia.org/wiki/Electronic_Frontier_Foundation&quot;&gt;Electronic Frontier Foundation&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
&lt;a href=&quot;http://en.wikipedia.org/wiki/Max_Keiser&quot;&gt;Max Keiser&lt;/a&gt; interviews John Perry Barlow about activism and &lt;a href=&quot;http://en.wikipedia.org/wiki/Open_source&quot;&gt;open source&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
A must see!&lt;br /&gt;
&lt;br /&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;iframe class=&quot;youtube-player&quot; frameborder=&quot;0&quot; height=&quot;240&quot; src=&quot;http://www.youtube.com/embed/_Tg-zsVhTr8&quot; type=&quot;text/html&quot; width=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/8544649643914287473/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2011/11/max-keiser-interviews-grateful-deads.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/8544649643914287473'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/8544649643914287473'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2011/11/max-keiser-interviews-grateful-deads.html' title='Max Keiser interviews Grateful Dead&#39;s John Perry Barlow about activism and open source'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img.youtube.com/vi/_Tg-zsVhTr8/default.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-290487569469472988</id><published>2011-11-05T09:57:00.000-06:00</published><updated>2011-11-05T09:57:22.350-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="media"/><category scheme="http://www.blogger.com/atom/ns#" term="nitf"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><title type='text'>Plone in news media</title><content type='html'>Lightning talk at the Plone Conference 2011.&lt;br /&gt;
&lt;br /&gt;
&lt;iframe frameborder=&quot;0&quot; height=&quot;355&quot; marginheight=&quot;0&quot; marginwidth=&quot;0&quot; scrolling=&quot;no&quot; src=&quot;http://www.slideshare.net/slideshow/embed_code/10031624&quot; width=&quot;425&quot;&gt;&lt;/iframe&gt; &lt;br /&gt;
&lt;div style=&quot;padding: 5px 0 12px;&quot;&gt;View more &lt;a href=&quot;http://www.slideshare.net/&quot; target=&quot;_blank&quot;&gt;presentations&lt;/a&gt; from &lt;a href=&quot;http://www.slideshare.net/hvelarde&quot; target=&quot;_blank&quot;&gt;Héctor Velarde&lt;/a&gt; &lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/290487569469472988/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2011/11/plone-in-news-media.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/290487569469472988'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/290487569469472988'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2011/11/plone-in-news-media.html' title='Plone in news media'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total><georss:featurename>San Francisco, California, EEUU</georss:featurename><georss:point>37.7749295 -122.41941550000001</georss:point><georss:box>37.7206295 -122.50881550000001 37.8292295 -122.33001550000002</georss:box></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-6470923807097160948</id><published>2011-10-20T11:18:00.000-05:00</published><updated>2011-10-20T11:18:58.853-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="community"/><category scheme="http://www.blogger.com/atom/ns#" term="latinoware"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><title type='text'>Plone: pasado, presente y futuro</title><content type='html'>&lt;div style=&quot;width:425px&quot; id=&quot;__ss_9796096&quot;&gt;&lt;strong style=&quot;display:block;margin:12px 0 4px&quot;&gt;&lt;a href=&quot;http://www.slideshare.net/hvelarde/plone-pasado-presente-y-futuro&quot; title=&quot;Plone: pasado, presente y futuro&quot; target=&quot;_blank&quot;&gt;Plone: pasado, presente y futuro&lt;/a&gt;&lt;/strong&gt; &lt;iframe src=&quot;http://www.slideshare.net/slideshow/embed_code/9796096&quot; width=&quot;425&quot; height=&quot;355&quot; frameborder=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; scrolling=&quot;no&quot;&gt;&lt;/iframe&gt; &lt;div style=&quot;padding:5px 0 12px&quot;&gt;View more &lt;a href=&quot;http://www.slideshare.net/&quot; target=&quot;_blank&quot;&gt;presentations&lt;/a&gt; from &lt;a href=&quot;http://www.slideshare.net/hvelarde&quot; target=&quot;_blank&quot;&gt;Héctor Velarde&lt;/a&gt; &lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/6470923807097160948/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2011/10/plone-pasado-presente-y-futuro.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/6470923807097160948'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/6470923807097160948'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2011/10/plone-pasado-presente-y-futuro.html' title='Plone: pasado, presente y futuro'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total><georss:featurename>Foz do Iguaçu - Estado de Paraná, Brasil</georss:featurename><georss:point>-25.5468978 -54.58817160000001</georss:point><georss:box>-25.7884863 -54.750576100000011 -25.3053093 -54.425767100000009</georss:box></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-8630666668974164957</id><published>2011-10-13T20:52:00.002-05:00</published><updated>2011-10-14T11:11:57.713-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="css"/><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="problem solving"/><title type='text'>Cómo habilitar la selección y copia de texto en el sitio de El Universal</title><content type='html'>Hace un rato me topé con la novedad de que algunas páginas de &lt;a href=&quot;http://www.eluniversal.com.mx/&quot;&gt;El Universal&lt;/a&gt; no permiten seleccionar texto para copiarlo.&lt;br /&gt;
&lt;br /&gt;
Al principio pensé que se trataba de algún &lt;b&gt;script&lt;/b&gt; pero pronto descubrí que se era otra cosa: la inclusión en la hoja de estilo de la página de los seudoelementos &lt;a href=&quot;https://developer.mozilla.org/en/CSS/-moz-user-select&quot;&gt;-moz-user-select&lt;/a&gt; y &lt;a href=&quot;https://developer.mozilla.org/en/CSS/-moz-user-focus&quot;&gt;-moz-user-focus&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
Si usas &lt;a href=&quot;http://www.mozilla.org/firefox/&quot;&gt;Firefox&lt;/a&gt;, la solución es muy sencilla: crea un archivo &lt;a href=&quot;http://www-archive.mozilla.org/unix/customizing.html&quot;&gt;userContent.css&lt;/a&gt; dentro la carpeta &lt;b&gt;chrome&lt;/b&gt; de tu perfil y agrega las siguientes líneas:&lt;br /&gt;
&lt;code&gt;&lt;br /&gt;
* { -moz-user-select: auto !important }&lt;br /&gt;
* { -moz-user-focus: normal !important }&lt;br /&gt;
&lt;/code&gt;&lt;br /&gt;
&lt;br /&gt;
Reinicia tu navegador y listo.&lt;br /&gt;
&lt;br /&gt;
Que nadie coarte tu derecho a compartir conocimiento.</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/8630666668974164957/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2011/10/como-habilitar-la-seleccion-y-copia-de.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/8630666668974164957'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/8630666668974164957'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2011/10/como-habilitar-la-seleccion-y-copia-de.html' title='Cómo habilitar la selección y copia de texto en el sitio de El Universal'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-173554915568005818</id><published>2011-08-02T19:27:00.003-05:00</published><updated>2011-08-02T21:54:37.668-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="templates"/><title type='text'>Displaying (and filtering) the contents of a folderish content type</title><content type='html'>If you need to list the contents of a folderish content type you can include something like this in your page template:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&amp;lt;fieldset id=&quot;folder-listing&quot;&amp;gt;
    &amp;lt;legend i18n:translate=&quot;&quot;&amp;gt;Contents&amp;lt;/legend&amp;gt;
    &amp;lt;tal:block define=&quot;listing_macro context/folder_listing/macros/listing&quot;&amp;gt;
        &amp;lt;metal:use_macro use-macro=&quot;listing_macro&quot; /&amp;gt;
    &amp;lt;/tal:block&amp;gt;
&amp;lt;/fieldset&amp;gt;
&lt;/pre&gt;&lt;br /&gt;
Note that &lt;b&gt;listing&lt;/b&gt; is a huge macro inside the &lt;a href=&quot;http://dev.plone.org/plone/browser/Products.CMFPlone/tags/4.1/Products/CMFPlone/skins/plone_content/folder_listing.pt&quot;&gt;skins/plone_content/folder_listing.pt&lt;/a&gt; template in the &lt;b&gt;CMFPlone&lt;/b&gt; product.&lt;br /&gt;
&lt;br /&gt;
You can even do things like content filtering or limiting the number of items shown with this macro:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&amp;lt;fieldset id=&quot;folder-listing&quot;&amp;gt;
    &amp;lt;legend i18n:translate=&quot;&quot;&amp;gt;Contents&amp;lt;/legend&amp;gt;
    &amp;lt;tal:block define=&quot;listing_macro context/folder_listing/macros/listing;
                       &lt;b&gt;contentFilter&lt;/b&gt; python:{&#39;portal_type&#39;: [&#39;File&#39;]};
                       &lt;b&gt;limit_display&lt;/b&gt; python:5&quot;&amp;gt;
        &amp;lt;metal:use_macro use-macro=&quot;listing_macro&quot; /&amp;gt;
    &amp;lt;/tal:block&amp;gt;
&amp;lt;/fieldset&amp;gt;
&lt;/pre&gt;&lt;br /&gt;
Enjoy!&lt;br /&gt;
&lt;br /&gt;
(Tested under Plone 4.1)</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/173554915568005818/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2011/08/displaying-and-filtering-contents-of.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/173554915568005818'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/173554915568005818'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2011/08/displaying-and-filtering-contents-of.html' title='Displaying (and filtering) the contents of a folderish content type'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-1974359673186595359</id><published>2011-07-06T20:31:00.002-05:00</published><updated>2011-07-06T20:35:17.863-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="community"/><category scheme="http://www.blogger.com/atom/ns#" term="open source"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><title type='text'>Plone: software libre y comunidad</title><content type='html'>Charla sobre Plone y software libre en el marco del OpenSource Day de la XIII Semana de Ingeniería e Informática de la &lt;a href=&quot;http://www.justosierra.com/&quot;&gt;Universidad Justo Sierra&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
&lt;div align=&quot;center&quot;&gt;&lt;div id=&quot;__ss_8527445&quot; style=&quot;width: 425px;&quot;&gt;&lt;b style=&quot;display: block; margin: 12px 0 4px;&quot;&gt;&lt;a href=&quot;http://www.slideshare.net/hvelarde/charla-sobre-plone-y-software-libre-en-la-universidad-justo-sierra&quot; target=&quot;_blank&quot; title=&quot;Plone: software libre y comunidad&quot;&gt;Plone: software libre y comunidad&lt;/a&gt;&lt;/b&gt; &lt;iframe frameborder=&quot;0&quot; height=&quot;355&quot; marginheight=&quot;0&quot; marginwidth=&quot;0&quot; scrolling=&quot;no&quot; src=&quot;http://www.slideshare.net/slideshow/embed_code/8527445&quot; width=&quot;425&quot;&gt;&lt;/iframe&gt; &lt;br /&gt;
&lt;div style=&quot;padding: 5px 0 12px;&quot;&gt;View more &lt;a href=&quot;http://www.slideshare.net/&quot; target=&quot;_blank&quot;&gt;presentations&lt;/a&gt; from &lt;a href=&quot;http://www.slideshare.net/hvelarde&quot; target=&quot;_blank&quot;&gt;Héctor Velarde&lt;/a&gt; &lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/1974359673186595359/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2011/07/plone-software-libre-y-comunidad.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/1974359673186595359'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/1974359673186595359'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2011/07/plone-software-libre-y-comunidad.html' title='Plone: software libre y comunidad'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total><georss:featurename>Universidad Justo Sierra</georss:featurename><georss:point>19.5124325 -99.154472</georss:point><georss:box>14.3409005 -106.625175 24.6839645 -91.683769</georss:box></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-2380377914017318286</id><published>2011-03-10T21:53:00.002-06:00</published><updated>2011-07-06T22:28:23.684-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="testing"/><title type='text'>Redirecting log output to sys.stdout on tests</title><content type='html'>Use the following code inside your &lt;b&gt;tests/base.py&lt;/b&gt; file if you need to redirect the log output to &lt;b&gt;sys.stdout&lt;/b&gt; for debugging purposes:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;import logging
import sys

logger = logging.getLogger(&#39;YOUR LOGGER NAME&#39;)
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter(&quot;%(asctime)s %(levelname)s %(name)s %(message)s&quot;)
handler.setFormatter(formatter)
logger.addHandler(handler)
&lt;/pre&gt;&lt;br /&gt;
This way you can see &lt;b&gt;debug()&lt;/b&gt;, &lt;b&gt;info()&lt;/b&gt;, &lt;b&gt;warning()&lt;/b&gt;, &lt;b&gt;error()&lt;/b&gt; and &lt;b&gt;critical()&lt;/b&gt; messages printed in the console. &lt;br /&gt;
&lt;br /&gt;
More information: &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://docs.python.org/library/logging.html&quot;&gt;Logging facility for Python&lt;/a&gt; and &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/documentation/manual/plone-community-developer-documentation/testing-and-debugging/logging&quot;&gt;Logging on Plone&lt;/a&gt;.</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/2380377914017318286/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2011/03/redirecting-log-output-to-sysstdout-on.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/2380377914017318286'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/2380377914017318286'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2011/03/redirecting-log-output-to-sysstdout-on.html' title='Redirecting log output to sys.stdout on tests'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-5295828423935898424</id><published>2011-01-28T15:17:00.003-06:00</published><updated>2011-02-04T16:10:36.263-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="transmogrifier"/><title type='text'>An Excel source pipeline section for collective.transmogrifier</title><content type='html'>Using &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://www.lexicon.net/sjmachin/xlrd.html&quot;&gt;xlrd&lt;/a&gt;, and based on &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://pypi.python.org/pypi/collective.transmogrifier#csv-source-section&quot;&gt;CSV source section&lt;/a&gt;, I&#39;ve written a pipeline section to read data stored inside &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://en.wikipedia.org/wiki/Microsoft_Excel&quot;&gt;Excel&lt;/a&gt; spreadsheets.&lt;br /&gt;
&lt;br /&gt;
The code looks like this:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;import xlrd

    from zope.interface import classProvides
    from zope.interface import implements

    from collective.transmogrifier.interfaces import ISection
    from collective.transmogrifier.interfaces import ISectionBlueprint
    from collective.transmogrifier.utils import resolvePackageReferenceOrFile


    class ExcelReader:
        def __init__(self, f, s, *args, **kwds):
            self.fieldnames = None
            book = xlrd.open_workbook(f)
            sheet = book.sheet_by_name(s)
            self.reader = list()
            for row in range(0, sheet.nrows):
                self.reader.append([sheet.cell_value(row, col)
                                    for col in range(0, sheet.ncols)])
            self.reader = iter(self.reader)

        def __iter__(self):
            return self

        def next(self):
            row = self.reader.next()
            if self.fieldnames is None:
                self.fieldnames = row
                row = self.reader.next()
            return dict(zip(self.fieldnames, row))


    class ExcelSource(object):
        classProvides(ISectionBlueprint)
        implements(ISection)

        def __init__(self, transmogrifier, name, options, previous):
            self.previous = previous

            filename = resolvePackageReferenceOrFile(options[&#39;filename&#39;])
            sheet = options[&#39;sheet&#39;]

            self.reader = ExcelReader(filename, sheet)

        def __iter__(self):
            for item in self.previous:
                yield item

            for item in self.reader:
                yield item
&lt;/pre&gt;&lt;br /&gt;
The pipeline section assumes that the field names are in the first row of the spreadsheet. Note also the use of &lt;b&gt;iter()&lt;/b&gt; to convert the list of rows into an &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://www.python.org/dev/peps/pep-0234/&quot;&gt;iterator&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
To use the pipeline section you only need to add something like this in your transmogrifier&#39;s configuration:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;[excelsource]
blueprint = &lt;b&gt;your.package.excelsource&lt;/b&gt;
filename = &lt;b&gt;excel_book&lt;/b&gt;
sheet = &lt;b&gt;sheet_inside_the_book&lt;/b&gt;&lt;/pre&gt;&lt;br /&gt;
Remember to register the utility in your configure.zcml file:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&amp;lt;utility
    component=&quot;&lt;b&gt;your.package.ExcelSource&lt;/b&gt;&quot;
    name=&quot;&lt;b&gt;your.package.excelsource&lt;/b&gt;&quot;
    /&amp;gt;&lt;/pre&gt;&lt;br /&gt;
&lt;b&gt;xlrd&lt;/b&gt; supports Excel spreadsheets &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://www.lexicon.net/sjmachin/README.html&quot;&gt;up to version 2007&lt;/a&gt;.</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/5295828423935898424/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2011/01/excel-source-pipeline-section-for.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/5295828423935898424'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/5295828423935898424'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2011/01/excel-source-pipeline-section-for.html' title='An Excel source pipeline section for collective.transmogrifier'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-7747367255182414844</id><published>2009-11-12T22:18:00.003-06:00</published><updated>2011-07-06T22:52:47.578-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="howto"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="testing"/><title type='text'>Pruebas en Plone: conceptos básicos y ejemplos</title><content type='html'>Charla sobre pruebas en Plone para la reunión de noviembre de 2009 del Grupo de Usuarios de Plone en México.&lt;br /&gt;
&lt;br /&gt;
&lt;div align=&quot;center&quot;&gt;&lt;div id=&quot;__ss_816573&quot; style=&quot;width: 425px;&quot;&gt;&lt;b style=&quot;display: block; margin: 12px 0 4px;&quot;&gt;&lt;a href=&quot;http://www.slideshare.net/hvelarde/pruebas-en-plone-presentation&quot; target=&quot;_blank&quot; title=&quot;Pruebas en Plone&quot;&gt;Pruebas en Plone&lt;/a&gt;&lt;/b&gt; &lt;iframe frameborder=&quot;0&quot; height=&quot;355&quot; marginheight=&quot;0&quot; marginwidth=&quot;0&quot; scrolling=&quot;no&quot; src=&quot;http://www.slideshare.net/slideshow/embed_code/816573&quot; width=&quot;425&quot;&gt;&lt;/iframe&gt; &lt;br /&gt;
&lt;div style=&quot;padding: 5px 0 12px;&quot;&gt;View more &lt;a href=&quot;http://www.slideshare.net/&quot; target=&quot;_blank&quot;&gt;presentations&lt;/a&gt; from &lt;a href=&quot;http://www.slideshare.net/hvelarde&quot; target=&quot;_blank&quot;&gt;Héctor Velarde&lt;/a&gt; &lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/7747367255182414844/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2009/11/pruebas-en-plone-conceptos-basicos-y.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/7747367255182414844'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/7747367255182414844'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2009/11/pruebas-en-plone-conceptos-basicos-y.html' title='Pruebas en Plone: conceptos básicos y ejemplos'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total><georss:featurename>Universidad Nacional Autónoma de México</georss:featurename><georss:point>19.3232457 -99.192044899999985</georss:point><georss:box>19.307872200000002 -99.20590039999999 19.3386192 -99.17818939999998</georss:box></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-732232563826989509</id><published>2008-11-11T20:55:00.006-06:00</published><updated>2011-07-06T21:04:07.358-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="la jornada"/><category scheme="http://www.blogger.com/atom/ns#" term="media"/><category scheme="http://www.blogger.com/atom/ns#" term="nitf"/><category scheme="http://www.blogger.com/atom/ns#" term="open source"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><title type='text'>Plone en La Jornada</title><content type='html'>Charla sobre la implementación de Plone en &lt;a href=&quot;http://www.jornada.unam.mx/&quot;&gt;La Jornada&lt;/a&gt; para la reunión de noviembre de 2008 del Grupo de Usuarios de Plone en México.&lt;br /&gt;
&lt;br /&gt;
&lt;div align=&quot;center&quot;&gt;&lt;div id=&quot;__ss_773759&quot; style=&quot;width: 425px;&quot;&gt;&lt;b style=&quot;display: block; margin: 12px 0 4px;&quot;&gt;&lt;a href=&quot;http://www.slideshare.net/hvelarde/plone-en-la-jornada-presentation&quot; target=&quot;_blank&quot; title=&quot;Plone en La Jornada&quot;&gt;Plone en La Jornada&lt;/a&gt;&lt;/b&gt; &lt;iframe frameborder=&quot;0&quot; height=&quot;355&quot; marginheight=&quot;0&quot; marginwidth=&quot;0&quot; scrolling=&quot;no&quot; src=&quot;http://www.slideshare.net/slideshow/embed_code/773759&quot; width=&quot;425&quot;&gt;&lt;/iframe&gt; &lt;br /&gt;
&lt;div style=&quot;padding: 5px 0 12px;&quot;&gt;View more &lt;a href=&quot;http://www.slideshare.net/&quot; target=&quot;_blank&quot;&gt;presentations&lt;/a&gt; from &lt;a href=&quot;http://www.slideshare.net/hvelarde&quot; target=&quot;_blank&quot;&gt;Héctor Velarde&lt;/a&gt; &lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/732232563826989509/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2008/11/plone-en-la-jornada.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/732232563826989509'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/732232563826989509'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2008/11/plone-en-la-jornada.html' title='Plone en La Jornada'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total><georss:featurename>Universidad Nacional Autónoma de México</georss:featurename><georss:point>19.3232457 -99.192044899999985</georss:point><georss:box>19.307872200000002 -99.20590039999999 19.3386192 -99.17818939999998</georss:box></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-4825130696868343756</id><published>2008-01-11T12:42:00.007-06:00</published><updated>2011-02-04T16:55:20.964-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="la jornada"/><category scheme="http://www.blogger.com/atom/ns#" term="nitf"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="problem solving"/><category scheme="http://www.blogger.com/atom/ns#" term="schemaextender"/><title type='text'>When adapting content types, schemaextender is the way to go</title><content type='html'>&lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://en.wikipedia.org/wiki/Lightning_Talks&quot;&gt;Lightning Talks&lt;/a&gt; are among the best investments of time you can make on any &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/&quot;&gt;Plone&lt;/a&gt; Conference: there&#39;s always a lot of smart people doing great things.&lt;br /&gt;
&lt;br /&gt;
During the &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/events/conferences/2007-naples&quot;&gt;Plone Conference 2007&lt;/a&gt;, in Naples, Florian Schulze spoke about &lt;b&gt;schemaxtender&lt;/b&gt;, a package that allows you to inject new fields into an &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/products/archetypes&quot;&gt;Archetypes&lt;/a&gt; schema using an &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://en.wikipedia.org/wiki/Adapter_pattern&quot;&gt;adapter&lt;/a&gt;. When I saw his talk, I knew I was going to use it for what I had in mind.&lt;br /&gt;
&lt;br /&gt;
As I mentioned before in &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://hvelarde.blogspot.com/2007/12/mapping-nitf-into-plones-metadata.html&quot;&gt;Mapping NITF into Plone&#39;s metadata&lt;/a&gt;, at &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://www.jornada.unam.mx/&quot;&gt;La Jornada&lt;/a&gt; we were looking for a way to adapt Plone&#39;s standard News Item content type to:&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;&lt;li&gt;add new fields to it (&lt;b&gt;property&lt;/b&gt;, &lt;b&gt;section&lt;/b&gt;, &lt;b&gt;urgency &lt;/b&gt;and &lt;b&gt;byline&lt;/b&gt;) &lt;/li&gt;
&lt;li&gt;change fields&#39; order among different schematas to make the edition of new content easier for the publishers&lt;/li&gt;
&lt;/ul&gt;&lt;br /&gt;
Using &lt;b&gt;schemaextender&lt;/b&gt; to accomplish these tasks was so easy that I was really excited when I finished. I spent about 40 hours (in fact, a little bit more after the second release) to read Part 1 of &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://worldcookery.com/&quot;&gt;Philip von Weitershausen&lt;/a&gt;&#39;s excellent book &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://www.amazon.com/gp/product/354076447X?ie=UTF8&amp;amp;tag=hvelarde-20&amp;amp;linkCode=as2&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=354076447X&quot;&gt;Web Component Development with Zope 3&lt;/a&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;http://www.assoc-amazon.com/e/ir?t=hvelarde-20&amp;amp;l=as2&amp;amp;o=1&amp;amp;a=354076447X&quot; style=&quot;border: medium none ! important; margin: 0px ! important;&quot; width=&quot;1&quot; /&gt;, to understand the way &lt;b&gt;schemaextender&lt;/b&gt; works, to find out how to make it work on Plone 2.5, to start using the adapted content with Smart Folders, and even to write some tests for it.&lt;br /&gt;
&lt;br /&gt;
All this work is available in a product called &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/products/nitf4plone&quot;&gt;nitf4plone&lt;/a&gt; in case you want to try it (be aware this is an &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://en.wikipedia.org/wiki/Beta_release&quot;&gt;beta release&lt;/a&gt;). The product works on both, Plone 2.5 and Plone 3.0.&lt;br /&gt;
&lt;br /&gt;
Let&#39;s analize the code... but first, a disclaimer: &lt;u&gt;don&#39;t try this on Plone 2.5&lt;/u&gt;.&lt;br /&gt;
&lt;br /&gt;
Why? &lt;b&gt;schemaextender&lt;/b&gt; was written to work with Archetypes version 1.5 or later (that&#39;s, Plone 3.0 and later). There&#39;s &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://dev.plone.org/archetypes/browser/Archetypes/branches/1.4-schemaextender-support&quot;&gt;a branch to make it work with Plone 2.5&lt;/a&gt; patched by &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://weblion.psu.edu/blog/erik-rose&quot;&gt;Erik Rose&lt;/a&gt; of the &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://weblion.psu.edu/&quot;&gt;WebLion Project Team at PSU&lt;/a&gt;, but it will never be merged into the maintenance one. If you use this branch you are on your own in the event of a bug. Also, have in mind that there&#39;s no way back on doing this and after installing &lt;b&gt;schemaextender&lt;/b&gt; in Plone 2.5, the adapter will be available for all sites in the instance.&lt;br /&gt;
&lt;br /&gt;
So, yes, &lt;i&gt;do as I say, not as I did&lt;/i&gt;.&lt;br /&gt;
&lt;br /&gt;
Having warned you, let&#39;s dive a little bit into the code; &lt;b&gt;schemaextender&lt;/b&gt; includes 3 types of adapters:&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;&lt;li&gt;&lt;b&gt;ISchemaExtender&lt;/b&gt; lets you can add new fields to a schema&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IOrderableSchemaExtender&lt;/b&gt; lets you add new fields and reorder them&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ISchemaModifier&lt;/b&gt; is a low-level hook that allows direct manipulation of the schema&lt;/li&gt;
&lt;/ul&gt;&lt;br /&gt;
You can find information and examples on all of them on the &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://dev.plone.org/archetypes/browser/archetypes.schemaextender/trunk&quot;&gt;source code&lt;/a&gt;. As you might have expected, I decided to use &lt;b&gt;IOrderableSchemaExtender&lt;/b&gt;.&lt;br /&gt;
&lt;br /&gt;
To write the adapter, we need to declare the new fields first; let&#39;s take a look to the section field as an example:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;class &lt;b&gt;SectionField&lt;/b&gt;(&lt;b&gt;ExtensionField&lt;/b&gt;, &lt;b&gt;atapi.StringField&lt;/b&gt;):
     &quot;&quot;&quot;Named section of a publication where a news object appear
     &quot;&quot;&quot;

     def &lt;b&gt;getDefault&lt;/b&gt;(self, instance):
          …
          return nitf.default_section

     def &lt;b&gt;Vocabulary&lt;/b&gt;(self, instance):
          …
          return atapi.DisplayList([(x, x) for x in nitf.sections])
&lt;/pre&gt;&lt;br /&gt;
As you can see, we need to subclass from &lt;b&gt;ExtensionField&lt;/b&gt; and &lt;b&gt;StringField&lt;/b&gt;; please note that it&#39;s mandatory to keep this order. &lt;b&gt;ExtensionField&lt;/b&gt; will provide standard accessors and mutators which are not generated on the class. &lt;b&gt;StringField&lt;/b&gt; will provide standard attributes and the widget for the field. Also we override &lt;b&gt;getDefault()&lt;/b&gt; and &lt;b&gt;Vocabulary()&lt;/b&gt; methods to set the default value and vocabulary.&lt;br /&gt;
&lt;br /&gt;
Let&#39;s take a look to the adapter&#39;s class now:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;class &lt;b&gt;NITFExtender&lt;/b&gt;(object):
    &quot;&quot;&quot;Adapter to add NITF fields to News Items
    &quot;&quot;&quot;
    implements(&lt;b&gt;IOrderableSchemaExtender&lt;/b&gt;)
    adapts(&lt;b&gt;IATNewsItem&lt;/b&gt;)

    fields = [
        …
        SectionField(&#39;section&#39;,
        languageIndependent = 1,
        enforceVocabulary = 1,
        required = 1,
        widget = atapi.SelectionWidget(
            label = &#39;Section&#39;,
            label_msgid = &#39;section&#39;,
            description = &#39;Named section where the news object appear&#39;,
            description_msgid = &#39;help_section&#39;,
            i18n_domain = &#39;nitf4plone&#39;)),
    …
    ]

def &lt;b&gt;__init__&lt;/b&gt;(self, context):
    self.context = context

def &lt;b&gt;getFields&lt;/b&gt;(self):
    return self.fields

def &lt;b&gt;getOrder&lt;/b&gt;(self, original):
    # we only need to change the order of the fields in Plone 2.5
    if &#39;metadata&#39; in original:
        # first we remove the fields from whichever schemata they are
        for schemata in original.keys():
            if &#39;relatedItems&#39; in original[schemata]:
                original[schemata].remove(&#39;relatedItems&#39;)
            if &#39;subject&#39; in original[schemata]:
                original[schemata].remove(&#39;subject&#39;)

        # now we insert them where we want them to appear
        idx = original[&#39;default&#39;].index(&#39;property&#39;)
        original[&#39;default&#39;].insert(idx, &#39;subject&#39;)
        original[&#39;metadata&#39;].insert(0, &#39;relatedItems&#39;)
    return original
&lt;/pre&gt;&lt;br /&gt;
As I mentioned, our adapter implements &lt;b&gt;IOrderableSchemaExtender&lt;/b&gt;. In Plone 3.0 adapters can be registered locally at &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://dev.plone.org/collective/browser/nitf4plone/tags/0.2/Extensions/Install.py&quot;&gt;installation&lt;/a&gt; time:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;sm = portal.getSiteManager()
sm.registerAdapter(&lt;b&gt;NITFExtender&lt;/b&gt;, (&lt;b&gt;IATNewsItem&lt;/b&gt;, ), &lt;b&gt;IOrderableSchemaExtender&lt;/b&gt;)&lt;/pre&gt;&lt;br /&gt;
In Plone 2.5 we can&#39;t have local adapters and registrations aren&#39;t persistent, so we have to handle this in &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://dev.plone.org/collective/browser/nitf4plone/tags/0.2/__init__.py&quot;&gt;a different way&lt;/a&gt;:&lt;br /&gt;
&lt;br /&gt;
Here you can see the way news items look after applying the adapter:&lt;br /&gt;
&lt;br /&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhE6ieFaoAcK0ii_2yInBYHnwBOUB_zF6c8jGanrAlE-HM77eTkR2aajddGMS5cqBwbJ155Lrwm-nDqVKDTkDGRW1VnuYp8DXTjW13sq3CfOZAACSlA17-22WoNJ_0QnzQgxVH3sCHASY4/s1600/6a00e398ba6cb3000200e398d178d00003-pi.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhE6ieFaoAcK0ii_2yInBYHnwBOUB_zF6c8jGanrAlE-HM77eTkR2aajddGMS5cqBwbJ155Lrwm-nDqVKDTkDGRW1VnuYp8DXTjW13sq3CfOZAACSlA17-22WoNJ_0QnzQgxVH3sCHASY4/s640/6a00e398ba6cb3000200e398d178d00003-pi.png&quot; width=&quot;251&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;b&gt;schemaextender&lt;/b&gt;&amp;nbsp;in action: the news item now contains new attributes and the order of the fields is modified to make the work of the publishers easy.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhE6ieFaoAcK0ii_2yInBYHnwBOUB_zF6c8jGanrAlE-HM77eTkR2aajddGMS5cqBwbJ155Lrwm-nDqVKDTkDGRW1VnuYp8DXTjW13sq3CfOZAACSlA17-22WoNJ_0QnzQgxVH3sCHASY4/s1600/6a00e398ba6cb3000200e398d178d00003-pi.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;/a&gt;&lt;/div&gt;The &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://dev.plone.org/collective/browser/nitf4plone/tags/0.2/extender.py&quot;&gt;code of the adapter&lt;/a&gt; is pretty clean and easy to understand.&lt;br /&gt;
&lt;br /&gt;
Having finished it, we followed &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/documentation/how-to/adding-new-fields-to-smart-folders-search&quot;&gt;Mikko Ohtamaa&#39;s procedure to adding new fields to Smart Folders search&lt;/a&gt; in order to display all news articles for a given section and it worked fine.&lt;br /&gt;
&lt;br /&gt;
Right now we are working on the migration of the content of our site to use the new fields; we are also preparing some templates to display the new information and some &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/products/compositepack&quot;&gt;CompositePack&lt;/a&gt;&#39;s viewlets to use them to create the front pages in a better way.&lt;br /&gt;
&lt;br /&gt;
Please let me know if you find this development interesting or if you want to participate in some way.&lt;br /&gt;
&lt;br /&gt;
I want to thank all the members of the Plone community who helped me answering my questions at the &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/support/chat&quot;&gt;#plone channel on IRC&lt;/a&gt; and the &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://www.nabble.com/Product-Developers-f20094.html&quot;&gt;Product Developers forum&lt;/a&gt;, specially &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://martinaspeli.net/&quot;&gt;Martin Aspeli&lt;/a&gt; and Florian Schulze (who helped me with the schemaxtender internals and were really patient with me), &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://eibar.org/blogak/erral/es&quot;&gt;Mikel Larreategui&lt;/a&gt; and Erik Rose (who helped me with the installer), Wichert Akkerman and &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://blog.zopyx.com/&quot;&gt;Andreas Jung&lt;/a&gt; (who are always available answering all sort of questions on the forums).</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/4825130696868343756/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2008/01/when-adapting-content-types.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/4825130696868343756'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/4825130696868343756'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2008/01/when-adapting-content-types.html' title='When adapting content types, schemaextender is the way to go'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhE6ieFaoAcK0ii_2yInBYHnwBOUB_zF6c8jGanrAlE-HM77eTkR2aajddGMS5cqBwbJ155Lrwm-nDqVKDTkDGRW1VnuYp8DXTjW13sq3CfOZAACSlA17-22WoNJ_0QnzQgxVH3sCHASY4/s72-c/6a00e398ba6cb3000200e398d178d00003-pi.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4871829230797738545.post-1453920680165518783</id><published>2008-01-02T12:33:00.000-06:00</published><updated>2011-01-28T18:19:03.374-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="haystack"/><category scheme="http://www.blogger.com/atom/ns#" term="plone"/><category scheme="http://www.blogger.com/atom/ns#" term="problem solving"/><category scheme="http://www.blogger.com/atom/ns#" term="webservice"/><title type='text'>Relating content automatically in Plone</title><content type='html'>A question arose today at the &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/&quot;&gt;Plone&lt;/a&gt; &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://news.gmane.org/thread.php?group=gmane.comp.web.zope.plone.user&quot;&gt;general mailing list&lt;/a&gt; (a.k.a. &lt;b&gt;Plone-users&lt;/b&gt;): &lt;i&gt;it is possible to create a list of related content automatically?&lt;/i&gt;&lt;br /&gt;
&lt;br /&gt;
Well, the answer is yes and I&#39;m going to tell you how.&lt;br /&gt;
&lt;br /&gt;
Some time ago &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://bcsaller.blogspot.com/&quot;&gt;Benjamin Saller&lt;/a&gt; created a &lt;b&gt;proof-of-concept&lt;/b&gt; product called &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://svn.objectrealms.net/view/public/browser/archived/haystack/trunk/Haystack&quot;&gt;Haystack&lt;/a&gt; to do auto-classification of content. &lt;b&gt;Haystack&lt;/b&gt; was built around &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://libots.sourceforge.net/&quot;&gt;Open Text Summarizer&lt;/a&gt; and the &lt;b&gt;haystack_tool&lt;/b&gt; included a couple of methods to summarize text and to get a list of &quot;topics&quot; extracted from the content. &lt;b&gt;Haystack&lt;/b&gt; also included some portlets to demonstrate its functionality.&lt;br /&gt;
&lt;br /&gt;
We used &lt;b&gt;Haystack&lt;/b&gt; in &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://www.jornada.unam.mx/&quot;&gt;La Jornada&lt;/a&gt; for some time with mixed results: the summarizer worked well; we called it to create the description field of our content using &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://en.wikipedia.org/wiki/Ajax_%28programming%29&quot;&gt;Ajax&lt;/a&gt; in order to reduce the work of our publishers at edition time.&lt;br /&gt;
&lt;br /&gt;
On the other hand, with the &quot;topics&quot; obtained we were creating a portlet that retrieved the related content. The main problems with this were the low quality of the &quot;topics&quot; and the implementation of the relation. Sometimes we had some embarrassing results relating content from &lt;i&gt;Iraq&lt;/i&gt; with some other of, let&#39;s say, &lt;i&gt;Shakira&lt;/i&gt;, just because they shared some &quot;topic&quot;.&lt;br /&gt;
&lt;br /&gt;
&lt;b&gt;Haystack&lt;/b&gt; didn&#39;t understood the meaning of words and, of course, Ben Saller was aware of that. Last time I saw him was during the &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/events/conferences/seattle-2006&quot;&gt;Plone Conference 2006&lt;/a&gt; in Seattle. He gave a talk on &lt;b&gt;Haystack 2.0&lt;/b&gt; and he was really excited about its new features: &lt;i&gt;linguistic mapping and automated conceptual mapping, providing high-quality relationships with little or no human effort&lt;/i&gt;. &lt;br /&gt;
&lt;br /&gt;
Unfortunately for us, Ben has been a little bit away from the Plone community for some time. So I don&#39;t know what&#39;s the status on his work.&lt;br /&gt;
&lt;br /&gt;
Going back to the original question in the mailing list, &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://m.odul.us/&quot;&gt;Matt Bowen&lt;/a&gt; pointed out to me that &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://www.yahoo.com/&quot;&gt;Yahoo!&lt;/a&gt; has a &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://en.wikipedia.org/wiki/Web_service&quot;&gt;web service&lt;/a&gt; called &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://developer.yahoo.com/search/content/V1/termExtraction.html&quot;&gt;Term Extraction&lt;/a&gt; that does almost the same thing and he even found a &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://effbot.org/zone/yahoo-term-extraction.htm&quot;&gt;python implementation&lt;/a&gt; for it.&lt;br /&gt;
&lt;br /&gt;
I tested Term Extraction with &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://www.jornada.unam.mx/ultimas/2008/01/02/sean-penn-sera-el-presidente-del-jurado-en-el-festival-de-cannes&quot;&gt;some text in Spanish&lt;/a&gt; and I was very pleased with the results:&lt;br /&gt;
&lt;br /&gt;
&lt;blockquote&gt;&amp;lt;ResultSet xsi:schemaLocation=&quot;urn:yahoo:cate http://api.search.yahoo.com/ContentAnalysisService/V1/TermExtractionResponse.xsd&quot;&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;wong kar wai&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;stephen frears&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;festival de cannes&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;sean penn&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;25 de mayo&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;cines&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;organizadores&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;evidencia&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;el presidente&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;hace mucho tiempo&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;afp&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Result&amp;gt;&lt;b&gt;ya&lt;/b&gt;&amp;lt;/Result&amp;gt;&lt;br /&gt;
&amp;lt;/ResultSet&amp;gt;&lt;/blockquote&gt;&lt;br /&gt;
Implementing this in Plone seems not to be quite complicated: you can trigger a script in a workflow transition, or use &lt;a bitly=&quot;BITLY_PROCESSED&quot; href=&quot;http://plone.org/documentation/tutorial/creating-content-rule-conditions-and-actions&quot;&gt;Content Rules&lt;/a&gt; in Plone 3.0, to fill the &lt;b&gt;Subject&lt;/b&gt; field or, better, add an additional field to store this information. Just remember the Term Extraction web service is limited to 5,000 queries per IP address per day.&lt;br /&gt;
&lt;br /&gt;
Yes, I know this solution suffers from the same problems that &lt;b&gt;Haystack&lt;/b&gt;, but the &quot;topics&quot; obtained here have better  quality and you can always find a better algorithm to do the relation, like testing for more than one &quot;topic&quot; or using only &quot;topics&quot; longer than one word.&lt;br /&gt;
&lt;br /&gt;
Anyway I will put this in my list of pending stuff to test (with a little help of Matt Bowen, of course).</content><link rel='replies' type='application/atom+xml' href='http://hvelarde.blogspot.com/feeds/1453920680165518783/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://hvelarde.blogspot.com/2008/01/relating-content-automatically-in-plone.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/1453920680165518783'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4871829230797738545/posts/default/1453920680165518783'/><link rel='alternate' type='text/html' href='http://hvelarde.blogspot.com/2008/01/relating-content-automatically-in-plone.html' title='Relating content automatically in Plone'/><author><name>hvelarde</name><uri>http://www.blogger.com/profile/11976331418713388498</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3HYZSOtESiZYNVPy_ZaltxRCQK_svj3d2BU1lIj97DN3MOAgnm3VTIoH5i6h3ERUR8wopH2NDKAv1VzMK_YO_b_2NVV8yA9UWUU0Dtmq-OGaSgZMRItTU-4yjQBwGMic/s220/hvelarde.jpg'/></author><thr:total>0</thr:total></entry></feed>