Added a SAP connector and integrated the test system with coverage.py
This commit is contained in:
parent
b541ecb651
commit
01487db688
87
bin/asksap.py
Normal file
87
bin/asksap.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
'''This script allows to get information about a given SAP RFC function
|
||||||
|
module.'''
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
import sys, getpass
|
||||||
|
from optparse import OptionParser
|
||||||
|
from appy.shared.sap import Sap, SapError
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
WRONG_NG_OF_ARGS = 'Wrong number of arguments.'
|
||||||
|
ERROR_CODE = 1
|
||||||
|
P_OPTION = 'The password related to SAP user.'
|
||||||
|
G_OPTION = 'The name of a SAP group of functions'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class AskSap:
|
||||||
|
'''This script allows to get information about a given RCF function module
|
||||||
|
exposed by a distant SAP system.
|
||||||
|
|
||||||
|
usage: %prog [options] host sysnr client user functionName
|
||||||
|
|
||||||
|
"host" is the server name or IP address where SAP runs.
|
||||||
|
"sysnr" is the SAP system/gateway number (example: 0)
|
||||||
|
"client" is the SAP client number (example: 040)
|
||||||
|
"user" is a valid SAP login
|
||||||
|
"sapElement" is the name of a SAP function (the default) or a given
|
||||||
|
group of functions (if option -g is given). If -g is
|
||||||
|
specified, sapElement can be "_all_" and all functions of
|
||||||
|
all groups are shown.
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
1) Retrieve info about the function named "ZFX":
|
||||||
|
python asksap.py 127.0.0.1 0 040 USERGDY ZFX -p passwd
|
||||||
|
|
||||||
|
2) Retrieve info about group of functions "Z_API":
|
||||||
|
python asksap.py 127.0.0.1 0 040 USERGDY Z_API -p passwd -g
|
||||||
|
|
||||||
|
3) Retrieve info about all functions in all groups:
|
||||||
|
python asksap.py 127.0.0.1 0 040 USERGDY _all_ -p passwd -g
|
||||||
|
'''
|
||||||
|
def manageArgs(self, parser, options, args):
|
||||||
|
# Check number of args
|
||||||
|
if len(args) != 5:
|
||||||
|
print WRONG_NG_OF_ARGS
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(ERROR_CODE)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
optParser = OptionParser(usage=AskSap.__doc__)
|
||||||
|
optParser.add_option("-p", "--password", action='store', type='string',
|
||||||
|
dest='password', default='', help=P_OPTION)
|
||||||
|
optParser.add_option("-g", "--group", action='store_true',
|
||||||
|
dest='isGroup', default='', help=G_OPTION)
|
||||||
|
(options, args) = optParser.parse_args()
|
||||||
|
try:
|
||||||
|
self.manageArgs(optParser, options, args)
|
||||||
|
# Ask the password, if it was not given as an option.
|
||||||
|
password = options.password
|
||||||
|
if not password:
|
||||||
|
password = getpass.getpass('Password for the SAP user: ')
|
||||||
|
connectionParams = args[:4] + [password]
|
||||||
|
print 'Connecting to SAP...'
|
||||||
|
sap = Sap(*connectionParams)
|
||||||
|
sap.connect()
|
||||||
|
print 'Connected.'
|
||||||
|
sapElement = args[4]
|
||||||
|
if options.isGroup:
|
||||||
|
# Returns info about the functions available in this group of
|
||||||
|
# functions.
|
||||||
|
info = sap.getGroupInfo(sapElement)
|
||||||
|
prefix = 'Group'
|
||||||
|
else:
|
||||||
|
# Return info about a given function.
|
||||||
|
info = sap.getFunctionInfo(sapElement)
|
||||||
|
prefix = 'Function'
|
||||||
|
print '%s: %s' % (prefix, sapElement)
|
||||||
|
print info
|
||||||
|
sap.disconnect()
|
||||||
|
except SapError, se:
|
||||||
|
sys.stderr.write(str(se))
|
||||||
|
sys.stderr.write('\n')
|
||||||
|
sys.exit(ERROR_CODE)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
AskSap().run()
|
||||||
|
# ------------------------------------------------------------------------------
|
116
bin/generate.py
Normal file
116
bin/generate.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
'''This script allows to generate a product from a Appy application.'''
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
import sys, os.path
|
||||||
|
from optparse import OptionParser
|
||||||
|
from appy.gen.generator import GeneratorError
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
ERROR_CODE = 1
|
||||||
|
VALID_PRODUCT_TYPES = ('plone25', 'odt')
|
||||||
|
APP_NOT_FOUND = 'Application not found at %s.'
|
||||||
|
WRONG_NG_OF_ARGS = 'Wrong number of arguments.'
|
||||||
|
WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.'
|
||||||
|
PRODUCT_TYPE_ERROR = 'Wrong product type. Product type may be one of the ' \
|
||||||
|
'following: %s' % str(VALID_PRODUCT_TYPES)
|
||||||
|
C_OPTION = 'Removes from i18n files all labels that are not automatically ' \
|
||||||
|
'generated from your gen-application. It can be useful during ' \
|
||||||
|
'development, when you do lots of name changes (classes, ' \
|
||||||
|
'attributes, states, transitions, etc): in this case, the Appy ' \
|
||||||
|
'i18n label generation machinery produces lots of labels that ' \
|
||||||
|
'then become obsolete.'
|
||||||
|
S_OPTION = 'Sorts all i18n labels. If you use this option, among the ' \
|
||||||
|
'generated i18n files, you will find first all labels ' \
|
||||||
|
'that are automatically generated by appy.gen, in some logical ' \
|
||||||
|
'order (ie: field-related labels appear together, in the order ' \
|
||||||
|
'they are declared in the gen-class). Then, if you have added ' \
|
||||||
|
'labels manually, they will appear afterwards. Sorting labels ' \
|
||||||
|
'may not be desired under development. Indeed, when no sorting ' \
|
||||||
|
'occurs, every time you add or modify a field, class, state, etc, ' \
|
||||||
|
'newly generated labels will all appear together at the end of ' \
|
||||||
|
'the file; so it will be easy to translate them all. When sorting ' \
|
||||||
|
'occurs, those elements may be spread at different places in the ' \
|
||||||
|
'i18n file. When the development is finished, it may be a good ' \
|
||||||
|
'idea to sort the labels to get a clean and logically ordered ' \
|
||||||
|
'set of translation files.'
|
||||||
|
|
||||||
|
class GeneratorScript:
|
||||||
|
'''usage: %prog [options] app productType outputFolder
|
||||||
|
|
||||||
|
"app" is the path to your Appy application, which may be a
|
||||||
|
Python module (= a file than ends with .py) or a Python
|
||||||
|
package (= a folder containing a file named __init__.py).
|
||||||
|
Your app may reside anywhere (but it needs to be
|
||||||
|
accessible by the underlying application server, ie Zope),
|
||||||
|
excepted within the generated product. Typically, if you
|
||||||
|
generate a Plone product, it may reside within
|
||||||
|
<yourZopeInstance>/lib/python, but not within the
|
||||||
|
generated product (typically stored in
|
||||||
|
<yourZopeInstance>/Products).
|
||||||
|
"productType" is the kind of product you want to generate
|
||||||
|
(currently, only "plone25" and 'odt' are supported;
|
||||||
|
in the near future, the "plone25" target will also produce
|
||||||
|
Plone 3-compliant code that will still work with
|
||||||
|
Plone 2.5).
|
||||||
|
"outputFolder" is the folder where the product will be generated.
|
||||||
|
For example, if you specify /my/output/folder for your
|
||||||
|
application /home/gde/MyApp.py, this script will create
|
||||||
|
a folder /my/output/folder/MyApp and put in it the
|
||||||
|
generated product.
|
||||||
|
|
||||||
|
Example: generating a Plone product
|
||||||
|
-----------------------------------
|
||||||
|
In your Zope instance named myZopeInstance, create a folder
|
||||||
|
"myZopeInstance/lib/python/MyApp". Create into it your Appy application
|
||||||
|
(we suppose here that it is a Python package, containing a __init__.py
|
||||||
|
file and other files). Then, chdir into this folder and type
|
||||||
|
"python <appyPath>/gen/generator.py . plone25 ../../../Products" and the
|
||||||
|
product will be generated in myZopeInstance/Products/MyApp.
|
||||||
|
"python" must refer to a Python interpreter that knows package appy.'''
|
||||||
|
|
||||||
|
def generateProduct(self, options, application, productType, outputFolder):
|
||||||
|
exec 'from appy.gen.%s.generator import Generator' % productType
|
||||||
|
Generator(application, outputFolder, options).run()
|
||||||
|
|
||||||
|
def manageArgs(self, parser, options, args):
|
||||||
|
# Check number of args
|
||||||
|
if len(args) != 3:
|
||||||
|
print WRONG_NG_OF_ARGS
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(ERROR_CODE)
|
||||||
|
# Check productType
|
||||||
|
if args[1] not in VALID_PRODUCT_TYPES:
|
||||||
|
print PRODUCT_TYPE_ERROR
|
||||||
|
sys.exit(ERROR_CODE)
|
||||||
|
# Check existence of application
|
||||||
|
if not os.path.exists(args[0]):
|
||||||
|
print APP_NOT_FOUND % args[0]
|
||||||
|
sys.exit(ERROR_CODE)
|
||||||
|
# Check existence of outputFolder basic type
|
||||||
|
if not os.path.exists(args[2]):
|
||||||
|
print WRONG_OUTPUT_FOLDER
|
||||||
|
sys.exit(ERROR_CODE)
|
||||||
|
# Convert all paths in absolute paths
|
||||||
|
for i in (0,2):
|
||||||
|
args[i] = os.path.abspath(args[i])
|
||||||
|
def run(self):
|
||||||
|
optParser = OptionParser(usage=GeneratorScript.__doc__)
|
||||||
|
optParser.add_option("-c", "--i18n-clean", action='store_true',
|
||||||
|
dest='i18nClean', default=False, help=C_OPTION)
|
||||||
|
optParser.add_option("-s", "--i18n-sort", action='store_true',
|
||||||
|
dest='i18nSort', default=False, help=S_OPTION)
|
||||||
|
(options, args) = optParser.parse_args()
|
||||||
|
try:
|
||||||
|
self.manageArgs(optParser, options, args)
|
||||||
|
print 'Generating %s product in %s...' % (args[1], args[2])
|
||||||
|
self.generateProduct(options, *args)
|
||||||
|
except GeneratorError, ge:
|
||||||
|
sys.stderr.write(str(ge))
|
||||||
|
sys.stderr.write('\n')
|
||||||
|
optParser.print_help()
|
||||||
|
sys.exit(ERROR_CODE)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
GeneratorScript().run()
|
||||||
|
# ------------------------------------------------------------------------------
|
110
gen/generator.py
110
gen/generator.py
|
@ -1,6 +1,5 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import os, os.path, sys, parser, symbol, token
|
import os, os.path, sys, parser, symbol, token
|
||||||
from optparse import OptionParser
|
|
||||||
from appy.gen import Type, State, Config, Tool, Flavour
|
from appy.gen import Type, State, Config, Tool, Flavour
|
||||||
from appy.gen.descriptors import *
|
from appy.gen.descriptors import *
|
||||||
from appy.gen.utils import produceNiceMessage
|
from appy.gen.utils import produceNiceMessage
|
||||||
|
@ -322,113 +321,4 @@ class Generator:
|
||||||
for wfDescr in self.workflows: self.generateWorkflow(wfDescr)
|
for wfDescr in self.workflows: self.generateWorkflow(wfDescr)
|
||||||
self.finalize()
|
self.finalize()
|
||||||
print 'Done.'
|
print 'Done.'
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
ERROR_CODE = 1
|
|
||||||
VALID_PRODUCT_TYPES = ('plone25', 'odt')
|
|
||||||
APP_NOT_FOUND = 'Application not found at %s.'
|
|
||||||
WRONG_NG_OF_ARGS = 'Wrong number of arguments.'
|
|
||||||
WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.'
|
|
||||||
PRODUCT_TYPE_ERROR = 'Wrong product type. Product type may be one of the ' \
|
|
||||||
'following: %s' % str(VALID_PRODUCT_TYPES)
|
|
||||||
C_OPTION = 'Removes from i18n files all labels that are not automatically ' \
|
|
||||||
'generated from your gen-application. It can be useful during ' \
|
|
||||||
'development, when you do lots of name changes (classes, ' \
|
|
||||||
'attributes, states, transitions, etc): in this case, the Appy ' \
|
|
||||||
'i18n label generation machinery produces lots of labels that ' \
|
|
||||||
'then become obsolete.'
|
|
||||||
S_OPTION = 'Sorts all i18n labels. If you use this option, among the ' \
|
|
||||||
'generated i18n files, you will find first all labels ' \
|
|
||||||
'that are automatically generated by appy.gen, in some logical ' \
|
|
||||||
'order (ie: field-related labels appear together, in the order ' \
|
|
||||||
'they are declared in the gen-class). Then, if you have added ' \
|
|
||||||
'labels manually, they will appear afterwards. Sorting labels ' \
|
|
||||||
'may not be desired under development. Indeed, when no sorting ' \
|
|
||||||
'occurs, every time you add or modify a field, class, state, etc, ' \
|
|
||||||
'newly generated labels will all appear together at the end of ' \
|
|
||||||
'the file; so it will be easy to translate them all. When sorting ' \
|
|
||||||
'occurs, those elements may be spread at different places in the ' \
|
|
||||||
'i18n file. When the development is finished, it may be a good ' \
|
|
||||||
'idea to sort the labels to get a clean and logically ordered ' \
|
|
||||||
'set of translation files.'
|
|
||||||
|
|
||||||
class GeneratorScript:
|
|
||||||
'''usage: %prog [options] app productType outputFolder
|
|
||||||
|
|
||||||
"app" is the path to your Appy application, which may be a
|
|
||||||
Python module (= a file than ends with .py) or a Python
|
|
||||||
package (= a folder containing a file named __init__.py).
|
|
||||||
Your app may reside anywhere (but it needs to be
|
|
||||||
accessible by the underlying application server, ie Zope),
|
|
||||||
excepted within the generated product. Typically, if you
|
|
||||||
generate a Plone product, it may reside within
|
|
||||||
<yourZopeInstance>/lib/python, but not within the
|
|
||||||
generated product (typically stored in
|
|
||||||
<yourZopeInstance>/Products).
|
|
||||||
"productType" is the kind of product you want to generate
|
|
||||||
(currently, only "plone25" and 'odt' are supported;
|
|
||||||
in the near future, the "plone25" target will also produce
|
|
||||||
Plone 3-compliant code that will still work with
|
|
||||||
Plone 2.5).
|
|
||||||
"outputFolder" is the folder where the product will be generated.
|
|
||||||
For example, if you specify /my/output/folder for your
|
|
||||||
application /home/gde/MyApp.py, this script will create
|
|
||||||
a folder /my/output/folder/MyApp and put in it the
|
|
||||||
generated product.
|
|
||||||
|
|
||||||
Example: generating a Plone product
|
|
||||||
-----------------------------------
|
|
||||||
In your Zope instance named myZopeInstance, create a folder
|
|
||||||
"myZopeInstance/lib/python/MyApp". Create into it your Appy application
|
|
||||||
(we suppose here that it is a Python package, containing a __init__.py
|
|
||||||
file and other files). Then, chdir into this folder and type
|
|
||||||
"python <appyPath>/gen/generator.py . plone25 ../../../Products" and the
|
|
||||||
product will be generated in myZopeInstance/Products/MyApp.
|
|
||||||
"python" must refer to a Python interpreter that knows package appy.'''
|
|
||||||
|
|
||||||
def generateProduct(self, options, application, productType, outputFolder):
|
|
||||||
exec 'from appy.gen.%s.generator import Generator' % productType
|
|
||||||
Generator(application, outputFolder, options).run()
|
|
||||||
|
|
||||||
def manageArgs(self, parser, options, args):
|
|
||||||
# Check number of args
|
|
||||||
if len(args) != 3:
|
|
||||||
print WRONG_NG_OF_ARGS
|
|
||||||
parser.print_help()
|
|
||||||
sys.exit(ERROR_CODE)
|
|
||||||
# Check productType
|
|
||||||
if args[1] not in VALID_PRODUCT_TYPES:
|
|
||||||
print PRODUCT_TYPE_ERROR
|
|
||||||
sys.exit(ERROR_CODE)
|
|
||||||
# Check existence of application
|
|
||||||
if not os.path.exists(args[0]):
|
|
||||||
print APP_NOT_FOUND % args[0]
|
|
||||||
sys.exit(ERROR_CODE)
|
|
||||||
# Check existence of outputFolder basic type
|
|
||||||
if not os.path.exists(args[2]):
|
|
||||||
print WRONG_OUTPUT_FOLDER
|
|
||||||
sys.exit(ERROR_CODE)
|
|
||||||
# Convert all paths in absolute paths
|
|
||||||
for i in (0,2):
|
|
||||||
args[i] = os.path.abspath(args[i])
|
|
||||||
def run(self):
|
|
||||||
optParser = OptionParser(usage=GeneratorScript.__doc__)
|
|
||||||
optParser.add_option("-c", "--i18n-clean", action='store_true',
|
|
||||||
dest='i18nClean', default=False, help=C_OPTION)
|
|
||||||
optParser.add_option("-s", "--i18n-sort", action='store_true',
|
|
||||||
dest='i18nSort', default=False, help=S_OPTION)
|
|
||||||
(options, args) = optParser.parse_args()
|
|
||||||
try:
|
|
||||||
self.manageArgs(optParser, options, args)
|
|
||||||
print 'Generating %s product in %s...' % (args[1], args[2])
|
|
||||||
self.generateProduct(options, *args)
|
|
||||||
except GeneratorError, ge:
|
|
||||||
sys.stderr.write(str(ge))
|
|
||||||
sys.stderr.write('\n')
|
|
||||||
optParser.print_help()
|
|
||||||
sys.exit(ERROR_CODE)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
if __name__ == '__main__':
|
|
||||||
GeneratorScript().run()
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
import os, os.path, sys
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class TestMixin:
|
class TestMixin:
|
||||||
'''This class is mixed in with any PloneTestCase.'''
|
'''This class is mixed in with any PloneTestCase.'''
|
||||||
|
@ -12,16 +15,81 @@ class TestMixin:
|
||||||
self.logout()
|
self.logout()
|
||||||
self.login(userId)
|
self.login(userId)
|
||||||
|
|
||||||
|
def getNonEmptySubModules(self, moduleName):
|
||||||
|
'''Returns the list fo sub-modules of p_app that are non-empty.'''
|
||||||
|
res = []
|
||||||
|
try:
|
||||||
|
exec 'import %s' % moduleName
|
||||||
|
exec 'moduleObj = %s' % moduleName
|
||||||
|
moduleFile = moduleObj.__file__
|
||||||
|
if moduleFile.endswith('.pyc'):
|
||||||
|
moduleFile = moduleFile[:-1]
|
||||||
|
except ImportError, ie:
|
||||||
|
return res
|
||||||
|
except SyntaxError, se:
|
||||||
|
return res
|
||||||
|
# Include the module if not empty. "Emptyness" is determined by the
|
||||||
|
# absence of names beginning with other chars than "__".
|
||||||
|
for elem in moduleObj.__dict__.iterkeys():
|
||||||
|
if not elem.startswith('__'):
|
||||||
|
print 'Element found in this module!!!', moduleObj, elem
|
||||||
|
res.append(moduleObj)
|
||||||
|
break
|
||||||
|
# Include sub-modules if any
|
||||||
|
if moduleFile.find("__init__.py") != -1:
|
||||||
|
# Potentially, sub-modules exist.
|
||||||
|
moduleFolder = os.path.dirname(moduleFile)
|
||||||
|
for elem in os.listdir(moduleFolder):
|
||||||
|
if elem.startswith('.'): continue
|
||||||
|
subModuleName, ext = os.path.splitext(elem)
|
||||||
|
if ((ext == '.py') and (subModuleName != '__init__')) or \
|
||||||
|
os.path.isdir(os.path.join(moduleFolder, subModuleName)):
|
||||||
|
# Submodules may be sub-folders or Python files
|
||||||
|
subModuleName = '%s.%s' % (moduleName, subModuleName)
|
||||||
|
res += self.getNonEmptySubModules(subModuleName)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def getCovFolder(self):
|
||||||
|
'''Returns the folder where to put the coverage folder if needed.'''
|
||||||
|
for arg in sys.argv:
|
||||||
|
if arg.startswith('[coverage'):
|
||||||
|
return arg[10:].strip(']')
|
||||||
|
return None
|
||||||
|
|
||||||
# Functions executed before and after every test -------------------------------
|
# Functions executed before and after every test -------------------------------
|
||||||
def beforeTest(test):
|
def beforeTest(test):
|
||||||
|
'''Is executed before every test.'''
|
||||||
g = test.globs
|
g = test.globs
|
||||||
g['tool'] = test.app.plone.get('portal_%s' % g['appName'].lower()).appy()
|
g['tool'] = test.app.plone.get('portal_%s' % g['appName'].lower()).appy()
|
||||||
g['appFolder'] = g['tool'].o.getProductConfig().diskFolder
|
cfg = g['tool'].o.getProductConfig()
|
||||||
|
g['appFolder'] = cfg.diskFolder
|
||||||
moduleOrClassName = g['test'].name # Not used yet.
|
moduleOrClassName = g['test'].name # Not used yet.
|
||||||
# Initialize the test
|
# Initialize the test
|
||||||
test.createUser('admin', ('Member','Manager'))
|
test.createUser('admin', ('Member','Manager'))
|
||||||
test.login('admin')
|
test.login('admin')
|
||||||
g['t'] = g['test']
|
g['t'] = g['test']
|
||||||
|
# Must we perform test coverage ?
|
||||||
|
covFolder = test.getCovFolder()
|
||||||
|
if covFolder:
|
||||||
|
try:
|
||||||
|
print 'COV!!!!', covFolder
|
||||||
|
import coverage
|
||||||
|
app = getattr(cfg, g['tool'].o.getAppName())
|
||||||
|
from coverage import coverage
|
||||||
|
cov = coverage()
|
||||||
|
g['cov'] = cov
|
||||||
|
g['covFolder'] = covFolder
|
||||||
|
cov.start()
|
||||||
|
except ImportError:
|
||||||
|
print 'You must install the "coverage" product.'
|
||||||
|
|
||||||
def afterTest(test): pass
|
def afterTest(test):
|
||||||
|
'''Is executed after every test.'''
|
||||||
|
g = test.globs
|
||||||
|
if g.has_key('covFolder'):
|
||||||
|
cov = g['cov']
|
||||||
|
cov.stop()
|
||||||
|
# Dumps the coverage report
|
||||||
|
appModules = test.getNonEmptySubModules(g['tool'].o.getAppName())
|
||||||
|
cov.html_report(directory=g['covFolder'], morfs=appModules)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -190,7 +190,7 @@
|
||||||
</tal:editField>
|
</tal:editField>
|
||||||
<tal:viewField tal:condition="not: isEdit">
|
<tal:viewField tal:condition="not: isEdit">
|
||||||
<tal:fileField condition="python: (appyType['type'] == 'File')">
|
<tal:fileField condition="python: (appyType['type'] == 'File')">
|
||||||
<span tal:condition="showLabel" tal:content="label"></span>
|
<span tal:condition="showLabel" tal:content="label" class="appyLabel"></span>
|
||||||
<metal:viewField use-macro="python: contextObj.widget(field.getName(), 'view', use_label=0)"/>
|
<metal:viewField use-macro="python: contextObj.widget(field.getName(), 'view', use_label=0)"/>
|
||||||
</tal:fileField>
|
</tal:fileField>
|
||||||
<tal:date condition="python: appyType['type'] == 'Date'">
|
<tal:date condition="python: appyType['type'] == 'Date'">
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.appyList {
|
.appyList {
|
||||||
line-height: 0;
|
line-height: 1.1em;
|
||||||
margin: 0 0 0.5em 1.2em;
|
margin: 0 0 0.5em 1.2em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class ToolWrapper:
|
class ToolWrapper:
|
||||||
|
|
||||||
def getInitiator(self):
|
def getInitiator(self):
|
||||||
'''Retrieves the object that triggered the creation of the object
|
'''Retrieves the object that triggered the creation of the object
|
||||||
being currently created (if any).'''
|
being currently created (if any).'''
|
||||||
|
@ -13,4 +12,8 @@ class ToolWrapper:
|
||||||
def getObject(self, uid):
|
def getObject(self, uid):
|
||||||
'''Allow to retrieve an object from its unique identifier p_uid.'''
|
'''Allow to retrieve an object from its unique identifier p_uid.'''
|
||||||
return self.o.getObject(uid, appy=True)
|
return self.o.getObject(uid, appy=True)
|
||||||
|
|
||||||
|
def getDiskFolder(self):
|
||||||
|
'''Returns the disk folder where the Appy application is stored.'''
|
||||||
|
return self.o.getProductConfig().diskFolder
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
171
shared/sap.py
Normal file
171
shared/sap.py
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
'''This module allows to call RFC functions exposed by a distant SAP system.
|
||||||
|
It requires the "pysap" module available at http://pysaprfc.sourceforge.net
|
||||||
|
and the library librfccm.so that one can download from the "SAP MarketPlace",
|
||||||
|
a website by SAP requiring a login/password.'''
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
from appy.gen.utils import sequenceTypes
|
||||||
|
|
||||||
|
class SapError(Exception): pass
|
||||||
|
SAP_MODULE_ERROR = 'Module pysap was not found (you can get it at ' \
|
||||||
|
'http://pysaprfc.sourceforge.net)'
|
||||||
|
SAP_CONNECT_ERROR = 'Error while connecting to SAP (conn_string: %s). %s'
|
||||||
|
SAP_FUNCTION_ERROR = 'Error while calling function "%s". %s'
|
||||||
|
SAP_DISCONNECT_ERROR = 'Error while disconnecting from SAP. %s'
|
||||||
|
SAP_TABLE_PARAM_ERROR = 'Param "%s" does not correspond to a valid table ' \
|
||||||
|
'parameter for function "%s".'
|
||||||
|
SAP_FUNCTION_NOT_FOUND = 'Function "%s" does not exist.'
|
||||||
|
SAP_FUNCTION_INFO_ERROR = 'Error while asking information about function ' \
|
||||||
|
'"%s". %s'
|
||||||
|
SAP_GROUP_NOT_FOUND = 'Group of functions "%s" does not exist or is empty.'
|
||||||
|
|
||||||
|
# Is the pysap module present or not ?
|
||||||
|
hasSap = True
|
||||||
|
try:
|
||||||
|
import pysap
|
||||||
|
except ImportError:
|
||||||
|
hasSap = False
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class Sap:
|
||||||
|
'''Represents a remote SAP system. This class allows to connect to a distant
|
||||||
|
SAP system and perform RFC calls.'''
|
||||||
|
def __init__(self, host, sysnr, client, user, password):
|
||||||
|
self.host = host # Hostname or IP address of SAP server
|
||||||
|
self.sysnr = sysnr # The system number of SAP server/gateway
|
||||||
|
self.client = client # The instance/client number
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
self.sap = None # Will hold the handler to the SAP distant system.
|
||||||
|
if not hasSap: raise SapError(SAP_MODULE_ERROR)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
'''Connects to the SAP system.'''
|
||||||
|
params = 'ASHOST=%s SYSNR=%s CLIENT=%s USER=%s PASSWD=%s' % (self.host,
|
||||||
|
self.sysnr, self.client, self.user, self.password)
|
||||||
|
try:
|
||||||
|
self.sap = pysap.Rfc_connection(conn_string = params)
|
||||||
|
self.sap.open()
|
||||||
|
except pysap.BaseSapRfcError, se:
|
||||||
|
# Put in the error message the connection string without the
|
||||||
|
# password.
|
||||||
|
connNoPasswd = params[:params.index('PASSWD')] + 'PASSWD=********'
|
||||||
|
raise SapError(SAP_CONNECT_ERROR % (connNoPasswd, str(se)))
|
||||||
|
|
||||||
|
def call(self, functionName, **params):
|
||||||
|
'''Calls a function on the SAP server.'''
|
||||||
|
try:
|
||||||
|
function = self.sap.get_interface(functionName)
|
||||||
|
# Specify the parameters
|
||||||
|
for name, value in params.iteritems():
|
||||||
|
if type(value) == dict:
|
||||||
|
# The param corresponds to a SAP/C "struct"
|
||||||
|
v = self.sap.get_structure(name)()
|
||||||
|
v.from_dict(value)
|
||||||
|
elif type(value) in sequenceTypes:
|
||||||
|
# The param must be a SAP/C "table" (a list of structs)
|
||||||
|
# Retrieve the name of the struct type related to this
|
||||||
|
# table.
|
||||||
|
fDesc = self.sap.get_interface_desc(functionName)
|
||||||
|
tableTypeName = ''
|
||||||
|
for tDesc in fDesc.tables:
|
||||||
|
if tDesc.name == name:
|
||||||
|
# We have found the correct table param
|
||||||
|
tableTypeName = tDesc.field_def
|
||||||
|
break
|
||||||
|
if not tableTypeName:
|
||||||
|
raise SapError(\
|
||||||
|
SAP_TABLE_PARAM_ERROR % (name, functionName))
|
||||||
|
v = self.sap.get_table(tableTypeName)
|
||||||
|
for dValue in value:
|
||||||
|
v.append_from_dict(dValue)
|
||||||
|
#v = v.handle
|
||||||
|
else:
|
||||||
|
v = value
|
||||||
|
function[name] = v
|
||||||
|
# Call the function
|
||||||
|
function()
|
||||||
|
except pysap.BaseSapRfcError, se:
|
||||||
|
raise SapError(SAP_FUNCTION_ERROR % (functionName, str(se)))
|
||||||
|
|
||||||
|
def getTypeInfo(self, typeName):
|
||||||
|
'''Returns information about the type (structure) named p_typeName.'''
|
||||||
|
res = ''
|
||||||
|
tInfo = self.sap.get_structure(typeName)
|
||||||
|
for fName, fieldType in tInfo._fields_:
|
||||||
|
res += ' %s: %s (%s)\n' % (fName, tInfo.sap_def(fName),
|
||||||
|
tInfo.sap_type(fName))
|
||||||
|
return res
|
||||||
|
|
||||||
|
def getFunctionInfo(self, functionName):
|
||||||
|
'''Returns information about the RFC function named p_functionName.'''
|
||||||
|
try:
|
||||||
|
res = ''
|
||||||
|
usedTypes = set() # Names of type definitions used in parameters.
|
||||||
|
fDesc = self.sap.get_interface_desc(functionName)
|
||||||
|
functionDescr = str(fDesc).strip()
|
||||||
|
if functionDescr: res += functionDescr
|
||||||
|
# Import parameters
|
||||||
|
if fDesc.imports:
|
||||||
|
res += '\nIMPORTS\n'
|
||||||
|
for iDesc in fDesc.imports:
|
||||||
|
res += ' %s\n' % str(iDesc)
|
||||||
|
usedTypes.add(iDesc.field_def)
|
||||||
|
# Export parameters
|
||||||
|
if fDesc.exports:
|
||||||
|
res += '\nEXPORTS\n'
|
||||||
|
for eDesc in fDesc.exports:
|
||||||
|
res += ' %s\n' % str(eDesc)
|
||||||
|
usedTypes.add(eDesc.field_def)
|
||||||
|
if fDesc.tables:
|
||||||
|
res += '\nTABLES\n'
|
||||||
|
for tDesc in fDesc.tables:
|
||||||
|
res += ' %s\n' % str(tDesc)
|
||||||
|
usedTypes.add(tDesc.field_def)
|
||||||
|
if fDesc.exceptions:
|
||||||
|
res += '\nEXCEPTIONS\n'
|
||||||
|
for eDesc in fDesc.exceptions:
|
||||||
|
res += ' %s\n' % str(eDesc)
|
||||||
|
# Add information about used types
|
||||||
|
if usedTypes:
|
||||||
|
res += '\nTypes used by the parameters:\n'
|
||||||
|
for typeName in usedTypes:
|
||||||
|
# Dump info only if it is a structure, not a simple type
|
||||||
|
try:
|
||||||
|
self.sap.get_structure(typeName)
|
||||||
|
res += '%s\n%s\n\n' % \
|
||||||
|
(typeName, self.getTypeInfo(typeName))
|
||||||
|
except pysap.BaseSapRfcError, ee:
|
||||||
|
pass
|
||||||
|
return res
|
||||||
|
except pysap.BaseSapRfcError, se:
|
||||||
|
if se.value == 'FU_NOT_FOUND':
|
||||||
|
raise SapError(SAP_FUNCTION_NOT_FOUND % (functionName))
|
||||||
|
else:
|
||||||
|
raise SapError(SAP_FUNCTION_INFO_ERROR % (functionName,str(se)))
|
||||||
|
|
||||||
|
def getGroupInfo(self, groupName):
|
||||||
|
'''Gets information about the functions that are available in group of
|
||||||
|
functions p_groupName.'''
|
||||||
|
if groupName == '_all_':
|
||||||
|
# Search everything.
|
||||||
|
functions = self.sap.search_functions('*')
|
||||||
|
else:
|
||||||
|
functions = self.sap.search_functions('*', grpname=groupName)
|
||||||
|
if not functions:
|
||||||
|
raise SapError(SAP_GROUP_NOT_FOUND % (groupName))
|
||||||
|
res = 'Available functions:\n'
|
||||||
|
for f in functions:
|
||||||
|
res += ' %s' % f.funcname
|
||||||
|
if groupName == '_all_':
|
||||||
|
res += ' (group: %s)' % f.groupname
|
||||||
|
res += '\n'
|
||||||
|
return res
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
'''Disconnects from SAP.'''
|
||||||
|
try:
|
||||||
|
self.sap.close()
|
||||||
|
except pysap.BaseSapRfcError, se:
|
||||||
|
raise SapError(SAP_DISCONNECT_ERROR % str(se))
|
||||||
|
# ------------------------------------------------------------------------------
|
Loading…
Reference in a new issue