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
# ------------------------------------------------------------------------------