appy.bin: updated publish.py, that is now able to generate a DistUtils tar.gz for Appy; publish.py can now be called with option '-s' (silent): in this mode no question is asked to the user, default values are used; updated new.py that generates a better Plone4-ready simple Zope instance; appy: moved FileWrapper from appy.gen.utils to appy.shared.utils to avoid circular package dependencies; appy.gen: use of .pyt extensions for template Python classes in appy.gen.templates in order to avoid byte-compilation errors when distutils installs the package; appy.pod: when using function 'document' in 'from' statements, first arg can now be a appy.shared.utils.FileWrapper instance.

This commit is contained in:
Gaetan Delannay 2011-12-15 22:56:53 +01:00
parent e78cf62694
commit 6ece750d9a
15 changed files with 237 additions and 210 deletions

View file

@ -109,8 +109,9 @@ class NewScript:
f.write(self.patchRex.sub('<!--Del. includePlugins-->',fileContent)) f.write(self.patchRex.sub('<!--Del. includePlugins-->',fileContent))
f.close() f.close()
filesToPatch2 = ('profiles/default/skins.xml') missingIncludes = ('plone.app.upgrade', 'plonetheme.sunburst',
def patchPlone4(self): 'plonetheme.classic')
def patchPlone4(self, versions):
'''Patches Plone 4 that can't live without buildout as-is.''' '''Patches Plone 4 that can't live without buildout as-is.'''
self.patchPlone3x() # We still need this for Plone 4 as well. self.patchPlone3x() # We still need this for Plone 4 as well.
# bin/zopectl # bin/zopectl
@ -124,42 +125,44 @@ class NewScript:
f.write(content) f.write(content)
f.close() f.close()
j = os.path.join j = os.path.join
themeFolder = '%s/plonetheme' % self.libFolder # As eggs have been deleted, versions of components are lost. Reify
for theme in os.listdir(themeFolder): # them from p_versions.
# Create a simlink to every theme in self.productsFolder dVersions = ['"%s":"%s"' % (n, v) for n, v in versions.iteritems()]
tFolder = j(themeFolder, theme) sVersions = 'appyVersions = {' + ','.join(dVersions) + '}'
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.
codeFile = "%s/pkg_resources.py" % self.libFolder codeFile = "%s/pkg_resources.py" % self.libFolder
f = file(codeFile) f = file(codeFile)
content = f.read() content = sVersions + '\n' + f.read()
f.close() f.close()
content = content.replace("raise DistributionNotFound(req)", content = content.replace("raise DistributionNotFound(req)",
"dist = Distribution(project_name=req.project_name, " \ "dist = Distribution(project_name=req.project_name, " \
"version='1.1.1', platform='linux2', location='%s')" % \ "version=appyVersions[req.project_name], platform='linux2', " \
self.instancePath) "location='%s')" % self.instancePath)
f = file(codeFile, 'w') f = file(codeFile, 'w')
f.write(content) f.write(content)
f.close() 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 += ' <include package="%s"/>\n' % missingInclude
content = content.replace('</configure>', '%s\n</configure>' % missing)
f = file(configPlone, 'w')
f.write(content)
f.close()
def copyEggs(self): 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 j = os.path.join
eggsFolder = j(self.plonePath, 'buildout-cache/eggs') eggsFolder = j(self.plonePath, 'buildout-cache/eggs')
self.ploneThemes = [] res = {}
for name in os.listdir(eggsFolder): for name in os.listdir(eggsFolder):
if name == 'EGG-INFO': continue if name == 'EGG-INFO': continue
splittedName = name.split('-')
res[splittedName[0]] = splittedName[1]
absName = j(eggsFolder, name) absName = j(eggsFolder, name)
# Copy every file or sub-folder into self.libFolder or # Copy every file or sub-folder into self.libFolder or
# self.productsFolder. # self.productsFolder.
@ -175,6 +178,7 @@ class NewScript:
copyFolder(absFileName, j(self.libFolder, fileName)) copyFolder(absFileName, j(self.libFolder, fileName))
else: else:
shutil.copy(absFileName, self.libFolder) shutil.copy(absFileName, self.libFolder)
return res
def createInstance(self, linksForProducts): def createInstance(self, linksForProducts):
'''Calls the Zope script that allows to create a Zope instance and copy '''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'): if self.ploneVersion in ('plone25', 'plone30'):
self.installPlone25or30Stuff(linksForProducts) self.installPlone25or30Stuff(linksForProducts)
elif self.ploneVersion in ('plone3x', 'plone4'): elif self.ploneVersion in ('plone3x', 'plone4'):
self.copyEggs() versions = self.copyEggs()
if self.ploneVersion == 'plone3x': if self.ploneVersion == 'plone3x':
self.patchPlone3x() self.patchPlone3x()
elif self.ploneVersion == 'plone4': elif self.ploneVersion == 'plone4':
@ -234,7 +238,7 @@ class NewScript:
j(self.instancePath, 'lib/python')) j(self.instancePath, 'lib/python'))
print cmd print cmd
os.system(cmd) os.system(cmd)
self.patchPlone4() self.patchPlone4(versions)
# Remove .bat files under Linux # Remove .bat files under Linux
if os.name == 'posix': if os.name == 'posix':
cleanFolder(j(self.instancePath, 'bin'), exts=('.bat',)) cleanFolder(j(self.instancePath, 'bin'), exts=('.bat',))

181
bin/publish.py Executable file → Normal file
View file

@ -1,6 +1,6 @@
#!/usr/bin/python2.4.4 #!/usr/bin/python
# Imports ---------------------------------------------------------------------- # Imports ----------------------------------------------------------------------
import os, os.path, shutil, re, zipfile, sys, ftplib, time import os, os.path, sys, shutil, re, zipfile, sys, ftplib, time
import appy import appy
from appy.shared import appyPath from appy.shared import appyPath
from appy.shared.utils import FolderDeleter, LinesCounter from appy.shared.utils import FolderDeleter, LinesCounter
@ -9,17 +9,23 @@ from appy.gen.utils import produceNiceMessage
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
versionRex = re.compile('(\d+\.\d+\.\d+)') versionRex = re.compile('(\d+\.\d+\.\d+)')
eggInfo = '''import os, setuptools distInfo = '''from distutils.core import setup
setuptools.setup( setup(name = "appy", version = "%s",
name = "appy", version = "%s", description = "The Appy framework", description = "The Appy framework",
long_description = "See http://appyframework.org", long_description = "Appy builds simple but complex web Python apps.",
author = "Gaetan Delannay", author_email = "gaetan.delannay AT gmail.com", author = "Gaetan Delannay",
license = "GPL", keywords = "plone, pod, pdf, odt, document", author_email = "gaetan.delannay AT geezteem.com",
url = 'http://appyframework.org', license = "GPL", platforms="all",
classifiers = ['Development Status :: 4 - Beta', "License :: OSI Approved"], url = 'http://appyframework.org',
packages = setuptools.find_packages('src'), include_package_data = True, packages = [%s],
package_dir = {'':'src'}, data_files = [('.', [])], package_data = {'':["*.*"]})
namespace_packages = ['appy'], zip_safe = False)''' '''
manifestInfo = '''
recursive-include appy/bin *
recursive-include appy/gen *
recursive-include appy/pod *
recursive-include appy/shared *
'''
def askLogin(): def askLogin():
print 'Login: ', print 'Login: ',
@ -28,29 +34,6 @@ def askLogin():
passwd = sys.stdin.readline().strip() passwd = sys.stdin.readline().strip()
return (login, passwd) 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: class FtpFolder:
'''Represents a folder on a FTP site.''' '''Represents a folder on a FTP site.'''
def __init__(self, name): def __init__(self, name):
@ -247,52 +230,95 @@ class Publisher:
self.versionLong = '%s (%s)' % (self.versionShort, self.versionLong = '%s (%s)' % (self.versionShort,
time.strftime('%Y/%m/%d %H:%M')) time.strftime('%Y/%m/%d %H:%M'))
f.close() 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): def executeCommand(self, cmd):
'''Executes the system command p_cmd.''' '''Executes the system command p_cmd.'''
print 'Executing %s...' % cmd print 'Executing %s...' % cmd
os.system(cmd) os.system(cmd)
def createCodeAndEggReleases(self): distExcluded = ('appy/doc', 'appy/temp', 'appy/versions', 'appy/gen/test')
'''Publishes the egg on pypi.python.org.''' 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() curdir = os.getcwd()
if askQuestion('Upload eggs on PyPI?', default='no'): distFolder = '%s/dist' % self.genFolder
# Create egg structure # Create setup.py
eggFolder = '%s/egg' % self.genFolder os.mkdir(distFolder)
os.mkdir(eggFolder) f = file('%s/setup.py' % distFolder, 'w')
f = file('%s/setup.py' % eggFolder, 'w') # List all packages to include
f.write(eggInfo % self.versionShort) packages = []
f.close() os.chdir(os.path.dirname(appyPath))
os.mkdir('%s/docs' % eggFolder) for dir, dirnames, filenames in os.walk('appy'):
os.mkdir('%s/src' % eggFolder) if self.isDistExcluded(dir): continue
os.mkdir('%s/src/appy' % eggFolder) packageName = dir.replace('/', '.')
shutil.copy('%s/doc/version.txt' % appyPath, packages.append('"%s"' % packageName)
'%s/docs/HISTORY.txt' % eggFolder) f.write(distInfo % (self.versionShort, ','.join(packages)))
shutil.copy('%s/doc/license.txt' % appyPath, f.close()
'%s/docs/LICENSE.txt' % eggFolder) # Create MANIFEST.in
# Move appy sources within the egg f = file('%s/MANIFEST.in' % distFolder, 'w')
os.rename('%s/appy' % self.genFolder, '%s/src/appy' % eggFolder) f.write(manifestInfo)
# Create eggs and publish them on pypi f.close()
os.chdir(eggFolder) # Move appy sources within the dist folder
print 'Uploading appy%s source egg on PyPI...' % self.versionShort os.rename('%s/appy' % self.genFolder, '%s/appy' % distFolder)
#self.executeCommand('python setup.py sdist upload') # Create the source distribution
self.executeCommand('python setup.py sdist') os.chdir(distFolder)
for pythonTarget in self.pythonTargets: self.executeCommand('python setup.py sdist')
print 'Uploading appy%s binary egg for python%s...' % \ # DistUtils has created the .tar.gz file. Copy it into folder "versions"
(self.versionShort, pythonTarget) name = 'appy-%s.tar.gz' % self.versionShort
#self.executeCommand('python%s setup.py bdist_egg upload' % \ os.rename('%s/dist/%s' % (distFolder, name),
# pythonTarget) '%s/versions/%s' % (appyPath, name))
self.executeCommand('python%s setup.py bdist_egg' % \ # Clean temp files
pythonTarget)
os.chdir(curdir) 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): def createZipRelease(self):
'''Creates a zip file with the appy sources.''' '''Creates a zip file with the appy sources.'''
newZipRelease = '%s/versions/appy%s.zip' % (appyPath, self.versionShort) newZipRelease = '%s/versions/appy%s.zip' % (appyPath, self.versionShort)
if os.path.exists(newZipRelease): if os.path.exists(newZipRelease):
if not askQuestion('"%s" already exists. Replace it?' % \ if not self.askQuestion('"%s" already exists. Replace it?' % \
newZipRelease, default='yes'): newZipRelease, default='yes'):
print 'Publication cancelled.' print 'Publication canceled.'
sys.exit(1) sys.exit(1)
print 'Removing obsolete %s...' % newZipRelease print 'Removing obsolete %s...' % newZipRelease
os.remove(newZipRelease) os.remove(newZipRelease)
@ -306,8 +332,6 @@ class Publisher:
# [2:] is there to avoid havin './' in the path in the zip file. # [2:] is there to avoid havin './' in the path in the zip file.
zipFile.close() zipFile.close()
os.chdir(curdir) os.chdir(curdir)
# Remove the "appy" folder within the gen folder.
FolderDeleter.delete(os.path.join(self.genFolder, 'appy'))
def applyTemplate(self): def applyTemplate(self):
'''Decorates each page with the template.''' '''Decorates each page with the template.'''
@ -405,7 +429,7 @@ class Publisher:
# Create a temp clean copy of appy sources (without .svn folders, etc) # Create a temp clean copy of appy sources (without .svn folders, etc)
genSrcFolder = '%s/appy' % self.genFolder genSrcFolder = '%s/appy' % self.genFolder
os.mkdir(genSrcFolder) os.mkdir(genSrcFolder)
for aFile in ('__init__.py', 'install.txt'): for aFile in ('__init__.py',):
shutil.copy('%s/%s' % (appyPath, aFile), genSrcFolder) shutil.copy('%s/%s' % (appyPath, aFile), genSrcFolder)
for aFolder in ('gen', 'pod', 'shared', 'bin'): for aFolder in ('gen', 'pod', 'shared', 'bin'):
shutil.copytree('%s/%s' % (appyPath, aFolder), shutil.copytree('%s/%s' % (appyPath, aFolder),
@ -438,17 +462,18 @@ class Publisher:
# Perform a small analysis on the Appy code # Perform a small analysis on the Appy code
LinesCounter(appy).run() LinesCounter(appy).run()
print 'Generating site in %s...' % self.genFolder print 'Generating site in %s...' % self.genFolder
minimalist = askQuestion('Minimalist (shipped without tests)?', minimalist = self.askQuestion('Minimalist (shipped without tests)?',
default='no') default='no')
self.prepareGenFolder(minimalist) self.prepareGenFolder(minimalist)
self.createDocToc() self.createDocToc()
self.applyTemplate() self.applyTemplate()
self.createZipRelease() self.createZipRelease()
#self.createCodeAndEggReleases() tarball = self.createDistRelease()
if askQuestion('Do you want to publish the site on ' \ if self.askQuestion('Upload %s on PyPI?' % tarball, default='no'):
'appyframework.org?', default='no'): self.uploadOnPypi(tarball)
if self.askQuestion('Publish on appyframework.org?', default='no'):
AppySite().publish() AppySite().publish()
if askQuestion('Delete locally generated site ?', default='no'): if self.askQuestion('Delete locally generated site ?', default='yes'):
FolderDeleter.delete(self.genFolder) FolderDeleter.delete(self.genFolder)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -5,12 +5,13 @@ from appy import Object
from appy.gen.layout import Table from appy.gen.layout import Table
from appy.gen.layout import defaultFieldLayouts from appy.gen.layout import defaultFieldLayouts
from appy.gen.po import PoMessage from appy.gen.po import PoMessage
from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \ from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, getClassName, \
getClassName, SomeObjects SomeObjects
import appy.pod import appy.pod
from appy.pod.renderer import Renderer from appy.pod.renderer import Renderer
from appy.shared.data import countries 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 ----------------------------------------------------- # Default Appy permissions -----------------------------------------------------
r, w, d = ('read', 'write', 'delete') r, w, d = ('read', 'write', 'delete')
@ -1505,7 +1506,7 @@ class File(Type):
def getRequestValue(self, request): def getRequestValue(self, request):
return request.get('%s_file' % self.name) 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): def isEmptyValue(self, value, obj=None):
'''Must p_value be considered as empty?''' '''Must p_value be considered as empty?'''
@ -1536,8 +1537,9 @@ class File(Type):
* an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In * an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In
this case, it is file content coming from a HTTP POST; this case, it is file content coming from a HTTP POST;
* an instance of Zope class OFS.Image.File; * an instance of Zope class OFS.Image.File;
* an instance of appy.gen.utils.FileWrapper, which wraps an instance * an instance of appy.shared.utils.FileWrapper, which wraps an
of OFS.Image.File and adds useful methods for manipulating it; 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 * a string. In this case, the string represents the path of a file
on disk; on disk;
* a 2-tuple (fileName, fileContent) where: * a 2-tuple (fileName, fileContent) where:

View file

@ -675,7 +675,7 @@ class ZopeGenerator(Generator):
repls['languages'] = ','.join('"%s"' % l for l in self.config.languages) repls['languages'] = ','.join('"%s"' % l for l in self.config.languages)
repls['languageSelector'] = self.config.languageSelector repls['languageSelector'] = self.config.languageSelector
repls['sourceLanguage'] = self.config.sourceLanguage repls['sourceLanguage'] = self.config.sourceLanguage
self.copyFile('config.py', repls) self.copyFile('config.pyt', repls, destName='config.py')
def generateInit(self): def generateInit(self):
# Compute imports # Compute imports
@ -690,7 +690,7 @@ class ZopeGenerator(Generator):
repls['imports'] = '\n'.join(imports) repls['imports'] = '\n'.join(imports)
repls['classes'] = ','.join(classNames) repls['classes'] = ','.join(classNames)
repls['totalNumberOfTests'] = self.totalNumberOfTests repls['totalNumberOfTests'] = self.totalNumberOfTests
self.copyFile('__init__.py', repls) self.copyFile('__init__.pyt', repls, destName='__init__.py')
def getClassesInOrder(self, allClasses): def getClassesInOrder(self, allClasses):
'''When generating wrappers, classes mut be dumped in order (else, it '''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'): for klass in self.getClasses(include='predefined'):
modelClass = klass.modelClass modelClass = klass.modelClass
repls['%s' % modelClass.__name__] = modelClass._appy_getBody() repls['%s' % modelClass.__name__] = modelClass._appy_getBody()
self.copyFile('wrappers.py', repls) self.copyFile('wrappers.pyt', repls, destName='wrappers.py')
def generateTests(self): def generateTests(self):
'''Generates the file needed for executing tests.''' '''Generates the file needed for executing tests.'''
@ -765,7 +765,8 @@ class ZopeGenerator(Generator):
modules = self.modulesWithTests modules = self.modulesWithTests
repls['imports'] = '\n'.join(['import %s' % m for m in modules]) repls['imports'] = '\n'.join(['import %s' % m for m in modules])
repls['modulesWithTests'] = ','.join(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): def generateTool(self):
'''Generates the tool that corresponds to this application.''' '''Generates the tool that corresponds to this application.'''
@ -790,7 +791,7 @@ class ZopeGenerator(Generator):
repls.update({'methods': klass.methods, 'genClassName': klass.name, repls.update({'methods': klass.methods, 'genClassName': klass.name,
'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem', 'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem',
'classDoc': 'Standard Appy class', 'icon':'object.gif'}) '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 # Before generating the Tool class, finalize it with query result
# columns, with fields to propagate, workflow-related fields. # columns, with fields to propagate, workflow-related fields.
@ -817,7 +818,7 @@ class ZopeGenerator(Generator):
'genClassName': self.tool.name, 'baseMixin':'ToolMixin', 'genClassName': self.tool.name, 'baseMixin':'ToolMixin',
'parents': 'ToolMixin, Folder', 'icon': 'folder.gif', 'parents': 'ToolMixin, Folder', 'icon': 'folder.gif',
'classDoc': 'Tool class for %s' % self.applicationName}) '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): def generateClass(self, classDescr):
'''Is called each time an Appy class is found in the application, for '''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: if poMsg not in self.labels:
self.labels.append(poMsg) self.labels.append(poMsg)
# Generate the resulting Zope class. # Generate the resulting Zope class.
self.copyFile('Class.py', repls, destName=fileName) self.copyFile('Class.pyt', repls, destName=fileName)
def generateWorkflow(self, wfDescr): def generateWorkflow(self, wfDescr):
'''This method creates the i18n labels related to the workflow described '''This method creates the i18n labels related to the workflow described

View file

@ -781,7 +781,7 @@ class ToolMixin(BaseMixin):
if brain: if brain:
sibling = brain.getObject() sibling = brain.getObject()
res[urlKey] = sibling.getUrl(nav=newNav % (index + 1), res[urlKey] = sibling.getUrl(nav=newNav % (index + 1),
page='main') page=self.REQUEST.get('page', 'main'))
return res return res
def tabularize(self, data, numberOfRows): def tabularize(self, data, numberOfRows):

View file

@ -1,7 +1,6 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import re, os, os.path, time import re, os, os.path
import appy.pod import appy.pod
from appy.shared.utils import getOsTempFolder, normalizeString, executeCommand
sequenceTypes = (list, tuple) sequenceTypes = (list, tuple)
# Function for creating a Zope object ------------------------------------------ # Function for creating a Zope object ------------------------------------------
@ -242,84 +241,6 @@ class Keywords:
return op.join(self.keywords)+'*' return op.join(self.keywords)+'*'
return '' 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): def getClassName(klass, appName=None):
'''Generates, from appy-class p_klass, the name of the corresponding '''Generates, from appy-class p_klass, the name of the corresponding

View file

@ -1,11 +0,0 @@
Installation under Windows or MacOS
-----------------------------------
Copy the content of this folder to
<where_you_installed_python>\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").

View file

@ -20,6 +20,7 @@
import os, os.path, time, shutil, struct, random import os, os.path, time, shutil, struct, random
from appy.pod import PodError from appy.pod import PodError
from appy.pod.odf_parser import OdfEnvironment 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." 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) self.importPath = self.moveFile(at, self.importPath)
else: else:
# We need to dump the file content (in self.content) in a temp file # 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): if isinstance(self.content, file):
fileContent = self.content.read() fileContent = self.content.read()
elif isinstance(self.content, FileWrapper):
fileContent = content.content
else: else:
fileContent = self.content fileContent = self.content
f = file(self.importPath, 'wb') f = file(self.importPath, 'wb')

View file

@ -26,6 +26,7 @@ from appy.pod import PodError
from appy.shared import mimeTypesExts from appy.shared import mimeTypesExts
from appy.shared.xml_parser import XmlElement from appy.shared.xml_parser import XmlElement
from appy.shared.utils import FolderDeleter, executeCommand 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.pod_parser import PodParser, PodEnvironment, OdInsert
from appy.pod.converter import FILE_TYPES from appy.pod.converter import FILE_TYPES
from appy.pod.buffers import FileBuffer from appy.pod.buffers import FileBuffer
@ -281,6 +282,8 @@ class Renderer:
if not content and not at: if not content and not at:
raise PodError(DOC_NOT_SPECIFIED) raise PodError(DOC_NOT_SPECIFIED)
# Guess document format # Guess document format
if isinstance(content, FileWrapper):
format = content.mimeType
if not format: if not format:
# It should be deduced from p_at # It should be deduced from p_at
if not at: if not at:

View file

@ -410,4 +410,82 @@ class LinesCounter:
elif ext in exts['pt']: elif ext in exts['pt']:
self.zpt[self.inTest].analyseFile(j(root, fileName)) self.zpt[self.inTest].analyseFile(j(root, fileName))
self.printReport() 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------