Added script eggify.py for wrapping a Python module into an egg, and plenty of minor improvements and refactorings.
This commit is contained in:
parent
aea19a819e
commit
52816ec343
|
@ -6,7 +6,7 @@ from appy.shared.utils import FolderDeleter, cleanFolder
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class Cleaner:
|
class Cleaner:
|
||||||
def run(self, verbose=True):
|
def run(self, verbose=True):
|
||||||
cleanFolder(appyPath, verbose=True)
|
cleanFolder(appyPath, verbose=verbose)
|
||||||
# Remove all files in temp folders
|
# Remove all files in temp folders
|
||||||
for tempFolder in ('%s/temp' % appyPath,
|
for tempFolder in ('%s/temp' % appyPath,
|
||||||
'%s/pod/test/temp' % appyPath):
|
'%s/pod/test/temp' % appyPath):
|
||||||
|
|
178
bin/eggify.py
Normal file
178
bin/eggify.py
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
'''This sript allows to wrap a Python module into an egg.'''
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
import os, os.path, sys, zipfile, appy
|
||||||
|
from appy.bin.clean import Cleaner
|
||||||
|
from appy.shared.utils import FolderDeleter, copyFolder
|
||||||
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class EggifierError(Exception): pass
|
||||||
|
ERROR_CODE = 1
|
||||||
|
eggInfo = '''from setuptools import setup, find_packages
|
||||||
|
import os
|
||||||
|
setup(name = "%s", version = "%s", description = "%s",
|
||||||
|
long_description = "%s",
|
||||||
|
author = "%s", author_email = "%s",
|
||||||
|
license = "GPL", keywords = "plone, appy", url = '%s',
|
||||||
|
classifiers = ["Framework :: Appy", "Programming Language :: Python",],
|
||||||
|
packages=find_packages(exclude=['ez_setup']), include_package_data = True,
|
||||||
|
namespace_packages=['%s'], zip_safe = False,
|
||||||
|
install_requires=['setuptools'],)'''
|
||||||
|
initInfo = '''
|
||||||
|
# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
|
||||||
|
try:
|
||||||
|
__import__('pkg_resources').declare_namespace(__name__)
|
||||||
|
except ImportError:
|
||||||
|
from pkgutil import extend_path
|
||||||
|
__path__ = extend_path(__path__, __name__)
|
||||||
|
'''
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class EggifyScript:
|
||||||
|
'''usage: python eggify.py pythonModule [options]
|
||||||
|
pythonModule is the path to a Python module or the name of a Python file.
|
||||||
|
|
||||||
|
Available options are:
|
||||||
|
-a --appy If specified, the Appy module (light version, without
|
||||||
|
test code) will be included in the egg.
|
||||||
|
-r --result The path where to create the egg (defaults to the
|
||||||
|
current working directory)
|
||||||
|
-p --products If specified, the module will be packaged in the
|
||||||
|
"Products" namespace.
|
||||||
|
-v --version Egg version. Defaults to 1.0.0.
|
||||||
|
'''
|
||||||
|
def createSetupFile(self, eggTempFolder):
|
||||||
|
'''Creates the setup.py file in the egg.'''
|
||||||
|
content = eggInfo % (self.moduleName, self.version, 'Appy module',
|
||||||
|
'Appy module', 'Gaetan Delannay',
|
||||||
|
'gaetan.delannay AT gmail.com',
|
||||||
|
'http://appyframework.org',
|
||||||
|
self.moduleName.split('.')[0])
|
||||||
|
f = file(os.path.join(eggTempFolder, 'setup.py'), 'w')
|
||||||
|
f.write(content)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def createInitFile(self, eggTempFolder):
|
||||||
|
'''Creates the ez_setup-compliant __init__ files.'''
|
||||||
|
initPath = os.path.join(eggTempFolder,self.moduleName.split('.')[0])
|
||||||
|
f = file(os.path.join(initPath, '__init__.py'), 'w')
|
||||||
|
f.write(initInfo)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def getEggName(self):
|
||||||
|
'''Creates the egg name.'''
|
||||||
|
return '%s-%s.egg' % (self.moduleName, self.version)
|
||||||
|
|
||||||
|
zipExclusions = ('.bzr', 'doc', 'test', 'versions')
|
||||||
|
def dirInZip(self, dir):
|
||||||
|
'''Returns True if the p_dir must be included in the zip.'''
|
||||||
|
for exclusion in self.zipExclusions:
|
||||||
|
if dir.endswith(exclusion) or ('/%s/' % exclusion in dir):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def zipResult(self, eggFullName, eggTempFolder):
|
||||||
|
'''Zips the result and removes the egg temp folder.'''
|
||||||
|
zipFile = zipfile.ZipFile(eggFullName, 'w', zipfile.ZIP_DEFLATED)
|
||||||
|
# Put the Python module inside the egg.
|
||||||
|
prefix = os.path.dirname(eggTempFolder)
|
||||||
|
for dir, dirnames, filenames in os.walk(eggTempFolder):
|
||||||
|
for f in filenames:
|
||||||
|
fileName = os.path.join(dir, f)
|
||||||
|
zipFile.write(fileName, fileName[len(prefix):])
|
||||||
|
# Put the Appy module inside it if required.
|
||||||
|
if self.includeAppy:
|
||||||
|
eggPrefix = '%s/%s' % (eggTempFolder[len(prefix):],
|
||||||
|
self.moduleName.replace('.', '/'))
|
||||||
|
# Where is Appy?
|
||||||
|
appyPath = os.path.dirname(appy.__file__)
|
||||||
|
appyPrefix = os.path.dirname(appyPath)
|
||||||
|
# Clean the Appy folder
|
||||||
|
Cleaner().run(verbose=False)
|
||||||
|
# Insert appy files into the zip
|
||||||
|
for dir, dirnames, filenames in os.walk(appyPath):
|
||||||
|
if not self.dirInZip(dir): continue
|
||||||
|
for f in filenames:
|
||||||
|
fileName = os.path.join(dir, f)
|
||||||
|
zipName = eggPrefix + fileName[len(appyPrefix):]
|
||||||
|
zipFile.write(fileName, zipName)
|
||||||
|
zipFile.close()
|
||||||
|
# Remove the temp egg folder.
|
||||||
|
FolderDeleter.delete(eggTempFolder)
|
||||||
|
|
||||||
|
def eggify(self):
|
||||||
|
'''Let's wrap a nice Python module into an ugly egg.'''
|
||||||
|
j = os.path.join
|
||||||
|
# Create the egg folder
|
||||||
|
eggFullName = j(self.eggFolder, self.eggName)
|
||||||
|
if os.path.exists(eggFullName):
|
||||||
|
os.remove(eggFullName)
|
||||||
|
print 'Existing "%s" was removed.' % eggFullName
|
||||||
|
# Create a temp folder where to store the egg
|
||||||
|
eggTempFolder = os.path.splitext(eggFullName)[0]
|
||||||
|
if os.path.exists(eggTempFolder):
|
||||||
|
FolderDeleter.delete(eggTempFolder)
|
||||||
|
print 'Removed "%s" that was in my way.' % eggTempFolder
|
||||||
|
os.mkdir(eggTempFolder)
|
||||||
|
# Create the "Products" sub-folder if we must wrap the package in this
|
||||||
|
# namespace
|
||||||
|
eggModulePath = j(j(eggTempFolder, self.moduleName.replace('.', '/')))
|
||||||
|
# Copy the Python module into the egg.
|
||||||
|
os.makedirs(eggModulePath)
|
||||||
|
copyFolder(self.pythonModule, eggModulePath)
|
||||||
|
# Create setup files in the root egg folder
|
||||||
|
self.createSetupFile(eggTempFolder)
|
||||||
|
self.createInitFile(eggTempFolder)
|
||||||
|
self.zipResult(eggFullName, eggTempFolder)
|
||||||
|
|
||||||
|
def checkArgs(self, options, args):
|
||||||
|
# Check that we have the correct number of args.
|
||||||
|
if len(args) != 1: raise EggifierError('Wrong number of arguments.')
|
||||||
|
# Check that the arg corresponds to an existing Python module
|
||||||
|
if not os.path.exists(args[0]):
|
||||||
|
raise EggifierError('Path "%s" does not correspond to an ' \
|
||||||
|
'existing Python package.' % args[0])
|
||||||
|
self.pythonModule = args[0]
|
||||||
|
# At present I only manage Python modules, not 1-file Python packages.
|
||||||
|
if not os.path.isdir(self.pythonModule):
|
||||||
|
raise EggifierError('"%s" is not a folder. One-file Python ' \
|
||||||
|
'packages are not supported yet.' % args[0])
|
||||||
|
self.eggFolder = options.result
|
||||||
|
if not os.path.exists(self.eggFolder):
|
||||||
|
raise EggifierError('"%s" does not exist. Please create this ' \
|
||||||
|
'folder first.' % self.eggFolder)
|
||||||
|
self.includeAppy = options.appy
|
||||||
|
self.inProducts = options.products
|
||||||
|
self.version = options.version
|
||||||
|
self.moduleName = os.path.basename(self.pythonModule)
|
||||||
|
if self.inProducts:
|
||||||
|
self.moduleName = 'Products.' + self.moduleName
|
||||||
|
self.eggName = self.getEggName()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
optParser = OptionParser(usage=EggifyScript.__doc__)
|
||||||
|
optParser.add_option("-r", "--result", dest="result",
|
||||||
|
help="The folder where to create the egg",
|
||||||
|
default=os.getcwd(), metavar="RESULT",
|
||||||
|
type='string')
|
||||||
|
optParser.add_option("-a", "--appy", action="store_true",
|
||||||
|
help="Includes the Appy module in the egg")
|
||||||
|
optParser.add_option("-p", "--products", action="store_true",
|
||||||
|
help="Includes the module in the 'Products' " \
|
||||||
|
"namespace")
|
||||||
|
optParser.add_option("-v", "--version", dest="version",
|
||||||
|
help="The module version", default='1.0.0',
|
||||||
|
metavar="VERSION", type='string')
|
||||||
|
options, args = optParser.parse_args()
|
||||||
|
try:
|
||||||
|
self.checkArgs(options, args)
|
||||||
|
self.eggify()
|
||||||
|
except EggifierError, ee:
|
||||||
|
sys.stderr.write(str(ee) + '\nRun eggify.py -h for getting help.\n')
|
||||||
|
sys.exit(ERROR_CODE)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
EggifyScript().run()
|
||||||
|
# ------------------------------------------------------------------------------
|
|
@ -395,7 +395,7 @@ class Publisher:
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
privateScripts = ('publish.py', 'zip.py', 'runOpenOffice.sh')
|
privateScripts = ('publish.py', 'zip.py', 'runOpenOffice.sh')
|
||||||
def prepareGenFolder(self):
|
def prepareGenFolder(self, minimalist=False):
|
||||||
'''Creates the basic structure of the temp folder where the appy
|
'''Creates the basic structure of the temp folder where the appy
|
||||||
website will be generated.'''
|
website will be generated.'''
|
||||||
# Reinitialise temp folder where the generated website will be dumped
|
# Reinitialise temp folder where the generated website will be dumped
|
||||||
|
@ -413,6 +413,8 @@ class Publisher:
|
||||||
# Remove some scripts from bin
|
# Remove some scripts from bin
|
||||||
for script in self.privateScripts:
|
for script in self.privateScripts:
|
||||||
os.remove('%s/bin/%s' % (genSrcFolder, script))
|
os.remove('%s/bin/%s' % (genSrcFolder, script))
|
||||||
|
if minimalist:
|
||||||
|
FolderDeleter.delete('%s/pod/test' % genSrcFolder)
|
||||||
# Write the appy version into the code itself (in appy/version.py)'''
|
# Write the appy version into the code itself (in appy/version.py)'''
|
||||||
print 'Publishing version %s...' % self.versionShort
|
print 'Publishing version %s...' % self.versionShort
|
||||||
# Dump version info in appy/version.py
|
# Dump version info in appy/version.py
|
||||||
|
@ -436,7 +438,9 @@ 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
|
||||||
self.prepareGenFolder()
|
minimalist = askQuestion('Minimalist (shipped without tests)?',
|
||||||
|
default='no')
|
||||||
|
self.prepareGenFolder(minimalist)
|
||||||
self.createDocToc()
|
self.createDocToc()
|
||||||
self.applyTemplate()
|
self.applyTemplate()
|
||||||
self.createZipRelease()
|
self.createZipRelease()
|
||||||
|
|
143
gen/__init__.py
143
gen/__init__.py
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import re, time, copy, sys, types, os, os.path
|
import re, time, copy, sys, types, os, os.path, mimetypes
|
||||||
from appy.shared.utils import Traceback
|
from appy.shared.utils import Traceback
|
||||||
from appy.gen.layout import Table
|
from appy.gen.layout import Table
|
||||||
from appy.gen.layout import defaultFieldLayouts
|
from appy.gen.layout import defaultFieldLayouts
|
||||||
|
@ -1203,7 +1203,7 @@ class String(Type):
|
||||||
def store(self, obj, value):
|
def store(self, obj, value):
|
||||||
if self.isMultiValued() and isinstance(value, basestring):
|
if self.isMultiValued() and isinstance(value, basestring):
|
||||||
value = [value]
|
value = [value]
|
||||||
setattr(obj, self.name, value)
|
exec 'obj.%s = value' % self.name
|
||||||
|
|
||||||
class Boolean(Type):
|
class Boolean(Type):
|
||||||
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
||||||
|
@ -1352,31 +1352,80 @@ class File(Type):
|
||||||
|
|
||||||
defaultMimeType = 'application/octet-stream'
|
defaultMimeType = 'application/octet-stream'
|
||||||
def store(self, obj, value):
|
def store(self, obj, value):
|
||||||
'''Stores the p_value (produced by m_getStorableValue) that complies to
|
'''Stores the p_value that represents some file. p_value can be:
|
||||||
p_self type definition on p_obj.'''
|
* 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;
|
||||||
|
* a string. In this case, the string represents the path of a file
|
||||||
|
on disk;
|
||||||
|
* a 2-tuple (fileName, fileContent) where:
|
||||||
|
- fileName is the name of the file (ie "myFile.odt")
|
||||||
|
- fileContent is the binary or textual content of the file or an
|
||||||
|
open file handler.
|
||||||
|
* a 3-tuple (fileName, fileContent, mimeType) where
|
||||||
|
- fileName and fileContent have the same meaning than above;
|
||||||
|
- mimeType is the MIME type of the file.
|
||||||
|
'''
|
||||||
if value:
|
if value:
|
||||||
# Retrieve the existing value, or create one if None
|
ZFileUpload = self.o.getProductConfig().FileUpload
|
||||||
existingValue = getattr(obj, self.name, None)
|
OFSImageFile = self.o.getProductConfig().File
|
||||||
if not existingValue:
|
if isinstance(value, ZFileUpload):
|
||||||
import OFS.Image
|
# The file content comes from a HTTP POST.
|
||||||
existingValue = OFS.Image.File(self.name, '', '')
|
# Retrieve the existing value, or create one if None
|
||||||
# Set mimetype
|
existingValue = getattr(obj, self.name, None)
|
||||||
if value.headers.has_key('content-type'):
|
if not existingValue:
|
||||||
mimeType = value.headers['content-type']
|
existingValue = OFSImageFile(self.name, '', '')
|
||||||
else:
|
# Set mimetype
|
||||||
mimeType = File.defaultMimeType
|
if value.headers.has_key('content-type'):
|
||||||
existingValue.content_type = mimeType
|
mimeType = value.headers['content-type']
|
||||||
# Set filename
|
else:
|
||||||
fileName = value.filename
|
mimeType = File.defaultMimeType
|
||||||
filename = fileName[max(fileName.rfind('/'), fileName.rfind('\\'),
|
existingValue.content_type = mimeType
|
||||||
fileName.rfind(':'))+1:]
|
# Set filename
|
||||||
existingValue.filename = fileName
|
fileName = value.filename
|
||||||
# Set content
|
filename= fileName[max(fileName.rfind('/'),fileName.rfind('\\'),
|
||||||
existingValue.manage_upload(value)
|
fileName.rfind(':'))+1:]
|
||||||
setattr(obj, self.name, existingValue)
|
existingValue.filename = fileName
|
||||||
|
# Set content
|
||||||
|
existingValue.manage_upload(value)
|
||||||
|
setattr(obj, self.name, existingValue)
|
||||||
|
elif isinstance(value, OFSImageFile):
|
||||||
|
setattr(obj, self.name, value)
|
||||||
|
elif isinstance(value, FileWrapper):
|
||||||
|
setattr(obj, self.name, value._atFile)
|
||||||
|
elif isinstance(value, basestring):
|
||||||
|
f = file(value)
|
||||||
|
fileName = os.path.basename(value)
|
||||||
|
fileId = 'file.%f' % time.time()
|
||||||
|
zopeFile = OFSImageFile(fileId, fileName, f)
|
||||||
|
zopeFile.filename = fileName
|
||||||
|
zopeFile.content_type = mimetypes.guess_type(fileName)[0]
|
||||||
|
setattr(obj, self.name, zopeFile)
|
||||||
|
f.close()
|
||||||
|
elif type(value) in sequenceTypes:
|
||||||
|
# It should be a 2-tuple or 3-tuple
|
||||||
|
fileName = None
|
||||||
|
mimeType = None
|
||||||
|
if len(value) == 2:
|
||||||
|
fileName, fileContent = value
|
||||||
|
elif len(value) == 3:
|
||||||
|
fileName, fileContent, mimeType = value
|
||||||
|
else:
|
||||||
|
raise WRONG_FILE_TUPLE
|
||||||
|
if fileName:
|
||||||
|
fileId = 'file.%f' % time.time()
|
||||||
|
zopeFile = OFSImageFile(fileId, fileName, fileContent)
|
||||||
|
zopeFile.filename = fileName
|
||||||
|
if not mimeType:
|
||||||
|
mimeType = mimetypes.guess_type(fileName)[0]
|
||||||
|
zopeFile.content_type = mimeType
|
||||||
|
setattr(obj, self.name, zopeFile)
|
||||||
else:
|
else:
|
||||||
# What must I do: delete the existing file or keep it ?
|
# I store value "None", excepted if I find in the request the desire
|
||||||
action = obj.REQUEST.get('%s_delete' % self.name)
|
# to keep the file unchanged.
|
||||||
|
action = obj.REQUEST.get('%s_delete' % self.name, None)
|
||||||
if action == 'nochange': pass
|
if action == 'nochange': pass
|
||||||
else: setattr(obj, self.name, None)
|
else: setattr(obj, self.name, None)
|
||||||
|
|
||||||
|
@ -1542,21 +1591,30 @@ class Ref(Type):
|
||||||
return obj.translate('max_ref_violated')
|
return obj.translate('max_ref_violated')
|
||||||
|
|
||||||
def store(self, obj, value):
|
def store(self, obj, value):
|
||||||
'''Stores on p_obj, the p_value, which can be None, an object UID or a
|
'''Stores on p_obj, the p_value, which can be:
|
||||||
list of UIDs coming from the request. This method is only called for
|
* None;
|
||||||
Ref fields with link=True.'''
|
* an object UID (=string);
|
||||||
# Security check
|
* a list of object UIDs (=list of strings). Generally, UIDs or lists
|
||||||
if not self.isShowable(obj, 'edit'): return
|
of UIDs come from Ref fields with link:True edited through the web;
|
||||||
|
* a Zope object;
|
||||||
|
* a Appy object;
|
||||||
|
* a list of Appy or Zope objects.
|
||||||
|
'''
|
||||||
# Standardize the way p_value is expressed
|
# Standardize the way p_value is expressed
|
||||||
uids = value
|
refs = value
|
||||||
if not value: uids = []
|
if not refs: refs = []
|
||||||
if isinstance(value, basestring): uids = [value]
|
if type(refs) not in sequenceTypes: refs = [refs]
|
||||||
|
for i in range(len(refs)):
|
||||||
|
if isinstance(refs[i], basestring):
|
||||||
|
# Get the Zope object from the UID
|
||||||
|
refs[i] = obj.uid_catalog(UID=refs[i])[0].getObject()
|
||||||
|
else:
|
||||||
|
refs[i] = refs[i].o # Now we are sure to have a Zope object.
|
||||||
# Update the field storing on p_obj the ordered list of UIDs
|
# Update the field storing on p_obj the ordered list of UIDs
|
||||||
sortedRefs = obj._appy_getSortedField(self.name)
|
sortedRefs = obj._appy_getSortedField(self.name)
|
||||||
del sortedRefs[:]
|
del sortedRefs[:]
|
||||||
for uid in uids: sortedRefs.append(uid)
|
for ref in refs: sortedRefs.append(ref.UID())
|
||||||
# Update the refs
|
# Update the Archetypes Ref field.
|
||||||
refs = [obj.uid_catalog(UID=uid)[0].getObject() for uid in uids]
|
|
||||||
exec 'obj.set%s%s(refs)' % (self.name[0].upper(), self.name[1:])
|
exec 'obj.set%s%s(refs)' % (self.name[0].upper(), self.name[1:])
|
||||||
|
|
||||||
def clone(self, forTool=True):
|
def clone(self, forTool=True):
|
||||||
|
@ -1618,11 +1676,14 @@ class Action(Type):
|
||||||
master=None, masterValue=None, focus=False, historized=False):
|
master=None, masterValue=None, focus=False, historized=False):
|
||||||
# Can be a single method or a list/tuple of methods
|
# Can be a single method or a list/tuple of methods
|
||||||
self.action = action
|
self.action = action
|
||||||
# For the following field, value 'computation' means that the action
|
# For the following field:
|
||||||
# will simply compute things and redirect the user to the same page,
|
# * value 'computation' means that the action will simply compute
|
||||||
# with some status message about execution of the action. 'file' means
|
# things and redirect the user to the same page, with some status
|
||||||
# that the result is the binary content of a file that the user will
|
# message about execution of the action;
|
||||||
# download.
|
# * 'file' means that the result is the binary content of a file that
|
||||||
|
# the user will download.
|
||||||
|
# * 'redirect' means that the action will lead to the user being
|
||||||
|
# redirected to some other page.
|
||||||
self.result = result
|
self.result = result
|
||||||
# If following field "confirm" is True, a popup will ask the user if
|
# If following field "confirm" is True, a popup will ask the user if
|
||||||
# she is really sure about triggering this action.
|
# she is really sure about triggering this action.
|
||||||
|
|
|
@ -452,6 +452,16 @@ class ToolMixin(BaseMixin):
|
||||||
return pythonClass.maySearch(self.appy())
|
return pythonClass.maySearch(self.appy())
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def userMayNavigate(self, someClass):
|
||||||
|
'''This method checks if the currently logged user can display the
|
||||||
|
navigation panel within the portlet. This is done by calling method
|
||||||
|
"mayNavigate" on the class whose currently shown object is an
|
||||||
|
instance of. If no such method exists, we return True.'''
|
||||||
|
pythonClass = self.getAppyClass(someClass)
|
||||||
|
if 'mayNavigate' in pythonClass.__dict__:
|
||||||
|
return pythonClass.mayNavigate(self.appy())
|
||||||
|
return True
|
||||||
|
|
||||||
def onImportObjects(self):
|
def onImportObjects(self):
|
||||||
'''This method is called when the user wants to create objects from
|
'''This method is called when the user wants to create objects from
|
||||||
external data.'''
|
external data.'''
|
||||||
|
|
|
@ -77,6 +77,10 @@ class BaseMixin:
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
'''This methods is self's suicide.'''
|
'''This methods is self's suicide.'''
|
||||||
|
# Call a custom "onDelete" if it exists
|
||||||
|
appyObj = self.appy()
|
||||||
|
if hasattr(appyObj, 'onDelete'): appyObj.onDelete()
|
||||||
|
# Delete the object
|
||||||
self.getParentNode().manage_delObjects([self.id])
|
self.getParentNode().manage_delObjects([self.id])
|
||||||
|
|
||||||
def onDelete(self):
|
def onDelete(self):
|
||||||
|
@ -245,8 +249,11 @@ class BaseMixin:
|
||||||
return self.goto(obj.getUrl())
|
return self.goto(obj.getUrl())
|
||||||
if rq.get('buttonNext.x', None):
|
if rq.get('buttonNext.x', None):
|
||||||
# Go to the next page for this object
|
# Go to the next page for this object
|
||||||
|
# We remember page name, because the next method may set a new
|
||||||
|
# current page if the current one is not visible anymore.
|
||||||
|
pageName = rq['page']
|
||||||
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
||||||
pageName, pageInfo = self.getNextPage(phaseInfo, rq['page'])
|
pageName, pageInfo = self.getNextPage(phaseInfo, pageName)
|
||||||
if pageName:
|
if pageName:
|
||||||
# Return to the edit or view page?
|
# Return to the edit or view page?
|
||||||
if pageInfo['showOnEdit']:
|
if pageInfo['showOnEdit']:
|
||||||
|
@ -576,7 +583,13 @@ class BaseMixin:
|
||||||
|
|
||||||
def getPreviousPage(self, phase, page):
|
def getPreviousPage(self, phase, page):
|
||||||
'''Returns the page that precedes p_page which is in p_phase.'''
|
'''Returns the page that precedes p_page which is in p_phase.'''
|
||||||
pageIndex = phase['pages'].index(page)
|
try:
|
||||||
|
pageIndex = phase['pages'].index(page)
|
||||||
|
except ValueError:
|
||||||
|
# The current page is probably not visible anymore. Return the
|
||||||
|
# first available page in current phase.
|
||||||
|
res = phase['pages'][0]
|
||||||
|
return res, phase['pagesInfo'][res]
|
||||||
if pageIndex > 0:
|
if pageIndex > 0:
|
||||||
# We stay on the same phase, previous page
|
# We stay on the same phase, previous page
|
||||||
res = phase['pages'][pageIndex-1]
|
res = phase['pages'][pageIndex-1]
|
||||||
|
@ -594,7 +607,13 @@ class BaseMixin:
|
||||||
|
|
||||||
def getNextPage(self, phase, page):
|
def getNextPage(self, phase, page):
|
||||||
'''Returns the page that follows p_page which is in p_phase.'''
|
'''Returns the page that follows p_page which is in p_phase.'''
|
||||||
pageIndex = phase['pages'].index(page)
|
try:
|
||||||
|
pageIndex = phase['pages'].index(page)
|
||||||
|
except ValueError:
|
||||||
|
# The current page is probably not visible anymore. Return the
|
||||||
|
# first available page in current phase.
|
||||||
|
res = phase['pages'][0]
|
||||||
|
return res, phase['pagesInfo'][res]
|
||||||
if pageIndex < len(phase['pages'])-1:
|
if pageIndex < len(phase['pages'])-1:
|
||||||
# We stay on the same phase, next page
|
# We stay on the same phase, next page
|
||||||
res = phase['pages'][pageIndex+1]
|
res = phase['pages'][pageIndex+1]
|
||||||
|
@ -735,7 +754,9 @@ class BaseMixin:
|
||||||
'''Executes action with p_fieldName on this object.'''
|
'''Executes action with p_fieldName on this object.'''
|
||||||
appyType = self.getAppyType(actionName)
|
appyType = self.getAppyType(actionName)
|
||||||
actionRes = appyType(self.appy())
|
actionRes = appyType(self.appy())
|
||||||
self.reindexObject()
|
if self.getParentNode().get(self.id):
|
||||||
|
# Else, it means that the action has led to self's removal.
|
||||||
|
self.reindexObject()
|
||||||
return appyType.result, actionRes
|
return appyType.result, actionRes
|
||||||
|
|
||||||
def onExecuteAppyAction(self):
|
def onExecuteAppyAction(self):
|
||||||
|
@ -755,13 +776,17 @@ class BaseMixin:
|
||||||
if (resultType == 'computation') or not successfull:
|
if (resultType == 'computation') or not successfull:
|
||||||
self.plone_utils.addPortalMessage(msg)
|
self.plone_utils.addPortalMessage(msg)
|
||||||
return self.goto(self.getUrl(rq['HTTP_REFERER']))
|
return self.goto(self.getUrl(rq['HTTP_REFERER']))
|
||||||
else:
|
elif resultType == 'file':
|
||||||
# msg does not contain a message, but a complete file to show as is.
|
# msg does not contain a message, but a complete file to show as is.
|
||||||
# (or, if your prefer, the message must be shown directly to the
|
# (or, if your prefer, the message must be shown directly to the
|
||||||
# user, not encapsulated in a Plone page).
|
# user, not encapsulated in a Plone page).
|
||||||
res = self.getProductConfig().File(msg.name, msg.name, msg,
|
res = self.getProductConfig().File(msg.name, msg.name, msg,
|
||||||
content_type=mimetypes.guess_type(msg.name)[0])
|
content_type=mimetypes.guess_type(msg.name)[0])
|
||||||
return res.index_html(rq, rq.RESPONSE)
|
return res.index_html(rq, rq.RESPONSE)
|
||||||
|
elif resultType == 'redirect':
|
||||||
|
# msg does not contain a message, but the URL where to redirect
|
||||||
|
# the user.
|
||||||
|
return self.goto(msg)
|
||||||
|
|
||||||
def onTriggerTransition(self):
|
def onTriggerTransition(self):
|
||||||
'''This method is called whenever a user wants to trigger a workflow
|
'''This method is called whenever a user wants to trigger a workflow
|
||||||
|
|
|
@ -28,11 +28,8 @@
|
||||||
</td>
|
</td>
|
||||||
</dt>
|
</dt>
|
||||||
|
|
||||||
<tal:publishedObject condition="python: contextObj">
|
<tal:publishedObject condition="python: contextObj and tool.userMayNavigate(contextObj.meta_type)">
|
||||||
<dt class="portletAppyItem portletCurrent">
|
<dt class="portletAppyItem portletCurrent"><b tal:content="contextObj/Title"></b></dt>
|
||||||
<a tal:attributes="href python: contextObj.getUrl(page='main')"
|
|
||||||
tal:content="contextObj/Title"></a>
|
|
||||||
</dt>
|
|
||||||
<dt class="portletAppyItem"><metal:phases use-macro="here/skyn/portlet/macros/phases"/></dt>
|
<dt class="portletAppyItem"><metal:phases use-macro="here/skyn/portlet/macros/phases"/></dt>
|
||||||
</tal:publishedObject>
|
</tal:publishedObject>
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CustomizationPolicy = None
|
CustomizationPolicy = None
|
||||||
from OFS.Image import File
|
from OFS.Image import File
|
||||||
|
from ZPublisher.HTTPRequest import FileUpload
|
||||||
from DateTime import DateTime
|
from DateTime import DateTime
|
||||||
from Products.CMFCore import utils as cmfutils
|
from Products.CMFCore import utils as cmfutils
|
||||||
from Products.CMFCore.utils import getToolByName
|
from Products.CMFCore.utils import getToolByName
|
||||||
|
|
|
@ -100,4 +100,13 @@ class UserWrapper(AbstractWrapper):
|
||||||
if groupName in userGroups:
|
if groupName in userGroups:
|
||||||
group.removeMember(self.login)
|
group.removeMember(self.login)
|
||||||
self._callCustom('onEdit', created)
|
self._callCustom('onEdit', created)
|
||||||
|
|
||||||
|
def onDelete(self):
|
||||||
|
'''Before deleting myself, I must delete the corresponding Plone
|
||||||
|
user.'''
|
||||||
|
# Delete the corresponding Plone user
|
||||||
|
self.o.acl_users._doDelUser(self.login)
|
||||||
|
self.log('Plone user "%s" deleted.' % self.login)
|
||||||
|
# Call a custom "onDelete" if any.
|
||||||
|
self._callCustom('onDelete')
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import os, os.path, time, mimetypes, random
|
import os, os.path, time, mimetypes, random
|
||||||
import appy.pod
|
import appy.pod
|
||||||
from appy.gen import Search
|
from appy.gen import Search
|
||||||
from appy.gen.utils import sequenceTypes, FileWrapper
|
from appy.gen.utils import sequenceTypes
|
||||||
from appy.shared.utils import getOsTempFolder, executeCommand, normalizeString
|
from appy.shared.utils import getOsTempFolder, executeCommand, normalizeString
|
||||||
from appy.shared.xml_parser import XmlMarshaller
|
from appy.shared.xml_parser import XmlMarshaller
|
||||||
|
|
||||||
|
@ -18,97 +18,62 @@ WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
|
||||||
class AbstractWrapper:
|
class AbstractWrapper:
|
||||||
'''Any real web framework object has a companion object that is an instance
|
'''Any real web framework object has a companion object that is an instance
|
||||||
of this class.'''
|
of this class.'''
|
||||||
def __init__(self, o):
|
def __init__(self, o): self.__dict__['o'] = o
|
||||||
self.__dict__['o'] = o
|
|
||||||
def _set_file_attribute(self, name, v):
|
def __setattr__(self, name, value):
|
||||||
'''Updates the value of a file attribute named p_name with value p_v.
|
|
||||||
p_v may be:
|
|
||||||
- a string value containing the path to a file on disk;
|
|
||||||
- a 2-tuple (fileName, fileContent) where
|
|
||||||
* fileName = the name of the file (ie "myFile.odt")
|
|
||||||
* fileContent = the binary or textual content of the file or an
|
|
||||||
open file handler.
|
|
||||||
- a 3-tuple (fileName, fileContent, mimeType) where mimeType is the
|
|
||||||
v MIME type of the file.'''
|
|
||||||
ploneFileClass = self.o.getProductConfig().File
|
|
||||||
if isinstance(v, ploneFileClass):
|
|
||||||
setattr(self.o, name, v)
|
|
||||||
elif isinstance(v, FileWrapper):
|
|
||||||
setattr(self.o, name, v._atFile)
|
|
||||||
elif isinstance(v, basestring):
|
|
||||||
f = file(v)
|
|
||||||
fileName = os.path.basename(v)
|
|
||||||
fileId = 'file.%f' % time.time()
|
|
||||||
ploneFile = ploneFileClass(fileId, fileName, f)
|
|
||||||
ploneFile.filename = fileName
|
|
||||||
ploneFile.content_type = mimetypes.guess_type(fileName)[0]
|
|
||||||
setattr(self.o, name, ploneFile)
|
|
||||||
f.close()
|
|
||||||
elif type(v) in sequenceTypes:
|
|
||||||
# It should be a 2-tuple or 3-tuple
|
|
||||||
fileName = None
|
|
||||||
mimeType = None
|
|
||||||
if len(v) == 2:
|
|
||||||
fileName, fileContent = v
|
|
||||||
elif len(v) == 3:
|
|
||||||
fileName, fileContent, mimeType = v
|
|
||||||
else:
|
|
||||||
raise WRONG_FILE_TUPLE
|
|
||||||
if fileName:
|
|
||||||
fileId = 'file.%f' % time.time()
|
|
||||||
ploneFile = ploneFileClass(fileId, fileName, fileContent)
|
|
||||||
ploneFile.filename = fileName
|
|
||||||
if not mimeType:
|
|
||||||
mimeType = mimetypes.guess_type(fileName)[0]
|
|
||||||
ploneFile.content_type = mimeType
|
|
||||||
setattr(self.o, name, ploneFile)
|
|
||||||
def __setattr__(self, name, v):
|
|
||||||
if name == 'title':
|
|
||||||
self.o.setTitle(v)
|
|
||||||
return
|
|
||||||
appyType = self.o.getAppyType(name)
|
appyType = self.o.getAppyType(name)
|
||||||
if not appyType:
|
if not appyType:
|
||||||
raise 'Attribute "%s" does not exist.' % name
|
raise 'Attribute "%s" does not exist.' % name
|
||||||
if appyType.type == 'File':
|
appyType.store(self.o, value)
|
||||||
self._set_file_attribute(name, v)
|
|
||||||
elif appyType.type == 'Ref':
|
|
||||||
raise "Use methods 'link' or 'create' to modify references."
|
|
||||||
else:
|
|
||||||
setattr(self.o, name, v)
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s wrapper at %s>' % (self.klass.__name__, id(self))
|
return '<%s appyobj at %s>' % (self.klass.__name__, id(self))
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, other):
|
||||||
if other: return cmp(self.o, other.o)
|
if other: return cmp(self.o, other.o)
|
||||||
else: return 1
|
else: return 1
|
||||||
|
|
||||||
def get_tool(self): return self.o.getTool().appy()
|
def get_tool(self): return self.o.getTool().appy()
|
||||||
tool = property(get_tool)
|
tool = property(get_tool)
|
||||||
|
|
||||||
def get_request(self): return self.o.REQUEST
|
def get_request(self): return self.o.REQUEST
|
||||||
request = property(get_request)
|
request = property(get_request)
|
||||||
|
|
||||||
def get_session(self): return self.o.REQUEST.SESSION
|
def get_session(self): return self.o.REQUEST.SESSION
|
||||||
session = property(get_session)
|
session = property(get_session)
|
||||||
|
|
||||||
def get_typeName(self): return self.__class__.__bases__[-1].__name__
|
def get_typeName(self): return self.__class__.__bases__[-1].__name__
|
||||||
typeName = property(get_typeName)
|
typeName = property(get_typeName)
|
||||||
|
|
||||||
def get_id(self): return self.o.id
|
def get_id(self): return self.o.id
|
||||||
id = property(get_id)
|
id = property(get_id)
|
||||||
|
|
||||||
def get_uid(self): return self.o.UID()
|
def get_uid(self): return self.o.UID()
|
||||||
uid = property(get_uid)
|
uid = property(get_uid)
|
||||||
|
|
||||||
def get_state(self):
|
def get_state(self):
|
||||||
return self.o.portal_workflow.getInfoFor(self.o, 'review_state')
|
return self.o.portal_workflow.getInfoFor(self.o, 'review_state')
|
||||||
state = property(get_state)
|
state = property(get_state)
|
||||||
|
|
||||||
def get_stateLabel(self):
|
def get_stateLabel(self):
|
||||||
appName = self.o.getProductConfig().PROJECTNAME
|
appName = self.o.getProductConfig().PROJECTNAME
|
||||||
return self.o.utranslate(self.o.getWorkflowLabel(), domain=appName)
|
return self.o.translate(self.o.getWorkflowLabel(), domain=appName)
|
||||||
stateLabel = property(get_stateLabel)
|
stateLabel = property(get_stateLabel)
|
||||||
|
|
||||||
def get_klass(self): return self.__class__.__bases__[-1]
|
def get_klass(self): return self.__class__.__bases__[-1]
|
||||||
klass = property(get_klass)
|
klass = property(get_klass)
|
||||||
|
|
||||||
def get_url(self): return self.o.absolute_url()
|
def get_url(self): return self.o.absolute_url()
|
||||||
url = property(get_url)
|
url = property(get_url)
|
||||||
|
|
||||||
def get_history(self):
|
def get_history(self):
|
||||||
key = self.o.workflow_history.keys()[0]
|
key = self.o.workflow_history.keys()[0]
|
||||||
return self.o.workflow_history[key]
|
return self.o.workflow_history[key]
|
||||||
history = property(get_history)
|
history = property(get_history)
|
||||||
|
|
||||||
def get_user(self): return self.o.portal_membership.getAuthenticatedMember()
|
def get_user(self): return self.o.portal_membership.getAuthenticatedMember()
|
||||||
user = property(get_user)
|
user = property(get_user)
|
||||||
|
|
||||||
def get_fields(self): return self.o.getAllAppyTypes()
|
def get_fields(self): return self.o.getAllAppyTypes()
|
||||||
fields = property(get_fields)
|
fields = property(get_fields)
|
||||||
|
|
||||||
|
@ -210,6 +175,10 @@ class AbstractWrapper:
|
||||||
ploneObj.reindexObject()
|
ploneObj.reindexObject()
|
||||||
return appyObj
|
return appyObj
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
'''Deletes myself.'''
|
||||||
|
self.o.delete()
|
||||||
|
|
||||||
def translate(self, label, mapping={}, domain=None, language=None):
|
def translate(self, label, mapping={}, domain=None, language=None):
|
||||||
'''Check documentation of self.o.translate.'''
|
'''Check documentation of self.o.translate.'''
|
||||||
return self.o.translate(label, mapping, domain, language=language)
|
return self.o.translate(label, mapping, domain, language=language)
|
||||||
|
|
|
@ -486,11 +486,18 @@ class MemoryBuffer(Buffer):
|
||||||
else:
|
else:
|
||||||
res = self.getLength()
|
res = self.getLength()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
reTagContent = re.compile('<(?P<p>[\w-]+):(?P<f>[\w-]+)(.*?)>.*</(?P=p):' \
|
||||||
|
'(?P=f)>', re.S)
|
||||||
def evaluate(self, subElements=True, removeMainElems=False):
|
def evaluate(self, subElements=True, removeMainElems=False):
|
||||||
#print 'Evaluating buffer', self.content.encode('utf-8'), self.elements
|
|
||||||
result = self.getFileBuffer()
|
result = self.getFileBuffer()
|
||||||
if not subElements:
|
if not subElements:
|
||||||
result.write(self.content)
|
# Dump the root tag in this buffer, but not its content.
|
||||||
|
res = self.reTagContent.match(self.content.strip())
|
||||||
|
if not res: result.write(self.content)
|
||||||
|
else:
|
||||||
|
g = res.group
|
||||||
|
result.write('<%s:%s%s></%s:%s>' % (g(1),g(2),g(3),g(1),g(2)))
|
||||||
else:
|
else:
|
||||||
iter = BufferIterator(self)
|
iter = BufferIterator(self)
|
||||||
currentIndex = self.getStartIndex(removeMainElems)
|
currentIndex = self.getStartIndex(removeMainElems)
|
||||||
|
@ -505,8 +512,6 @@ class MemoryBuffer(Buffer):
|
||||||
PodError.dump(result, EVAL_EXPR_ERROR % (
|
PodError.dump(result, EVAL_EXPR_ERROR % (
|
||||||
evalEntry.expr, e), dumpTb=False)
|
evalEntry.expr, e), dumpTb=False)
|
||||||
else: # It is a subBuffer
|
else: # It is a subBuffer
|
||||||
#print '******Subbuffer*************'
|
|
||||||
# This is a bug.
|
|
||||||
if evalEntry.action:
|
if evalEntry.action:
|
||||||
evalEntry.action.execute()
|
evalEntry.action.execute()
|
||||||
else:
|
else:
|
||||||
|
|
3300
pod/test/Tests.rtf
3300
pod/test/Tests.rtf
File diff suppressed because it is too large
Load diff
17
pod/test/contexts/ForCell6.py
Normal file
17
pod/test/contexts/ForCell6.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
class Student:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for k, v in kwargs.iteritems():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
students = [
|
||||||
|
Student(parent_guardian='Parent 1', street='Street 1', city='Flawinne',
|
||||||
|
state='Namur', zip='5020', lname='Name 1', fname='First name 1'),
|
||||||
|
Student(parent_guardian='Parent 2', street='Street 2', city='Flawinne',
|
||||||
|
state='Namur', zip='5020', lname='Name 2', fname='First name 2'),
|
||||||
|
Student(parent_guardian='Parent 3', street='Street 3', city='Flawinne',
|
||||||
|
state='Namur', zip='5020', lname='Name 3', fname='First name 3'),
|
||||||
|
Student(parent_guardian='Parent 4', street='Street 4', city='Flawinne',
|
||||||
|
state='Namur', zip='5020', lname='Name 4', fname='First name 4'),
|
||||||
|
Student(parent_guardian='Parent 5', street='Street 5', city='Flawinne',
|
||||||
|
state='Namur', zip='5020', lname='Name 5', fname='First name 5'),
|
||||||
|
]
|
Binary file not shown.
BIN
pod/test/results/forCellBug.odt
Normal file
BIN
pod/test/results/forCellBug.odt
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
pod/test/templates/ForCell6.odt
Normal file
BIN
pod/test/templates/ForCell6.odt
Normal file
Binary file not shown.
Loading…
Reference in a new issue