[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" />
+          &nbsp;&nbsp;<b>Object Name</b>&nbsp;&nbsp;
+          <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" />
+
+        &nbsp;&nbsp;<b i18n:translate="">Object Name</b>&nbsp;&nbsp;
+        <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"/>
+        &nbsp;&nbsp;
+        <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"/>
+        &nbsp;&nbsp;
+        <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'; // &nbsp;
+            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">
+    &nbsp;<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">
+    &nbsp;<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