diff --git a/bin/new.py b/bin/new.py
index 6470712..dfc1af8 100644
--- a/bin/new.py
+++ b/bin/new.py
@@ -109,8 +109,9 @@ class NewScript:
             f.write(self.patchRex.sub('',fileContent))
             f.close()
 
-    filesToPatch2 = ('profiles/default/skins.xml')
-    def patchPlone4(self):
+    missingIncludes = ('plone.app.upgrade', 'plonetheme.sunburst',
+                       'plonetheme.classic')
+    def patchPlone4(self, versions):
         '''Patches Plone 4 that can't live without buildout as-is.'''
         self.patchPlone3x() # We still need this for Plone 4 as well.
         # bin/zopectl
@@ -124,42 +125,44 @@ class NewScript:
         f.write(content)
         f.close()
         j = os.path.join
-        themeFolder = '%s/plonetheme' % self.libFolder
-        for theme in os.listdir(themeFolder):
-            # Create a simlink to every theme in self.productsFolder
-            tFolder = j(themeFolder, theme)
-            if not os.path.isdir(tFolder): continue
-            os.system('ln -s %s %s/%s' % (tFolder, self.productsFolder, theme))
-            # Patch skins.xml
-            fileName = '%s/profiles/default/skins.xml' % tFolder
-            f = file(fileName)
-            content = f.read()
-            f.close()
-            f = file(fileName, 'w')
-            f.write(content.replace('plonetheme.%s:' % theme, '%s/' % theme))
-            f.close()
-        # As eggs have been deleted, Plone can't tell which version of Zope and
-        # Plone are there. So we patch the code that tries to get Plone and Zope
-        # versions.
+        # As eggs have been deleted, versions of components are lost. Reify
+        # them from p_versions.
+        dVersions = ['"%s":"%s"' % (n, v) for n, v in versions.iteritems()]
+        sVersions = 'appyVersions = {' + ','.join(dVersions) + '}'
         codeFile = "%s/pkg_resources.py" % self.libFolder
         f = file(codeFile)
-        content = f.read()
+        content = sVersions + '\n' + f.read()
         f.close()
         content = content.replace("raise DistributionNotFound(req)",
             "dist = Distribution(project_name=req.project_name, " \
-            "version='1.1.1', platform='linux2', location='%s')" % \
-            self.instancePath)
+            "version=appyVersions[req.project_name], platform='linux2', " \
+            "location='%s')" % self.instancePath)
         f = file(codeFile, 'w')
         f.write(content)
         f.close()
+        # Some 'include' directives must be added with our install.
+        configPlone = j(self.productsFolder, 'CMFPlone', 'configure.zcml')
+        f = file(configPlone)
+        content = f.read()
+        f.close()
+        missing = ''
+        for missingInclude in self.missingIncludes:
+            missing += '  \n' % missingInclude
+        content = content.replace('', '%s\n' % missing)
+        f = file(configPlone, 'w')
+        f.write(content)
+        f.close()
 
     def copyEggs(self):
-        '''Copy content of eggs into the Zope instance.'''
+        '''Copy content of eggs into the Zope instance. This method also
+           retrieves every egg version and returns a dict {s_egg:s_version}.'''
         j = os.path.join
         eggsFolder = j(self.plonePath, 'buildout-cache/eggs')
-        self.ploneThemes = []
+        res = {}
         for name in os.listdir(eggsFolder):
             if name == 'EGG-INFO': continue
+            splittedName = name.split('-')
+            res[splittedName[0]] = splittedName[1]
             absName = j(eggsFolder, name)
             # Copy every file or sub-folder into self.libFolder or
             # self.productsFolder.
@@ -175,6 +178,7 @@ class NewScript:
                     copyFolder(absFileName, j(self.libFolder, fileName))
                 else:
                     shutil.copy(absFileName, self.libFolder)
+        return res
 
     def createInstance(self, linksForProducts):
         '''Calls the Zope script that allows to create a Zope instance and copy
@@ -224,7 +228,7 @@ class NewScript:
         if self.ploneVersion in ('plone25', 'plone30'):
             self.installPlone25or30Stuff(linksForProducts)
         elif self.ploneVersion in ('plone3x', 'plone4'):
-            self.copyEggs()
+            versions = self.copyEggs()
             if self.ploneVersion == 'plone3x':
                 self.patchPlone3x()
             elif self.ploneVersion == 'plone4':
@@ -234,7 +238,7 @@ class NewScript:
                      j(self.instancePath, 'lib/python'))
                 print cmd
                 os.system(cmd)
-                self.patchPlone4()
+                self.patchPlone4(versions)
         # Remove .bat files under Linux
         if os.name == 'posix':
             cleanFolder(j(self.instancePath, 'bin'), exts=('.bat',))
diff --git a/bin/publish.py b/bin/publish.py
old mode 100755
new mode 100644
index eedda85..2b3e6ef
--- a/bin/publish.py
+++ b/bin/publish.py
@@ -1,6 +1,6 @@
-#!/usr/bin/python2.4.4
+#!/usr/bin/python
 # Imports ----------------------------------------------------------------------
-import os, os.path, shutil, re, zipfile, sys, ftplib, time
+import os, os.path, sys, shutil, re, zipfile, sys, ftplib, time
 import appy
 from appy.shared import appyPath
 from appy.shared.utils import FolderDeleter, LinesCounter
@@ -9,17 +9,23 @@ from appy.gen.utils import produceNiceMessage
 
 # ------------------------------------------------------------------------------
 versionRex = re.compile('(\d+\.\d+\.\d+)')
-eggInfo = '''import os, setuptools
-setuptools.setup(
-    name = "appy", version = "%s", description = "The Appy framework",
-    long_description = "See http://appyframework.org",
-    author = "Gaetan Delannay", author_email = "gaetan.delannay AT gmail.com",
-    license = "GPL", keywords = "plone, pod, pdf, odt, document",
-    url = 'http://appyframework.org',
-    classifiers = ['Development Status :: 4 - Beta', "License :: OSI Approved"],
-    packages = setuptools.find_packages('src'), include_package_data = True,
-    package_dir = {'':'src'}, data_files = [('.', [])],
-    namespace_packages = ['appy'], zip_safe = False)'''
+distInfo = '''from distutils.core import setup
+setup(name = "appy", version = "%s",
+      description = "The Appy framework",
+      long_description = "Appy builds simple but complex web Python apps.",
+      author = "Gaetan Delannay",
+      author_email = "gaetan.delannay AT geezteem.com",
+      license = "GPL", platforms="all",
+      url = 'http://appyframework.org',
+      packages = [%s],
+      package_data = {'':["*.*"]})
+'''
+manifestInfo = '''
+recursive-include appy/bin *
+recursive-include appy/gen *
+recursive-include appy/pod *
+recursive-include appy/shared *
+'''
 
 def askLogin():
     print 'Login: ',
@@ -28,29 +34,6 @@ def askLogin():
     passwd = sys.stdin.readline().strip()
     return (login, passwd)
 
-def askQuestion(question, default='yes'):
-    '''Asks a question to the user (yes/no) and returns True if the user
-        answered "yes".'''
-    defaultIsYes = (default.lower() in ('y', 'yes'))
-    if defaultIsYes:
-        yesNo = '[Y/n]'
-    else:
-        yesNo = '[y/N]'
-    print question + ' ' + yesNo + ' ',
-    response = sys.stdin.readline().strip().lower()
-    res = False
-    if response in ('y', 'yes'):
-        res = True
-    elif response in ('n', 'no'):
-        res = False
-    elif not response:
-        # It depends on default value
-        if defaultIsYes:
-            res = True
-        else:
-            res = False
-    return res
-
 class FtpFolder:
     '''Represents a folder on a FTP site.'''
     def __init__(self, name):
@@ -247,52 +230,95 @@ class Publisher:
         self.versionLong = '%s (%s)' % (self.versionShort,
                                         time.strftime('%Y/%m/%d %H:%M'))
         f.close()
+        # In silent mode (option -s), no question is asked, default answers are
+        # automatically given.
+        if (len(sys.argv) > 1) and (sys.argv[1] == '-s'):
+            self.silent = True
+        else:
+            self.silent = False
+
+    def askQuestion(self, question, default='yes'):
+        '''Asks a question to the user (yes/no) and returns True if the user
+            answered "yes".'''
+        if self.silent: return (default == 'yes')
+        defaultIsYes = (default.lower() in ('y', 'yes'))
+        if defaultIsYes:
+            yesNo = '[Y/n]'
+        else:
+            yesNo = '[y/N]'
+        print question + ' ' + yesNo + ' ',
+        response = sys.stdin.readline().strip().lower()
+        res = False
+        if response in ('y', 'yes'):
+            res = True
+        elif response in ('n', 'no'):
+            res = False
+        elif not response:
+            # It depends on default value
+            if defaultIsYes:
+                res = True
+            else:
+                res = False
+        return res
 
     def executeCommand(self, cmd):
         '''Executes the system command p_cmd.'''
         print 'Executing %s...' % cmd
         os.system(cmd)
 
-    def createCodeAndEggReleases(self):
-        '''Publishes the egg on pypi.python.org.'''
+    distExcluded = ('appy/doc', 'appy/temp', 'appy/versions', 'appy/gen/test')
+    def isDistExcluded(self, name):
+        '''Returns True if folder named p_name must be included in the
+           distribution.'''
+        if '.bzr' in name: return True
+        for prefix in self.distExcluded:
+            if name.startswith(prefix): return True
+
+    def createDistRelease(self):
+        '''Create the distutils package.'''
         curdir = os.getcwd()
-        if askQuestion('Upload eggs on PyPI?', default='no'):
-            # Create egg structure
-            eggFolder = '%s/egg' % self.genFolder
-            os.mkdir(eggFolder)
-            f = file('%s/setup.py' % eggFolder, 'w')
-            f.write(eggInfo % self.versionShort)
-            f.close()
-            os.mkdir('%s/docs' % eggFolder)
-            os.mkdir('%s/src' % eggFolder)
-            os.mkdir('%s/src/appy' % eggFolder)
-            shutil.copy('%s/doc/version.txt' % appyPath,
-                        '%s/docs/HISTORY.txt' % eggFolder)
-            shutil.copy('%s/doc/license.txt' % appyPath,
-                        '%s/docs/LICENSE.txt' % eggFolder)
-            # Move appy sources within the egg
-            os.rename('%s/appy' % self.genFolder, '%s/src/appy' % eggFolder)
-            # Create eggs and publish them on pypi
-            os.chdir(eggFolder)
-            print 'Uploading appy%s source egg on PyPI...' % self.versionShort
-            #self.executeCommand('python setup.py sdist upload')
-            self.executeCommand('python setup.py sdist')
-            for pythonTarget in self.pythonTargets:
-                print 'Uploading appy%s binary egg for python%s...' % \
-                      (self.versionShort, pythonTarget)
-                #self.executeCommand('python%s setup.py bdist_egg upload' % \
-                #    pythonTarget)
-                self.executeCommand('python%s setup.py bdist_egg' % \
-                    pythonTarget)
+        distFolder = '%s/dist' % self.genFolder
+        # Create setup.py
+        os.mkdir(distFolder)
+        f = file('%s/setup.py' % distFolder, 'w')
+        # List all packages to include
+        packages = []
+        os.chdir(os.path.dirname(appyPath))
+        for dir, dirnames, filenames in os.walk('appy'):
+            if self.isDistExcluded(dir): continue
+            packageName = dir.replace('/', '.')
+            packages.append('"%s"' % packageName)
+        f.write(distInfo % (self.versionShort, ','.join(packages)))
+        f.close()
+        # Create MANIFEST.in
+        f = file('%s/MANIFEST.in' % distFolder, 'w')
+        f.write(manifestInfo)
+        f.close()
+        # Move appy sources within the dist folder
+        os.rename('%s/appy' % self.genFolder, '%s/appy' % distFolder)
+        # Create the source distribution
+        os.chdir(distFolder)
+        self.executeCommand('python setup.py sdist')
+        # DistUtils has created the .tar.gz file. Copy it into folder "versions"
+        name = 'appy-%s.tar.gz' % self.versionShort
+        os.rename('%s/dist/%s' % (distFolder, name),
+                  '%s/versions/%s' % (appyPath, name))
+        # Clean temp files
         os.chdir(curdir)
+        FolderDeleter.delete(os.path.join(self.genFolder, 'dist'))
+        return name
+
+    def uploadOnPypi(self, name):
+        print 'Uploading %s on PyPI...' % name
+        #self.executeCommand('python setup.py sdist upload')
 
     def createZipRelease(self):
         '''Creates a zip file with the appy sources.'''
         newZipRelease = '%s/versions/appy%s.zip' % (appyPath, self.versionShort)
         if os.path.exists(newZipRelease):
-            if not askQuestion('"%s" already exists. Replace it?' % \
-                               newZipRelease, default='yes'):
-                print 'Publication cancelled.'
+            if not self.askQuestion('"%s" already exists. Replace it?' % \
+                                    newZipRelease, default='yes'):
+                print 'Publication canceled.'
                 sys.exit(1)
             print 'Removing obsolete %s...' % newZipRelease
             os.remove(newZipRelease)
@@ -306,8 +332,6 @@ class Publisher:
                 # [2:] is there to avoid havin './' in the path in the zip file.
         zipFile.close()
         os.chdir(curdir)
-        # Remove the "appy" folder within the gen folder.
-        FolderDeleter.delete(os.path.join(self.genFolder, 'appy'))
 
     def applyTemplate(self):
         '''Decorates each page with the template.'''
@@ -405,7 +429,7 @@ class Publisher:
         # Create a temp clean copy of appy sources (without .svn folders, etc)
         genSrcFolder = '%s/appy' % self.genFolder
         os.mkdir(genSrcFolder)
-        for aFile in ('__init__.py', 'install.txt'):
+        for aFile in ('__init__.py',):
             shutil.copy('%s/%s' % (appyPath, aFile), genSrcFolder)
         for aFolder in ('gen', 'pod', 'shared', 'bin'):
             shutil.copytree('%s/%s' % (appyPath, aFolder),
@@ -438,17 +462,18 @@ class Publisher:
         # Perform a small analysis on the Appy code
         LinesCounter(appy).run()
         print 'Generating site in %s...' % self.genFolder
-        minimalist = askQuestion('Minimalist (shipped without tests)?',
-                                 default='no')
+        minimalist = self.askQuestion('Minimalist (shipped without tests)?',
+                                      default='no')
         self.prepareGenFolder(minimalist)
         self.createDocToc()
         self.applyTemplate()
         self.createZipRelease()
-        #self.createCodeAndEggReleases()
-        if askQuestion('Do you want to publish the site on ' \
-                       'appyframework.org?', default='no'):
+        tarball = self.createDistRelease()
+        if self.askQuestion('Upload %s on PyPI?' % tarball, default='no'):
+            self.uploadOnPypi(tarball)
+        if self.askQuestion('Publish on appyframework.org?', default='no'):
             AppySite().publish()
-        if askQuestion('Delete locally generated site ?', default='no'):
+        if self.askQuestion('Delete locally generated site ?', default='yes'):
             FolderDeleter.delete(self.genFolder)
 
 # ------------------------------------------------------------------------------
diff --git a/gen/__init__.py b/gen/__init__.py
index 7cb9a3a..2955325 100644
--- a/gen/__init__.py
+++ b/gen/__init__.py
@@ -5,12 +5,13 @@ from appy import Object
 from appy.gen.layout import Table
 from appy.gen.layout import defaultFieldLayouts
 from appy.gen.po import PoMessage
-from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \
-                           getClassName, SomeObjects
+from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, getClassName, \
+                           SomeObjects
 import appy.pod
 from appy.pod.renderer import Renderer
 from appy.shared.data import countries
-from appy.shared.utils import Traceback, getOsTempFolder, formatNumber
+from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \
+                              FileWrapper
 
 # Default Appy permissions -----------------------------------------------------
 r, w, d = ('read', 'write', 'delete')
@@ -1505,7 +1506,7 @@ class File(Type):
     def getRequestValue(self, request):
         return request.get('%s_file' % self.name)
 
-    def getDefaultLayouts(self): return {'view':'lf','edit':'lrv-f'}
+    def getDefaultLayouts(self): return {'view':'l-f','edit':'lrv-f'}
 
     def isEmptyValue(self, value, obj=None):
         '''Must p_value be considered as empty?'''
@@ -1536,8 +1537,9 @@ class File(Type):
            * an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In
              this case, it is file content coming from a HTTP POST;
            * an instance of Zope class OFS.Image.File;
-           * an instance of appy.gen.utils.FileWrapper, which wraps an instance
-             of OFS.Image.File and adds useful methods for manipulating it;
+           * an instance of appy.shared.utils.FileWrapper, which wraps an
+             instance of OFS.Image.File and adds useful methods for manipulating
+             it;
            * a string. In this case, the string represents the path of a file
              on disk;
            * a 2-tuple (fileName, fileContent) where:
diff --git a/gen/generator.py b/gen/generator.py
index 8ca3c75..390db3a 100644
--- a/gen/generator.py
+++ b/gen/generator.py
@@ -675,7 +675,7 @@ class ZopeGenerator(Generator):
         repls['languages'] = ','.join('"%s"' % l for l in self.config.languages)
         repls['languageSelector'] = self.config.languageSelector
         repls['sourceLanguage'] = self.config.sourceLanguage
-        self.copyFile('config.py', repls)
+        self.copyFile('config.pyt', repls, destName='config.py')
 
     def generateInit(self):
         # Compute imports
@@ -690,7 +690,7 @@ class ZopeGenerator(Generator):
         repls['imports'] = '\n'.join(imports)
         repls['classes'] = ','.join(classNames)
         repls['totalNumberOfTests'] = self.totalNumberOfTests
-        self.copyFile('__init__.py', repls)
+        self.copyFile('__init__.pyt', repls, destName='__init__.py')
 
     def getClassesInOrder(self, allClasses):
         '''When generating wrappers, classes mut be dumped in order (else, it
@@ -757,7 +757,7 @@ class ZopeGenerator(Generator):
         for klass in self.getClasses(include='predefined'):
             modelClass = klass.modelClass
             repls['%s' % modelClass.__name__] = modelClass._appy_getBody()
-        self.copyFile('wrappers.py', repls)
+        self.copyFile('wrappers.pyt', repls, destName='wrappers.py')
 
     def generateTests(self):
         '''Generates the file needed for executing tests.'''
@@ -765,7 +765,8 @@ class ZopeGenerator(Generator):
         modules = self.modulesWithTests
         repls['imports'] = '\n'.join(['import %s' % m for m in modules])
         repls['modulesWithTests'] = ','.join(modules)
-        self.copyFile('testAll.py', repls, destFolder='tests')
+        self.copyFile('testAll.pyt', repls, destName='testAll.py',
+                      destFolder='tests')
 
     def generateTool(self):
         '''Generates the tool that corresponds to this application.'''
@@ -790,7 +791,7 @@ class ZopeGenerator(Generator):
             repls.update({'methods': klass.methods, 'genClassName': klass.name,
               'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem',
               'classDoc': 'Standard Appy class', 'icon':'object.gif'})
-            self.copyFile('Class.py', repls, destName='%s.py' % klass.name)
+            self.copyFile('Class.pyt', repls, destName='%s.py' % klass.name)
 
         # Before generating the Tool class, finalize it with query result
         # columns, with fields to propagate, workflow-related fields.
@@ -817,7 +818,7 @@ class ZopeGenerator(Generator):
           'genClassName': self.tool.name, 'baseMixin':'ToolMixin',
           'parents': 'ToolMixin, Folder', 'icon': 'folder.gif',
           'classDoc': 'Tool class for %s' % self.applicationName})
-        self.copyFile('Class.py', repls, destName='%s.py' % self.tool.name)
+        self.copyFile('Class.pyt', repls, destName='%s.py' % self.tool.name)
 
     def generateClass(self, classDescr):
         '''Is called each time an Appy class is found in the application, for
@@ -863,7 +864,7 @@ class ZopeGenerator(Generator):
                 if poMsg not in self.labels:
                     self.labels.append(poMsg)
         # Generate the resulting Zope class.
-        self.copyFile('Class.py', repls, destName=fileName)
+        self.copyFile('Class.pyt', repls, destName=fileName)
 
     def generateWorkflow(self, wfDescr):
         '''This method creates the i18n labels related to the workflow described
diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py
index f613f4d..c5df7c6 100644
--- a/gen/mixins/ToolMixin.py
+++ b/gen/mixins/ToolMixin.py
@@ -781,7 +781,7 @@ class ToolMixin(BaseMixin):
                     if brain:
                         sibling = brain.getObject()
                         res[urlKey] = sibling.getUrl(nav=newNav % (index + 1),
-                                                     page='main')
+                                          page=self.REQUEST.get('page', 'main'))
         return res
 
     def tabularize(self, data, numberOfRows):
diff --git a/gen/templates/Class.py b/gen/templates/Class.pyt
similarity index 100%
rename from gen/templates/Class.py
rename to gen/templates/Class.pyt
diff --git a/gen/templates/__init__.py b/gen/templates/__init__.pyt
similarity index 100%
rename from gen/templates/__init__.py
rename to gen/templates/__init__.pyt
diff --git a/gen/templates/config.py b/gen/templates/config.pyt
similarity index 100%
rename from gen/templates/config.py
rename to gen/templates/config.pyt
diff --git a/gen/templates/testAll.py b/gen/templates/testAll.pyt
similarity index 100%
rename from gen/templates/testAll.py
rename to gen/templates/testAll.pyt
diff --git a/gen/templates/wrappers.py b/gen/templates/wrappers.pyt
similarity index 100%
rename from gen/templates/wrappers.py
rename to gen/templates/wrappers.pyt
diff --git a/gen/utils.py b/gen/utils.py
index 5389cfd..783f1c4 100644
--- a/gen/utils.py
+++ b/gen/utils.py
@@ -1,7 +1,6 @@
 # ------------------------------------------------------------------------------
-import re, os, os.path, time
+import re, os, os.path
 import appy.pod
-from appy.shared.utils import getOsTempFolder, normalizeString, executeCommand
 sequenceTypes = (list, tuple)
 
 # Function for creating a Zope object ------------------------------------------
@@ -242,84 +241,6 @@ class Keywords:
             return op.join(self.keywords)+'*'
         return ''
 
-# ------------------------------------------------------------------------------
-CONVERSION_ERROR = 'An error occurred while executing command "%s". %s'
-class FileWrapper:
-    '''When you get, from an appy object, the value of a File attribute, you
-       get an instance of this class.'''
-    def __init__(self, zopeFile):
-        '''This constructor is only used by Appy to create a nice File instance
-           from a Zope corresponding instance (p_zopeFile). If you need to
-           create a new file and assign it to a File attribute, use the
-           attribute setter, do not create yourself an instance of this
-           class.'''
-        d = self.__dict__
-        d['_zopeFile'] = zopeFile # Not for you!
-        d['name'] = zopeFile.filename
-        d['content'] = zopeFile.data
-        d['mimeType'] = zopeFile.content_type
-        d['size'] = zopeFile.size # In bytes
-
-    def __setattr__(self, name, v):
-        d = self.__dict__
-        if name == 'name':
-            self._zopeFile.filename = v
-            d['name'] = v
-        elif name == 'content':
-            self._zopeFile.update_data(v, self.mimeType, len(v))
-            d['content'] = v
-            d['size'] = len(v)
-        elif name == 'mimeType':
-            self._zopeFile.content_type = self.mimeType = v
-        else:
-            raise 'Impossible to set attribute %s. "Settable" attributes ' \
-                  'are "name", "content" and "mimeType".' % name
-
-    def dump(self, filePath=None, format=None, tool=None):
-        '''Writes the file on disk. If p_filePath is specified, it is the
-           path name where the file will be dumped; folders mentioned in it
-           must exist. If not, the file will be dumped in the OS temp folder.
-           The absolute path name of the dumped file is returned.
-           If an error occurs, the method returns None. If p_format is
-           specified, OpenOffice will be called for converting the dumped file
-           to the desired format. In this case, p_tool, a Appy tool, must be
-           provided. Indeed, any Appy tool contains parameters for contacting
-           OpenOffice in server mode.'''
-        if not filePath:
-            filePath = '%s/file%f.%s' % (getOsTempFolder(), time.time(),
-                normalizeString(self.name))
-        f = file(filePath, 'w')
-        if self.content.__class__.__name__ == 'Pdata':
-            # The file content is splitted in several chunks.
-            f.write(self.content.data)
-            nextPart = self.content.next
-            while nextPart:
-                f.write(nextPart.data)
-                nextPart = nextPart.next
-        else:
-            # Only one chunk
-            f.write(self.content)
-        f.close()
-        if format:
-            if not tool: return
-            # Convert the dumped file using OpenOffice
-            errorMessage = tool.convert(filePath, format)
-            # Even if we have an "error" message, it could be a simple warning.
-            # So we will continue here and, as a subsequent check for knowing if
-            # an error occurred or not, we will test the existence of the
-            # converted file (see below).
-            os.remove(filePath)
-            # Return the name of the converted file.
-            baseName, ext = os.path.splitext(filePath)
-            if (ext == '.%s' % format):
-                filePath = '%s.res.%s' % (baseName, format)
-            else:
-                filePath = '%s.%s' % (baseName, format)
-            if not os.path.exists(filePath):
-                tool.log(CONVERSION_ERROR % (cmd, errorMessage), type='error')
-                return
-        return filePath
-
 # ------------------------------------------------------------------------------
 def getClassName(klass, appName=None):
     '''Generates, from appy-class p_klass, the name of the corresponding
diff --git a/install.txt b/install.txt
deleted file mode 100644
index bbae2a1..0000000
--- a/install.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-Installation under Windows or MacOS
------------------------------------
-Copy the content of this folder to
-    \Lib\site-packages\appy
-
-Installation under Linux
-------------------------
-Copy the content of this folder wherever you want (in /opt/appy for example)
-and make a symbolic link in your Python lib folder (for example:
-"ln -s /opt/appy /usr/lib/python2.5/site-packages/appy").
-
diff --git a/pod/doc_importers.py b/pod/doc_importers.py
index 5c3a180..0040cc7 100644
--- a/pod/doc_importers.py
+++ b/pod/doc_importers.py
@@ -20,6 +20,7 @@
 import os, os.path, time, shutil, struct, random
 from appy.pod import PodError
 from appy.pod.odf_parser import OdfEnvironment
+from appy.shared.utils import FileWrapper
 
 # ------------------------------------------------------------------------------
 FILE_NOT_FOUND = "'%s' does not exist or is not a file."
@@ -59,9 +60,12 @@ class DocImporter:
             self.importPath = self.moveFile(at, self.importPath)
         else:
             # We need to dump the file content (in self.content) in a temp file
-            # first. self.content may be binary or a file handler.
+            # first. self.content may be binary, a file handler or a
+            # FileWrapper.
             if isinstance(self.content, file):
                 fileContent = self.content.read()
+            elif isinstance(self.content, FileWrapper):
+                fileContent = content.content
             else:
                 fileContent = self.content
             f = file(self.importPath, 'wb')
diff --git a/pod/renderer.py b/pod/renderer.py
index 034cf47..d0cb701 100644
--- a/pod/renderer.py
+++ b/pod/renderer.py
@@ -26,6 +26,7 @@ from appy.pod import PodError
 from appy.shared import mimeTypesExts
 from appy.shared.xml_parser import XmlElement
 from appy.shared.utils import FolderDeleter, executeCommand
+from appy.shared.utils import FileWrapper
 from appy.pod.pod_parser import PodParser, PodEnvironment, OdInsert
 from appy.pod.converter import FILE_TYPES
 from appy.pod.buffers import FileBuffer
@@ -281,6 +282,8 @@ class Renderer:
         if not content and not at:
             raise PodError(DOC_NOT_SPECIFIED)
         # Guess document format
+        if isinstance(content, FileWrapper):
+            format = content.mimeType
         if not format:
             # It should be deduced from p_at
             if not at:
diff --git a/shared/utils.py b/shared/utils.py
index 000a407..c347768 100644
--- a/shared/utils.py
+++ b/shared/utils.py
@@ -410,4 +410,82 @@ class LinesCounter:
                 elif ext in exts['pt']:
                     self.zpt[self.inTest].analyseFile(j(root, fileName))
         self.printReport()
+
+# ------------------------------------------------------------------------------
+CONVERSION_ERROR = 'An error occurred while executing command "%s". %s'
+class FileWrapper:
+    '''When you get, from an appy object, the value of a File attribute, you
+       get an instance of this class.'''
+    def __init__(self, zopeFile):
+        '''This constructor is only used by Appy to create a nice File instance
+           from a Zope corresponding instance (p_zopeFile). If you need to
+           create a new file and assign it to a File attribute, use the
+           attribute setter, do not create yourself an instance of this
+           class.'''
+        d = self.__dict__
+        d['_zopeFile'] = zopeFile # Not for you!
+        d['name'] = zopeFile.filename
+        d['content'] = zopeFile.data
+        d['mimeType'] = zopeFile.content_type
+        d['size'] = zopeFile.size # In bytes
+
+    def __setattr__(self, name, v):
+        d = self.__dict__
+        if name == 'name':
+            self._zopeFile.filename = v
+            d['name'] = v
+        elif name == 'content':
+            self._zopeFile.update_data(v, self.mimeType, len(v))
+            d['content'] = v
+            d['size'] = len(v)
+        elif name == 'mimeType':
+            self._zopeFile.content_type = self.mimeType = v
+        else:
+            raise 'Impossible to set attribute %s. "Settable" attributes ' \
+                  'are "name", "content" and "mimeType".' % name
+
+    def dump(self, filePath=None, format=None, tool=None):
+        '''Writes the file on disk. If p_filePath is specified, it is the
+           path name where the file will be dumped; folders mentioned in it
+           must exist. If not, the file will be dumped in the OS temp folder.
+           The absolute path name of the dumped file is returned.
+           If an error occurs, the method returns None. If p_format is
+           specified, OpenOffice will be called for converting the dumped file
+           to the desired format. In this case, p_tool, a Appy tool, must be
+           provided. Indeed, any Appy tool contains parameters for contacting
+           OpenOffice in server mode.'''
+        if not filePath:
+            filePath = '%s/file%f.%s' % (getOsTempFolder(), time.time(),
+                normalizeString(self.name))
+        f = file(filePath, 'w')
+        if self.content.__class__.__name__ == 'Pdata':
+            # The file content is splitted in several chunks.
+            f.write(self.content.data)
+            nextPart = self.content.next
+            while nextPart:
+                f.write(nextPart.data)
+                nextPart = nextPart.next
+        else:
+            # Only one chunk
+            f.write(self.content)
+        f.close()
+        if format:
+            if not tool: return
+            # Convert the dumped file using OpenOffice
+            errorMessage = tool.convert(filePath, format)
+            # Even if we have an "error" message, it could be a simple warning.
+            # So we will continue here and, as a subsequent check for knowing if
+            # an error occurred or not, we will test the existence of the
+            # converted file (see below).
+            os.remove(filePath)
+            # Return the name of the converted file.
+            baseName, ext = os.path.splitext(filePath)
+            if (ext == '.%s' % format):
+                filePath = '%s.res.%s' % (baseName, format)
+            else:
+                filePath = '%s.%s' % (baseName, format)
+            if not os.path.exists(filePath):
+                tool.log(CONVERSION_ERROR % (cmd, errorMessage), type='error')
+                return
+        return filePath
 # ------------------------------------------------------------------------------