[Checkins] SVN: zmi.core/trunk/s Port browser packages from zope.app.catalog, zope.app.file,
Yusei Tahara
yusei at domen.cx
Sat Nov 21 01:58:59 EST 2009
Log message for revision 105932:
Port browser packages from zope.app.catalog, zope.app.file,
zope.app.i18nfile, zope.app.onlinehelp, zope.app.tree.
Update configure.zcml and include ported browser packages.
Changed:
U zmi.core/trunk/setup.py
A zmi.core/trunk/src/zmi/core/catalog/
A zmi.core/trunk/src/zmi/core/catalog/README.txt
A zmi.core/trunk/src/zmi/core/catalog/__init__.py
A zmi.core/trunk/src/zmi/core/catalog/advanced.pt
A zmi.core/trunk/src/zmi/core/catalog/catalog.py
A zmi.core/trunk/src/zmi/core/catalog/catalog_icon.gif
A zmi.core/trunk/src/zmi/core/catalog/configure.zcml
A zmi.core/trunk/src/zmi/core/catalog/tests.py
U zmi.core/trunk/src/zmi/core/configure.zcml
A zmi.core/trunk/src/zmi/core/file/
A zmi.core/trunk/src/zmi/core/file/__init__.py
A zmi.core/trunk/src/zmi/core/file/configure.zcml
A zmi.core/trunk/src/zmi/core/file/file.py
A zmi.core/trunk/src/zmi/core/file/file.txt
A zmi.core/trunk/src/zmi/core/file/file_add.pt
A zmi.core/trunk/src/zmi/core/file/file_icon.gif
A zmi.core/trunk/src/zmi/core/file/file_upload.pt
A zmi.core/trunk/src/zmi/core/file/image.py
A zmi.core/trunk/src/zmi/core/file/image_edit.pt
A zmi.core/trunk/src/zmi/core/file/image_icon.gif
A zmi.core/trunk/src/zmi/core/file/preview.pt
A zmi.core/trunk/src/zmi/core/file/tests/
A zmi.core/trunk/src/zmi/core/file/tests/__init__.py
A zmi.core/trunk/src/zmi/core/file/tests/test_file.py
A zmi.core/trunk/src/zmi/core/file/tests/test_functional.py
A zmi.core/trunk/src/zmi/core/file/tests/test_imagedata.py
A zmi.core/trunk/src/zmi/core/file/url.txt
A zmi.core/trunk/src/zmi/core/i18nfile/
A zmi.core/trunk/src/zmi/core/i18nfile/__init__.py
A zmi.core/trunk/src/zmi/core/i18nfile/configure.zcml
A zmi.core/trunk/src/zmi/core/i18nfile/file_edit.pt
A zmi.core/trunk/src/zmi/core/i18nfile/i18nfile.py
A zmi.core/trunk/src/zmi/core/i18nfile/i18nfile.txt
A zmi.core/trunk/src/zmi/core/i18nfile/i18nimage.py
A zmi.core/trunk/src/zmi/core/i18nfile/i18nimage.txt
A zmi.core/trunk/src/zmi/core/i18nfile/image_edit.pt
A zmi.core/trunk/src/zmi/core/i18nfile/tests.py
A zmi.core/trunk/src/zmi/core/onlinehelp/
A zmi.core/trunk/src/zmi/core/onlinehelp/CHANGES.txt
A zmi.core/trunk/src/zmi/core/onlinehelp/__init__.py
A zmi.core/trunk/src/zmi/core/onlinehelp/configure.zcml
A zmi.core/trunk/src/zmi/core/onlinehelp/helptopic.pt
A zmi.core/trunk/src/zmi/core/onlinehelp/item.gif
A zmi.core/trunk/src/zmi/core/onlinehelp/minus.gif
A zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp.css
A zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp_macros.pt
A zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp_navigation_macros.pt
A zmi.core/trunk/src/zmi/core/onlinehelp/plus.gif
A zmi.core/trunk/src/zmi/core/onlinehelp/tests.py
A zmi.core/trunk/src/zmi/core/onlinehelp/tree.css
A zmi.core/trunk/src/zmi/core/onlinehelp/tree.js
A zmi.core/trunk/src/zmi/core/onlinehelp/tree.py
A zmi.core/trunk/src/zmi/core/tree/
A zmi.core/trunk/src/zmi/core/tree/__init__.py
A zmi.core/trunk/src/zmi/core/tree/configure.zcml
A zmi.core/trunk/src/zmi/core/tree/cookie.py
A zmi.core/trunk/src/zmi/core/tree/example1.pt
A zmi.core/trunk/src/zmi/core/tree/images/
A zmi.core/trunk/src/zmi/core/tree/images/lline.png
A zmi.core/trunk/src/zmi/core/tree/images/minus.png
A zmi.core/trunk/src/zmi/core/tree/images/minus_vline.png
A zmi.core/trunk/src/zmi/core/tree/images/plus.png
A zmi.core/trunk/src/zmi/core/tree/images/plus_vline.png
A zmi.core/trunk/src/zmi/core/tree/images/shim.gif
A zmi.core/trunk/src/zmi/core/tree/images/tline.png
A zmi.core/trunk/src/zmi/core/tree/images/vline.png
A zmi.core/trunk/src/zmi/core/tree/navigation_macros.pt
A zmi.core/trunk/src/zmi/core/tree/tests.py
-=-
Modified: zmi.core/trunk/setup.py
===================================================================
--- zmi.core/trunk/setup.py 2009-11-21 06:46:28 UTC (rev 105931)
+++ zmi.core/trunk/setup.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -52,23 +52,29 @@
'zope.app.authentication',
'zope.app.broken',
'zope.app.cache',
+ 'zope.app.catalog',
'zope.app.component',
'zope.app.container',
'zope.app.error',
'zope.app.exception',
+ 'zope.app.file',
'zope.app.folder',
'zope.app.i18n',
+ 'zope.app.i18nfile',
'zope.app.intid',
+ 'zope.app.onlinehelp',
'zope.app.principalannotation',
'zope.app.securitypolicy',
'zope.app.sqlscript',
+ 'zope.app.tree',
'zope.app.undo',
],
extras_require=dict(test=['zope.app.testing',
'zope.securitypolicy',
'zope.testbrowser',
'zope.app.zptpage',
- 'zope.app.file',
+ 'zope.app.preference',
+ 'zope.app.apidoc',
]),
include_package_data = True,
zip_safe = False,
Added: zmi.core/trunk/src/zmi/core/catalog/README.txt
===================================================================
--- zmi.core/trunk/src/zmi/core/catalog/README.txt (rev 0)
+++ zmi.core/trunk/src/zmi/core/catalog/README.txt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,382 @@
+Catalogs
+========
+
+Catalogs are simple tools used to supppot searching. A catalog
+manages a collection of indexes, and aranges for objects to indexed
+with it's contained indexes.
+
+TODO: Filters
+ Catalogs should provide the option to filter the objects the
+ catalog. This would facilitate the use of separate catalogs for
+ separate purposes. It should be possible to specify a a
+ collection of types (interfaces) to be cataloged and a filtering
+ expression. Perhaps another option would be to be the ability
+ to specify a names filter adapter.
+
+Catalogs use a unique-id tool to assign short (integer) ids to
+objects. Before creating a catalog, you must create a intid tool:
+
+ >>> print http(r"""
+ ... POST /++etc++site/default/@@+/action.html HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 78
+ ... Content-Type: application/x-www-form-urlencoded
+ ... Referer: http://localhost:8081/++etc++site/default/@@+
+ ...
+ ... type_name=BrowserAdd__zope.intid.IntIds&id=&add=+Add+""",
+ ... handle_errors=False)
+ HTTP/1.1 303 ...
+
+And register it:
+
+ >>> print http(r"""
+ ... POST /++etc++site/default/IntIds/addRegistration.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Referer: http://localhost:8081/++etc++site/default/IntIds/
+ ... Content-Type: multipart/form-data; boundary=----------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ
+ ...
+ ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ
+ ... Content-Disposition: form-data; name="field.name"
+ ...
+ ...
+ ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ
+ ... Content-Disposition: form-data; name="field.provided"
+ ...
+ ... zope.intid.interfaces.IIntIds
+ ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ
+ ... Content-Disposition: form-data; name="field.provided-empty-marker"
+ ...
+ ... 1
+ ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ
+ ... Content-Disposition: form-data; name="field.comment"
+ ...
+ ...
+ ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ
+ ... Content-Disposition: form-data; name="field.actions.register"
+ ...
+ ... Register
+ ... ------------CedQTrEQIEPbgfYhvcITAhQi2aJdgu3tYfJ0WYQmkpLQTt6OTOpd5GJ--
+ ... """, handle_errors=False)
+ HTTP/1.1 303 ...
+ ...
+
+
+Moving short-id management outside of catalogs make it possible to
+join searches accross multiple catalogs and indexing tools
+(e.g. relationship indexes).
+
+TODO: Filters?
+ Maybe unique-id tools should be filtered as well, however, this
+ would limit the value of unique id tools for providing
+ cross-catalog/cross-index merging. At least the domain for a
+ unique id tool would be broader than the domain of a catalog.
+ The value of filtering in the unique id tool is that it limits
+ the amount of work that needs to be done by catalogs.
+ One obvious aplication is to provide separate domains for
+ ordinary and meta content. If we did this, then we'd need to be
+ able to select, and, perhaps, alter, the unique-id tool used by
+ a catalog.
+
+Once we have a unique-id tool, you can add a catalog:
+
+ >>> print http(r"""
+ ... POST /++etc++site/default/@@+/action.html HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 77
+ ... Content-Type: application/x-www-form-urlencoded
+ ... Referer: http://localhost:8081/++etc++site/default/@@+
+ ...
+ ... type_name=BrowserAdd__zope.catalog.catalog.Catalog&id=&add=+Add+""")
+ HTTP/1.1 303 ...
+
+and register it:
+
+ >>> print http(r"""
+ ... POST /++etc++site/default/Catalog/addRegistration.html HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Referer: http://localhost:8081/++etc++site/default/Catalog/
+ ... Content-Type: multipart/form-data; boundary=----------61t9UJyoacebBevQVdNrlvXP6T9Ik3Xo4RyXkwJJWvuhao65RTuAPRa
+ ...
+ ... ------------61t9UJyoacebBevQVdNrlvXP6T9Ik3Xo4RyXkwJJWvuhao65RTuAPRa
+ ... Content-Disposition: form-data; name="field.name"
+ ...
+ ...
+ ... ------------61t9UJyoacebBevQVdNrlvXP6T9Ik3Xo4RyXkwJJWvuhao65RTuAPRa
+ ... Content-Disposition: form-data; name="field.provided"
+ ...
+ ... zope.catalog.interfaces.ICatalog
+ ... ------------61t9UJyoacebBevQVdNrlvXP6T9Ik3Xo4RyXkwJJWvuhao65RTuAPRa
+ ... Content-Disposition: form-data; name="field.provided-empty-marker"
+ ...
+ ... 1
+ ... ------------61t9UJyoacebBevQVdNrlvXP6T9Ik3Xo4RyXkwJJWvuhao65RTuAPRa
+ ... Content-Disposition: form-data; name="field.comment"
+ ...
+ ...
+ ... ------------61t9UJyoacebBevQVdNrlvXP6T9Ik3Xo4RyXkwJJWvuhao65RTuAPRa
+ ... Content-Disposition: form-data; name="field.actions.register"
+ ...
+ ... Register
+ ... ------------61t9UJyoacebBevQVdNrlvXP6T9Ik3Xo4RyXkwJJWvuhao65RTuAPRa--
+ ... """)
+ HTTP/1.1 303 ...
+
+
+Once we have a catalog, we can add indexes to it. Before we add an
+index, let's add a templated page. When adding indexes, existing
+objects are indexed, so the document we add now will appear in the
+index:
+
+ >>> print http(r"""
+ ... POST /+/zope.app.zptpage.ZPTPage%3D HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 780
+ ... Content-Type: multipart/form-data; boundary=---------------------------1425445234777458421417366789
+ ... Referer: http://localhost:8081/+/zope.app.zptpage.ZPTPage=
+ ...
+ ... -----------------------------1425445234777458421417366789
+ ... Content-Disposition: form-data; name="field.source"
+ ...
+ ... <html>
+ ... <body>
+ ... Now is the time, for all good dudes to come to the aid of their country.
+ ... </body>
+ ... </html>
+ ... -----------------------------1425445234777458421417366789
+ ... Content-Disposition: form-data; name="field.expand.used"
+ ...
+ ...
+ ... -----------------------------1425445234777458421417366789
+ ... Content-Disposition: form-data; name="field.evaluateInlineCode.used"
+ ...
+ ...
+ ... -----------------------------1425445234777458421417366789
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Add
+ ... -----------------------------1425445234777458421417366789
+ ... Content-Disposition: form-data; name="add_input_name"
+ ...
+ ... dudes
+ ... -----------------------------1425445234777458421417366789--
+ ... """)
+ HTTP/1.1 303 ...
+
+Perhaps the most common type of index to be added is a text index.
+Most indexes require the specification of an interface, an attribute,
+and an indication of whether the attribute must be called.
+
+TODO: Simplify the UI for selecting interfaces and attributes
+ There are a number of ways the UI for this could be made more
+ user friendly:
+
+ - If the user selects an interface, we could then provide a
+ select list of possible attributes and we could determine the
+ callability. Perhaps selection of an interface should be
+ required.
+
+ - An index should have a way to specify default values. In
+ particular, text indexes usially use ISearchableText and
+ searchableText.
+
+For text indexes, one generally uses
+`zope.index.text.interfaces.ISearchableText`,
+`getSearchableText` and True.
+
+ >>> print http(r"""
+ ... POST /++etc++site/default/Catalog/+/AddTextIndex%3D HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 1008
+ ... Content-Type: multipart/form-data; boundary=---------------------------12609588153518590761493918424
+ ... Referer: http://localhost:8081/++etc++site/default/Catalog/+/AddTextIndex=
+ ...
+ ... -----------------------------12609588153518590761493918424
+ ... Content-Disposition: form-data; name="field.interface"
+ ...
+ ... zope.index.text.interfaces.ISearchableText
+ ... -----------------------------12609588153518590761493918424
+ ... Content-Disposition: form-data; name="field.interface-empty-marker"
+ ...
+ ... 1
+ ... -----------------------------12609588153518590761493918424
+ ... Content-Disposition: form-data; name="field.field_name"
+ ...
+ ... getSearchableText
+ ... -----------------------------12609588153518590761493918424
+ ... Content-Disposition: form-data; name="field.field_callable.used"
+ ...
+ ...
+ ... -----------------------------12609588153518590761493918424
+ ... Content-Disposition: form-data; name="field.field_callable"
+ ...
+ ... on
+ ... -----------------------------12609588153518590761493918424
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Add
+ ... -----------------------------12609588153518590761493918424
+ ... Content-Disposition: form-data; name="add_input_name"
+ ...
+ ...
+ ... -----------------------------12609588153518590761493918424--
+ ... """, handle_errors=False)
+ HTTP/1.1 303 ...
+
+
+We can visit the advanced tab of the catalog to get some index
+statistics. Doing so, we see that we have a single document and that
+the total word count is 8. The word count is only 8 because ssome stop
+words have been eliminated.
+
+
+ >>> print http(r"""
+ ... GET /++etc++site/default/Catalog/@@advanced.html HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Referer: http://localhost:8081/++etc++site/default/Catalog/@@contents.html
+ ... """)
+ HTTP/1.1 200 ...
+ ...
+ <table class="listing" summary="Indexes">
+ <tr><th>Index</th>
+ <th>Document Count</th>
+ <th>Word Count</th>
+ </tr>
+ <tr>
+ <td>TextIndex</td>
+ <td>1</td>
+ <td>8</td>
+ </tr>
+ </table>
+ ...
+
+Now lets add some more pages:
+
+ >>> print http(r"""
+ ... POST /+/zope.app.zptpage.ZPTPage%3D HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 754
+ ... Content-Type: multipart/form-data; boundary=---------------------------1213614620286666602740364725
+ ... Referer: http://localhost:8081/+/zope.app.zptpage.ZPTPage=
+ ...
+ ... -----------------------------1213614620286666602740364725
+ ... Content-Disposition: form-data; name="field.source"
+ ...
+ ... <html>
+ ... <body>
+ ... Dudes, we really need to switch to Zope 3 now.
+ ... </body>
+ ... </html>
+ ... -----------------------------1213614620286666602740364725
+ ... Content-Disposition: form-data; name="field.expand.used"
+ ...
+ ...
+ ... -----------------------------1213614620286666602740364725
+ ... Content-Disposition: form-data; name="field.evaluateInlineCode.used"
+ ...
+ ...
+ ... -----------------------------1213614620286666602740364725
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Add
+ ... -----------------------------1213614620286666602740364725
+ ... Content-Disposition: form-data; name="add_input_name"
+ ...
+ ... zope3
+ ... -----------------------------1213614620286666602740364725--
+ ... """)
+ HTTP/1.1 303 ...
+
+ >>> print http(r"""
+ ... POST /+/zope.app.zptpage.ZPTPage%3D HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 838
+ ... Content-Type: multipart/form-data; boundary=---------------------------491825988706308579952614349
+ ... Referer: http://localhost:8081/+/zope.app.zptpage.ZPTPage=
+ ...
+ ... -----------------------------491825988706308579952614349
+ ... Content-Disposition: form-data; name="field.source"
+ ...
+ ... <html>
+ ... <body>
+ ... <p>Writing tests as doctests makes them much more understandable.</p>
+ ... <p>Python 2.4 has major enhancements to the doctest module.</p>
+ ... </body>
+ ... </html>
+ ... -----------------------------491825988706308579952614349
+ ... Content-Disposition: form-data; name="field.expand.used"
+ ...
+ ...
+ ... -----------------------------491825988706308579952614349
+ ... Content-Disposition: form-data; name="field.evaluateInlineCode.used"
+ ...
+ ...
+ ... -----------------------------491825988706308579952614349
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Add
+ ... -----------------------------491825988706308579952614349
+ ... Content-Disposition: form-data; name="add_input_name"
+ ...
+ ... doctest
+ ... -----------------------------491825988706308579952614349--
+ ... """)
+ HTTP/1.1 303 ...
+
+Now, if we visit the catalog advanced tab, we can see that the 3
+documents have been indexed and that the word count has increased to 30:
+
+ >>> print http(r"""
+ ... GET /++etc++site/default/Catalog/@@advanced.html HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Referer: http://localhost:8081/++etc++site/default/Catalog/@@contents.html
+ ... """)
+ HTTP/1.1 200 ...
+ ...
+ <table class="listing" summary="Indexes">
+ <tr><th>Index</th>
+ <th>Document Count</th>
+ <th>Word Count</th>
+ </tr>
+ <tr>
+ <td>TextIndex</td>
+ <td>3</td>
+ <td>30</td>
+ </tr>
+ </table>
+ ...
+
+
+Now that we have a catalog with some documents indexed, we can search
+it. The catalog is really meant to be used from Python:
+
+ >>> root = getRootFolder()
+
+We'll make our root folder the site (this would normally be done by
+the publisher):
+
+ >>> from zope.component.hooks import setSite
+ >>> setSite(root)
+
+Now, we'll get the catalog:
+
+ >>> import zope.component
+ >>> from zope.catalog.interfaces import ICatalog
+ >>> catalog = zope.component.getUtility(ICatalog)
+
+And search it to find the names of all of the documents that contain
+the word 'now':
+
+ >>> results = catalog.searchResults(TextIndex='now')
+ >>> [result.__name__ for result in results]
+ [u'dudes', u'zope3']
+
+TODO
+ This stuff needs a lot of work. The indexing interfaces, despite
+ being rather elaborate are still a bit too simple. There really
+ should be more provision for combining result. In particular,
+ catalog should have a search interface that returns ranked docids,
+ rather than documents.
+
+You don't have to use the search algorithm build into the catalog. You
+can implement your own search algorithms and use them with a catalog's
+indexes.
Property changes on: zmi.core/trunk/src/zmi/core/catalog/README.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zmi.core/trunk/src/zmi/core/catalog/__init__.py
===================================================================
--- zmi.core/trunk/src/zmi/core/catalog/__init__.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/catalog/__init__.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1 @@
+#
Added: zmi.core/trunk/src/zmi/core/catalog/advanced.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/catalog/advanced.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/catalog/advanced.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,32 @@
+<html metal:use-macro="context/@@standard_macros/view"
+ i18n:domain="zope">
+<body>
+<div metal:fill-slot="body">
+
+<h2 i18n:translate="">Catalog statistics</h2>
+
+<table class="listing" summary="Indexes">
+ <tr><th i18n:translate="">Index</th>
+ <th i18n:translate="">Document Count</th>
+ <th i18n:translate="">Word Count</th>
+ </tr>
+ <tr tal:repeat="indexname context">
+ <td tal:content="indexname">foo</td>
+ <td tal:content="context/?indexname/documentCount | default">-</td>
+ <td tal:content="context/?indexname/wordCount | default">-</td>
+ </tr>
+</table>
+
+<form method="post" action="reindex.html">
+ <input type="submit" value="Reindex"
+ i18n:attributes="value reindex-button"/>
+</form>
+
+<div>
+ Please make sure to install an IntId utility before using the reindex
+ function.
+</div>
+
+</div>
+</body>
+</html>
Added: zmi.core/trunk/src/zmi/core/catalog/catalog.py
===================================================================
--- zmi.core/trunk/src/zmi/core/catalog/catalog.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/catalog/catalog.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,32 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Catalog Views
+
+$Id: catalog.py 95862 2009-02-01 16:20:36Z nadako $
+"""
+from zope.catalog.interfaces import ICatalog
+
+class Advanced:
+ "Provides a user interface for configuring a catalog"
+
+ __used_for__ = ICatalog
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def reindex(self):
+ self.context.clear()
+ self.context.updateIndexes()
+ self.request.response.redirect('@@advanced.html')
Added: zmi.core/trunk/src/zmi/core/catalog/catalog_icon.gif
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/catalog/catalog_icon.gif
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream
Added: zmi.core/trunk/src/zmi/core/catalog/configure.zcml
===================================================================
--- zmi.core/trunk/src/zmi/core/catalog/configure.zcml (rev 0)
+++ zmi.core/trunk/src/zmi/core/catalog/configure.zcml 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,139 @@
+<configure
+ xmlns="http://namespaces.zope.org/browser"
+ i18n_domain="zope"
+ >
+
+<!-- Allow a catalog to be added to content space -->
+<addMenuItem
+ title="Catalog"
+ description="A Catalog allows indexing and searching of objects"
+ class="zope.catalog.catalog.Catalog"
+ permission="zope.ManageServices"
+ />
+
+<icon
+ name="zmi_icon"
+ for="zope.catalog.interfaces.ICatalog"
+ file="catalog_icon.gif"
+ />
+
+<containerViews
+ for="zope.catalog.interfaces.ICatalog"
+ contents="zope.ManageServices"
+ index="zope.ManageServices"
+ add="zope.ManageServices"
+ />
+
+<pages
+ for="zope.catalog.interfaces.ICatalog"
+ class="zmi.core.catalog.catalog.Advanced"
+ permission="zope.ManageContent">
+
+ <page name="advanced.html" template="advanced.pt"
+ menu="zmi_views" title="Advanced"/>
+ <page name="reindex.html" attribute="reindex"/>
+</pages>
+
+<!-- Indexes -->
+
+<addform
+ name="AddFieldIndex"
+ label="Add a field index"
+ schema="zope.catalog.interfaces.IAttributeIndex"
+ permission="zope.ManageServices"
+ content_factory="zope.catalog.field.FieldIndex"
+ arguments="field_name"
+ keyword_arguments="interface field_callable"
+ />
+
+<addMenuItem
+ title="Field Index"
+ description="Index items based on an orderable field value"
+ class="zope.catalog.field.FieldIndex"
+ permission="zope.ManageServices"
+ view="AddFieldIndex"
+ />
+
+<schemadisplay
+ name="index.html"
+ schema="zope.catalog.field.IFieldIndex"
+ label="Field Index"
+ fields="interface field_name field_callable"
+ permission="zope.ManageServices"
+ menu="zmi_views" title="Configuration"
+ />
+
+<addform
+ name="AddKeywordIndex"
+ label="Add a keyword index"
+ schema="zope.catalog.interfaces.IAttributeIndex"
+ permission="zope.ManageServices"
+ content_factory="zope.catalog.keyword.KeywordIndex"
+ arguments="field_name"
+ keyword_arguments="interface field_callable"
+ />
+
+<addMenuItem
+ title="Keyword Index"
+ description="Index items based on sequence of keywords"
+ class="zope.catalog.keyword.KeywordIndex"
+ permission="zope.ManageServices"
+ view="AddKeywordIndex"
+ />
+
+<addform
+ name="AddCaseInsensitiveKeywordIndex"
+ label="Add a keyword index (case-insensitive)"
+ schema="zope.catalog.interfaces.IAttributeIndex"
+ permission="zope.ManageServices"
+ content_factory="zope.catalog.keyword.CaseInsensitiveKeywordIndex"
+ arguments="field_name"
+ keyword_arguments="interface field_callable"
+ />
+
+<addMenuItem
+ title="Keyword Index (case-insensitive)"
+ description="Index items based on sequence of keywords"
+ class="zope.catalog.keyword.CaseInsensitiveKeywordIndex"
+ permission="zope.ManageServices"
+ view="AddCaseInsensitiveKeywordIndex"
+ />
+
+<schemadisplay
+ name="index.html"
+ schema="zope.catalog.keyword.IKeywordIndex"
+ label="Keyword Index"
+ fields="interface field_name field_callable"
+ permission="zope.ManageServices"
+ menu="zmi_views" title="Configuration"
+ />
+
+<addform
+ name="AddTextIndex"
+ label="Add a text index"
+ schema="zope.catalog.text.ITextIndex"
+ fields="interface field_name field_callable"
+ permission="zope.ManageServices"
+ content_factory="zope.catalog.text.TextIndex"
+ arguments="field_name"
+ keyword_arguments="interface field_callable"
+ />
+
+<addMenuItem
+ title="Text Index"
+ description="Index items by their text values"
+ class="zope.catalog.text.TextIndex"
+ permission="zope.ManageServices"
+ view="AddTextIndex"
+ />
+
+<schemadisplay
+ name="index.html"
+ fields="interface field_name field_callable"
+ schema="zope.catalog.text.ITextIndex"
+ label="Text Index"
+ permission="zope.ManageServices"
+ menu="zmi_views" title="Configuration"
+ />
+
+</configure>
Added: zmi.core/trunk/src/zmi/core/catalog/tests.py
===================================================================
--- zmi.core/trunk/src/zmi/core/catalog/tests.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/catalog/tests.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,28 @@
+##############################################################################
+#
+# Copyright (c) 2004-2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Functional tests for zope.app.catalog
+
+$Id: tests.py 80833 2007-10-11 06:25:28Z srichter $
+"""
+from zope.app.testing.functional import FunctionalDocFileSuite
+from zope.app.catalog.testing import AppCatalogLayer
+
+def test_suite():
+ suite = FunctionalDocFileSuite('README.txt')
+ suite.layer = AppCatalogLayer
+ return suite
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main(defaultTest='test_suite')
Modified: zmi.core/trunk/src/zmi/core/configure.zcml
===================================================================
--- zmi.core/trunk/src/zmi/core/configure.zcml 2009-11-21 06:46:28 UTC (rev 105931)
+++ zmi.core/trunk/src/zmi/core/configure.zcml 2009-11-21 06:58:58 UTC (rev 105932)
@@ -2,12 +2,21 @@
<include package=".applicationcontrol" />
<include package=".authentication" />
<include package=".broken" />
+ <include package=".cache" />
+ <include package=".catalog" />
<include package=".component" />
<include package=".container" />
<include package=".error" />
<include package=".exception" />
+ <include package=".file" />
<include package=".folder" />
<include package=".i18n" />
+ <include package=".i18nfile" />
<include package=".intid" />
+ <include package=".onlinehelp" />
<include package=".principalannotation" />
+ <include package=".securitypolicy" />
+ <include package=".sqlscript" />
+ <include package=".tree" />
+ <include package=".undo" />
</configure>
Added: zmi.core/trunk/src/zmi/core/file/__init__.py
===================================================================
--- zmi.core/trunk/src/zmi/core/file/__init__.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/__init__.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,2 @@
+#
+# This file is necessary to make this directory a package.
Added: zmi.core/trunk/src/zmi/core/file/configure.zcml
===================================================================
--- zmi.core/trunk/src/zmi/core/file/configure.zcml (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/configure.zcml 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,126 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ i18n_domain="zope"
+ >
+
+ <!-- directives for File -->
+
+ <browser:form
+ name="edit.html"
+ for="zope.app.file.interfaces.IFile"
+ schema="zmi.core.file.file.IFileEditForm"
+ label="Change a file"
+ permission="zope.ManageContent"
+ class="zmi.core.file.file.FileEdit"
+ >
+ </browser:form>
+
+ <browser:menuItem
+ menu="zmi_views" title="Edit"
+ for="zope.app.file.interfaces.IFile"
+ action="edit.html"
+ filter="python:context.contentType.startswith('text/')"
+ permission="zope.ManageContent" />
+
+ <browser:page
+ name="upload.html"
+ menu="zmi_views" title="Upload"
+ for="zope.app.file.interfaces.IFile"
+ template="file_upload.pt"
+ class=".file.FileUpload"
+ permission="zope.ManageContent"
+ />
+
+ <browser:page
+ for="zope.app.file.interfaces.IFile"
+ name="index.html"
+ permission="zope.View"
+ class=".file.FileView"
+ attribute="show" />
+
+
+ <browser:addMenuItem
+ class="zope.app.file.File"
+ title="File"
+ description="A File"
+ permission="zope.ManageContent"
+ view="zope.app.file.File"
+ />
+
+ <browser:page
+ name="zope.app.file.File"
+ for="zope.app.container.interfaces.IAdding"
+ template="file_add.pt"
+ class=".file.FileAdd"
+ permission="zope.ManageContent"
+ />
+
+ <browser:icon
+ name="zmi_icon"
+ for="zope.app.file.interfaces.IFile"
+ file="file_icon.gif"
+ />
+
+ <!-- Directives for Image -->
+
+ <browser:editform
+ schema="zope.app.file.interfaces.IImage"
+ name="upload.html"
+ menu="zmi_views" title="Upload"
+ label="Upload an image"
+ permission="zope.ManageContent"
+ class=".image.ImageUpload"
+ template="image_edit.pt"
+ />
+
+ <browser:page
+ name="index.html"
+ for="zope.app.file.interfaces.IImage"
+ permission="zope.View"
+ allowed_attributes="__call__ tag"
+ class=".image.ImageData"
+ />
+
+ <browser:icon
+ name="zmi_icon"
+ for="zope.app.file.interfaces.IImage"
+ file="image_icon.gif"
+ />
+
+ <browser:addMenuItem
+ class="zope.app.file.image.Image"
+ title="Image"
+ description="An Image"
+ permission="zope.ManageContent"
+ view="zope.app.file.Image"
+ />
+
+ <browser:addform
+ schema="zope.app.file.interfaces.IImage"
+ label="Add an Image"
+ content_factory="zope.app.file.image.Image"
+ class=".image.ImageAdd"
+ name="zope.app.file.Image"
+ permission="zope.ManageContent"
+ />
+
+ <browser:page
+ for="zope.app.file.interfaces.IFile"
+ name="preview.html"
+ template="preview.pt"
+ permission="zope.ManageContent"
+ menu="zmi_views" title="Preview"
+ />
+
+ <browser:page
+ for="zope.app.file.interfaces.IImage"
+ name="preview.html"
+ template="preview.pt"
+ permission="zope.ManageContent"
+ menu="zmi_views" title="Preview"
+ />
+
+
+
+</configure>
Added: zmi.core/trunk/src/zmi/core/file/file.py
===================================================================
--- zmi.core/trunk/src/zmi/core/file/file.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/file.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,503 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""File views.
+
+$Id: file.py 90898 2008-09-05 19:30:25Z nadako $
+"""
+import zope.event
+from zope import lifecycleevent
+from zope.contenttype import guess_content_type
+import zope.contenttype.parse
+from zope.schema import Text
+from zope.exceptions.interfaces import UserError
+
+from zope.app.file.file import File
+from zope.app.file.interfaces import IFile
+from zope.app.file.i18n import ZopeMessageFactory as _
+from zope.dublincore.interfaces import IDCTimes
+import zope.datetime
+
+import time
+from datetime import datetime
+
+__docformat__ = 'restructuredtext'
+
+class FileView(object):
+
+ def show(self):
+
+ """Sets various headers if the request is present and returns the
+ data of the file. If the "If-Modified-Since" header is set and
+ the context is adaptable to IDCTimes, data is only returned if
+ the modification date of the context is newer than the date in the
+ "If-Modified-Since" header
+ >>> from zope.publisher.browser import BrowserView, TestRequest
+ >>> class FileTestView(FileView, BrowserView): pass
+ >>> import pytz
+ >>> class MyFile(object):
+ ... contentType='text/plain'
+ ... data="data of file"
+ ... modified = datetime(2006,1,1,tzinfo=pytz.utc)
+ ... def getSize(self):
+ ... return len(self.data)
+
+ >>> aFile = MyFile()
+ >>> request = TestRequest()
+ >>> view = FileTestView(aFile,request)
+ >>> view.show()
+ 'data of file'
+ >>> request.response.getHeader('Content-Type')
+ 'text/plain'
+ >>> request.response.getHeader('Content-Length')
+ '12'
+
+ If the file is adaptable to IDCTimes the "Last-Modified" header is also
+ set.
+
+ >>> request.response.getHeader('Last-Modified') is None
+ True
+
+ For the test we just declare that our file provides
+ IZopeDublinCore
+ >>> from zope.dublincore.interfaces import IZopeDublinCore
+ >>> from zope.interface import directlyProvides
+ >>> directlyProvides(aFile,IZopeDublinCore)
+ >>> request = TestRequest()
+ >>> view = FileTestView(aFile,request)
+ >>> view.show()
+ 'data of file'
+ >>> request.response.getHeader('Last-Modified')
+ 'Sun, 01 Jan 2006 00:00:00 GMT'
+
+ If the "If-Modified-Since" header is set and is newer a 304
+ status is returned and the value is empty.
+
+ >>> modified = datetime(2007,12,31,tzinfo=pytz.utc)
+ >>> modHeader = zope.datetime.rfc1123_date(zope.datetime.time(modified.isoformat()))
+ >>> request = TestRequest(IF_MODIFIED_SINCE=modHeader)
+
+ >>> view = FileTestView(aFile,request)
+ >>> view.show()
+ ''
+ >>> request.response.getStatus()
+ 304
+
+ """
+
+ if self.request is not None:
+ self.request.response.setHeader('Content-Type',
+ self.context.contentType)
+ self.request.response.setHeader('Content-Length',
+ self.context.getSize())
+ try:
+ modified = IDCTimes(self.context).modified
+ except TypeError:
+ modified=None
+ if modified is None or not isinstance(modified,datetime):
+ return self.context.data
+
+ header= self.request.getHeader('If-Modified-Since', None)
+ lmt = zope.datetime.time(modified.isoformat())
+ if header is not None:
+ header = header.split(';')[0]
+ try: mod_since=long(zope.datetime.time(header))
+ except: mod_since=None
+ if mod_since is not None:
+ if lmt <= mod_since:
+ self.request.response.setStatus(304)
+ return ''
+ self.request.response.setHeader('Last-Modified',
+ zope.datetime.rfc1123_date(lmt))
+
+ return self.context.data
+
+def cleanupFileName(filename):
+ return filename.split('\\')[-1].split('/')[-1]
+
+class FileUpdateView(object):
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def errors(self):
+ form = self.request.form
+ if "UPDATE_SUBMIT" in form:
+ filename = getattr(form["field.data"], "filename", None)
+ contenttype = form.get("field.contentType")
+ if filename:
+ filename = cleanupFileName(filename)
+ if not contenttype:
+ contenttype = guess_content_type(filename)[0]
+ if not form.get("add_input_name"):
+ form["add_input_name"] = filename
+ return self.update_object(form["field.data"], contenttype)
+ return ''
+
+
+class FileAdd(FileUpdateView):
+ """View that adds a new File object based on a file upload.
+
+ >>> class FauxAdding(object):
+ ... def add(self, content):
+ ... self.content = content
+ ... def nextURL(self):
+ ... return 'next url'
+
+ >>> from zope.publisher.browser import TestRequest
+ >>> import StringIO
+ >>> sio = StringIO.StringIO("some data")
+ >>> sio.filename = 'abc.txt'
+
+ Let's make sure we can use the uploaded file name if one isn't
+ specified by the user, and can use the content type when
+ specified.
+
+ >>> request = TestRequest(form={'field.data': sio,
+ ... 'field.contentType': 'text/foobar',
+ ... 'UPDATE_SUBMIT': 'Add'})
+ >>> adding = FauxAdding()
+ >>> view = FileAdd(adding, request)
+ >>> view.errors()
+ ''
+ >>> adding.content.contentType
+ 'text/foobar'
+ >>> adding.content.data
+ 'some data'
+ >>> request.form['add_input_name']
+ 'abc.txt'
+
+ Now let's guess the content type, but also use a provided file
+ name for adding the new content object:
+
+ >>> request = TestRequest(form={'field.data': sio,
+ ... 'field.contentType': '',
+ ... 'add_input_name': 'splat.txt',
+ ... 'UPDATE_SUBMIT': 'Add'})
+ >>> adding = FauxAdding()
+ >>> view = FileAdd(adding, request)
+ >>> view.errors()
+ ''
+ >>> adding.content.contentType
+ 'text/plain'
+ >>> request.form['add_input_name']
+ 'splat.txt'
+
+ """
+
+ def update_object(self, data, contenttype):
+ f = File(data, contenttype)
+ zope.event.notify(lifecycleevent.ObjectCreatedEvent(f))
+ self.context.add(f)
+ self.request.response.redirect(self.context.nextURL())
+ return ''
+
+
+class FileUpload(FileUpdateView):
+ """View that updates an existing File object with a new upload.
+ Fires an ObjectModifiedEvent.
+
+
+ >>> from zope.publisher.browser import TestRequest
+ >>> import StringIO
+ >>> sio = StringIO.StringIO("some data")
+ >>> sio.filename = 'abc.txt'
+
+ Before we instanciate the request, we need to make sure that the
+ ``IUserPreferredLanguages`` adapter exists, so that the request's
+ locale exists. This is necessary because the ``update_object``
+ method uses the locale formatter for the status message:
+
+ >>> from zope.app.testing import ztapi
+ >>> from zope.publisher.browser import BrowserLanguages
+ >>> from zope.publisher.interfaces.http import IHTTPRequest
+ >>> from zope.i18n.interfaces import IUserPreferredLanguages
+ >>> ztapi.provideAdapter(IHTTPRequest, IUserPreferredLanguages,
+ ... BrowserLanguages)
+
+ We install an event logger so we can see the events generated:
+
+ >>> def eventLog(event):
+ ... print 'ModifiedEvent:', event.descriptions[0].attributes
+ >>> zope.event.subscribers.append(eventLog)
+
+ Let's make sure we can use the uploaded file name if one isn't
+ specified by the user, and can use the content type when
+ specified.
+
+
+ >>> request = TestRequest(form={'field.data': sio,
+ ... 'field.contentType': 'text/foobar',
+ ... 'UPDATE_SUBMIT': 'Update'})
+ >>> file = File()
+ >>> view = FileUpload(file, request)
+ >>> view.errors()
+ ModifiedEvent: ('contentType', 'data')
+ u'Updated on ${date_time}'
+ >>> file.contentType
+ 'text/foobar'
+ >>> file.data
+ 'some data'
+
+ Now let's guess the content type, but also use a provided file
+ name for adding the new content object:
+
+ >>> request = TestRequest(form={'field.data': sio,
+ ... 'field.contentType': '',
+ ... 'add_input_name': 'splat.txt',
+ ... 'UPDATE_SUBMIT': 'Update'})
+ >>> file = File()
+ >>> view = FileUpload(file, request)
+ >>> view.errors()
+ ModifiedEvent: ('contentType', 'data')
+ u'Updated on ${date_time}'
+ >>> file.contentType
+ 'text/plain'
+
+ The ObjectModifiedEvent lists only the contentType if the data
+ are omitted:
+
+ >>> request = TestRequest(form={'field.data': None,
+ ... 'field.contentType': '',
+ ... 'add_input_name': 'splat.txt',
+ ... 'UPDATE_SUBMIT': 'Update'})
+ >>> file = File()
+ >>> view = FileUpload(file, request)
+ >>> view.errors()
+ ModifiedEvent: ('contentType',)
+ u'Updated on ${date_time}'
+
+
+ Cleanup:
+
+ >>> zope.event.subscribers.remove(eventLog)
+
+ """
+
+ def update_object(self, data, contenttype):
+ self.context.contentType = contenttype
+
+ descriptor = lifecycleevent.Attributes(IFile, "contentType")
+
+ # Update *only* if a new value is specified
+ if data:
+ self.context.data = data
+ descriptor.attributes += "data",
+
+ event = lifecycleevent.ObjectModifiedEvent(self.context, descriptor)
+ zope.event.notify(event)
+
+ formatter = self.request.locale.dates.getFormatter(
+ 'dateTime', 'medium')
+ return _("Updated on ${date_time}",
+ mapping={'date_time': formatter.format(datetime.utcnow())})
+
+
+class IFileEditForm(IFile):
+ """Schema for the File edit form.
+
+ Replaces the Bytes `data` field with a Text field.
+ """
+
+ data = Text(
+ title=_(u'Data'),
+ description=_(u'The actual content of the object.'),
+ default=u'',
+ missing_value=u'',
+ required=False,
+ )
+
+
+class UnknownCharset(Exception):
+ """Unknown character set."""
+
+class CharsetTooWeak(Exception):
+ """Character set cannot encode all characters in text."""
+
+
+class FileEdit(object):
+ r"""File edit form mixin.
+
+ Lets the user edit a text file directly via a browser form.
+
+ Converts between Unicode strings used in browser forms and 8-bit strings
+ stored internally.
+
+ >>> from zope.publisher.browser import BrowserView, TestRequest
+ >>> class FileEditView(FileEdit, BrowserView): pass
+ >>> view = FileEditView(File(), TestRequest())
+ >>> view.getData()
+ {'data': u'', 'contentType': ''}
+
+ We install an event logger so we can see the events generated.
+
+ >>> def eventLog(event):
+ ... print event.__class__.__name__, event.descriptions[0].attributes
+ >>> zope.event.subscribers.append(eventLog)
+
+ >>> view.setData({'contentType': 'text/plain; charset=ISO-8859-13',
+ ... 'data': u'text \u0105'}) # doctest:+ELLIPSIS
+ ObjectModifiedEvent ('data', 'contentType')
+ u'Updated on ${date_time}'
+
+ >>> view.context.contentType
+ 'text/plain; charset=ISO-8859-13'
+ >>> view.context.data
+ 'text \xe0'
+
+ >>> view.getData()['data']
+ u'text \u0105'
+
+ Cleanup eventlog.
+
+ >>> zope.event.subscribers.remove(eventLog)
+
+ You will get an error if you try to specify a charset that cannot encode
+ all the characters
+
+ >>> view.setData({'contentType': 'text/xml; charset=ISO-8859-1',
+ ... 'data': u'text \u0105'})
+ Traceback (most recent call last):
+ ...
+ CharsetTooWeak: ISO-8859-1
+
+ You will get a different error if you try to specify an invalid charset
+
+ >>> view.setData({'contentType': 'text/xml; charset=UNKNOWN',
+ ... 'data': u'text \u0105'})
+ Traceback (most recent call last):
+ ...
+ UnknownCharset: UNKNOWN
+
+ The update method catches those errors and replaces them with error
+ messages
+
+ >>> from zope.i18n import translate
+ >>> class FakeFormView(BrowserView):
+ ... def update(self):
+ ... raise CharsetTooWeak('ASCII')
+ >>> class FileEditView(FileEdit, FakeFormView): pass
+ >>> view = FileEditView(File(), TestRequest())
+ >>> translate(view.update())
+ u'The character set you specified (ASCII) cannot encode all characters in text.'
+ >>> translate(view.update_status)
+ u'The character set you specified (ASCII) cannot encode all characters in text.'
+
+ >>> class FakeFormView(BrowserView):
+ ... def update(self):
+ ... raise UnknownCharset('UNKNOWN')
+ >>> class FileEditView(FileEdit, FakeFormView): pass
+ >>> view = FileEditView(File(), TestRequest())
+ >>> translate(view.update())
+ u'The character set you specified (UNKNOWN) is not supported.'
+ >>> translate(view.update_status)
+ u'The character set you specified (UNKNOWN) is not supported.'
+
+ Speaking about errors, if you trick the system and upload a file with
+ incorrect charset designation, you will get a UserError when you visit the
+ view:
+
+ >>> view.context.contentType = 'text/plain; charset=UNKNOWN'
+ >>> view.context.data = '\xff'
+ >>> view.getData()
+ Traceback (most recent call last):
+ ...
+ UserError: The character set specified in the content type ($charset) is not supported.
+
+ >>> view.context.contentType = 'text/plain; charset=UTF-8'
+ >>> view.context.data = '\xff'
+ >>> view.getData()
+ Traceback (most recent call last):
+ ...
+ UserError: The character set specified in the content type ($charset) does not match file content.
+
+ """
+
+ error = None
+
+ def getData(self):
+ charset = extractCharset(self.context.contentType)
+ try:
+ return {'contentType': self.context.contentType,
+ 'data': self.context.data.decode(charset)}
+ except LookupError:
+ msg = _("The character set specified in the content type"
+ " ($charset) is not supported.",
+ mapping={'charset': charset})
+ raise UserError(msg)
+ except UnicodeDecodeError:
+ msg = _("The character set specified in the content type"
+ " ($charset) does not match file content.",
+ mapping={'charset': charset})
+ raise UserError(msg)
+
+ def setData(self, data):
+ charset = extractCharset(data['contentType'])
+ try:
+ encodeddata = data['data'].encode(charset)
+ except LookupError:
+ raise UnknownCharset(charset)
+ except UnicodeEncodeError:
+ raise CharsetTooWeak(charset)
+
+ modified = []
+ if encodeddata != self.context.data:
+ self.context.data = encodeddata
+ modified.append('data')
+
+ if self.context.contentType != data['contentType']:
+ self.context.contentType = data['contentType']
+ modified.append('contentType')
+ formatter = self.request.locale.dates.getFormatter('dateTime',
+ 'medium')
+ if modified:
+ event = lifecycleevent.ObjectModifiedEvent(
+ self.context,
+ lifecycleevent.Attributes(IFile, *modified))
+ zope.event.notify(event)
+
+ return _("Updated on ${date_time}",
+ mapping={'date_time': formatter.format(datetime.utcnow())})
+
+ def update(self):
+ try:
+ return super(FileEdit, self).update()
+ except CharsetTooWeak, charset:
+ self.update_status = _("The character set you specified ($charset)"
+ " cannot encode all characters in text.",
+ mapping={'charset': charset})
+ return self.update_status
+ except UnknownCharset, charset:
+ self.update_status = _("The character set you specified ($charset)"
+ " is not supported.",
+ mapping={'charset': charset})
+ return self.update_status
+
+
+def extractCharset(content_type):
+ """Extract charset information from a MIME type.
+
+ >>> extractCharset('text/plain; charset=US-ASCII')
+ 'US-ASCII'
+ >>> extractCharset('text/html; charset=ISO-8859-1')
+ 'ISO-8859-1'
+ >>> extractCharset('text/plain')
+ 'UTF-8'
+
+ """
+ if content_type and content_type.strip():
+ major, minor, params = zope.contenttype.parse(content_type)
+ return params.get("charset", "UTF-8")
+ else:
+ return "UTF-8"
Added: zmi.core/trunk/src/zmi/core/file/file.txt
===================================================================
--- zmi.core/trunk/src/zmi/core/file/file.txt (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/file.txt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,654 @@
+File objects
+============
+
+Adding Files
+------------
+
+You can add File objects from the common tasks menu in the ZMI.
+
+ >>> print http(r"""
+ ... GET /@@contents.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ <BLANKLINE>
+ ...
+ <title>Z3: </title>
+ ...
+ <div class="box" id="commonTasks">
+ <h4>Add:</h4>
+ <div class="body">
+ ...
+ <div class="content...">
+ <a href="http://localhost/@@+/action.html?type_name=zope.app.file.File"
+ class="">File</a>
+ </div>
+ ...
+
+Let's follow that link.
+
+ >>> print http(r"""
+ ... GET /@@+/action.html?type_name=zope.app.file.File HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, handle_errors=False)
+ HTTP/1.1 303 See Other
+ Content-Length: ...
+ Location: http://localhost/+/zope.app.file.File=
+ <BLANKLINE>
+
+The file add form lets you specify the content type, the object name, and
+optionally upload the contents of the file.
+
+ >>> print http(r"""
+ ... GET /+/zope.app.file.File= HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ <BLANKLINE>
+ ...
+ <title>Z3: +</title>
+ ...
+ ...
+ <form action="http://localhost/+/zope.app.file.File%3D"
+ method="post" enctype="multipart/form-data">
+ <h3>Add a File</h3>
+ ...<input class="textType" id="field.contentType"
+ name="field.contentType" size="20" type="text" value="" />...
+ ...<input class="fileType" id="field.data" name="field.data" size="20"
+ type="file" />...
+ <div class="controls"><hr />
+ <input type="submit" value="Refresh" />
+ <input type="submit" value="Add"
+ name="UPDATE_SUBMIT" />
+ <b>Object Name</b>
+ <input type="text" name="add_input_name" value="" />
+ </div>
+ ...
+ </form>
+ ...
+
+Binary Files
+------------
+
+Let us upload a binary file.
+
+ >>> print http("""
+ ... POST /+/zope.app.file.File%3D HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Type: multipart/form-data; boundary=---------------------------73793505419963331401738523176
+ ...
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... application/octet-stream
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="field.data"; filename="hello.txt.gz"
+ ... Content-Type: application/x-gzip
+ ...
+ ... \x1f\x8b\x08\x08\xcb\x48\xea\x42\x00\x03\x68\x65\x6c\x6c\x6f\x2e\
+ ... \x74\x78\x74\x00\xcb\x48\xcd\xc9\xc9\xe7\x02\x00\x20\x30\x3a\x36\
+ ... \x06\x00\x00\x00
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Add
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="add_input_name"
+ ...
+ ...
+ ... -----------------------------73793505419963331401738523176--
+ ... """)
+ HTTP/1.1 303 See Other
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ Location: http://localhost/@@contents.html
+ <BLANKLINE>
+ ...
+
+Since we did not specify the object name in the form, Zope 3 will use the
+filename.
+
+ >>> response = http("""
+ ... GET /hello.txt.gz HTTP/1.1
+ ... """)
+ >>> print response
+ HTTP/1.1 200 OK
+ Content-Length: 36
+ Content-Type: application/octet-stream
+ <BLANKLINE>
+ ...
+
+Let's make sure the (binary) content of the file is correct
+
+ >>> response.getBody().encode('base64')
+ 'H4sICMtI6kIAA2hlbGxvLnR4dADLSM3JyecCACAwOjYGAAAA\n'
+
+Also, lets test a (bad) filename with full path that generates MS Internet Explorer,
+Zope should process it successfully and get the actual filename. Let's upload the
+same file with bad filename.
+
+ >>> print http("""
+ ... POST /+/zope.app.file.File%3D HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Type: multipart/form-data; boundary=---------------------------73793505419963331401738523176
+ ...
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... application/octet-stream
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="field.data"; filename="c:\\windows\\test.gz"
+ ... Content-Type: application/x-gzip
+ ...
+ ... \x1f\x8b\x08\x08\xcb\x48\xea\x42\x00\x03\x68\x65\x6c\x6c\x6f\x2e\
+ ... \x74\x78\x74\x00\xcb\x48\xcd\xc9\xc9\xe7\x02\x00\x20\x30\x3a\x36\
+ ... \x06\x00\x00\x00
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Add
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="add_input_name"
+ ...
+ ...
+ ... -----------------------------73793505419963331401738523176--
+ ... """)
+ HTTP/1.1 303 See Other
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ Location: http://localhost/@@contents.html
+ <BLANKLINE>
+ ...
+
+The file should be saved as "test.gz", let's check it name and contents.
+
+ >>> response = http("""
+ ... GET /test.gz HTTP/1.1
+ ... """)
+ >>> print response
+ HTTP/1.1 200 OK
+ Content-Length: 36
+ Content-Type: application/octet-stream
+ <BLANKLINE>
+ ...
+
+ >>> response.getBody().encode('base64')
+ 'H4sICMtI6kIAA2hlbGxvLnR4dADLSM3JyecCACAwOjYGAAAA\n'
+
+Text Files
+----------
+
+Let us now create a text file.
+
+ >>> print http(r"""
+ ... POST /+/zope.app.file.File%3D HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Type: multipart/form-data; boundary=---------------------------167769037320366690221542301033
+ ...
+ ... -----------------------------167769037320366690221542301033
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... text/plain
+ ... -----------------------------167769037320366690221542301033
+ ... Content-Disposition: form-data; name="field.data"; filename=""
+ ... Content-Type: application/octet-stream
+ ...
+ ...
+ ... -----------------------------167769037320366690221542301033
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Add
+ ... -----------------------------167769037320366690221542301033
+ ... Content-Disposition: form-data; name="add_input_name"
+ ...
+ ... sample.txt
+ ... -----------------------------167769037320366690221542301033--
+ ... """)
+ HTTP/1.1 303 See Other
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ Location: http://localhost/@@contents.html
+ <BLANKLINE>
+ ...
+
+The file is initially empty, since we did not upload anything.
+
+ >>> print http("""
+ ... GET /sample.txt HTTP/1.1
+ ... """)
+ HTTP/1.1 200 OK
+ Content-Length: 0
+ Content-Type: text/plain
+ Last-Modified: ...
+ <BLANKLINE>
+
+Since it is a text file, we can edit it directly in a web form.
+
+ >>> print http(r"""
+ ... GET /sample.txt/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, handle_errors=False)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ <BLANKLINE>
+ ...
+ <title>Z3: sample.txt</title>
+ ...
+ <form action="http://localhost/sample.txt/edit.html"
+ method="post" enctype="multipart/form-data">
+ <div>
+ <h3>Change a file</h3>
+ ...<input class="textType" id="field.contentType" name="field.contentType"
+ size="20" type="text" value="text/plain" />...
+ ...<textarea cols="60" id="field.data" name="field.data" rows="15" ></textarea>...
+ ...
+ <div class="controls">
+ <input type="submit" value="Refresh" />
+ <input type="submit" name="UPDATE_SUBMIT"
+ value="Change" />
+ </div>
+ ...
+ </form>
+ ...
+
+Files of type text/plain without any charset information can contain UTF-8 text.
+So you can use ASCII text.
+
+ >>> print http(r"""
+ ... POST /sample.txt/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
+ ...
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... text/plain
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.data"
+ ...
+ ... This is a sample text file.
+ ...
+ ... It can contain US-ASCII characters.
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Change
+ ... -----------------------------165727764114325486311042046845--
+ ... """, handle_errors=False)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ <BLANKLINE>
+ ...
+ <title>Z3: sample.txt</title>
+ ...
+ <form action="http://localhost/sample.txt/edit.html"
+ method="post" enctype="multipart/form-data">
+ <div>
+ <h3>Change a file</h3>
+ <BLANKLINE>
+ <p>Updated on ...</p>
+ <BLANKLINE>
+ <div class="row">
+ ...<input class="textType" id="field.contentType" name="field.contentType"
+ size="20" type="text" value="text/plain" />...
+ <div class="row">
+ ...<textarea cols="60" id="field.data" name="field.data" rows="15"
+ >This is a sample text file.
+ <BLANKLINE>
+ It can contain US-ASCII characters.</textarea></div>
+ ...
+ <div class="controls">
+ <input type="submit" value="Refresh" />
+ <input type="submit" name="UPDATE_SUBMIT"
+ value="Change" />
+ </div>
+ ...
+ </form>
+ ...
+
+Here's the file
+
+ >>> print http(r"""
+ ... GET /sample.txt HTTP/1.1
+ ... """)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/plain
+ Last-Modified: ...
+ <BLANKLINE>
+ This is a sample text file.
+ <BLANKLINE>
+ It can contain US-ASCII characters.
+
+
+Non-ASCII Text Files
+--------------------
+
+We can also use non-ASCII charactors in text file.
+
+ >>> print http("""
+ ... POST /sample.txt/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
+ ...
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... text/plain
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.data"
+ ...
+ ... This is a sample text file.
+ ...
+ ... It can contain non-ASCII(UTF-8) characters, e.g. \xe2\x98\xbb (U+263B BLACK SMILING FACE).
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Change
+ ... -----------------------------165727764114325486311042046845--
+ ... """)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ <BLANKLINE>
+ ...
+ <title>Z3: sample.txt</title>
+ ...
+ <form action="http://localhost/sample.txt/edit.html"
+ method="post" enctype="multipart/form-data">
+ <div>
+ <h3>Change a file</h3>
+ <BLANKLINE>
+ <p>Updated on ...</p>
+ <BLANKLINE>
+ <div class="row">
+ ...<input class="textType" id="field.contentType" name="field.contentType"
+ size="20" type="text" value="text/plain" />...
+ <div class="row">
+ ...<textarea cols="60" id="field.data" name="field.data" rows="15"
+ >This is a sample text file.
+ <BLANKLINE>
+ It can contain non-ASCII(UTF-8) characters, e.g. ... (U+263B BLACK SMILING FACE).</textarea></div>
+ ...
+ <div class="controls">
+ <input type="submit" value="Refresh" />
+ <input type="submit" name="UPDATE_SUBMIT"
+ value="Change" />
+ </div>
+ ...
+ </form>
+ ...
+
+Here's the file
+
+ >>> response = http(r"""
+ ... GET /sample.txt HTTP/1.1
+ ... """)
+ >>> print response
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/plain
+ Last-Modified: ...
+ <BLANKLINE>
+ This is a sample text file.
+ <BLANKLINE>
+ It can contain non-ASCII(UTF-8) characters, e.g. ... (U+263B BLACK SMILING FACE).
+
+ >>> u'\u263B' in response.getBody().decode('UTF-8')
+ True
+
+And you can explicitly specify the charset. Note that the browser form is always UTF-8.
+
+ >>> print http("""
+ ... POST /sample.txt/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
+ ...
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... text/plain; charset=ISO-8859-1
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.data"
+ ...
+ ... This is a sample text file.
+ ...
+ ... It now contains Latin-1 characters, e.g. \xc2\xa7 (U+00A7 SECTION SIGN).
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Change
+ ... -----------------------------165727764114325486311042046845--
+ ... """)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ <BLANKLINE>
+ ...
+ <title>Z3: sample.txt</title>
+ ...
+ <form action="http://localhost/sample.txt/edit.html"
+ method="post" enctype="multipart/form-data">
+ <div>
+ <h3>Change a file</h3>
+ <BLANKLINE>
+ <p>Updated on ...</p>
+ <BLANKLINE>
+ <div class="row">
+ ...<input class="textType" id="field.contentType" name="field.contentType"
+ size="20" type="text" value="text/plain; charset=ISO-8859-1" />...
+ <div class="row">
+ ...<textarea cols="60" id="field.data" name="field.data" rows="15"
+ >This is a sample text file.
+ <BLANKLINE>
+ It now contains Latin-1 characters, e.g. ... (U+00A7 SECTION SIGN).</textarea></div>
+ ...
+ <div class="controls">
+ <input type="submit" value="Refresh" />
+ <input type="submit" name="UPDATE_SUBMIT"
+ value="Change" />
+ </div>
+ ...
+ </form>
+ ...
+
+Here's the file
+
+ >>> response = http(r"""
+ ... GET /sample.txt HTTP/1.1
+ ... """)
+ >>> print response
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/plain; charset=ISO-8859-1
+ Last-Modified: ...
+ <BLANKLINE>
+ This is a sample text file.
+ <BLANKLINE>
+ It now contains Latin-1 characters, e.g. ... (U+00A7 SECTION SIGN).
+
+Body is actually encoded in ISO-8859-1, and not UTF-8
+
+ >>> response.getBody().splitlines()[-1]
+ 'It now contains Latin-1 characters, e.g. \xa7 (U+00A7 SECTION SIGN).'
+
+The user is not allowed to specify a character set that cannot represent all
+the characters.
+
+ >>> print http("""
+ ... POST /sample.txt/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
+ ...
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... text/plain; charset=US-ASCII
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.data"
+ ...
+ ... This is a slightly changed sample text file.
+ ...
+ ... It now contains Latin-1 characters, e.g. \xc2\xa7 (U+00A7 SECTION SIGN).
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Change
+ ... -----------------------------165727764114325486311042046845--
+ ... """, handle_errors=False)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ <BLANKLINE>
+ ...
+ <title>Z3: sample.txt</title>
+ ...
+ <form action="http://localhost/sample.txt/edit.html"
+ method="post" enctype="multipart/form-data">
+ <div>
+ <h3>Change a file</h3>
+ <BLANKLINE>
+ <p>The character set you specified (US-ASCII) cannot encode all characters in text.</p>
+ <BLANKLINE>
+ <div class="row">
+ ...<input class="textType" id="field.contentType" name="field.contentType" size="20" type="text" value="text/plain; charset=US-ASCII" />...
+ <div class="row">
+ ...<textarea cols="60" id="field.data" name="field.data" rows="15" >This is a slightly changed sample text file.
+ <BLANKLINE>
+ It now contains Latin-1 characters, e.g. ... (U+00A7 SECTION SIGN).</textarea></div>
+ ...
+ <div class="controls">
+ <input type="submit" value="Refresh" />
+ <input type="submit" name="UPDATE_SUBMIT"
+ value="Change" />
+ </div>
+ ...
+ </form>
+ ...
+
+Likewise, the user is not allowed to specify a character set that is not supported by Python.
+
+ >>> print http("""
+ ... POST /sample.txt/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Type: multipart/form-data; boundary=---------------------------165727764114325486311042046845
+ ...
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... text/plain; charset=I-INVENT-MY-OWN
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="field.data"
+ ...
+ ... This is a slightly changed sample text file.
+ ...
+ ... It now contains just ASCII characters.
+ ... -----------------------------165727764114325486311042046845
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Change
+ ... -----------------------------165727764114325486311042046845--
+ ... """, handle_errors=False)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ <BLANKLINE>
+ ...
+ <title>Z3: sample.txt</title>
+ ...
+ <form action="http://localhost/sample.txt/edit.html"
+ method="post" enctype="multipart/form-data">
+ <div>
+ <h3>Change a file</h3>
+ <BLANKLINE>
+ <p>The character set you specified (I-INVENT-MY-OWN) is not supported.</p>
+ <BLANKLINE>
+ <div class="row">
+ ...<input class="textType" id="field.contentType" name="field.contentType" size="20" type="text" value="text/plain; charset=I-INVENT-MY-OWN" />...
+ <div class="row">
+ ...<textarea cols="60" id="field.data" name="field.data" rows="15" >This is a slightly changed sample text file.
+ <BLANKLINE>
+ It now contains just ASCII characters.</textarea></div>
+ ...
+ <div class="controls">
+ <input type="submit" value="Refresh" />
+ <input type="submit" name="UPDATE_SUBMIT"
+ value="Change" />
+ </div>
+ ...
+ </form>
+ ...
+
+If you trick Zope and upload a file with a content type that does not match the
+file contents, you will not be able to access the edit view.
+
+ >>> print http(r"""
+ ... GET /hello.txt.gz/@@edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ <BLANKLINE>
+ ...
+ <li>The character set specified in the content type (UTF-8) does not match file content.</li>
+ ...
+
+
+Non-ASCII Filenames
+-------------------
+
+Filenames are not restricted to ASCII.
+
+ >>> print http("""
+ ... POST /+/zope.app.file.File%3D HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Type: multipart/form-data; boundary=---------------------------73793505419963331401738523176
+ ...
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... application/octet-stream
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="field.data"; filename="bj\xc3\xb6rn.txt.gz"
+ ... Content-Type: application/x-gzip
+ ...
+ ... \x1f\x8b\x08\x08\xcb\x48\xea\x42\x00\x03\x68\x65\x6c\x6c\x6f\x2e\
+ ... \x74\x78\x74\x00\xcb\x48\xcd\xc9\xc9\xe7\x02\x00\x20\x30\x3a\x36\
+ ... \x06\x00\x00\x00
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Add
+ ... -----------------------------73793505419963331401738523176
+ ... Content-Disposition: form-data; name="add_input_name"
+ ...
+ ...
+ ... -----------------------------73793505419963331401738523176--
+ ... """)
+ HTTP/1.1 303 See Other
+ Content-Length: ...
+ Content-Type: text/html;charset=utf-8
+ Location: http://localhost/@@contents.html
+ <BLANKLINE>
+ ...
+
+Since we did not specify the object name in the form, Zope 3 will use the
+filename.
+
+ >>> response = http("""
+ ... GET /bj%C3%B6rn.txt.gz HTTP/1.1
+ ... """)
+ >>> print response
+ HTTP/1.1 200 OK
+ Content-Length: 36
+ Content-Type: application/octet-stream
+ <BLANKLINE>
+ ...
+
Property changes on: zmi.core/trunk/src/zmi/core/file/file.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zmi.core/trunk/src/zmi/core/file/file_add.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/file/file_add.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/file_add.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,62 @@
+<html metal:use-macro="context/@@standard_macros/page"
+ i18n:domain="zope">
+<body>
+<div metal:fill-slot="body">
+
+ <form action="." tal:attributes="action request/URL"
+ method="post" enctype="multipart/form-data">
+
+ <h3 i18n:translate="">Add a File</h3>
+
+ <div tal:define="errors view/errors" tal:content="errors"
+ i18n:translate=""/>
+
+ <div class="row">
+ <div class="label">
+ <label for="field.contentType"
+ title="The content type identifies the type of data."
+ i18n:attributes="title" i18n:translate="">Content Type</label>
+ </div>
+ <div class="field">
+ <input class="textType"
+ id="field.contentType"
+ name="field.contentType"
+ size="20"
+ type="text"
+ value="" /></div>
+ </div>
+
+ <div class="row">
+ <div class="label">
+ <label for="field.data"
+ title="The actual content of the object."
+ i18n:attributes="title" i18n:translate="">Data</label>
+ </div>
+ <div class="field">
+ <input class="fileType"
+ id="field.data"
+ name="field.data"
+ size="20"
+ type="file" /></div>
+ </div>
+
+ <div class="row">
+ <div class="controls"><hr />
+
+ <input type="submit" i18n:attributes="value refresh-button"
+ value="Refresh" />
+ <input type="submit" i18n:attributes="value add-button"
+ value="Add" name="UPDATE_SUBMIT" />
+
+ <b i18n:translate="">Object Name</b>
+ <input type="text" name="add_input_name" value="" />
+
+ </div>
+ </div>
+
+ </form>
+
+</div>
+</body>
+
+</html>
Added: zmi.core/trunk/src/zmi/core/file/file_icon.gif
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/file/file_icon.gif
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream
Added: zmi.core/trunk/src/zmi/core/file/file_upload.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/file/file_upload.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/file_upload.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,61 @@
+<html metal:use-macro="context/@@standard_macros/view"
+ i18n:domain="zope">
+<body>
+<div metal:fill-slot="body">
+
+ <form action="." tal:attributes="action request/URL"
+ method="post" enctype="multipart/form-data">
+
+ <h3 i18n:translate="">Upload a file</h3>
+
+ <div tal:define="errors view/errors" tal:content="errors"
+ i18n:translate=""/>
+
+ <div class="row">
+ <div class="label">
+ <label for="field.contentType"
+ title="The content type identifies the type of data."
+ i18n:attributes="title" i18n:translate="">Content Type</label>
+ </div>
+ <div class="field">
+ <input class="textType"
+ id="field.contentType"
+ name="field.contentType"
+ size="20"
+ type="text"
+ value=""
+ tal:attributes="value context/contentType"/>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="label">
+ <label for="field.data"
+ title="The actual content of the object."
+ i18n:attributes="title" i18n:translate="">Data</label>
+ </div>
+ <div class="field">
+ <input class="fileType"
+ id="field.data"
+ name="field.data"
+ size="20"
+ type="file"/></div>
+ </div>
+
+ <div class="row">
+ <div class="controls"><hr />
+
+ <input type="submit" i18n:attributes="value refresh-button"
+ value="Refresh" />
+ <input type="submit" i18n:attributes="value update-button"
+ value="Update" name="UPDATE_SUBMIT" />
+
+ </div>
+ </div>
+
+ </form>
+
+</div>
+</body>
+
+</html>
Added: zmi.core/trunk/src/zmi/core/file/image.py
===================================================================
--- zmi.core/trunk/src/zmi/core/file/image.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/image.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,112 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Define view component for naive file editing.
+
+$Id: image.py 90898 2008-09-05 19:30:25Z nadako $
+"""
+__docformat__ = 'restructuredtext'
+
+from zope.size.interfaces import ISized
+from zmi.core.file.file import FileView, cleanupFileName
+from zope.traversing.browser.absoluteurl import absoluteURL
+
+
+class ImageData(FileView):
+
+ def __call__(self):
+ return self.show()
+
+ def tag(self, height=None, width=None, alt=None,
+ scale=0, xscale=0, yscale=0, css_class=None, **args):
+ """
+ Generate an HTML IMG tag for this image, with customization.
+ Arguments to ``self.tag()`` can be any valid attributes of an IMG tag.
+ `src` will always be an absolute pathname, to prevent redundant
+ downloading of images. Defaults are applied intelligently for
+ `height`, `width`, and `alt`. If specified, the `scale`, `xscale`,
+ and `yscale` keyword arguments will be used to automatically adjust
+ the output height and width values of the image tag.
+
+ Since 'class' is a Python reserved word, it cannot be passed in
+ directly in keyword arguments which is a problem if you are
+ trying to use ``tag()`` to include a CSS class. The `tag()` method
+ will accept a `css_class` argument that will be converted to
+ ``class`` in the output tag to work around this.
+ """
+ if width is None:
+ width = self.context.getImageSize()[0]
+ if height is None:
+ height = self.context.getImageSize()[1]
+
+ # Auto-scaling support
+ xdelta = xscale or scale
+ ydelta = yscale or scale
+
+ if xdelta and width:
+ width = str(int(round(int(width) * xdelta)))
+ if ydelta and height:
+ height = str(int(round(int(height) * ydelta)))
+
+ if self.request is not None:
+ result = '<img src="%s"' % absoluteURL(self.context, self.request)
+ else:
+ result = '<img '
+
+ if alt is None:
+ alt = getattr(self, 'title', '')
+ result = '%s alt="%s"' % (result, alt)
+
+ if height is not None:
+ result = '%s height="%s"' % (result, height)
+
+ if width is not None:
+ result = '%s width="%s"' % (result, width)
+
+ if not 'border' in [a.lower() for a in args.keys()]:
+ result = '%s border="0"' % result
+
+ if css_class is not None:
+ result = '%s class="%s"' % (result, css_class)
+
+ for key in args.keys():
+ value = args.get(key)
+ result = '%s %s="%s"' % (result, key, value)
+
+ return '%s />' % result
+
+
+class ImageUpload(object):
+ """Image edit view mix-in that provides access to image size info"""
+
+ def size(self):
+ sized = ISized(self.context)
+ return sized.sizeForDisplay()
+
+
+class ImageAdd(object):
+
+ def update(self):
+
+ if self.update_status is not None:
+ # We've been called before. Just return the previous result.
+ return self.update_status
+
+ # set the name of the image if not defined
+ if not self.request.form.get('add_input_name'):
+ filename = getattr(self.request.form.get("field.data"),
+ "filename", None)
+ if filename is not None:
+ self.request.form["add_input_name"] = cleanupFileName(filename)
+
+ return super(ImageAdd,self).update()
Added: zmi.core/trunk/src/zmi/core/file/image_edit.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/file/image_edit.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/image_edit.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,24 @@
+<html metal:use-macro="context/@@standard_macros/view"
+ i18n:domain="zope">
+<body>
+<div metal:fill-slot="body">
+
+ <div metal:use-macro="view/generated_form/macros/body">
+
+ <form action=".">
+
+ <table metal:fill-slot="extra_top">
+ <tr>
+ <td i18n:translate="">Size</td>
+ <td tal:content="view/size"
+ i18n:translate="">103 x 45 pixels, 43KB</td>
+ </tr>
+ </table>
+
+ </form>
+
+ </div>
+</div>
+</body>
+
+</html>
Added: zmi.core/trunk/src/zmi/core/file/image_icon.gif
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/file/image_icon.gif
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream
Added: zmi.core/trunk/src/zmi/core/file/preview.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/file/preview.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/preview.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,9 @@
+<html metal:use-macro="context/@@standard_macros/view">
+<body>
+<div metal:fill-slot="body">
+
+ <iframe src="." height="500" width="100%"></iframe>
+
+</div>
+</body>
+</html>
Added: zmi.core/trunk/src/zmi/core/file/tests/__init__.py
===================================================================
--- zmi.core/trunk/src/zmi/core/file/tests/__init__.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/tests/__init__.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,2 @@
+#
+# This file is necessary to make this directory a package.
Added: zmi.core/trunk/src/zmi/core/file/tests/test_file.py
===================================================================
--- zmi.core/trunk/src/zmi/core/file/tests/test_file.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/tests/test_file.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,30 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Tests for zope.app.file.browser.file.
+
+$Id: test_file.py 70826 2006-10-20 03:41:16Z baijum $
+"""
+import unittest
+
+from zope.testing import doctest
+from zope.app.testing import placelesssetup
+
+
+def test_suite():
+ return doctest.DocTestSuite("zope.app.file.browser.file",
+ setUp=placelesssetup.setUp,
+ tearDown=placelesssetup.tearDown)
+
+if __name__ == "__main__":
+ unittest.main(defaultTest="test_suite")
Added: zmi.core/trunk/src/zmi/core/file/tests/test_functional.py
===================================================================
--- zmi.core/trunk/src/zmi/core/file/tests/test_functional.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/tests/test_functional.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,331 @@
+##############################################################################
+#
+# Copyright (c) 2003, 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Functional tests for File and Image.
+
+$Id: test_functional.py 81032 2007-10-24 14:11:43Z srichter $
+"""
+
+import re
+import unittest
+from xml.sax.saxutils import escape
+from StringIO import StringIO
+from zope.testing import renormalizing
+
+from zope.app.testing import functional
+from zope.app.testing.functional import BrowserTestCase
+from zope.app.file.file import File
+from zope.app.file.image import Image
+from zope.app.file.tests.test_image import zptlogo
+from zope.app.file.testing import AppFileLayer
+
+
+class FileTest(BrowserTestCase):
+
+ content = u'File <Data>'
+
+ def addFile(self):
+ file = File(self.content)
+ root = self.getRootFolder()
+ root['file'] = file
+ self.commit()
+
+ def testAddForm(self):
+ response = self.publish(
+ '/+/zope.app.file.File=',
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('Add a File' in body)
+ self.assert_('Content Type' in body)
+ self.assert_('Data' in body)
+ self.assert_('Object Name' in body)
+ self.assert_('"Add"' in body)
+ self.checkForBrokenLinks(body, '/+/zope.app.file.File=',
+ 'mgr:mgrpw')
+
+ def testAdd(self):
+ response = self.publish(
+ '/+/zope.app.file.File=',
+ form={'type_name': u'zope.app.file.File',
+ 'field.data': StringIO('A file'),
+ 'field.data.used': '',
+ 'add_input_name': u'file',
+ 'UPDATE_SUBMIT': u'Add'},
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 302)
+ self.assertEqual(response.getHeader('Location'),
+ 'http://localhost/@@contents.html')
+ root = self.getRootFolder()
+ self.assert_('file' in root)
+ file = root['file']
+ self.assertEqual(file.data, 'A file')
+
+ def testAddWithoutName(self):
+ data = StringIO('File Contents')
+ data.filename="test.txt"
+ response = self.publish(
+ '/+/zope.app.file.File=',
+ form={'type_name': u'zope.app.file.File',
+ 'field.data': data,
+ 'field.data.used': '',
+ 'UPDATE_SUBMIT': u'Add'},
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 302)
+ self.assertEqual(response.getHeader('Location'),
+ 'http://localhost/@@contents.html')
+ root = self.getRootFolder()
+ self.assert_('test.txt' in root)
+ file = root['test.txt']
+ self.assertEqual(file.data, 'File Contents')
+
+ def testEditForm(self):
+ self.addFile()
+ response = self.publish(
+ '/file/@@edit.html',
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('Change a file' in body)
+ self.assert_('Content Type' in body)
+ self.assert_('Data' in body)
+ self.assert_(escape(self.content) in body)
+ self.checkForBrokenLinks(body, '/file/@@edit.html', 'mgr:mgrpw')
+
+ def testEdit(self):
+ self.addFile()
+ response = self.publish(
+ '/file/@@edit.html',
+ form={'field.data': u'<h1>A File</h1>',
+ 'field.data.used': '',
+ 'field.contentType': u'text/plain',
+ 'UPDATE_SUBMIT': u'Edit'},
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('Change a file' in body)
+ self.assert_('Content Type' in body)
+ self.assert_('Data' in body)
+ self.assert_(escape(u'<h1>A File</h1>') in body)
+ root = self.getRootFolder()
+ file = root['file']
+ self.assertEqual(file.data, '<h1>A File</h1>')
+ self.assertEqual(file.contentType, 'text/plain')
+
+ def testUploadForm(self):
+ self.addFile()
+ response = self.publish(
+ '/file/@@upload.html',
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('Upload a file' in body)
+ self.assert_('Content Type' in body)
+ self.assert_('Data' in body)
+ self.failIf(escape(self.content) in body)
+ self.checkForBrokenLinks(body, '/file/@@upload.html', 'mgr:mgrpw')
+
+ def testUpload(self):
+ self.addFile()
+ response = self.publish(
+ '/file/@@upload.html',
+ form={'field.data': StringIO('<h1>A file</h1>'),
+ 'field.data.used': '',
+ 'field.contentType': u'text/plain',
+ 'UPDATE_SUBMIT': u'Change'},
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('Upload a file' in body)
+ self.assert_('Content Type' in body)
+ self.assert_('Data' in body)
+ self.failIf(escape(u'<h1>A File</h1>') in body)
+ root = self.getRootFolder()
+ file = root['file']
+ self.assertEqual(file.data, '<h1>A file</h1>')
+ self.assertEqual(file.contentType, 'text/plain')
+
+ def testIndex(self):
+ self.addFile()
+ response = self.publish(
+ '/file/@@index.html',
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assertEqual(body, self.content)
+ self.checkForBrokenLinks(body, '/file/@@index.html', 'mgr:mgrpw')
+
+ def testPreview(self):
+ self.addFile()
+ response = self.publish(
+ '/file/@@preview.html',
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('<iframe src="."' in body)
+ self.checkForBrokenLinks(body, '/file/@@preview.html', 'mgr:mgrpw')
+
+
+class ImageTest(BrowserTestCase):
+
+ content = zptlogo
+
+ def addImage(self):
+ image = Image(self.content)
+ root = self.getRootFolder()
+ root['image'] = image
+ self.commit()
+
+ def testAddForm(self):
+ response = self.publish(
+ '/+/zope.app.file.Image=',
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('Add an Image' in body)
+ self.assert_('Content Type' in body)
+ self.assert_('Data' in body)
+ self.assert_('Object Name' in body)
+ self.assert_('"Add"' in body)
+ self.checkForBrokenLinks(body, '/+/zope.app.file.Image=',
+ 'mgr:mgrpw')
+
+ def testAdd(self):
+ response = self.publish(
+ '/+/zope.app.file.Image=',
+ form={'type_name': u'zope.app.image.Image',
+ 'field.data': StringIO(self.content),
+ 'field.data.used': '',
+ 'add_input_name': u'image',
+ 'UPDATE_SUBMIT': u'Add'},
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 302)
+ self.assertEqual(response.getHeader('Location'),
+ 'http://localhost/@@contents.html')
+ root = self.getRootFolder()
+ self.assert_('image' in root)
+ image = root['image']
+ self.assertEqual(image.data, self.content)
+
+ def testAddWithoutName(self):
+ data = StringIO(self.content)
+ data.filename="test.gif"
+ response = self.publish(
+ '/+/zope.app.file.Image=',
+ form={'type_name': u'zope.app.image.Image',
+ 'field.data': data,
+ 'field.data.used': '',
+ 'UPDATE_SUBMIT': u'Add'},
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 302)
+ self.assertEqual(response.getHeader('Location'),
+ 'http://localhost/@@contents.html')
+ root = self.getRootFolder()
+ self.assert_('test.gif' in root)
+ image = root['test.gif']
+ self.assertEqual(image.data, self.content)
+
+ def testUploadForm(self):
+ self.addImage()
+ response = self.publish(
+ '/image/@@upload.html',
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('Upload an image' in body)
+ self.assert_('Content Type' in body)
+ self.assert_('Data' in body)
+ self.assert_('1 KB 16x16' in body)
+ self.checkForBrokenLinks(body, '/image/@@upload.html', 'mgr:mgrpw')
+
+ def testUpload(self):
+ self.addImage()
+ response = self.publish(
+ '/image/@@upload.html',
+ form={'field.data': StringIO(''),
+ 'field.data.used': '',
+ 'UPDATE_SUBMIT': u'Change'},
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('Upload an image' in body)
+ self.assert_('Content Type' in body)
+ self.assert_('Data' in body)
+ self.assert_('0 KB ?x?' in body)
+ root = self.getRootFolder()
+ image = root['image']
+ self.assertEqual(image.data, '')
+ self.assertEqual(image.contentType, 'image/gif')
+
+ def testUpload_only_change_content_type(self):
+ self.addImage()
+ response = self.publish(
+ '/image/@@upload.html',
+ form={'field.contentType': 'image/png',
+ 'UPDATE_SUBMIT': u'Change'},
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('Upload an image' in body)
+ self.assert_('Content Type' in body)
+ self.assert_('Data' in body)
+ self.assert_('1 KB 16x16' in body)
+ root = self.getRootFolder()
+ image = root['image']
+ self.assertEqual(image.data, self.content)
+ self.assertEqual(image.contentType, 'image/png')
+
+ def testIndex(self):
+ self.addImage()
+ response = self.publish(
+ '/image/@@index.html',
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assertEqual(body, self.content)
+ self.checkForBrokenLinks(body, '/image/@@index.html', 'mgr:mgrpw')
+
+ def testPreview(self):
+ self.addImage()
+ response = self.publish(
+ '/image/@@preview.html',
+ basic='mgr:mgrpw')
+ self.assertEqual(response.getStatus(), 200)
+ body = response.getBody()
+ self.assert_('<iframe src="."' in body)
+ self.checkForBrokenLinks(body, '/image/@@preview.html', 'mgr:mgrpw')
+
+
+checker = renormalizing.RENormalizing([
+ (re.compile(r"HTTP/1\.1 200 .*"), "HTTP/1.1 200 OK"),
+ (re.compile(r"HTTP/1\.1 303 .*"), "HTTP/1.1 303 See Other"),
+ ])
+
+
+def test_suite():
+ FileTest.layer = AppFileLayer
+ ImageTest.layer = AppFileLayer
+ url = functional.FunctionalDocFileSuite('../url.txt', checker=checker)
+ url.layer = AppFileLayer
+ file = functional.FunctionalDocFileSuite('../file.txt', checker=checker)
+ file.layer = AppFileLayer
+ return unittest.TestSuite((
+ unittest.makeSuite(FileTest),
+ unittest.makeSuite(ImageTest),
+ url,
+ file,
+ ))
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
Added: zmi.core/trunk/src/zmi/core/file/tests/test_imagedata.py
===================================================================
--- zmi.core/trunk/src/zmi/core/file/tests/test_imagedata.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/tests/test_imagedata.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,80 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Test Image Data handling
+
+$Id: test_imagedata.py 67630 2006-04-27 00:54:03Z jim $
+"""
+import unittest
+
+from zope.component import adapts, provideAdapter
+from zope.component.testing import PlacelessSetup
+from zope.interface import implements
+from zope.app.file.image import Image
+from zmi.core.file.image import ImageData
+from zope.traversing.browser.interfaces import IAbsoluteURL
+
+class FakeRequest(object):
+ pass
+
+class StubAbsoluteURL(object):
+ adapts(Image, FakeRequest)
+ implements(IAbsoluteURL)
+
+ def __init__(self, *objects):
+ pass
+
+ def __str__(self):
+ return '/img'
+
+ __call__ = __str__
+
+class ImageDataTest(PlacelessSetup, unittest.TestCase):
+
+ def testData(self):
+ image = Image('Data')
+ id = ImageData()
+ id.context = image
+ id.request = None
+ self.assertEqual(id(), 'Data')
+
+ def testTag(self):
+ provideAdapter(StubAbsoluteURL)
+ image = Image()
+ fe = ImageData()
+ fe.context = image
+ fe.request = FakeRequest()
+
+ self.assertEqual(fe.tag(),
+ '<img src="/img" alt="" height="-1" width="-1" border="0" />')
+ self.assertEqual(fe.tag(alt="Test Image"),
+ '<img src="/img" alt="Test Image" '
+ 'height="-1" width="-1" border="0" />')
+ self.assertEqual(fe.tag(height=100, width=100),
+ ('<img src="/img" alt="" height="100" '
+ 'width="100" border="0" />'))
+ self.assertEqual(fe.tag(border=1),
+ '<img src="/img" alt="" height="-1" width="-1" border="1" />')
+ self.assertEqual(fe.tag(css_class="Image"),
+ '<img src="/img" alt="" '
+ 'height="-1" width="-1" border="0" class="Image" />')
+ self.assertEqual(fe.tag(height=100, width="100",
+ border=1, css_class="Image"),
+ '<img src="/img" alt="" '
+ 'height="100" width="100" class="Image" border="1" />')
+
+def test_suite():
+ return unittest.makeSuite(ImageDataTest)
+
+if __name__=='__main__':
+ unittest.main()
Added: zmi.core/trunk/src/zmi/core/file/url.txt
===================================================================
--- zmi.core/trunk/src/zmi/core/file/url.txt (rev 0)
+++ zmi.core/trunk/src/zmi/core/file/url.txt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,95 @@
+Special URL handling for DTML pages
+===================================
+
+When an HTML File page containing a head tag is visited, without a
+trailing slash, the base href isn't set. When visited with a slash,
+it is:
+
+
+ >>> print http(r"""
+ ... POST /+/zope.app.file.File%3D HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Length: 610
+ ... Content-Type: multipart/form-data; boundary=---------------------------32826232819858510771857533856
+ ... Referer: http://localhost:8081/+/zope.app.file.File=
+ ...
+ ... -----------------------------32826232819858510771857533856
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... text/html
+ ... -----------------------------32826232819858510771857533856
+ ... Content-Disposition: form-data; name="field.data"; filename=""
+ ... Content-Type: application/octet-stream
+ ...
+ ...
+ ... -----------------------------32826232819858510771857533856
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Add
+ ... -----------------------------32826232819858510771857533856
+ ... Content-Disposition: form-data; name="add_input_name"
+ ...
+ ... file.html
+ ... -----------------------------32826232819858510771857533856--
+ ... """)
+ HTTP/1.1 303 See Other
+ ...
+
+ >>> print http(r"""
+ ... POST /file.html/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Content-Length: 507
+ ... Content-Type: multipart/form-data; boundary=---------------------------10196264131256436092131136054
+ ... Referer: http://localhost:8081/file.html/edit.html
+ ...
+ ... -----------------------------10196264131256436092131136054
+ ... Content-Disposition: form-data; name="field.contentType"
+ ...
+ ... text/html
+ ... -----------------------------10196264131256436092131136054
+ ... Content-Disposition: form-data; name="field.data"
+ ...
+ ... <html>
+ ... <head></head>
+ ... <body>
+ ... <a href="eek.html">Eek</a>
+ ... </body>
+ ... </html>
+ ... -----------------------------10196264131256436092131136054
+ ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+ ...
+ ... Change
+ ... -----------------------------10196264131256436092131136054--
+ ... """)
+ HTTP/1.1 200 OK
+ ...
+
+ >>> print http(r"""
+ ... GET /file.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ ...
+ <html>
+ <head></head>
+ <body>
+ <a href="eek.html">Eek</a>
+ </body>
+ </html>
+
+
+ >>> print http(r"""
+ ... GET /file.html/ HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ ...
+ <html>
+ <head>
+ <base href="http://localhost/file.html" />
+ </head>
+ <body>
+ <a href="eek.html">Eek</a>
+ </body>
+ </html>
+
Property changes on: zmi.core/trunk/src/zmi/core/file/url.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zmi.core/trunk/src/zmi/core/i18nfile/__init__.py
===================================================================
--- zmi.core/trunk/src/zmi/core/i18nfile/__init__.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/i18nfile/__init__.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,2 @@
+#
+# This file is necessary to make this directory a package.
Added: zmi.core/trunk/src/zmi/core/i18nfile/configure.zcml
===================================================================
--- zmi.core/trunk/src/zmi/core/i18nfile/configure.zcml (rev 0)
+++ zmi.core/trunk/src/zmi/core/i18nfile/configure.zcml 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,88 @@
+<configure
+ xmlns='http://namespaces.zope.org/zope'
+ xmlns:browser='http://namespaces.zope.org/browser'
+ i18n_domain='zope'
+ >
+
+ <!-- i18nfile directives -->
+
+ <browser:page
+ name="index.html"
+ for="zope.app.i18nfile.interfaces.II18nFile"
+ permission="zope.View"
+ class=".i18nfile.I18nFileView"
+ />
+
+ <browser:pages
+ for="zope.app.i18nfile.interfaces.II18nFile"
+ permission="zope.ManageContent"
+ class=".i18nfile.I18nFileEdit">
+
+ <browser:page name="editForm.html" template="file_edit.pt" />
+ <browser:page name="edit.html" attribute="action" />
+
+ </browser:pages>
+
+ <browser:menuItems
+ menu="zmi_views"
+ for="zope.app.i18nfile.interfaces.II18nFile">
+
+ <!-- Keep original edit view, for now -->
+ <browser:menuItem title="Edit" action="editForm.html" />
+
+ <!-- Supress the upload view from file -->
+ <browser:menuItem title="Upload" action="editForm.html"
+ filter="python: False" />
+
+ </browser:menuItems>
+
+ <browser:addMenuItem
+ class="zope.app.i18nfile.I18nFile"
+ title="I18n File"
+ description="A file that supports multiple locales."
+ permission="zope.ManageContent"
+ />
+
+
+ <!-- i18nimage directives -->
+
+ <browser:page
+ name="index.html"
+ for="zope.app.i18nfile.interfaces.II18nImage"
+ permission="zope.View"
+ allowed_attributes="__call__ tag"
+ class=".i18nimage.I18nImageData"
+ />
+
+ <browser:pages
+ for="zope.app.i18nfile.interfaces.II18nImage"
+ permission="zope.ManageContent"
+ class=".i18nimage.I18nImageEdit">
+
+ <browser:page name="upload.html" template="image_edit.pt" />
+ <browser:page name="uploadAction.html" attribute="action" />
+
+ </browser:pages>
+
+ <browser:menuItems
+ menu="zmi_views"
+ for="zope.app.i18nfile.interfaces.II18nImage"
+ >
+
+ <!-- Keep the old "edit" form -->
+ <browser:menuItem title="Edit" action="upload.html"/>
+
+ <!-- Suppress upload form (from IFile) -->
+ <browser:menuItem title="Upload" action="upload.html"
+ filter="python: False" />
+
+ </browser:menuItems>
+
+ <browser:addMenuItem
+ class="zope.app.i18nfile.I18nImage"
+ title="I18n Image"
+ description="A multi-locale version of an Image."
+ permission="zope.ManageContent"
+ />
+
+</configure>
Added: zmi.core/trunk/src/zmi/core/i18nfile/file_edit.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/i18nfile/file_edit.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/i18nfile/file_edit.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,97 @@
+<html metal:use-macro="context/@@standard_macros/view"
+ i18n:domain="zope">
+<head>
+ <style metal:fill-slot="headers" type="text/css">
+ <!--
+ .ContentIcon {
+ width: 20px;
+ }
+
+ .ContentTitle {
+ text-align: left;
+ }
+ -->
+ </style>
+</head>
+
+<body>
+<div metal:fill-slot="body">
+
+ <p tal:content="context/msg"
+ tal:condition="python: hasattr(context, 'msg')">
+ Message will go here.
+ </p>
+
+ <p tal:content="view/description" i18n:translate="">
+ Description of the Form.
+ </p>
+
+ <form action="edit.html" method="post">
+
+ <div class="row">
+ <div class="label" i18n:translate="">Content Type</div>
+ <div class="field">
+ <input name="contentType" type="text" size="20"
+ tal:attributes="value context/contentType" />
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="label" i18n:translate="">Default Language</div>
+ <div class="field">
+ <select name="defaultLanguage">
+ <span tal:repeat="lang context/getAvailableLanguages"
+ tal:omit-tag="">
+ <option tal:attributes="
+ value lang;
+ selected python:context.getDefaultLanguage() == lang"
+ tal:content="lang" />
+ </span>
+ </select>
+ </div>
+ </div>
+ <hr />
+ <div class="row">
+ <div class="label" i18n:translate="">Language</div>
+ <div class="field">
+ <select name="language">
+ <span tal:repeat="lang context/getAvailableLanguages"
+ tal:omit-tag="">
+ <option tal:attributes="
+ value lang;
+ selected python:request.get('language',
+ context.getDefaultLanguage()) == lang"
+ tal:content="lang" />
+ </span>
+ </select>
+ <input type="submit" name="selectLanguage" value="Show"
+ i18n:attributes="value show-button"/>
+ <input type="submit" name="removeLanguage" value="Remove"
+ i18n:attributes="value remove-button"/>
+
+ <input type="submit" name="addLanguage"
+ value="Add new language"
+ i18n:attributes="value" />
+ <input type="text" name="newLanguage" size="10" />
+ </div>
+ </div>
+ <div class="row">
+ <div class="label" i18n:translate="">Data</div>
+ <div class="field">
+ <textarea name="data" cols="70" rows="10"
+ tal:content="python:context.getData(request.get('language'))" />
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="controls">
+ <input type="submit" name="edit" value="Save Changes"
+ i18n:attributes="value save-changes-button"/>
+ </div>
+ </div>
+
+ </form>
+
+</div>
+</body>
+</html>
Added: zmi.core/trunk/src/zmi/core/i18nfile/i18nfile.py
===================================================================
--- zmi.core/trunk/src/zmi/core/i18nfile/i18nfile.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/i18nfile/i18nfile.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,65 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""I18n versions of several content objects.
+
+$Id: i18nfile.py 81056 2007-10-24 20:24:02Z srichter $
+"""
+__docformat__ = 'restructuredtext'
+
+from urllib import quote
+from zope.i18n.negotiator import negotiator
+from zope.i18nmessageid import ZopeMessageFactory as _
+
+class I18nFileView(object):
+
+ def __call__(self):
+ """Call the File
+ """
+ request = self.request
+ language = None
+ if request is not None:
+ langs = self.context.getAvailableLanguages()
+ language = negotiator.getLanguage(langs, request)
+
+ request.response.setHeader('Content-Type',
+ self.context.contentType)
+ request.response.setHeader('Content-Length',
+ self.context.getSize(language))
+
+ return self.context.getData(language)
+
+
+class I18nFileEdit(object):
+
+ name = 'editForm'
+ title = _('Edit Form')
+ description = _('This edit form allows you to make changes to the '
+ 'properties of this file.')
+
+ def action(self, contentType, data, language, defaultLanguage,
+ selectLanguage=None, removeLanguage=None,
+ addLanguage=None, newLanguage=None):
+ if selectLanguage:
+ pass
+ elif removeLanguage:
+ self.context.removeLanguage(language)
+ language = self.context.getDefaultLanguage()
+ else:
+ if addLanguage:
+ language = newLanguage
+ self.context.setDefaultLanguage(defaultLanguage)
+ self.context.setData(data, language)
+ self.context.contentType = contentType
+ return self.request.response.redirect(self.request.URL[-1] +
+ "/editForm.html?language=%s" % quote(language, ''))
Added: zmi.core/trunk/src/zmi/core/i18nfile/i18nfile.txt
===================================================================
--- zmi.core/trunk/src/zmi/core/i18nfile/i18nfile.txt (rev 0)
+++ zmi.core/trunk/src/zmi/core/i18nfile/i18nfile.txt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,98 @@
+==============
+I18nFile tests
+==============
+
+First, let's create an I18nFile instance:
+
+ >>> print http(r"""
+ ... POST /@@contents.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, form={"type_name": "BrowserAdd__zope.app.i18nfile.i18nfile.I18nFile",
+ ... "new_value": "i18nfile"})
+ HTTP/1.1 303 See Other
+ ...
+
+Then add some sample data for default (en) language:
+
+ >>> print http(r"""
+ ... POST /i18nfile/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, form={"contentType": "text/plain",
+ ... "defaultLanguage": "en",
+ ... "language": "en",
+ ... "newLanguage": "",
+ ... "data": "English",
+ ... "edit": "Save"})
+ HTTP/1.1 303 See Other
+ ...
+
+Ok, now we can view the data in the edit form:
+
+ >>> print http(r"""
+ ... GET /i18nfile/editForm.html?language=en HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ ...
+ <textarea ...>English</textarea>
+ ...
+
+and as file content:
+
+ >>> print http(r"""
+ ... GET /i18nfile/index.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ ...
+ English
+
+Let's add new (russian) language:
+
+ >>> print http(r"""
+ ... POST /i18nfile/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, form={"contentType": "text/plain",
+ ... "defaultLanguage": "en",
+ ... "language": "en",
+ ... "addLanguage": "Add new language",
+ ... "newLanguage": "ru",
+ ... "data": "English"})
+ HTTP/1.1 303 See Other
+ ...
+
+and add some sample data for russian (ru) language:
+
+ >>> print http(r"""
+ ... POST /i18nfile/edit.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, form={"contentType": "text/plain",
+ ... "defaultLanguage": "en",
+ ... "language": "ru",
+ ... "newLanguage": "",
+ ... "data": "Russian",
+ ... "edit": "Save"})
+ HTTP/1.1 303 See Other
+ ...
+
+Then we can view sample data for russain language in the edit form:
+
+ >>> print http(r"""
+ ... GET /i18nfile/editForm.html?language=ru HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ ...
+ <textarea ...>Russian</textarea>
+ ...
+
+and if our preferred language is russian as file content:
+
+ >>> print http(r"""
+ ... GET /i18nfile/index.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Accept-Language: ru,en
+ ... """)
+ HTTP/1.1 200 OK
+ ...
+ Russian
Property changes on: zmi.core/trunk/src/zmi/core/i18nfile/i18nfile.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zmi.core/trunk/src/zmi/core/i18nfile/i18nimage.py
===================================================================
--- zmi.core/trunk/src/zmi/core/i18nfile/i18nimage.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/i18nfile/i18nimage.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,86 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Define view component for image editing.
+
+$Id: i18nimage.py 81056 2007-10-24 20:24:02Z srichter $
+"""
+__docformat__ = 'restructuredtext'
+
+from urllib import quote
+
+from zope.i18n.negotiator import negotiator
+from zope.size import ISized
+
+from zope.i18nmessageid import ZopeMessageFactory as _
+from zmi.core.file.image import ImageData
+
+
+class I18nImageEdit(object):
+
+ name = 'editForm'
+ title = _('Edit Form')
+ description = _('This edit form allows you to make changes to the '
+ 'properties of this image.')
+
+ def size(self):
+ if self.request is not None:
+ language = self.request.get("language")
+ else:
+ language = None
+ sized = ISized(self.context.getObject(language))
+ return sized.sizeForDisplay()
+
+ def action(self, contentType, data, language, defaultLanguage,
+ selectLanguage=None, removeLanguage=None,
+ addLanguage=None, newLanguage=None):
+ if selectLanguage:
+ pass
+ elif removeLanguage:
+ self.context.removeLanguage(language)
+ language = self.context.getDefaultLanguage()
+ else:
+ if addLanguage:
+ language = newLanguage
+ self.context.setDefaultLanguage(defaultLanguage)
+ self.context.setData(data, language)
+ self.context.contentType = contentType
+ return self.request.response.redirect(self.request.URL[-1] +
+ "/upload.html?language=%s" % quote(language, ''))
+
+class I18nImageData(ImageData):
+
+ def __call__(self):
+ image = self.context
+ language = None
+ if self.request is not None:
+ langs = self.context.getAvailableLanguages()
+ language = negotiator.getLanguage(langs, self.request)
+ self.request.response.setHeader('content-type', image.contentType)
+ return image.getData(language)
+
+
+ def tag(self, height=None, width=None, **args):
+ """See `ImageData.tag.`"""
+
+ language = None
+ if self.request is not None and \
+ (width is None or height is None):
+ langs = self.context.getAvailableLanguages()
+ language = negotiator.getLanguage(langs, self.request)
+
+ if width is None:
+ width = self.context.getImageSize(language)[0]
+ if height is None:
+ height = self.context.getImageSize(language)[1]
+ return ImageData.tag(self, width=width, height=height, **args)
Added: zmi.core/trunk/src/zmi/core/i18nfile/i18nimage.txt
===================================================================
--- zmi.core/trunk/src/zmi/core/i18nfile/i18nimage.txt (rev 0)
+++ zmi.core/trunk/src/zmi/core/i18nfile/i18nimage.txt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,104 @@
+===============
+I18nImage tests
+===============
+
+First, let's create an I18nImage instance:
+
+ >>> print http(r"""
+ ... POST /@@contents.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, form={
+ ... "type_name": "BrowserAdd__zope.app.i18nfile.i18nimage.I18nImage",
+ ... "new_value": "i18nimage"})
+ HTTP/1.1 303 See Other
+ ...
+
+Then add some sample image data for default (en) language:
+
+ >>> print http(r"""
+ ... POST /i18nimage/uploadAction.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, form={"contentType": "image/gif",
+ ... "defaultLanguage": "en",
+ ... "language": "en",
+ ... "newLanguage": "",
+ ... "data": 'GIF89aENEN',
+ ... "edit": "Save"})
+ HTTP/1.1 303 See Other
+ ...
+
+Ok, now we can view the image size in the edit form:
+
+ >>> print http(r"""
+ ... GET /i18nimage/upload.html?language=en HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ ...
+ ...>1 KB 20037x20037</...
+ ...
+
+and the image data as file content:
+
+ >>> print http(r"""
+ ... GET /i18nimage/ HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ Content-Length: 10
+ Content-Type: image/gif
+ <BLANKLINE>
+ GIF89aENEN
+
+Let's add new (russian) language:
+
+ >>> print http(r"""
+ ... POST /i18nimage/uploadAction.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, form={"contentType": "image/gif",
+ ... "defaultLanguage": "en",
+ ... "language": "en",
+ ... "addLanguage": "Add new language",
+ ... "newLanguage": "ru",
+ ... "data": ""})
+ HTTP/1.1 303 See Other
+ ...
+
+and add some sample image data for russian (ru) language:
+
+ >>> print http(r"""
+ ... POST /i18nimage/uploadAction.html HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """, form={"contentType": "image/gif",
+ ... "defaultLanguage": "en",
+ ... "language": "ru",
+ ... "newLanguage": "",
+ ... "data": "GIF89aRURU",
+ ... "edit": "Save"})
+ HTTP/1.1 303 See Other
+ ...
+
+Then we can view the size of sample image for russain language in
+the edit form:
+
+ >>> print http(r"""
+ ... GET /i18nimage/upload.html?language=ru HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... """)
+ HTTP/1.1 200 OK
+ ...
+ ...>1 KB 21842x21842</...
+ ...
+
+and if our preferred language is russian we can view the image as file content:
+
+ >>> print http(r"""
+ ... GET /i18nimage/ HTTP/1.1
+ ... Authorization: Basic mgr:mgrpw
+ ... Accept-Language: ru,en
+ ... """)
+ HTTP/1.1 200 OK
+ Content-Length: 10
+ Content-Type: image/gif
+ <BLANKLINE>
+ GIF89aRURU
Property changes on: zmi.core/trunk/src/zmi/core/i18nfile/i18nimage.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zmi.core/trunk/src/zmi/core/i18nfile/image_edit.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/i18nfile/image_edit.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/i18nfile/image_edit.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,103 @@
+<html metal:use-macro="context/@@standard_macros/view"
+ i18n:domain="zope">
+<head>
+ <style metal:fill-slot="headers" type="text/css">
+ <!--
+ .ContentIcon {
+ width: 20px;
+ }
+
+ .ContentTitle {
+ text-align: left;
+ }
+ -->
+ </style>
+</head>
+
+<body>
+<div metal:fill-slot="body">
+
+
+ <p tal:content="options/msg | nothing">
+ Message will go here.
+ </p>
+
+ <p tal:content="view/description" i18n:translate="">
+ Description of the Form.
+ </p>
+
+ <form action="uploadAction.html" method="post"
+ enctype="multipart/form-data">
+
+ <div class="row">
+ <div class="label" i18n:translate="">Content Type</div>
+ <div class="field">
+ <input name="contentType" type="text" size="20"
+ tal:attributes="value context/contentType" />
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="label" i18n:translate="">Default Language</div>
+ <div class="field">
+ <select name="defaultLanguage">
+ <span tal:repeat="lang context/getAvailableLanguages"
+ tal:omit-tag="">
+ <option tal:attributes="
+ value lang;
+ selected python:context.getDefaultLanguage() == lang"
+ tal:content="lang" />
+ </span>
+ </select>
+ </div>
+ </div>
+ <hr />
+ <div class="row">
+ <div class="label" i18n:translate="">Language</div>
+ <div class="field">
+ <select name="language">
+ <span tal:repeat="lang context/getAvailableLanguages"
+ tal:omit-tag="">
+ <option tal:attributes="
+ value lang;
+ selected python:request.get('language',
+ context.getDefaultLanguage()) == lang"
+ tal:content="lang" />
+ </span>
+ </select>
+ <input type="submit" name="selectLanguage" value="Show"
+ i18n:attributes="value show-button"/>
+ <input type="submit" name="removeLanguage" value="Remove"
+ i18n:attributes="value remove-button"/>
+
+ <input type="submit" name="addLanguage"
+ value="Add new language"
+ i18n:attributes="value" />
+ <input type="text" name="newLanguage" size="10" />
+ </div>
+ </div>
+ <div class="row">
+ <div class="label" i18n:translate="">Data</div>
+ <div class="field">
+ <input type="file" name="data" size="20" />
+ </div>
+ </div>
+ <div class="row">
+ <div class="label" i18n:translate="">Dimensions</div>
+ <div class="field" tal:content="view/size" i18n:translate="">
+ 40 x 40 pixels, 10 kB
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="controls">
+ <input type="submit" name="edit" value="Save Changes"
+ i18n:attributes="value save-changes-button"/>
+ </div>
+ </div>
+
+ </form>
+
+</div>
+</body>
+</html>
Added: zmi.core/trunk/src/zmi/core/i18nfile/tests.py
===================================================================
--- zmi.core/trunk/src/zmi/core/i18nfile/tests.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/i18nfile/tests.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,42 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Functional tests for i18n versions of several content objects.
+
+$Id: tests.py 81056 2007-10-24 20:24:02Z srichter $
+"""
+
+__docformat__ = 'restructuredtext'
+
+import re
+import unittest
+from zope.testing import renormalizing
+from zope.app.testing.functional import FunctionalDocFileSuite
+from zope.app.i18nfile.testing import I18nFileLayer
+
+
+checker = renormalizing.RENormalizing([
+ (re.compile(r"HTTP/1\.1 200 .*"), "HTTP/1.1 200 OK"),
+ (re.compile(r"HTTP/1\.1 303 .*"), "HTTP/1.1 303 See Other"),
+ ])
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ i18nfile = FunctionalDocFileSuite("i18nfile.txt", checker=checker)
+ i18nfile.layer = I18nFileLayer
+ suite.addTest(i18nfile)
+ i18nimage = FunctionalDocFileSuite("i18nimage.txt", checker=checker)
+ i18nimage.layer = I18nFileLayer
+ suite.addTest(i18nimage)
+ return suite
Added: zmi.core/trunk/src/zmi/core/onlinehelp/CHANGES.txt
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/CHANGES.txt (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/CHANGES.txt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,20 @@
+=======
+CHANGES
+=======
+
+Make the onlinehelp utility more component oriented.
+
+- Use registred page/view instead of ViewPageTemplate for rendering topic tree
+ This way we can use/register own templates for tree layout.
+
+- Add page template based topic for rendering topics which has to
+ call other zope3 resources like javascripts and css styles sheets etc.
+ This resources can be rendered in the header area of the onlinehelp_macros.
+
+- Enhance the API of topics and simplyfie the view part.
+
+- Implemented getSubTopics() method on topics. This way we can sublist topics.
+
+- Remove unused onlinehelp code in rotterdam template.pt
+
+- Add type to directive, this supports registration of README.txt as 'rest' topics
Property changes on: zmi.core/trunk/src/zmi/core/onlinehelp/CHANGES.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zmi.core/trunk/src/zmi/core/onlinehelp/__init__.py
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/__init__.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/__init__.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,111 @@
+##############################################################################
+#
+# Copyright (c) 2002, 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""`OnlineHelp` views
+
+$Id: __init__.py 85849 2008-04-30 05:55:30Z lgs $
+"""
+__docformat__ = 'restructuredtext'
+
+from zope.component import createObject, getMultiAdapter
+from zope.security.proxy import removeSecurityProxy
+from zope.publisher.browser import BrowserView
+from zope.publisher.interfaces.browser import IBrowserView
+from zope.traversing.api import getName, getParent
+
+from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
+
+from zope.app.onlinehelp.interfaces import IOnlineHelpTopic, IOnlineHelp
+from zope.app.onlinehelp import getTopicFor
+
+
+class OnlineHelpTopicView(BrowserView):
+ """View for one particular help topic."""
+
+ def __init__(self, context, request):
+ super(OnlineHelpTopicView, self).__init__(context, request)
+
+ def topicContent(self):
+ """ render the source of the help topic """
+ source = createObject(self.context.type, self.context.source)
+ view = getMultiAdapter((source, self.request))
+ html = view.render()
+ return html
+
+ renderTopic = ViewPageTemplateFile('helptopic.pt')
+
+
+class ZPTOnlineHelpTopicView(BrowserView):
+ """View for a page template based help topic."""
+
+ def __init__(self, context, request):
+ super(ZPTOnlineHelpTopicView, self).__init__(context, request)
+
+ def renderTopic(self):
+ """Render the registred topic."""
+ path = self.context.path
+ view = ViewPageTemplateFile(path)
+ return view(self)
+
+
+class ContextHelpView(BrowserView):
+
+ def __init__(self, context, request):
+ super(ContextHelpView, self).__init__(context, request)
+ self.topic = None
+
+ def getContextualTopicView(self):
+ """Retrieve and render the source of a context help topic """
+ topic = self.getContextHelpTopic()
+ view = getMultiAdapter((topic, self.request), name='index.html')
+ return view.renderTopic()
+
+ def getContextHelpTopic(self):
+ """Retrieve a help topic based on the context of the
+ help namespace.
+
+ If the context is a view, try to find
+ a matching help topic for the view and its context.
+ If no help topic is found, try to get a help topic for
+ the context only.
+
+ If the context is not a view, try to retrieve a help topic
+ based on the context.
+
+ If nothing is found, return the onlinehelp root topic
+ """
+ if self.topic is not None:
+ return self.topic
+
+ onlinehelp = self.context
+ help_context = onlinehelp.context
+ self.topic = None
+ if IBrowserView.providedBy(help_context):
+ # called from a view
+ self.topic = getTopicFor(
+ getParent(help_context),
+ getName(help_context)
+ )
+ if self.topic is None:
+ # nothing found for view try context only
+ self.topic = getTopicFor(getParent(help_context))
+ else:
+ # called without view
+ self.topic = getTopicFor(help_context)
+
+ if self.topic is None:
+ self.topic = onlinehelp
+
+ return self.topic
+
+ contextHelpTopic = property(getContextHelpTopic)
Added: zmi.core/trunk/src/zmi/core/onlinehelp/configure.zcml
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/configure.zcml (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/configure.zcml 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,101 @@
+<zope:configure
+ xmlns:zope="http://namespaces.zope.org/zope"
+ xmlns="http://namespaces.zope.org/browser"
+ i18n_domain="zope"
+ >
+
+ <menu
+ id="help_actions"
+ title="Menu for displaying help actions to be performed with popup"
+ />
+
+ <!-- generic topic tree -->
+ <page
+ for="*"
+ name="getTopicTree"
+ permission="zope.View"
+ class=".tree.OnlineHelpTopicTreeView"
+ attribute="getTopicTree"
+ />
+
+ <!-- simply topic view -->
+ <page
+ name="index.html"
+ for="zope.app.onlinehelp.interfaces.IOnlineHelpTopic"
+ class=".OnlineHelpTopicView"
+ permission="zope.View"
+ attribute="renderTopic"
+ />
+
+ <!-- topic view for page template based topics -->
+ <page
+ name="index.html"
+ for="zope.app.onlinehelp.interfaces.IZPTOnlineHelpTopic"
+ class=".ZPTOnlineHelpTopicView"
+ permission="zope.View"
+ attribute="renderTopic"
+ />
+
+ <!-- generic contextual topic view -->
+ <page
+ name="contexthelp.html"
+ for="zope.app.onlinehelp.interfaces.IOnlineHelp"
+ class=".ContextHelpView"
+ permission="zope.View"
+ attribute="getContextualTopicView"
+ />
+
+ <menuItem
+ for="*"
+ filter="python:request.getURL().find('++help++')==-1"
+ menu="help_actions"
+ title="Help"
+ action="++help++/@@contexthelp.html"
+ />
+
+ <!-- resources for onlinhelp -->
+ <resource
+ name="onlinehelp.css"
+ file="onlinehelp.css"
+ />
+
+ <resource
+ name="tree.css"
+ file="tree.css"
+ />
+
+ <resource
+ name="tree.js"
+ file="tree.js"
+ />
+
+ <resource
+ name="minus.gif"
+ file="minus.gif"
+ />
+
+ <resource
+ name="plus.gif"
+ file="plus.gif"
+ />
+
+ <resource
+ name="item.gif"
+ file="item.gif"
+ />
+
+ <page
+ for="*"
+ name="onlinehelp_macros"
+ permission="zope.View"
+ template="onlinehelp_macros.pt"
+ />
+
+ <page
+ for="*"
+ name="onlinehelp_navigation_macros"
+ permission="zope.View"
+ template="onlinehelp_navigation_macros.pt"
+ />
+
+</zope:configure>
Added: zmi.core/trunk/src/zmi/core/onlinehelp/helptopic.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/helptopic.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/helptopic.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,16 @@
+<html metal:use-macro="views/onlinehelp_macros/page" i18n:domain="zope">
+<head>
+ <title metal:fill-slot="title"
+ tal:content="context/title"
+ i18n:translate="">Title</title>
+</head>
+<body>
+
+<div metal:fill-slot="body">
+
+ <p tal:content="structure view/topicContent">Content of Online Help.</p>
+
+</div>
+
+</body>
+</html>
Added: zmi.core/trunk/src/zmi/core/onlinehelp/item.gif
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/onlinehelp/item.gif
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream
Added: zmi.core/trunk/src/zmi/core/onlinehelp/minus.gif
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/onlinehelp/minus.gif
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream
Added: zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp.css
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp.css (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp.css 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,202 @@
+/*
+** Zope3 onlinehelp style sheet for CSS2-capable browsers.
+**
+*/
+
+/* Basic Elements
+*/
+
+body {
+ font: 85% Helvetica, Arial, sans-serif;
+ background: White;
+ color: Black;
+ margin: 0;
+ padding: 0;
+}
+
+table {
+ font-size: 85%;
+}
+
+a {
+ text-decoration: none;
+ color: #369;
+ background-color: transparent;
+}
+
+a:active {
+ text-decoration: underline;
+}
+
+img {
+ border: none;
+ vertical-align: middle;
+}
+
+p {
+ margin: 0.5em 0em 1em 0em;
+ line-height: 1.5em;
+}
+
+p a:visited {
+ color: Purple;
+ background-color: transparent;
+}
+
+p a:active {
+ color: Red;
+ background-color: transparent;
+}
+
+p img {
+ border: 0;
+ margin: 0;
+}
+
+
+hr {
+ clear: both;
+ height: 1px;
+ color: #369;
+ background-color: transparent;
+}
+
+
+h1, h2, h3, h4, h5, h6 {
+ color: Black;
+ font: 80% bold Verdana, Helvetica, Arial, sans-serif;
+ margin: 0;
+ padding-top: 0.5em;
+ border-bottom: 1px solid #369;
+}
+
+h1 {
+ font-size: 160%;
+}
+
+h2 {
+ font-size: 150%;
+}
+
+h3 {
+ font-size: 140%;
+}
+
+h4 {
+ font-size: 120%;
+}
+
+h5 {
+ font-size: 100%;
+}
+
+h6 {
+ font-size: 80%;
+}
+
+ul {
+ line-height: 1.5em;
+ /* list-style-image: url("bullet.gif"); */
+ margin-left: 2em;
+ padding:0;
+}
+
+ol {
+ line-height: 1.5em;
+ margin-left: 2em;
+ padding:0;
+}
+
+dl {
+}
+
+dt {
+ font-weight: bold;
+}
+
+dd {
+ line-height: 1.5em;
+ margin-bottom: 1em;
+}
+
+abbr, acronym, .explain {
+ border-bottom: 1px dotted Black;
+ color: Black;
+ background-color: transparent;
+ cursor: help;
+}
+
+q {
+ font-family: Times, "Times New Roman", serif;
+ font-size: 100%;
+}
+
+blockquote {
+ font-family: Times, "Times New Roman", serif;
+ font-size: 100%;
+}
+
+code {
+ font-family: "Courier New", Courier, monospace;
+ font-size: 100%;
+ color: Black;
+ background-color: #FFD900;
+}
+
+pre {
+ font-family: "Courier New", Courier, monospace;
+ font-size: 100%;
+ padding: 1em;
+ border: 1px solid #A0A0A0;
+ color: Black;
+ background-color: #C7CDE4;
+}
+
+/*
+*/
+
+a.active {
+ font-weight: bold;
+}
+
+/* layout styles
+*/
+
+.headline {
+ width: 100%;
+ height: 35px;
+ font-size: 150%;
+ color: white;
+ background: #0066CC;
+ border-bottom: 1px solid silver;
+}
+.headline div.title {
+ padding: 5px 0px 0px 5px;
+}
+
+table.layout {
+ font: 100% Helvetica, Arial, sans-serif;
+}
+
+table.layout td.navigation {
+ vertical-align: top;
+ background: #F0F0F0;
+ border-right: 1px solid silver;
+ border-bottom: 1px solid silver;
+}
+
+table.layout td.content {
+ vertical-align: top;
+ padding: 10px 5px 5px 20px;
+}
+
+div.box div.title {
+ font-weight: bold;
+ background: #FFD900;
+ border-bottom: 1px solid silver;
+ padding: 5px 0px 5px 0px;
+}
+
+div.box div.title div {
+ padding: 0px 0px 0px 5px;
+}
Added: zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp_macros.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp_macros.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp_macros.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,71 @@
+<metal:block define-macro="page">
+<metal:block define-slot="doctype">
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+</metal:block>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xml:lang="en"
+ lang="en"
+ i18n:domain="zope"
+ tal:define="sitemgmt python:'/++etc++site/' in str(request.URL)">
+
+ <head>
+ <title metal:define-slot="title"
+ tal:content="string:Z3: ${context/zope:title_or_name}">Z3 Onlinehelp</title>
+
+ <script type="text/javascript" src="tree.js"
+ tal:attributes="src string:${context/++resource++tree.js}" >
+ </script>
+
+ <style type="text/css" media="all"
+ tal:content="string:@import url(${context/++resource++onlinehelp.css});">
+ @import url(./onlinehelp.css);
+ </style>
+
+ <style type="text/css" media="all"
+ tal:content="string:@import url(${context/++resource++tree.css});">
+ @import url(./tree.css);
+ </style>
+
+ <meta http-equiv="Content-Type"
+ content="text/html;charset=utf-8" />
+
+ <metal:block define-slot="headers" />
+ <metal:block define-slot="style_slot" />
+ <metal:block define-slot="ecmascript_slot" />
+
+ <link rel="icon" type="image/png"
+ tal:attributes="href context/++resource++favicon.png" />
+ </head>
+
+ <body onload="buildTrees();">
+
+ <div class="headline">
+ <div class="title" i18n:translate="">Onlinehelp</div>
+ </div>
+
+ <!-- get rid of the crapy divs where place the content below the tree box
+ if somebody has a better solution, fell free the change it.
+ But I don't whana see the content below the navigation box. -->
+ <table class="layout" border="0" cellspacing="0" cellpadding="0">
+ <tr>
+ <td class="navigation" nowrap="nowrap">
+ <metal:block define-slot="navigation ">
+ <metal:navigation use-macro="views/onlinehelp_navigation_macros/navigation" />
+ </metal:block>
+ </td>
+ <td class="content">
+ <metal:block define-slot="body">
+ content
+ </metal:block>
+ </td>
+ </tr>
+ </table>
+
+ <div id="footer" metal:define-macro="footer" />
+
+ </body>
+
+</html>
+
+</metal:block>
Added: zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp_navigation_macros.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp_navigation_macros.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/onlinehelp_navigation_macros.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,7 @@
+<metal:navigation define-macro="navigation">
+ <div class="box" tal:define="topicList views/getTopicTree"
+ i18n:domain="zope">
+ <div class="title" i18n:translate="">Topics</div>
+ <tal:block content="structure views/getTopicTree">tree</tal:block>
+ </div>
+</metal:navigation>
Added: zmi.core/trunk/src/zmi/core/onlinehelp/plus.gif
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/onlinehelp/plus.gif
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream
Added: zmi.core/trunk/src/zmi/core/onlinehelp/tests.py
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/tests.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/tests.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,90 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Functional Tests for Onlinehelp
+
+$Id: tests.py 98297 2009-03-21 07:46:46Z icemac $
+"""
+import os
+import transaction
+import unittest
+
+from zope.site.interfaces import IRootFolder
+from zope.app.file import File
+from zope.app.testing.functional import BrowserTestCase
+from zope.app.onlinehelp.tests.test_onlinehelp import testdir
+from zope.app.onlinehelp import globalhelp
+from zope.app.onlinehelp.testing import OnlineHelpLayer
+
+class Test(BrowserTestCase):
+
+ def test_contexthelp(self):
+ path = os.path.join(testdir(), 'help.txt')
+ globalhelp.registerHelpTopic('help', 'Help', '', path, IRootFolder)
+ path = os.path.join(testdir(), 'help2.txt')
+ globalhelp.registerHelpTopic('help2', 'Help2', '', path, IRootFolder,
+ 'contents.html')
+
+ transaction.commit()
+
+ response = self.publish("/+/action.html", basic='mgr:mgrpw',
+ form={'type_name':u'zope.app.content.File',
+ 'id':u'file'})
+
+ self.assertEqual(response.getStatus(), 302)
+
+ response = self.publish('/contents.html', basic='mgr:mgrpw')
+
+ self.assertEqual(response.getStatus(), 200)
+ body = ' '.join(response.getBody().split())
+ self.assert_(body.find(
+ "javascript:popup('contents.html/++help++/@@contexthelp.html") >= 0)
+
+ response = self.publish(
+ '/contents.html/++help++/@@contexthelp.html', basic='mgr:mgrpw')
+
+ self.assertEqual(response.getStatus(), 200)
+ body = ' '.join(response.getBody().split())
+ self.assert_(body.find("This is another help!") >= 0)
+
+ response = self.publish('/index.html/++help++/@@contexthelp.html',
+ basic='mgr:mgrpw')
+
+ self.assertEqual(response.getStatus(), 200)
+ body = ' '.join(response.getBody().split())
+ self.assert_(body.find("This is a help!") >= 0)
+
+ response = self.publish('/file/edit.html/++help++/@@contexthelp.html',
+ basic='mgr:mgrpw')
+
+ self.assertEqual(response.getStatus(), 200)
+ body = ' '.join(response.getBody().split())
+ self.assert_(body.find(
+ "Welcome to the Zope 3 Online Help System.") >= 0)
+
+ path = '/contents.html/++help++'
+ response = self.publish(path, basic='mgr:mgrpw')
+
+ self.assertEqual(response.getStatus(), 200)
+ body = ' '.join(response.getBody().split())
+ self.assert_(body.find("Topics") >= 0)
+
+ self.checkForBrokenLinks(body, path, basic='mgr:mgrpw')
+
+
+def test_suite():
+ Test.layer = OnlineHelpLayer
+ return unittest.makeSuite(Test)
+
+if __name__ == '__main__':
+ unittest.main()
Added: zmi.core/trunk/src/zmi/core/onlinehelp/tree.css
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/tree.css (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/tree.css 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,41 @@
+ul.tree {
+ padding-right: 20px;
+}
+
+ul.tree li {
+ list-style: none;
+}
+
+ul.tree {
+ margin-left: 0px;
+}
+ul.tree ul , ul.tree li {
+ margin-left: 10px;
+}
+
+ul.tree li .link {
+ padding-left: 15px;
+}
+
+ul.tree li.expand .link {
+ cursor: pointer;
+ background: url(/@@/minus.gif) center left no-repeat;
+}
+
+ul.tree li.collapse .link {
+ cursor: pointer;
+ background: url(/@@/plus.gif) center left no-repeat;
+}
+
+ul.tree li.item .link {
+ cursor: default;
+ background: url(/@@/item.gif) center left no-repeat;
+}
+
+ul.tree li.expand ul {
+ display: block;
+}
+
+ul.tree li.collapse ul {
+ display: none;
+}
Added: zmi.core/trunk/src/zmi/core/onlinehelp/tree.js
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/tree.js (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/tree.js 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,140 @@
+//----------------------------------------------------------------------------
+// unordered list based tree
+//----------------------------------------------------------------------------
+
+var nodeCollapseClass = "collapse";
+var nodeExpandClass = "expand";
+var nodeItemClass = "item";
+var nodeLinkClass = "link";
+var activeNodeId = "activeTreeNode";
+
+//----------------------------------------------------------------------------
+// public API
+//----------------------------------------------------------------------------
+function buildTrees() {
+ if (!document.createElement) {
+ return;
+ }
+ uls = document.getElementsByTagName("ul");
+ for (var i=0; i<uls.length; i++) {
+ var ul = uls[i];
+ if (ul.nodeName == "UL" && ul.className == "tree") {
+ _renderTreeList(ul);
+ _toggleTreeList(ul, nodeExpandClass, activeNodeId);
+ }
+ }
+}
+
+function expandTree(id) {
+ var ul = document.getElementById(id);
+ if (ul == null) {
+ return false;
+ }
+ _toggleTreeList(ul, nodeExpandClass);
+}
+
+function collapseTree(id) {
+ var ul = document.getElementById(id);
+ if (ul == null) {
+ return false;
+ }
+ _toggleTreeList(ul, nodeCollapseClass);
+}
+
+function expandItem(treeId, itemId) {
+ var ul = document.getElementById(treeId);
+ if (ul == null) {
+ return false;
+ }
+ var togList = _toggleTreeList(ul, nodeExpandClass, itemId);
+ if (togList) {
+ var o = document.getElementById(itemId);
+ if (o.scrollIntoView) {
+ o.scrollIntoView(false);
+ }
+ }
+}
+
+function expandActiveItem(treeId) {
+ expandItem(treeId, activeNodeId);
+}
+
+//----------------------------------------------------------------------------
+// private methods
+//----------------------------------------------------------------------------
+function _toggleTreeList(ul, clsName, itemId) {
+ if (!ul.childNodes || ul.childNodes.length==0) {
+ return false;
+ }
+ for (var i=0; i<ul.childNodes.length; i++) {
+ var item = ul.childNodes[i];
+ if (itemId != null && item.id == itemId) { return true; }
+ if (item.nodeName == "LI") {
+ var subLists = false;
+ for (var si=0; si<item.childNodes.length; si++) {
+ var subitem = item.childNodes[si];
+ if (subitem.nodeName == "UL") {
+ subLists = true;
+ var togList = _toggleTreeList(subitem, clsName, itemId);
+ if (itemId != null && togList) {
+ item.className = clsName;
+ return true;
+ }
+ }
+ }
+ if (subLists && itemId == null) {
+ item.className = clsName;
+ }
+ }
+ }
+}
+
+function _renderTreeList(ul) {
+ if (!ul.childNodes || ul.childNodes.length == 0) {
+ return;
+ }
+ for (var i=0; i<ul.childNodes.length; i++) {
+ var item = ul.childNodes[i];
+ if (item.nodeName == "LI") {
+ var subLists = false;
+ for (var si=0; si<item.childNodes.length; si++) {
+ var subitem = item.childNodes[si];
+ if (subitem.nodeName == "UL") {
+ subLists = true;
+ _renderTreeList(subitem);
+ }
+ }
+ var span = document.createElement("SPAN");
+ var nbsp = '\u00A0'; //
+ span.className = nodeLinkClass;
+ if (subLists) {
+ if (item.className == null || item.className == "") {
+ item.className = nodeCollapseClass;
+ }
+ if (item.firstChild.nodeName == "#text") {
+ nbsp = nbsp + item.firstChild.nodeValue;
+ item.removeChild(item.firstChild);
+ }
+ span.onclick = function () {
+ if (this.parentNode.className == nodeExpandClass) {
+ this.parentNode.className = nodeCollapseClass
+ }else {
+ this.parentNode.className = nodeExpandClass
+ }
+ return false;
+ }
+ }
+ else {
+ item.className = nodeItemClass;
+ span.onclick = function () {
+ return false;
+ }
+ }
+ child = document.createTextNode(nbsp)
+ span.appendChild(child);
+ item.insertBefore(span, item.firstChild);
+ }
+ }
+}
+
+
Added: zmi.core/trunk/src/zmi/core/onlinehelp/tree.py
===================================================================
--- zmi.core/trunk/src/zmi/core/onlinehelp/tree.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/onlinehelp/tree.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,139 @@
+##############################################################################
+#
+# Copyright (c) 2002, 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""`OnlineHelp` tree view
+
+$Id: tree.py 85849 2008-04-30 05:55:30Z lgs $
+"""
+__docformat__ = 'restructuredtext'
+
+from zope.component import getUtility
+from zope.i18n import translate
+from zope.publisher.browser import BrowserView
+from zope.traversing.api import getPath, joinPath
+
+from zope.app.onlinehelp.interfaces import IOnlineHelp
+
+class OnlineHelpTopicTreeView(BrowserView):
+ """Online help topic tree view."""
+
+ def __init__(self, context, request):
+ super(OnlineHelpTopicTreeView, self).__init__(context, request)
+ self.onlinehelp = getUtility(IOnlineHelp, "OnlineHelp")
+
+ def getTopicTree(self):
+ """Return the tree of help topics.
+
+ We build a flat list of tpoics info dict.
+ Iterate this dict oan build from the level info
+ a navigation tree in the page tmeplate.
+ Each time you get a level 0 means this is a subitem of the
+ Onlinehelp itself::
+
+ >>> info = [('id',{infoDict}),(),()]
+
+ <ul class="tree" id="tree">
+ <li><a href="#">items</a>
+ <ul>
+ <li><a href="#">item</a></li>
+ </ul>
+ </li>
+ <li><a href="#">items</a>
+ <ul>
+ <li><a href="#">items</a>
+ <ul>
+ <li><a href="#">item</a></li>
+ <li><a href="#">item</a></li>
+ <li><a href="#">item</a></li>
+ </ul>
+ </li>
+ <li><a href="#">items</a>
+ <ul>
+ <li><a href="#">item</a></li>
+ <li id="activeTreeNode"><a href="#">active item</a></li>
+ <li><a href="#">item</a></li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <ul>
+ """
+ return self.renderTree(self.onlinehelp)
+
+ def renderTree(self, root):
+ """Reder a unordered list 'ul' tree with a class name 'tree'."""
+ res = []
+ intend = " "
+ res.append('<ul class="tree" id="tree">')
+ for topic in root.getSubTopics():
+ item = self.renderLink(topic)
+
+ # expand if context is in tree
+ if self.isExpanded(topic):
+ res.append(' <li class="expand">%s' % item)
+ else:
+ res.append(' <li>%s' % item)
+
+ if len(topic.getSubTopics()) > 0:
+ res.append(self.renderItemList(topic, intend))
+ res.append(' </li>')
+
+ res.append('<ul>')
+
+ return '\n'.join(res)
+
+ def renderItemList(self, topic, intend):
+ """Render a 'ul' elements as childs of the 'ul' tree."""
+ res = []
+ intend = intend + " "
+ res.append('%s<ul>' % intend)
+
+ for item in topic.getSubTopics():
+
+ # expand if context is in tree
+ if self.isExpanded(topic):
+ res.append(' %s<li class="expand">' % intend)
+ else:
+ res.append(' %s<li>' % intend)
+
+ res.append(self.renderLink(item))
+ if len(item.getSubTopics()) > 0:
+ res.append(' %s%s' % (
+ self.renderItemList(item, intend), intend))
+ res.append(' %s</li>' % intend)
+ res.append('%s</ul>' % intend)
+
+ return '\n'.join(res)
+
+ def renderLink(self, topic):
+ """Render a href element."""
+ title = translate(topic.title, context=self.request,
+ default=topic.title)
+ if topic.parentPath:
+ url = joinPath(topic.parentPath, topic.id)
+ else:
+ url = topic.id
+ return '<a href="/++help++/%s">%s</a>\n' % (url, title)
+
+ def isExpanded(self, topic):
+ if topic.parentPath:
+ path = joinPath(topic.parentPath, topic.id)
+ else:
+ path = topic.id
+ try:
+ if getPath(self.context).startswith('/' + path):
+ return True
+ except:
+ # TODO: fix it, functional test doesn't like getPath? ri
+ pass
+ return False
Added: zmi.core/trunk/src/zmi/core/tree/__init__.py
===================================================================
--- zmi.core/trunk/src/zmi/core/tree/__init__.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/tree/__init__.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,51 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Browser views
+
+$Id: __init__.py 94797 2009-01-17 16:14:21Z nadako $
+"""
+__docformat__ = 'restructuredtext'
+
+import zope.component
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.publisher.browser import BrowserView
+
+from zope.app.tree.interfaces import ITreeStateEncoder
+from zope.app.tree.node import Node
+
+class IStaticTreeLayer(IBrowserRequest):
+ """Layer that we can register our own navigation macro for."""
+
+try: # we try not to depend on zope.app.rotterdam hardly
+ from zope.app.rotterdam import Rotterdam
+ class IStaticTreeSkin(IStaticTreeLayer, Rotterdam):
+ """Skin based on Rotterdam that includes the static tree
+ navigation macro."""
+except ImportError:
+ pass
+
+class StatefulTreeView(BrowserView):
+
+ def statefulTree(self, root=None, filter=None, tree_state=None):
+ """Build a tree with tree state information from a request.
+ """
+ if root is None:
+ root = self.context
+ expanded_nodes = []
+ if tree_state is not None:
+ encoder = zope.component.getUtility(ITreeStateEncoder)
+ expanded_nodes = encoder.decodeTreeState(tree_state)
+ node = Node(root, expanded_nodes, filter)
+ node.expand()
+ return node
Added: zmi.core/trunk/src/zmi/core/tree/configure.zcml
===================================================================
--- zmi.core/trunk/src/zmi/core/tree/configure.zcml (rev 0)
+++ zmi.core/trunk/src/zmi/core/tree/configure.zcml 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,60 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ xmlns:zcml="http://namespaces.zope.org/zcml"
+ i18n_domain="zope"
+ >
+
+ <!-- Register icons -->
+
+ <browser:resourceDirectory
+ name="tree_images"
+ directory="images" />
+
+ <!-- Cookie tree -->
+
+ <browser:pages
+ for="*"
+ class=".cookie.CookieTreeView"
+ permission="zope.View"
+ >
+ <browser:page
+ name="cookie_tree"
+ attribute="cookieTree"
+ />
+ <browser:page
+ name="folder_cookie_tree"
+ attribute="folderTree"
+ />
+ <browser:page
+ name="site_cookie_tree"
+ attribute="siteTree"
+ />
+ <browser:page
+ name="root_cookie_tree"
+ attribute="rootTree"
+ />
+ <browser:page
+ name="virtualhost_cookie_tree"
+ attribute="virtualHostTree"
+ />
+ </browser:pages>
+
+ <!-- Set up the 'StaticTree' skin -->
+
+ <interface
+ zcml:condition="installed zope.app.rotterdam"
+ interface=".IStaticTreeSkin"
+ type="zope.publisher.interfaces.browser.IBrowserSkinType"
+ name="StaticTree"
+ />
+
+ <browser:page
+ for="*"
+ name="navigation_macros"
+ permission="zope.View"
+ layer=".IStaticTreeLayer"
+ template="navigation_macros.pt"
+ />
+
+</configure>
Added: zmi.core/trunk/src/zmi/core/tree/cookie.py
===================================================================
--- zmi.core/trunk/src/zmi/core/tree/cookie.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/tree/cookie.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,80 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Stateful cookie tree
+
+$Id: cookie.py 95459 2009-01-29 18:01:14Z faassen $
+"""
+import zope.traversing.api
+from zope.traversing.interfaces import IContainmentRoot
+from zope.component.interfaces import IComponentLookup
+
+from zope.container.interfaces import IContainer
+
+from zope.app.tree.filters import OnlyInterfacesFilter
+from zmi.core.tree import StatefulTreeView
+
+import zope.component.interfaces
+
+
+class CookieTreeView(StatefulTreeView):
+ """A stateful tree view using cookies to remember the tree state"""
+
+ request_variable = 'tree-state'
+
+ def cookieTree(self, root=None, filter=None):
+ """Build a tree with tree state information from a request.
+ """
+ request = self.request
+ tree_state = request.get(self.request_variable, "")
+ tree_state = str(tree_state)
+ tree_state = tree_state or None
+ if tree_state is not None:
+ # set a cookie right away
+ request.response.setCookie(self.request_variable,
+ tree_state)
+ return self.statefulTree(root, filter, tree_state)
+
+ def folderTree(self, root=None):
+ """Cookie tree with only folders (and site managers).
+ """
+ filter = OnlyInterfacesFilter(IContainer)
+ return self.cookieTree(root, filter)
+
+ def siteTree(self):
+ """Cookie tree with only folders and the nearest site as root
+ node.
+ """
+ parent = self.context
+ for parent in zope.traversing.api.getParents(self.context):
+ if zope.component.interfaces.ISite.providedBy(parent):
+ break
+ return self.folderTree(parent)
+
+ def rootTree(self):
+ """Cookie tree with only folders and the root container as
+ root node.
+ """
+ root = zope.traversing.api.getRoot(self.context)
+ return self.folderTree(root)
+
+ def virtualHostTree(self):
+ """Cookie tree with only folders and the root container as
+ root node.
+ """
+ vh = self.request.getVirtualHostRoot()
+ if vh:
+ return self.folderTree(vh)
+ else:
+ root = zope.traversing.api.getRoot(self.context)
+ return self.folderTree(root)
Added: zmi.core/trunk/src/zmi/core/tree/example1.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/tree/example1.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/tree/example1.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,112 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ metal:use-macro="context/@@standard_macros/page"
+ >
+<body>
+
+<!--
+This example template demonstrates the direct use of the supplemented
+browser view (context/@@static_cookie_tree). No extra python code was
+required, only ZCML configuration.
+
+Feel free to use this template or parts of it in your own works
+without any license restrictions, but please give credit where credit
+is due.
+
+Tip: Change the 'context/@@cookie_tree' expression below to
+'context/@@folder_cookie_tree' for a tree with only folders.
+-->
+
+<div metal:fill-slot="body">
+
+<table cellspacing="0" cellpadding="0"
+ tal:define="root context/@@cookie_tree;
+ result root/getFlatDicts;
+ nodeDictList python:result[0];
+ maxDepth python:result[1]">
+
+<!-- the root needs some special treatment, since it is not in nodeDictList -->
+<tr>
+ <td width="16">
+ <img src="" tal:define="icon context/@@zmi_icon | nothing"
+ tal:replace="structure icon" />
+ </td>
+
+ <td class="list-item"
+ tal:attributes="colspan python:maxDepth+2">
+ <b tal:content="root/getId() | string:[top]"></b>
+ </td>
+</tr>
+
+<tr tal:repeat="nodeInfo nodeDictList">
+<tal:block tal:define="node nodeInfo/node">
+
+ <!-- generate the lines that will point to the next node in the level -->
+ <td style="width:16px" tal:repeat="state nodeInfo/row-state">
+ <img tal:attributes="src context/++resource++tree_images/vline.png"
+ tal:condition="state" alt="|" border="0" />
+ </td>
+
+ <td style="width:16px">
+ <!-- if we have children, let's allow them to be expanded and collapsed -->
+ <a href=""
+ tal:attributes="href string:?tree-state=${nodeInfo/tree-state}"
+ tal:condition="node/hasChildren">
+
+ <!-- If the node is the last node of the level, then we need to use
+ different plus and minus that do not have a line going off
+ downward -->
+ <tal:block condition="not:nodeInfo/last-level-node">
+ <img tal:attributes="src context/++resource++tree_images/plus_vline.png"
+ tal:condition="not:node/expanded" alt="+" border="0" />
+ <img tal:attributes="src context/++resource++tree_images/minus_vline.png"
+ tal:condition="node/expanded" alt="-" border="0" />
+ </tal:block>
+
+ <tal:block condition="nodeInfo/last-level-node">
+ <img tal:attributes="src context/++resource++tree_images/plus.png"
+ tal:condition="not:node/expanded" alt="+" border="0" />
+ <img tal:attributes="src context/++resource++tree_images/minus.png"
+ tal:condition="node/expanded" alt="-" border="0" />
+ </tal:block>
+ </a>
+
+ <!-- this node has no children, so either display a T or L as
+ lines, depending on whether we're the last node in this level
+ or not -->
+ <tal:block condition="not:node/hasChildren">
+ <img tal:attributes="src context/++resource++tree_images/tline.png"
+ tal:condition="not:nodeInfo/last-level-node" alt="" border="0" />
+ <img tal:attributes="src context/++resource++tree_images/lline.png"
+ tal:condition="nodeInfo/last-level-node" alt="" border="0" />
+ </tal:block>
+ </td>
+
+ <td style="width:16px"
+ tal:define="object nocall:node/context;
+ icon object/@@zmi_icon | nothing">
+ <img src="" tal:replace="structure icon" />
+ </td>
+
+ <td class="list-item"
+ tal:attributes="colspan python:maxDepth-len(nodeInfo['row-state'])+1">
+ <a href=""
+ tal:attributes="href
+ string:${node/context/@@absolute_url}/@@SelectedManagementView.html"
+ tal:content="node/context/zope:name">
+ node/id
+ </a>
+ </td>
+
+</tal:block>
+</tr>
+
+</table>
+
+</div>
+</body>
+</html>
Added: zmi.core/trunk/src/zmi/core/tree/images/lline.png
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/tree/images/lline.png
___________________________________________________________________
Added: svn:mime-type
+ image/png
Added: zmi.core/trunk/src/zmi/core/tree/images/minus.png
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/tree/images/minus.png
___________________________________________________________________
Added: svn:mime-type
+ image/png
Added: zmi.core/trunk/src/zmi/core/tree/images/minus_vline.png
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/tree/images/minus_vline.png
___________________________________________________________________
Added: svn:mime-type
+ image/png
Added: zmi.core/trunk/src/zmi/core/tree/images/plus.png
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/tree/images/plus.png
___________________________________________________________________
Added: svn:mime-type
+ image/png
Added: zmi.core/trunk/src/zmi/core/tree/images/plus_vline.png
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/tree/images/plus_vline.png
___________________________________________________________________
Added: svn:mime-type
+ image/png
Added: zmi.core/trunk/src/zmi/core/tree/images/shim.gif
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/tree/images/shim.gif
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream
Added: zmi.core/trunk/src/zmi/core/tree/images/tline.png
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/tree/images/tline.png
___________________________________________________________________
Added: svn:mime-type
+ image/png
Added: zmi.core/trunk/src/zmi/core/tree/images/vline.png
===================================================================
(Binary files differ)
Property changes on: zmi.core/trunk/src/zmi/core/tree/images/vline.png
___________________________________________________________________
Added: svn:mime-type
+ image/png
Added: zmi.core/trunk/src/zmi/core/tree/navigation_macros.pt
===================================================================
--- zmi.core/trunk/src/zmi/core/tree/navigation_macros.pt (rev 0)
+++ zmi.core/trunk/src/zmi/core/tree/navigation_macros.pt 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,101 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ i18n:domain="zope">
+<body>
+
+ <!-- Java scripts for the navigation tree - none! -->
+
+ <metal:tree define-macro="navigation_tree_js">
+ </metal:tree>
+
+ <!-- Box containing the actual navigation tree -->
+
+ <metal:tree define-macro="navigation_tree_box">
+ <div class="box" id="navigationTree">
+ <h4 i18n:translate="">Navigation</h4>
+ <div class="treebody">
+<tal:block define="root context/@@virtualhost_cookie_tree;
+ result root/getFlatDicts;
+ nodeDictList python:result[0];
+ maxDepth python:result[1]">
+
+<table cellspacing="0" cellpadding="0">
+<tr>
+ <td width="16">
+ <img src="" tal:define="icon root/context/@@zmi_icon | nothing"
+ tal:replace="structure icon" />
+ </td>
+
+ <td class="list-item"
+ tal:attributes="colspan python:maxDepth+2">
+ <a href=""
+ tal:attributes="href
+ string:${root/context/@@absolute_url}/@@SelectedManagementView.html"
+ tal:content="root/getId() | string:[top]"></a>
+ </td>
+</tr>
+</table>
+
+<table cellspacing="0" cellpadding="0" tal:repeat="nodeInfo nodeDictList">
+<tr tal:define="node nodeInfo/node">
+ <td style="width:16px" tal:repeat="state nodeInfo/row-state">
+ <img tal:attributes="src context/++resource++tree_images/vline.png"
+ tal:condition="state" alt="|" border="0" />
+ <img src="" width="16" height="16"
+ tal:condition="not:state"
+ tal:attributes="src context/++resource++tree_images/shim.gif" />
+ </td>
+
+ <td style="width:16px">
+ <a href=""
+ tal:attributes="href string:?tree-state=${nodeInfo/tree-state}"
+ tal:condition="node/hasChildren">
+ <tal:block condition="not:nodeInfo/last-level-node">
+ <img tal:attributes="src context/++resource++tree_images/plus_vline.png"
+ tal:condition="not:node/expanded" alt="+" border="0" />
+ <img tal:attributes="src context/++resource++tree_images/minus_vline.png"
+ tal:condition="node/expanded" alt="-" border="0" />
+ </tal:block>
+ <tal:block condition="nodeInfo/last-level-node">
+ <img tal:attributes="src context/++resource++tree_images/plus.png"
+ tal:condition="not:node/expanded" alt="+" border="0" />
+ <img tal:attributes="src context/++resource++tree_images/minus.png"
+ tal:condition="node/expanded" alt="-" border="0" />
+ </tal:block>
+ </a>
+ <tal:block condition="not:node/hasChildren">
+ <img tal:attributes="src context/++resource++tree_images/tline.png"
+ tal:condition="not:nodeInfo/last-level-node" alt="" border="0" />
+ <img tal:attributes="src context/++resource++tree_images/lline.png"
+ tal:condition="nodeInfo/last-level-node" alt="" border="0" />
+ </tal:block>
+ </td>
+
+ <td align="left" style="width:16px"
+ tal:define="object nocall:node/context;
+ icon object/@@zmi_icon | nothing">
+ <img src="" tal:replace="structure icon" />
+ <img src="" width="16" height="16"
+ tal:condition="not:icon"
+ tal:attributes="src context/++resource++tree_images/shim.gif" />
+ </td>
+
+ <td class="list-item"
+ tal:attributes="colspan python:maxDepth-len(nodeInfo['row-state'])+1">
+ <a href=""
+ tal:attributes="href
+ string:${node/context/@@absolute_url}/@@SelectedManagementView.html"
+ tal:content="node/context/zope:name">
+ node/id
+ </a>
+ </td>
+</tr>
+</table>
+</tal:block>
+
+ </div>
+ </div>
+ </metal:tree>
+</body>
+</html>
Added: zmi.core/trunk/src/zmi/core/tree/tests.py
===================================================================
--- zmi.core/trunk/src/zmi/core/tree/tests.py (rev 0)
+++ zmi.core/trunk/src/zmi/core/tree/tests.py 2009-11-21 06:58:58 UTC (rev 105932)
@@ -0,0 +1,102 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Static Tree Tests
+
+$Id: tests.py 94797 2009-01-17 16:14:21Z nadako $
+"""
+
+import unittest
+import zope.component
+import zope.component.interfaces
+from zope.component import getMultiAdapter
+from zope.publisher.browser import TestRequest
+from zope.interface import alsoProvides
+from zope.traversing.interfaces import IContainmentRoot
+from zope.location.traversing import LocationPhysicallyLocatable
+from zope.app.testing import ztapi
+
+from zope.app.tree.utils import TreeStateEncoder
+from zmi.core.tree import StatefulTreeView
+from zmi.core.tree.cookie import CookieTreeView
+from zope.app.tree.tests.basetest import BaseTestCase
+
+class StatefulTreeViewTest(BaseTestCase):
+
+ def setUp(self):
+ super(StatefulTreeViewTest, self).setUp()
+ self.makeItems()
+ # provide the view for all objects (None)
+ ztapi.browserView(None, 'stateful_tree', StatefulTreeView)
+
+ def makeRequest(self):
+ request = self.request = TestRequest()
+
+ # TODO: test stateful tree view
+
+class CookieTreeViewTest(StatefulTreeViewTest):
+
+ def setUp(self):
+ super(CookieTreeViewTest, self).setUp()
+ ztapi.browserView(None, 'cookie_tree', CookieTreeView)
+ zope.component.provideAdapter(LocationPhysicallyLocatable)
+
+ def makeRequestWithVar(self):
+ varname = CookieTreeView.request_variable
+ encoder = TreeStateEncoder()
+ tree_state = encoder.encodeTreeState(self.expanded_nodes)
+ environ = {varname: tree_state}
+ request = TestRequest(environ=environ)
+ return request
+
+ def test_cookie_tree_pre_expanded(self):
+ request = self.makeRequestWithVar()
+ view = getMultiAdapter((self.root_obj, request),
+ name='cookie_tree')
+ cookie_tree = view.cookieTree()
+ self.assert_(self.root_node.expanded)
+ for node in self.root_node.getFlatNodes():
+ self.assertEqual(node.expanded, node.getId() in self.expanded_nodes)
+
+ def test_cookie_tree_sets_cookie(self):
+ request = self.makeRequestWithVar()
+ view = getMultiAdapter((self.root_obj, request),
+ name='cookie_tree')
+ cookie_tree = view.cookieTree()
+ self.failIf(request.response.getCookie(view.request_variable) is None)
+
+ def test_cookie_tree_site_tree(self):
+ request = self.makeRequestWithVar()
+ alsoProvides(self.items['a'], IContainmentRoot)
+ alsoProvides(self.items['c'], zope.component.interfaces.ISite)
+ view = getMultiAdapter((self.items['f'], request),
+ name='cookie_tree')
+ cookie_tree = view.siteTree()
+ self.assert_(cookie_tree.context is self.items['c'])
+
+ def test_cookie_tree_root_tree(self):
+ request = self.makeRequestWithVar()
+ alsoProvides(self.items['c'], IContainmentRoot)
+ view = getMultiAdapter((self.items['f'], request),
+ name='cookie_tree')
+ cookie_tree = view.rootTree()
+ self.assert_(cookie_tree.context is self.items['c'])
+
+
+def test_suite():
+ suite = unittest.makeSuite(StatefulTreeViewTest)
+ suite.addTest(unittest.makeSuite(CookieTreeViewTest))
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
More information about the checkins
mailing list