removed unnecessary things
|
@ -1,337 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, subprocess, md5, shutil
|
||||
from appy.shared.utils import getOsTempFolder, FolderDeleter, cleanFolder
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
debianInfo = '''Package: python-appy%s
|
||||
Version: %s
|
||||
Architecture: all
|
||||
Maintainer: Gaetan Delannay <gaetan.delannay@geezteem.com>
|
||||
Installed-Size: %d
|
||||
Depends: python (>= %s)%s
|
||||
Section: python
|
||||
Priority: optional
|
||||
Homepage: http://appyframework.org
|
||||
Description: Appy builds simple but complex web Python apps.
|
||||
'''
|
||||
appCtl = '''#! /usr/lib/zope2.12/bin/python
|
||||
import sys
|
||||
from appy.bin.zopectl import ZopeRunner
|
||||
args = ' '.join(sys.argv[1:])
|
||||
sys.argv = [sys.argv[0], '-C', '/etc/%s.conf', args]
|
||||
ZopeRunner().run()
|
||||
'''
|
||||
appRun = '''#! /bin/sh
|
||||
exec "/usr/lib/zope2.12/bin/runzope" -C "/etc/%s.conf" "$@"
|
||||
'''
|
||||
ooStart = '#! /bin/sh\nsoffice -invisible -headless -nofirststartwizard ' \
|
||||
'"-accept=socket,host=localhost,port=2002;urp;"'
|
||||
zopeConf = '''# Zope configuration.
|
||||
%%define INSTANCE %s
|
||||
%%define DATA %s
|
||||
%%define LOG %s
|
||||
%%define HTTPPORT %s
|
||||
%%define ZOPE_USER zope
|
||||
|
||||
instancehome $INSTANCE
|
||||
effective-user $ZOPE_USER
|
||||
%s
|
||||
<eventlog>
|
||||
level info
|
||||
<logfile>
|
||||
path $LOG/event.log
|
||||
level info
|
||||
</logfile>
|
||||
</eventlog>
|
||||
<logger access>
|
||||
level WARN
|
||||
<logfile>
|
||||
path $LOG/Z2.log
|
||||
format %%(message)s
|
||||
</logfile>
|
||||
</logger>
|
||||
<http-server>
|
||||
address $HTTPPORT
|
||||
</http-server>
|
||||
<zodb_db main>
|
||||
<filestorage>
|
||||
path $DATA/Data.fs
|
||||
</filestorage>
|
||||
mount-point /
|
||||
</zodb_db>
|
||||
<zodb_db temporary>
|
||||
<temporarystorage>
|
||||
name temporary storage for sessioning
|
||||
</temporarystorage>
|
||||
mount-point /temp_folder
|
||||
container-class Products.TemporaryFolder.TemporaryContainer
|
||||
</zodb_db>
|
||||
'''
|
||||
# initScript below will be used to define the scripts that will run the
|
||||
# app-powered Zope instance and OpenOffice in server mode at boot time.
|
||||
initScript = '''#! /bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: %s
|
||||
# Required-Start: $syslog $remote_fs
|
||||
# Required-Stop: $syslog $remote_fs
|
||||
# Should-Start: $remote_fs
|
||||
# Should-Stop: $remote_fs
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Start %s
|
||||
# Description: %s
|
||||
### END INIT INFO
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
%s
|
||||
;;
|
||||
restart|reload|force-reload)
|
||||
%s
|
||||
;;
|
||||
stop)
|
||||
%s
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 start|restart|stop" >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
'''
|
||||
|
||||
class Debianizer:
|
||||
'''This class allows to produce a Debian package from a Python (Appy)
|
||||
package.'''
|
||||
|
||||
def __init__(self, app, out, appVersion='0.1.0',
|
||||
pythonVersions=('2.6',), zopePort=8080,
|
||||
depends=('zope2.12', 'openoffice.org', 'imagemagick'),
|
||||
sign=False):
|
||||
# app is the path to the Python package to Debianize.
|
||||
self.app = app
|
||||
self.appName = os.path.basename(app)
|
||||
self.appNameLower = self.appName.lower()
|
||||
# Must we sign the Debian package? If yes, we make the assumption that
|
||||
# the currently logged user has a public/private key pair in ~/.gnupg,
|
||||
# generated with command "gpg --gen-key".
|
||||
self.sign = sign
|
||||
# out is the folder where the Debian package will be generated.
|
||||
self.out = out
|
||||
# What is the version number for this app ?
|
||||
self.appVersion = appVersion
|
||||
# On which Python versions will the Debian package depend?
|
||||
self.pythonVersions = pythonVersions
|
||||
# Port for Zope
|
||||
self.zopePort = zopePort
|
||||
# Debian package dependencies
|
||||
self.depends = depends
|
||||
# Zope 2.12 requires Python 2.6
|
||||
if 'zope2.12' in depends: self.pythonVersions = ('2.6',)
|
||||
|
||||
def run(self):
|
||||
'''Generates the Debian package.'''
|
||||
curdir = os.getcwd()
|
||||
j = os.path.join
|
||||
tempFolder = getOsTempFolder()
|
||||
# Create, in the temp folder, the required sub-structure for the Debian
|
||||
# package.
|
||||
debFolder = j(tempFolder, 'debian')
|
||||
if os.path.exists(debFolder):
|
||||
FolderDeleter.delete(debFolder)
|
||||
# Copy the Python package into it
|
||||
srcFolder = j(debFolder, 'usr', 'lib')
|
||||
for version in self.pythonVersions:
|
||||
libFolder = j(srcFolder, 'python%s' % version)
|
||||
os.makedirs(libFolder)
|
||||
destFolder = j(libFolder, self.appName)
|
||||
shutil.copytree(self.app, destFolder)
|
||||
# Clean dest folder (.svn/.bzr files)
|
||||
cleanFolder(destFolder, folders=('.svn', '.bzr'))
|
||||
# When packaging Appy itself, everything is in /usr/lib/pythonX. When
|
||||
# packaging an Appy app, we will generate more files for creating a
|
||||
# running instance.
|
||||
if self.appName != 'appy':
|
||||
# Create the folders that will collectively represent the deployed
|
||||
# Zope instance.
|
||||
binFolder = j(debFolder, 'usr', 'bin')
|
||||
os.makedirs(binFolder)
|
||||
# <app>ctl
|
||||
name = '%s/%sctl' % (binFolder, self.appNameLower)
|
||||
f = file(name, 'w')
|
||||
f.write(appCtl % self.appNameLower)
|
||||
os.chmod(name, 0o744) # Make it executable by owner.
|
||||
f.close()
|
||||
# <app>run
|
||||
name = '%s/%srun' % (binFolder, self.appNameLower)
|
||||
f = file(name, 'w')
|
||||
f.write(appRun % self.appNameLower)
|
||||
os.chmod(name, 0o744) # Make it executable by owner.
|
||||
f.close()
|
||||
# startoo
|
||||
name = '%s/startoo' % binFolder
|
||||
f = file(name, 'w')
|
||||
f.write(ooStart)
|
||||
f.close()
|
||||
os.chmod(name, 0o744) # Make it executable by owner.
|
||||
# /var/lib/<app> (will store Data.fs, lock files, etc)
|
||||
varLibFolder = j(debFolder, 'var', 'lib', self.appNameLower)
|
||||
os.makedirs(varLibFolder)
|
||||
f = file('%s/README' % varLibFolder, 'w')
|
||||
f.write('This folder stores the %s database.\n' % self.appName)
|
||||
f.close()
|
||||
# /var/log/<app> (will store event.log and Z2.log)
|
||||
varLogFolder = j(debFolder, 'var', 'log', self.appNameLower)
|
||||
os.makedirs(varLogFolder)
|
||||
f = file('%s/README' % varLogFolder, 'w')
|
||||
f.write('This folder stores the log files for %s.\n' % self.appName)
|
||||
f.close()
|
||||
# /etc/<app>.conf (Zope configuration file)
|
||||
etcFolder = j(debFolder, 'etc')
|
||||
os.makedirs(etcFolder)
|
||||
name = '%s/%s.conf' % (etcFolder, self.appNameLower)
|
||||
n = self.appNameLower
|
||||
f = file(name, 'w')
|
||||
productsFolder = '/usr/lib/python%s/%s/zope' % \
|
||||
(self.pythonVersions[0], self.appName)
|
||||
f.write(zopeConf % ('/var/lib/%s' % n, '/var/lib/%s' % n,
|
||||
'/var/log/%s' % n, str(self.zopePort),
|
||||
'products %s\n' % productsFolder))
|
||||
f.close()
|
||||
# /etc/init.d/<app> (start the app at boot time)
|
||||
initdFolder = j(etcFolder, 'init.d')
|
||||
os.makedirs(initdFolder)
|
||||
name = '%s/%s' % (initdFolder, self.appNameLower)
|
||||
f = file(name, 'w')
|
||||
n = self.appNameLower
|
||||
f.write(initScript % (n, n, 'Start Zope with the Appy-based %s ' \
|
||||
'application.' % n, '%sctl start' % n,
|
||||
'%sctl restart' % n, '%sctl stop' % n))
|
||||
f.close()
|
||||
os.chmod(name, 0o744) # Make it executable by owner.
|
||||
# /etc/init.d/oo (start OpenOffice at boot time)
|
||||
name = '%s/oo' % initdFolder
|
||||
f = file(name, 'w')
|
||||
f.write(initScript % ('oo', 'oo', 'Start OpenOffice in server mode',
|
||||
'startoo', 'startoo', "#Can't stop OO."))
|
||||
f.write('\n')
|
||||
f.close()
|
||||
os.chmod(name, 0o744) # Make it executable by owner.
|
||||
# Get the size of the app, in Kb.
|
||||
os.chdir(tempFolder)
|
||||
cmd = subprocess.Popen(['du', '-b', '-s', 'debian'],
|
||||
stdout=subprocess.PIPE)
|
||||
size = int(int(cmd.stdout.read().split()[0])/1024.0)
|
||||
os.chdir(debFolder)
|
||||
# Create data.tar.gz based on it.
|
||||
os.system('tar czvf data.tar.gz *')
|
||||
# Create the control file
|
||||
f = file('control', 'w')
|
||||
nameSuffix = ''
|
||||
dependencies = []
|
||||
if self.appName != 'appy':
|
||||
nameSuffix = '-%s' % self.appNameLower
|
||||
dependencies.append('python-appy')
|
||||
if self.depends:
|
||||
for d in self.depends: dependencies.append(d)
|
||||
depends = ''
|
||||
if dependencies:
|
||||
depends = ', ' + ', '.join(dependencies)
|
||||
f.write(debianInfo % (nameSuffix, self.appVersion, size,
|
||||
self.pythonVersions[0], depends))
|
||||
f.close()
|
||||
# Create md5sum file
|
||||
f = file('md5sums', 'w')
|
||||
toWalk = ['usr']
|
||||
if self.appName != 'appy':
|
||||
toWalk += ['etc', 'var']
|
||||
for folderToWalk in toWalk:
|
||||
for dir, dirnames, filenames in os.walk(folderToWalk):
|
||||
for name in filenames:
|
||||
m = md5.new()
|
||||
pathName = j(dir, name)
|
||||
currentFile = file(pathName, 'rb')
|
||||
while True:
|
||||
data = currentFile.read(8096)
|
||||
if not data:
|
||||
break
|
||||
m.update(data)
|
||||
currentFile.close()
|
||||
# Add the md5 sum to the file
|
||||
f.write('%s %s\n' % (m.hexdigest(), pathName))
|
||||
f.close()
|
||||
# Create postinst, a script that will:
|
||||
# - bytecompile Python files after the Debian install
|
||||
# - change ownership of some files if required
|
||||
# - [in the case of an app-package] call update-rc.d for starting it at
|
||||
# boot time.
|
||||
f = file('postinst', 'w')
|
||||
content = '#!/bin/sh\nset -e\n'
|
||||
for version in self.pythonVersions:
|
||||
bin = '/usr/bin/python%s' % version
|
||||
lib = '/usr/lib/python%s' % version
|
||||
cmds = ' %s -m compileall -q %s/%s 2> /dev/null\n' % (bin, lib,
|
||||
self.appName)
|
||||
content += 'if [ -e %s ]\nthen\n%sfi\n' % (bin, cmds)
|
||||
if self.appName != 'appy':
|
||||
# Allow user "zope", that runs the Zope instance, to write the
|
||||
# database and log files.
|
||||
content += 'chown -R zope:root /var/lib/%s\n' % self.appNameLower
|
||||
content += 'chown -R zope:root /var/log/%s\n' % self.appNameLower
|
||||
# Call update-rc.d for starting the app at boot time
|
||||
content += 'update-rc.d %s defaults\n' % self.appNameLower
|
||||
content += 'update-rc.d oo defaults\n'
|
||||
# (re-)start the app
|
||||
content += '%sctl restart\n' % self.appNameLower
|
||||
# (re-)start oo
|
||||
content += 'startoo\n'
|
||||
f.write(content)
|
||||
f.close()
|
||||
# Create prerm, a script that will remove all pyc files before removing
|
||||
# the Debian package.
|
||||
f = file('prerm', 'w')
|
||||
content = '#!/bin/sh\nset -e\n'
|
||||
for version in self.pythonVersions:
|
||||
content += 'find /usr/lib/python%s/%s -name "*.pyc" -delete\n' % \
|
||||
(version, self.appName)
|
||||
f.write(content)
|
||||
f.close()
|
||||
# Create control.tar.gz
|
||||
os.system('tar czvf control.tar.gz ./control ./md5sums ./postinst ' \
|
||||
'./prerm')
|
||||
# Create debian-binary
|
||||
f = file('debian-binary', 'w')
|
||||
f.write('2.0\n')
|
||||
f.close()
|
||||
# Create the signature if required
|
||||
if self.sign:
|
||||
# Create the concatenated version of all files within the deb
|
||||
os.system('cat debian-binary control.tar.gz data.tar.gz > ' \
|
||||
'/tmp/combined-contents')
|
||||
os.system('gpg -abs -o _gpgorigin /tmp/combined-contents')
|
||||
signFile = '_gpgorigin '
|
||||
os.remove('/tmp/combined-contents')
|
||||
# Export the public key and name it according to its ID as found by
|
||||
# analyzing the result of command "gpg --fingerprint".
|
||||
cmd = subprocess.Popen(['gpg', '--fingerprint'],
|
||||
stdout=subprocess.PIPE)
|
||||
fingerprint = cmd.stdout.read().split('\n')
|
||||
id = 'pubkey'
|
||||
for line in fingerprint:
|
||||
if '=' not in line: continue
|
||||
id = line.split('=')[1].strip()
|
||||
id = ''.join(id.split()[-4:])
|
||||
break
|
||||
os.system('gpg --export -a > %s/%s.asc' % (self.out, id))
|
||||
else:
|
||||
signFile = ''
|
||||
# Create the .deb package
|
||||
debName = 'python-appy%s-%s.deb' % (nameSuffix, self.appVersion)
|
||||
os.system('ar -r %s %sdebian-binary control.tar.gz data.tar.gz' % \
|
||||
(debName, signFile))
|
||||
# Move it to self.out
|
||||
os.rename(j(debFolder, debName), j(self.out, debName))
|
||||
# Clean temp files
|
||||
FolderDeleter.delete(debFolder)
|
||||
os.chdir(curdir)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,234 +0,0 @@
|
|||
'''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.shared.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_STRUCT_ELEM_NOT_FOUND = 'Structure used by parameter "%s" does not define '\
|
||||
'an attribute named "%s."'
|
||||
SAP_STRING_REQUIRED = 'Type mismatch for attribute "%s" used in parameter ' \
|
||||
'"%s": a string value is expected (SAP type is %s).'
|
||||
SAP_STRING_OVERFLOW = 'A string value for attribute "%s" used in parameter ' \
|
||||
'"%s" is too long (SAP type is %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 SapResult:
|
||||
'''Represents a result as returned by SAP. It defines a __getattr__ method
|
||||
that allows to retrieve SAP "output" parameters (export, tables) by their
|
||||
name (as if they were attributes of this class), in a Python format
|
||||
(list, dict, simple value).'''
|
||||
def __init__(self, function):
|
||||
# The pysap function obj that was called and that produced this result.
|
||||
self.function = function
|
||||
def __getattr__(self, name):
|
||||
'''Allows a smart access to self.function's results.'''
|
||||
if name.startswith('__'): raise AttributeError
|
||||
paramValue = self.function[name]
|
||||
paramType = paramValue.__class__.__name__
|
||||
if paramType == 'ItTable':
|
||||
return paramValue.to_list()
|
||||
elif paramType == 'STRUCT':
|
||||
return paramValue.to_dict()
|
||||
else:
|
||||
return paramValue
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
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.
|
||||
self.functionName = None # The name of the next function to call.
|
||||
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 as 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 createStructure(self, structDef, userData, paramName):
|
||||
'''Create a struct corresponding to SAP/C structure definition
|
||||
p_structDef and fills it with dict p_userData.'''
|
||||
res = structDef()
|
||||
for name, value in userData.items():
|
||||
if name not in structDef._sfield_names_:
|
||||
raise SapError(SAP_STRUCT_ELEM_NOT_FOUND % (paramName, name))
|
||||
sapType = structDef._sfield_sap_types_[name]
|
||||
# Check if the value is valid according to the required type
|
||||
if sapType[0] == 'C':
|
||||
sType = '%s%d' % (sapType[0], sapType[1])
|
||||
# "None" value is tolerated.
|
||||
if value == None: value = ''
|
||||
if not isinstance(value, str):
|
||||
raise SapError(
|
||||
SAP_STRING_REQUIRED % (name, paramName, sType))
|
||||
if len(value) > sapType[1]:
|
||||
raise SapError(
|
||||
SAP_STRING_OVERFLOW % (name, paramName, sType))
|
||||
# Left-fill the string with blanks.
|
||||
v = value.ljust(sapType[1])
|
||||
else:
|
||||
v = value
|
||||
res[name.lower()] = v
|
||||
return res
|
||||
|
||||
def call(self, functionName=None, **params):
|
||||
'''Calls a function on the SAP server.'''
|
||||
try:
|
||||
if not functionName:
|
||||
functionName = self.functionName
|
||||
function = self.sap.get_interface(functionName)
|
||||
# Specify the parameters
|
||||
for name, value in params.items():
|
||||
if type(value) == dict:
|
||||
# The param corresponds to a SAP/C "struct"
|
||||
v = self.createStructure(
|
||||
self.sap.get_structure(name),value, name)
|
||||
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(self.createStructure(v.struc, dValue, name))
|
||||
else:
|
||||
v = value
|
||||
function[name] = v
|
||||
# Call the function
|
||||
function()
|
||||
except pysap.BaseSapRfcError as se:
|
||||
raise SapError(SAP_FUNCTION_ERROR % (functionName, str(se)))
|
||||
return SapResult(function)
|
||||
|
||||
def __getattr__(self, name):
|
||||
'''The user can directly call self.<sapFunctionName>(params) instead of
|
||||
calling self.call(<sapFunctionName>, params).'''
|
||||
if name.startswith('__'): raise AttributeError
|
||||
self.functionName = name
|
||||
return self.call
|
||||
|
||||
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 as ee:
|
||||
pass
|
||||
return res
|
||||
except pysap.BaseSapRfcError as 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 as se:
|
||||
raise SapError(SAP_DISCONNECT_ERROR % str(se))
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,87 +0,0 @@
|
|||
'''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 as se:
|
||||
sys.stderr.write(str(se))
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(ERROR_CODE)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
AskSap().run()
|
||||
# ------------------------------------------------------------------------------
|
412
bin/backup.py
|
@ -1,412 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import sys, time, os, os.path, smtplib, socket, popen2, shutil
|
||||
from optparse import OptionParser
|
||||
import ZODB.FileStorage
|
||||
import ZODB.serialize
|
||||
from DateTime import DateTime
|
||||
from io import StringIO
|
||||
folderName = os.path.dirname(__file__)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class BackupError(Exception): pass
|
||||
ERROR_CODE = 1
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ZodbBackuper:
|
||||
'''This backuper will run every night (after 00.00). Every night excepted
|
||||
Sunday, it will perform an incremental backup. Every Sunday, the script
|
||||
will pack the ZODB, perform a full backup, and, if successful, remove all
|
||||
previous (full and incremental) backups.'''
|
||||
fullBackupExts = ('.fs', '.fsz')
|
||||
allBackupExts = ('.dat', '.deltafs', '.deltafsz') + fullBackupExts
|
||||
toRemoveExts = ('.doc', '.pdf', '.rtf', '.odt')
|
||||
def __init__(self, storageLocation, backupFolder, options):
|
||||
self.storageLocation = storageLocation
|
||||
self.backupFolder = backupFolder
|
||||
self.options = options
|
||||
# Unwrap some options directly on self.
|
||||
self.repozo = options.repozo or './repozo.py'
|
||||
self.zopectl = options.zopectl or './zopectl'
|
||||
self.logFile = file(options.logFile, 'a')
|
||||
self.logMem = StringIO() # We keep a log of the last script execution,
|
||||
# so we can send this info by email.
|
||||
self.emails = options.emails
|
||||
self.tempFolder = options.tempFolder
|
||||
self.logsBackupFolder = options.logsBackupFolder
|
||||
self.zopeUser = options.zopeUser
|
||||
self.keepSeconds = int(options.keepSeconds)
|
||||
|
||||
def log(self, msg):
|
||||
for logPlace in (self.logFile, self.logMem):
|
||||
logPlace.write(msg)
|
||||
logPlace.write('\n')
|
||||
|
||||
def executeCommand(self, cmd):
|
||||
'''Executes command p_cmd.'''
|
||||
w = self.log
|
||||
w('Executing "%s"...' % cmd)
|
||||
outstream, instream = popen2.popen4(cmd)
|
||||
outTxt = outstream.readlines()
|
||||
instream.close()
|
||||
outstream.close()
|
||||
for line in outTxt:
|
||||
w(line[:-1])
|
||||
w('Done.')
|
||||
|
||||
def packZodb(self):
|
||||
'''Packs the ZODB and keeps one week history.'''
|
||||
storage = ZODB.FileStorage.FileStorage(self.storageLocation)
|
||||
#storage.pack(time.time()-(7*24*60*60), ZODB.serialize.referencesf)
|
||||
storage.pack(time.time()-self.keepSeconds, ZODB.serialize.referencesf)
|
||||
for fileSuffix in ('', '.index'):
|
||||
fileName = self.storageLocation + fileSuffix
|
||||
os.system('chown %s %s' % (self.zopeUser, fileName))
|
||||
|
||||
def removeDataFsOld(self):
|
||||
'''Removes the file Data.fs.old if it exists.
|
||||
|
||||
In the process of packing the ZODB, an additional file Data.fs.pack
|
||||
is created, and renamed to Data.fs once finished. It means that, when
|
||||
we pack the ZODB, 3 copies of the DB can be present at the same time:
|
||||
Data.fs, Data.fs.old and Data.fs.pack. We prefer to remove the
|
||||
Data.fs.old copy to avoid missing disk space if the DB is big.
|
||||
'''
|
||||
old = self.storageLocation + '.old'
|
||||
if os.path.exists(old):
|
||||
self.log('Removing %s...' % old)
|
||||
os.remove(old)
|
||||
self.log('Done.')
|
||||
|
||||
folderCreateError = 'Could not create backup folder. Backup of log ' \
|
||||
'files will not take place. %s'
|
||||
def backupLogs(self):
|
||||
w = self.log
|
||||
if not os.path.exists(self.logsBackupFolder):
|
||||
# Try to create the folder when to store backups of the log files
|
||||
try:
|
||||
w('Try to create backup folder for logs "%s"...' % \
|
||||
self.logsBackupFolder)
|
||||
os.mkdir(self.logsBackupFolder)
|
||||
except IOError as ioe:
|
||||
w(folderCreateError % str(ioe))
|
||||
except OSError as oe:
|
||||
w(folderCreateError % str(oe))
|
||||
if os.path.exists(self.logsBackupFolder):
|
||||
# Ok, we can make the backup of the log files.
|
||||
# Get the folder where logs lie
|
||||
logsFolder = self.options.logsFolder
|
||||
d = os.path.dirname
|
||||
j = os.path.join
|
||||
if not logsFolder:
|
||||
logsFolder = j(d(d(self.storageLocation)), 'log')
|
||||
if not os.path.isdir(logsFolder):
|
||||
w('Cannot backup log files because folder "%s" does not ' \
|
||||
'exist. Try using option "-g".' % logsFolder)
|
||||
return
|
||||
for logFileName in os.listdir(logsFolder):
|
||||
if logFileName.endswith('.log'):
|
||||
backupTime = DateTime().strftime('%Y_%m_%d_%H_%M')
|
||||
parts = os.path.splitext(logFileName)
|
||||
copyFileName = '%s.%s%s' % (parts[0], backupTime, parts[1])
|
||||
absCopyFileName = j(self.logsBackupFolder, copyFileName)
|
||||
absLogFileName = j(logsFolder, logFileName)
|
||||
w('Moving "%s" to "%s"...' % (absLogFileName,
|
||||
absCopyFileName))
|
||||
shutil.copyfile(absLogFileName, absCopyFileName)
|
||||
os.remove(absLogFileName)
|
||||
# I do a "copy" + a "remove" instead of a "rename" because
|
||||
# a "rename" fails if the source and dest files are on
|
||||
# different physical devices.
|
||||
|
||||
def getDate(self, dateString):
|
||||
'''Returns a DateTime instance from p_dateString, which has the form
|
||||
YYYY-MM-DD-HH-MM-SS.'''
|
||||
return DateTime('%s/%s/%s %s:%s:%s' % tuple(dateString.split('-')))
|
||||
|
||||
def removeOldBackups(self):
|
||||
'''This method removes all files (full & incremental backups) that are
|
||||
older than the last full backup.'''
|
||||
w = self.log
|
||||
# Determine date of the oldest full backup
|
||||
oldestFullBackupDate = eighties = DateTime('1980/01/01')
|
||||
for backupFile in os.listdir(self.backupFolder):
|
||||
fileDate, ext = os.path.splitext(backupFile)
|
||||
if ext in self.fullBackupExts:
|
||||
# I have found a full backup
|
||||
fileDate = self.getDate(fileDate)
|
||||
if fileDate > oldestFullBackupDate:
|
||||
oldestFullBackupDate = fileDate
|
||||
# Remove all backup files older that oldestFullBackupDate
|
||||
if oldestFullBackupDate != eighties:
|
||||
w('Last full backup date: %s' % str(oldestFullBackupDate))
|
||||
for backupFile in os.listdir(self.backupFolder):
|
||||
fileDate, ext = os.path.splitext(backupFile)
|
||||
if (ext in self.allBackupExts) and \
|
||||
(self.getDate(fileDate) < oldestFullBackupDate):
|
||||
fullFileName = '%s/%s' % (self.backupFolder, backupFile)
|
||||
w('Removing old backup file %s...' % fullFileName)
|
||||
os.remove(fullFileName)
|
||||
|
||||
def sendEmails(self):
|
||||
'''Send content of self.logMem to self.emails.'''
|
||||
w = self.log
|
||||
subject = 'Backup notification.'
|
||||
msg = 'From: %s\nTo: %s\nSubject: %s\n\n%s' % (self.options.fromAddress,
|
||||
self.emails, subject, self.logMem.getvalue())
|
||||
try:
|
||||
w('> Sending mail notifications to %s...' % self.emails)
|
||||
smtpInfo = self.options.smtpServer.split(':', 3)
|
||||
login = password = None
|
||||
if len(smtpInfo) == 2:
|
||||
# We simply have server and port
|
||||
server, port = smtpInfo
|
||||
else:
|
||||
# We also have login and password
|
||||
server, port, login, password = smtpInfo
|
||||
smtpServer = smtplib.SMTP(server, port=int(port))
|
||||
if login:
|
||||
smtpServer.login(login, password)
|
||||
res = smtpServer.sendmail(self.options.fromAddress,
|
||||
self.emails.split(','), msg)
|
||||
smtpServer.quit()
|
||||
if res:
|
||||
w('Could not send mail to some recipients. %s' % str(res))
|
||||
w('Done.')
|
||||
except smtplib.SMTPException, sme:
|
||||
w('Error while contacting SMTP server %s (%s).' % \
|
||||
(self.options.smtpServer, str(se)))
|
||||
except socket.error as se:
|
||||
w('Could not connect to SMTP server %s (%s).' % \
|
||||
(self.options.smtpServer, str(se)))
|
||||
|
||||
def removeTempFiles(self):
|
||||
'''For EGW, OO produces temp files that EGW tries do delete at the time
|
||||
they are produced. But in some cases EGW can't do it (ie Zope runs
|
||||
with a given user and OO runs with root and produces files that can't
|
||||
be deleted by the user running Zope). This is why in this script we
|
||||
remove the temp files that could not be removed by Zope.'''
|
||||
w = self.log
|
||||
w('Removing temp files in "%s"...' % self.tempFolder)
|
||||
pdfCount = docCount = rtfCount = odtCount = 0
|
||||
for fileName in os.listdir(self.tempFolder):
|
||||
ext = os.path.splitext(fileName)[1]
|
||||
if ext in self.toRemoveExts:
|
||||
exec('%sCount += 1' % ext[1:])
|
||||
fullFileName = os.path.join(self.tempFolder, fileName)
|
||||
#w('Removing "%s"...' % fullFileName)
|
||||
try:
|
||||
os.remove(fullFileName)
|
||||
except OSError as oe:
|
||||
w('Could not remove "%s" (%s).' % (fullFileName, str(oe)))
|
||||
w('%d .pdf, %d .doc, %d .rtf and %d .odt file(s) removed.' % \
|
||||
(pdfCount, docCount, rtfCount, odtCount))
|
||||
|
||||
def run(self):
|
||||
w = self.log
|
||||
startTime = time.time()
|
||||
mode = self.options.mode
|
||||
w('\n****** Backup launched at %s (mode: %s) ******' % \
|
||||
(str(time.asctime()), mode))
|
||||
# Shutdown the Zope instance
|
||||
w('> Shutting down Zope instance...')
|
||||
self.executeCommand('%s stop' % self.zopectl)
|
||||
# Check if we are on the "full backup day"
|
||||
dayFull = self.options.dayFullBackup
|
||||
if time.asctime().startswith(dayFull):
|
||||
# If mode 'zodb', let's pack the ZODB first. Afterwards it will
|
||||
# trigger a full backup.
|
||||
if mode == 'zodb':
|
||||
# As a preamble to packing the ZODB, remove Data.fs.old if
|
||||
# present.
|
||||
self.removeDataFsOld()
|
||||
w('> Day is "%s", packing the ZODB...' % dayFull)
|
||||
self.packZodb()
|
||||
w('Pack done.')
|
||||
elif mode == 'copy':
|
||||
dest = os.path.join(self.backupFolder, 'Data.fs.new')
|
||||
w('> Day is "%s", copying %s to %s...' % \
|
||||
(dayFull, self.storageLocation, dest))
|
||||
# Perform a copy of Data.fs to the backup folder.
|
||||
shutil.copyfile(self.storageLocation, dest)
|
||||
# The copy has succeeded. Remove the previous copy and rename
|
||||
# this one.
|
||||
oldDest = os.path.join(self.backupFolder, 'Data.fs')
|
||||
w('> Copy successful. Renaming %s to %s...' % (dest, oldDest))
|
||||
if os.path.exists(oldDest):
|
||||
os.remove(oldDest)
|
||||
w('> (Old existing backup %s was removed).' % oldDest)
|
||||
os.rename(dest, oldDest)
|
||||
w('Done.')
|
||||
# Make a backup of the log files...
|
||||
w('> Make a backup of log files...')
|
||||
self.backupLogs()
|
||||
w('Log files copied.')
|
||||
else:
|
||||
if mode == 'copy':
|
||||
w('Copy mode: nothing to copy: day is not %s.' % dayFull)
|
||||
# Do the backup with repozo
|
||||
if mode == 'zodb':
|
||||
w('> Performing backup...')
|
||||
self.executeCommand('%s %s -BvzQ -r %s -f %s' % \
|
||||
(self.options.python, self.repozo, self.backupFolder,
|
||||
self.storageLocation))
|
||||
# Remove previous full backups.
|
||||
self.removeOldBackups()
|
||||
# If a command is specified, run Zope to execute this command
|
||||
if self.options.command:
|
||||
w('> Executing command "%s"...' % self.options.command)
|
||||
jobScript = '%s/job.py' % folderName
|
||||
cmd = '%s run %s %s' % (self.zopectl, jobScript,
|
||||
self.options.command)
|
||||
self.executeCommand(cmd)
|
||||
# Start the instance again, in normal mode.
|
||||
w('> Restarting Zope instance...')
|
||||
self.executeCommand('%s start' % self.zopectl)
|
||||
self.removeTempFiles()
|
||||
stopTime = time.time()
|
||||
w('Done in %d minute(s).' % ((stopTime-startTime)/60))
|
||||
if self.emails:
|
||||
self.sendEmails()
|
||||
self.logFile.close()
|
||||
print((self.logMem.getvalue()))
|
||||
self.logMem.close()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ZodbBackupScript:
|
||||
'''usage: python backup.py storageLocation backupFolder [options]
|
||||
storageLocation is the path to a ZODB database (file storage) (ie
|
||||
/opt/ZopeInstance/var/Data.fs);
|
||||
backupFolder is a folder exclusively dedicated for storing backups
|
||||
of the mentioned storage (ie /data/zodbbackups).'''
|
||||
|
||||
weekDays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
|
||||
def checkArgs(self, options, args):
|
||||
'''Check that the scripts arguments are correct.'''
|
||||
# Do I have the correct number of args?
|
||||
if len(args) != 2:
|
||||
raise BackupError('Wrong number of arguments.')
|
||||
# Check storageLocation
|
||||
if not os.path.exists(args[0]) or not os.path.isfile(args[0]):
|
||||
raise BackupError('"%s" does not exist or is not a file.' % args[0])
|
||||
# Check backupFolder
|
||||
if not os.path.isdir(args[1]):
|
||||
raise BackupError('"%s" does not exist or is not a folder.'%args[1])
|
||||
# Check logs folder
|
||||
if options.logsFolder and not os.path.isdir(options.logsFolder):
|
||||
raise BackupError('"%s" is not a folder.' % options.logsFolder)
|
||||
# Try to create a file in this folder to check if we have write
|
||||
# access in it.
|
||||
fileName = '%s/%s.tmp' % (args[1], str(time.time()))
|
||||
try:
|
||||
f = file(fileName, 'w')
|
||||
f.write('Hello.')
|
||||
f.close()
|
||||
os.remove(fileName)
|
||||
except OSError as oe:
|
||||
raise BackupError('I do not have the right to write in ' \
|
||||
'folder "%s".' % args[1])
|
||||
# Check temp folder
|
||||
if not os.path.isdir(options.tempFolder):
|
||||
raise BackupError('Temp folder "%s" does not exist or is not ' \
|
||||
'a folder.' % options.tempFolder)
|
||||
# Check day of week
|
||||
if options.dayFullBackup not in self.weekDays:
|
||||
raise BackupError(
|
||||
'Day of week must be one of %s' % str(self.weekDays))
|
||||
# Check command format
|
||||
if options.command:
|
||||
parts = options.command.split(':')
|
||||
if len(parts) not in (4,5):
|
||||
raise BackupError('Command format must be ' \
|
||||
'<ZopeAdmin><PloneInstancePath>:<ApplicationName>:' \
|
||||
'<ToolMethodName>[:<args>]')
|
||||
|
||||
def run(self):
|
||||
optParser = OptionParser(usage=ZodbBackupScript.__doc__)
|
||||
optParser.add_option("-p", "--python", dest="python",
|
||||
help="The path to the Python interpreter running Zope",
|
||||
default='python2.4',metavar="PYTHON",type='string')
|
||||
optParser.add_option("-r", "--repozo", dest="repozo",
|
||||
help="The path to repozo.py", default='', metavar="REPOZO",
|
||||
type='string')
|
||||
optParser.add_option("-z", "--zopectl", dest="zopectl",
|
||||
help="The path to Zope instance's zopectl script", default='',
|
||||
metavar="ZOPECTL", type='string')
|
||||
optParser.add_option("-l", "--logfile", dest="logFile",
|
||||
help="Log file where this script will append output (defaults to " \
|
||||
"./backup.log)", default='./backup.log', metavar="LOGFILE",
|
||||
type='string')
|
||||
optParser.add_option("-d", "--day-full-backup", dest="dayFullBackup",
|
||||
help="Day of the week where the full backup must be performed " \
|
||||
"(defaults to 'Sun'). Must be one of %s" % str(self.weekDays),
|
||||
default='Sun', metavar="DAYFULLBACKUP", type='string')
|
||||
optParser.add_option("-e", "--emails", dest="emails",
|
||||
help="Comma-separated list of emails that will receive the log " \
|
||||
"of this script.", default='', metavar="EMAILS", type='string')
|
||||
optParser.add_option("-f", "--from-address", dest="fromAddress",
|
||||
help="From address for the sent mails", default='',
|
||||
metavar="FROMADDRESS", type='string')
|
||||
optParser.add_option("-s", "--smtp-server", dest="smtpServer",
|
||||
help="SMTP server and port (ie: localhost:25) for sending mails. " \
|
||||
"You can also embed username and password if the SMTP " \
|
||||
"server requires authentication, ie localhost:25:myLogin:" \
|
||||
"myPassword", default='localhost:25', metavar="SMTPSERVER",
|
||||
type='string')
|
||||
optParser.add_option("-t", "--tempFolder", dest="tempFolder",
|
||||
help="Folder used by LibreOffice for producing temp files. " \
|
||||
"Defaults to /tmp.", default='/tmp', metavar="TEMP",
|
||||
type='string')
|
||||
optParser.add_option("-g", "--logsFolder",dest="logsFolder",
|
||||
help="Folder where Zope log files are (typically: event.log and " \
|
||||
"Z2.log). If no folder is provided, we will consider to " \
|
||||
"work on a standard Zope instance and decide that the log " \
|
||||
"folder is, from 'storageLocation', located at ../log",
|
||||
metavar="LOGSFOLDER", type='string')
|
||||
optParser.add_option("-b", "--logsBackupFolder",dest="logsBackupFolder",
|
||||
help="Folder where backups of log files (event.log and Z2.log) " \
|
||||
"will be stored.", default='./logsbackup',
|
||||
metavar="LOGSBACKUPFOLDER", type='string')
|
||||
|
||||
optParser.add_option("-u", "--user", dest="zopeUser",
|
||||
help="User and group that must own Data.fs. Defaults to " \
|
||||
"zope:www-data. If this script is launched by root, for " \
|
||||
"example, when packing the ZODB this script may produce a " \
|
||||
"new Data.fs that the user running Zope may not be able to " \
|
||||
"read anymore. After packing, this script makes a 'chmod' " \
|
||||
"on Data.fs.", default='zope:www-data', metavar="USER",
|
||||
type='string')
|
||||
optParser.add_option("-k", "--keep-seconds", dest="keepSeconds",
|
||||
help="Number of seconds to leave in the ZODB history when the " \
|
||||
"ZODB is packed.", default='86400', metavar="KEEPSECONDS",
|
||||
type='string')
|
||||
optParser.add_option("-m", "--mode", dest="mode", help="Default mode, "\
|
||||
"'zodb', uses repozo for performing backups. Mode 'copy' simply " \
|
||||
"performs a copy of the database to the specified backup folder.",
|
||||
default='zodb', metavar="MODE", type='string')
|
||||
optParser.add_option("-c", "--command", dest="command",
|
||||
help="Command to execute while Zope is running. It must have the " \
|
||||
"following format: <ZopeAdmin>:<PloneInstancePath>:" \
|
||||
"<ApplicationName>:<ToolMethodName>[:<args>]. <ZopeAdmin> is the " \
|
||||
"user name of the Zope administrator; <PloneInstancePath> is the " \
|
||||
"path, within Zope, to the Plone Site object (if not at the " \
|
||||
"root of the Zope hierarchy, use '/' as folder separator); " \
|
||||
"<ApplicationName> is the name of the Appy application; " \
|
||||
"<ToolMethodName> is the name of the method to call on the tool " \
|
||||
"in this Appy application; (optional) <args> are the arguments " \
|
||||
"to give to this method (only strings are supported). Several " \
|
||||
"arguments must be separated by '*'.", default='',
|
||||
metavar="COMMAND", type='string')
|
||||
(options, args) = optParser.parse_args()
|
||||
try:
|
||||
self.checkArgs(options, args)
|
||||
backuper = ZodbBackuper(args[0], args[1], options)
|
||||
backuper.run()
|
||||
except BackupError as be:
|
||||
sys.stderr.write(str(be) + '\nRun the script without any ' \
|
||||
'argument for getting help.\n')
|
||||
sys.exit(ERROR_CODE)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
ZodbBackupScript().run()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,48 +0,0 @@
|
|||
'''This script allows to check a LDAP connection.'''
|
||||
import sys
|
||||
from appy.shared.ldap_connector import LdapConnector
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class LdapTester:
|
||||
'''Usage: python checkldap.py ldapUri login password base attrs filter scope
|
||||
|
||||
ldapUri is, for example, "ldap://127.0.0.1:389"
|
||||
login is the login user DN, ie: "cn=gdy,o=geezteem"
|
||||
password is the password for this login
|
||||
base is the base DN where to perform the search, ie "ou=hr,o=GeezTeem"
|
||||
attrs is a comma-separated list of attrs we will retrieve in the LDAP,
|
||||
ie "uid,login"
|
||||
filter is the query filter, ie "(&(attr1=Geez*)(status=OK))"
|
||||
scope is the scope of the search, and can be:
|
||||
BASE To search the object itself on base
|
||||
ONELEVEL To search base's immediate children
|
||||
SUBTREE To search base and all its descendants
|
||||
'''
|
||||
def __init__(self):
|
||||
# Get params from shell args.
|
||||
if len(sys.argv) != 8:
|
||||
print((LdapTester.__doc__))
|
||||
sys.exit(0)
|
||||
s = self
|
||||
s.uri,s.login,s.password,s.base,s.attrs,s.filter,s.scope = sys.argv[1:]
|
||||
self.attrs = self.attrs.split(',')
|
||||
self.tentatives = 5
|
||||
self.timeout = 5
|
||||
self.attributes = ['cn']
|
||||
self.ssl = False
|
||||
|
||||
def test(self):
|
||||
# Connect the the LDAP
|
||||
print(('Connecting to... %s' % self.uri))
|
||||
connector = LdapConnector(self.uri)
|
||||
success, msg = connector.connect(self.login, self.password)
|
||||
if not success: return
|
||||
# Perform the query.
|
||||
print(('Querying %s...' % self.base))
|
||||
res = connector.search(self.base, self.scope, self.filter,
|
||||
self.attributes)
|
||||
print(('Got %d results' % len(res)))
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__': LdapTester().test()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,42 +0,0 @@
|
|||
'''This script allows to check the generation of PDF files via LibreOffice.'''
|
||||
import sys, os.path
|
||||
import appy
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
usage = '''Usage: python checklo.py [port]
|
||||
|
||||
If port is not speficied, it defaults to 2002.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class LoChecker:
|
||||
def __init__(self, port):
|
||||
self.port = port
|
||||
# Get an ODT file from the pod test suite.
|
||||
self.appyFolder = os.path.dirname(appy.__file__)
|
||||
self.odtFile = os.path.join(self.appyFolder, 'pod', 'test',
|
||||
'templates', 'NoPython.odt')
|
||||
|
||||
def run(self):
|
||||
# Call LO in server mode to convert self.odtFile to PDF
|
||||
converter = os.path.join(self.appyFolder, 'pod', 'converter.py')
|
||||
cmd = 'python %s %s pdf -p %d' % (converter, self.odtFile, self.port)
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
# Check if the PDF was generated
|
||||
pdfFile = '%s.pdf' % os.path.splitext(self.odtFile)[0]
|
||||
if not os.path.exists(pdfFile):
|
||||
print('PDF was not generated.')
|
||||
else:
|
||||
os.remove(pdfFile)
|
||||
print('Check successfull.')
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
nbOfArgs = len(sys.argv)
|
||||
if nbOfArgs not in (1, 2):
|
||||
print(usage)
|
||||
sys.exit()
|
||||
# Get the nb of args
|
||||
port = (nbOfArgs == 2) and int(sys.argv[1]) or 2002
|
||||
LoChecker(port).run()
|
||||
# ------------------------------------------------------------------------------
|
23
bin/clean.py
|
@ -1,23 +0,0 @@
|
|||
# Imports ----------------------------------------------------------------------
|
||||
import os, os.path
|
||||
from appy.shared import appyPath
|
||||
from appy.shared.utils import FolderDeleter, cleanFolder
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Cleaner:
|
||||
def run(self, verbose=True):
|
||||
cleanFolder(appyPath, verbose=verbose)
|
||||
# Remove all files in temp folders
|
||||
for tempFolder in ('%s/temp' % appyPath,
|
||||
'%s/pod/test/temp' % appyPath):
|
||||
if os.path.exists(tempFolder):
|
||||
FolderDeleter.delete(tempFolder)
|
||||
# Remove test reports if any
|
||||
for testReport in ('%s/pod/test/Tester.report.txt' % appyPath,):
|
||||
if os.path.exists(testReport):
|
||||
os.remove(testReport)
|
||||
|
||||
# Main program -----------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
Cleaner().run()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,35 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
'''This script can be created on a Linux machine for creating a local Debian
|
||||
(binary) repository.'''
|
||||
|
||||
import os, os.path
|
||||
|
||||
# Packages apache2 and dpkg-dev must be installed on the machine for enabling
|
||||
# the Debian repository.
|
||||
repoFolder = '/var/www/debianrepo'
|
||||
|
||||
# Create the repo folder if it does not exist
|
||||
binaryFolder = os.path.join(repoFolder, 'binary')
|
||||
if not os.path.exists(binaryFolder):
|
||||
os.makedirs(binaryFolder)
|
||||
|
||||
# Create the script that will allow to recompute indexes when packages are
|
||||
# added or updated into the repository.
|
||||
refreshScript = '''#!/bin/bash
|
||||
cd %s
|
||||
echo "(Re-)building indexes for binary packages..."
|
||||
dpkg-scanpackages binary /dev/null | gzip -9c > binary/Packages.gz
|
||||
echo "Done."
|
||||
''' % repoFolder
|
||||
|
||||
curdir = os.getcwd()
|
||||
os.chdir(repoFolder)
|
||||
scriptName = os.path.join(repoFolder, 'refresh.sh')
|
||||
if not os.path.exists(scriptName):
|
||||
f = file(scriptName, 'w')
|
||||
f.write(refreshScript)
|
||||
f.close()
|
||||
os.system('chmod -R 755 %s' % repoFolder)
|
||||
os.chdir(curdir)
|
||||
print('Repository created.')
|
180
bin/eggify.py
|
@ -1,180 +0,0 @@
|
|||
'''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, cleanFolder
|
||||
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
|
||||
# First, clean the Python module
|
||||
cleanFolder(self.pythonModule, verbose=False)
|
||||
# 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 as ee:
|
||||
sys.stderr.write(str(ee) + '\nRun eggify.py -h for getting help.\n')
|
||||
sys.exit(ERROR_CODE)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
EggifyScript().run()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,83 +0,0 @@
|
|||
'''This script allows to generate a Zope product from a Appy application.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import sys, os.path
|
||||
from optparse import OptionParser
|
||||
from appy.gen.generator import GeneratorError, ZopeGenerator
|
||||
from appy.shared.utils import LinesCounter
|
||||
from appy.shared.packaging import Debianizer
|
||||
import appy.version
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
ERROR_CODE = 1
|
||||
APP_NOT_FOUND = 'Application not found at %s.'
|
||||
WRONG_NG_OF_ARGS = 'Wrong number of arguments.'
|
||||
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.'
|
||||
D_OPTION = 'Generates a Debian package for this app. The Debian package will ' \
|
||||
'be generated at the same level as the root application folder.'
|
||||
|
||||
class GeneratorScript:
|
||||
'''usage: %prog [options] app
|
||||
|
||||
"app" is the path to your Appy application, which must be a
|
||||
Python package (= a folder containing a file named
|
||||
__init__.py). Your app may reside anywhere, but needs to
|
||||
be accessible by Zope. Typically, it may be or symlinked
|
||||
in <yourZopeInstance>/lib/python.
|
||||
|
||||
This command generates a Zope product in <app>/zope, which must be
|
||||
or symlinked in <yourZopeInstance>/Products.
|
||||
'''
|
||||
def manageArgs(self, parser, options, args):
|
||||
# Check number of args
|
||||
if len(args) != 1:
|
||||
print(WRONG_NG_OF_ARGS)
|
||||
parser.print_help()
|
||||
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)
|
||||
# Convert app path to an absolute path
|
||||
args[0] = os.path.abspath(args[0])
|
||||
|
||||
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("-d", "--debian", action='store_true',
|
||||
dest='debian', default=False, help=D_OPTION)
|
||||
(options, args) = optParser.parse_args()
|
||||
try:
|
||||
self.manageArgs(optParser, options, args)
|
||||
print(('Appy version: %s' % appy.version.verbose))
|
||||
print(('Generating Zope product in %s/zope...' % args[0]))
|
||||
ZopeGenerator(args[0], options).run()
|
||||
# Give the user some statistics about its code
|
||||
LinesCounter(args[0], excludes=['%szope' % os.sep]).run()
|
||||
# Generates a Debian package for this app if required
|
||||
if options.debian:
|
||||
app = args[0]
|
||||
appDir = os.path.dirname(app)
|
||||
appName = os.path.basename(app)
|
||||
# Get the app version from zope/version.txt
|
||||
f = file(os.path.join(app, 'zope', appName, 'version.txt'))
|
||||
version = f.read()
|
||||
f.close()
|
||||
version = version[:version.find('build')-1]
|
||||
Debianizer(app, appDir, appVersion=version).run()
|
||||
except GeneratorError as ge:
|
||||
sys.stderr.write(str(ge))
|
||||
sys.stderr.write('\n')
|
||||
optParser.print_help()
|
||||
sys.exit(ERROR_CODE)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
GeneratorScript().run()
|
||||
# ------------------------------------------------------------------------------
|
87
bin/job.py
|
@ -1,87 +0,0 @@
|
|||
'''job.py must be executed by a "zopectl run" command and, as single arg,
|
||||
must get a string with the following format:
|
||||
|
||||
<ZopeAdmin>:<PloneInstancePath>:<ApplicationName>:<ToolMethodName>[:<args>].
|
||||
|
||||
<ZopeAdmin> is the userName of the Zope administrator for this instance.
|
||||
<PloneInstancePath> is the path, within Zope, to the Plone Site object (if
|
||||
not at the root of the Zope hierarchy, use '/' as
|
||||
folder separator); leave blank if using appy.gen > 0.8
|
||||
|
||||
<ApplicationName> is the name of the Appy application. If it begins with
|
||||
"path=", it does not represent an Appy application, but
|
||||
the path, within <PloneInstancePath>, to any Zope object
|
||||
(use '/' as folder separator); leave blank if using
|
||||
appy.gen > 0.8;
|
||||
|
||||
<ToolMethodName> is the name of the method to call on the tool in this
|
||||
Appy application, or the method to call on the arbitrary
|
||||
Zope object if previous param starts with "path=".
|
||||
|
||||
<args> (optional) are the arguments to give to this method (only strings
|
||||
are supported). Several arguments must be separated by '*'.
|
||||
|
||||
Note that you can also specify several commands, separated with
|
||||
semicolons (";"). This scripts performs a single commit after all commands
|
||||
have been executed.
|
||||
'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import sys, transaction
|
||||
|
||||
# Check that job.py is called with the right parameters.
|
||||
if len(sys.argv) != 2:
|
||||
print('job.py was called with wrong args.')
|
||||
print(__doc__)
|
||||
else:
|
||||
commands = sys.argv[1].split(';')
|
||||
# Check that every command has the right number of sub-elelements.
|
||||
for command in commands:
|
||||
parts = command.split(':')
|
||||
if len(parts) not in (4,5):
|
||||
print('job.py was called with wrong args.')
|
||||
print(__doc__)
|
||||
|
||||
for command in commands:
|
||||
parts = command.split(':')
|
||||
# Unwrap parameters
|
||||
if len(parts) == 4:
|
||||
zopeUser, plonePath, appName, toolMethod = parts
|
||||
args = ()
|
||||
else:
|
||||
zopeUser, plonePath, appName, toolMethod, args = parts
|
||||
# Zope was initialized in a minimal way. Complete Zope install.
|
||||
from Testing import makerequest
|
||||
app = makerequest.makerequest(app)
|
||||
app.REQUEST._fake_ = True
|
||||
# Log as Zope admin
|
||||
from AccessControl.SecurityManagement import newSecurityManager
|
||||
user = app.acl_users.getUserById(zopeUser)
|
||||
if not user:
|
||||
# Try with user "admin"
|
||||
user = app.acl_users.getUserById('admin')
|
||||
if not hasattr(user, 'aq_base'):
|
||||
user = user.__of__(app.acl_users)
|
||||
newSecurityManager(None, user)
|
||||
# Find the root object.
|
||||
rootObject = app # Initialised with the Zope root object.
|
||||
if plonePath:
|
||||
for elem in plonePath.split('/'):
|
||||
rootObject = getattr(rootObject, elem)
|
||||
# If we are in a Appy application, the object on which we will call the
|
||||
# method is the config object on this root object.
|
||||
if not appName:
|
||||
targetObject = rootObject.config.appy()
|
||||
elif not appName.startswith('path='):
|
||||
objectName = 'portal_%s' % appName.lower()
|
||||
targetObject = getattr(rootObject, objectName).appy()
|
||||
else:
|
||||
# It can be any object.
|
||||
targetObject = rootObject
|
||||
for elem in appName[5:].split('/'):
|
||||
targetObject = getattr(targetObject, elem)
|
||||
# Execute the method on the target object
|
||||
if args: args = args.split('*')
|
||||
exec('targetObject.%s(*args)' % toolMethod)
|
||||
transaction.commit()
|
||||
# ------------------------------------------------------------------------------
|
395
bin/new.py
|
@ -1,395 +0,0 @@
|
|||
'''This script allows to create a brand new ready-to-use Plone/Zone instance.
|
||||
As prerequisite, you must have installed Plone through the Unifier installer
|
||||
available at http://plone.org.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, sys, shutil, re
|
||||
from optparse import OptionParser
|
||||
from appy.shared.utils import cleanFolder, copyFolder
|
||||
from appy.shared.packaging import ooStart, zopeConf
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class NewError(Exception): pass
|
||||
ERROR_CODE = 1
|
||||
WRONG_NB_OF_ARGS = 'Wrong number of args.'
|
||||
WRONG_PLONE_VERSION = 'Plone version must be among %s.'
|
||||
WRONG_PLONE_PATH = 'Path "%s" is not an existing folder.'
|
||||
PYTHON_NOT_FOUND = 'Python interpreter was not found in "%s". Are you sure ' \
|
||||
'we are in the folder hierarchy created by the Plone installer?'
|
||||
PYTHON_EXE_NOT_FOUND = '"%s" does not exist.'
|
||||
MKZOPE_NOT_FOUND = 'Script mkzopeinstance.py not found in "%s and ' \
|
||||
'subfolders. Are you sure we are in the folder hierarchy created by ' \
|
||||
'the Plone installer?'
|
||||
WRONG_INSTANCE_PATH = '"%s" must be an existing folder for creating the ' \
|
||||
'instance in it.'
|
||||
|
||||
# zopectl template file for a pure Zope instance -------------------------------
|
||||
zopeCtl = '''#!/bin/sh
|
||||
PYTHON="/usr/lib/zope2.12/bin/python"
|
||||
INSTANCE_HOME="%s"
|
||||
CONFIG_FILE="$INSTANCE_HOME/etc/zope.conf"
|
||||
ZDCTL="/usr/lib/zope2.12/bin/zopectl"
|
||||
export INSTANCE_HOME
|
||||
export PYTHON
|
||||
exec "$ZDCTL" -C "$CONFIG_FILE" "$@"
|
||||
'''
|
||||
|
||||
# runzope template file for a pure Zope instance -------------------------------
|
||||
runZope = '''#!/bin/sh
|
||||
INSTANCE_HOME="%s"
|
||||
CONFIG_FILE="$INSTANCE_HOME/etc/zope.conf"
|
||||
ZOPE_RUN="/usr/lib/zope2.12/bin/runzope"
|
||||
export INSTANCE_HOME
|
||||
exec "$ZOPE_RUN" -C "$CONFIG_FILE" "$@"
|
||||
'''
|
||||
|
||||
# zopectl template for a Plone (4) Zope instance -------------------------------
|
||||
zopeCtlPlone = '''#!/bin/sh
|
||||
PYTHON="%s"
|
||||
INSTANCE_HOME="%s"
|
||||
CONFIG_FILE="$INSTANCE_HOME/etc/zope.conf"
|
||||
PYTHONPATH="$INSTANCE_HOME/lib/python"
|
||||
ZDCTL="%s/Zope2/Startup/zopectl.py"
|
||||
export INSTANCE_HOME
|
||||
export PYTHON
|
||||
export PYTHONPATH
|
||||
exec "$PYTHON" "$ZDCTL" -C "$CONFIG_FILE" "$@"
|
||||
'''
|
||||
|
||||
# runzope template for a Plone (4) Zope instance -------------------------------
|
||||
runZopePlone = '''#! /bin/sh
|
||||
PYTHON="%s"
|
||||
INSTANCE_HOME="%s"
|
||||
CONFIG_FILE="$INSTANCE_HOME/etc/zope.conf"
|
||||
PYTHONPATH="$INSTANCE_HOME/lib/python"
|
||||
ZOPE_RUN="%s/Zope2/Startup/run.py"
|
||||
export INSTANCE_HOME
|
||||
export PYTHON
|
||||
export PYTHONPATH
|
||||
exec "$PYTHON" "$ZOPE_RUN" -C "$CONFIG_FILE" "$@"
|
||||
'''
|
||||
|
||||
# Patch to apply to file pkg_resources.py in a Plone4 Zope instance ------------
|
||||
pkgResourcesPatch = '''import os, os.path
|
||||
productsFolder = os.path.join(os.environ["INSTANCE_HOME"], "Products")
|
||||
for name in os.listdir(productsFolder):
|
||||
if os.path.isdir(os.path.join(productsFolder, name)):
|
||||
if name not in appyVersions:
|
||||
appyVersions[name] = "1.0"
|
||||
appyVersions['Products.%s' % name] = "1.0"
|
||||
|
||||
def getAppyVersion(req, location):
|
||||
global appyVersions
|
||||
if req.project_name not in appyVersions:
|
||||
raise DistributionNotFound(req)
|
||||
return Distribution(project_name=req.project_name,
|
||||
version=appyVersions[req.project_name],
|
||||
platform='linux2', location=location)
|
||||
'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ZopeInstanceCreator:
|
||||
'''This class allows to create a Zope instance. It makes the assumption that
|
||||
Zope was installed via the Debian package zope2.12.'''
|
||||
|
||||
def __init__(self, instancePath):
|
||||
self.instancePath = instancePath
|
||||
|
||||
def run(self):
|
||||
# Create the instance folder hierarchy
|
||||
if not os.path.exists(self.instancePath):
|
||||
os.makedirs(self.instancePath)
|
||||
curdir = os.getcwd()
|
||||
# Create bin/zopectl
|
||||
os.chdir(self.instancePath)
|
||||
os.mkdir('bin')
|
||||
f = file('bin/zopectl', 'w')
|
||||
f.write(zopeCtl % self.instancePath)
|
||||
f.close()
|
||||
os.chmod('bin/zopectl', 0o744) # Make it executable by owner.
|
||||
# Create bin/runzope
|
||||
f = file('bin/runzope', 'w')
|
||||
f.write(runZope % self.instancePath)
|
||||
f.close()
|
||||
os.chmod('bin/runzope', 0o744) # Make it executable by owner.
|
||||
# Create bin/startoo
|
||||
f = file('bin/startoo', 'w')
|
||||
f.write(ooStart)
|
||||
f.close()
|
||||
os.chmod('bin/startoo', 0o744) # Make it executable by owner.
|
||||
# Create etc/zope.conf
|
||||
os.mkdir('etc')
|
||||
f = file('etc/zope.conf', 'w')
|
||||
f.write(zopeConf % (self.instancePath, '%s/var' % self.instancePath,
|
||||
'%s/log' % self.instancePath, '8080', ''))
|
||||
f.close()
|
||||
# Create other folders
|
||||
for name in ('Extensions', 'log', 'Products', 'var'): os.mkdir(name)
|
||||
f = file('Products/__init__.py', 'w')
|
||||
f.write('#Makes me a Python package.\n')
|
||||
f.close()
|
||||
# Create 'inituser' file with admin password
|
||||
import binascii
|
||||
try:
|
||||
from hashlib import sha1 as sha
|
||||
except:
|
||||
from sha import new as sha
|
||||
f = open('inituser', 'w')
|
||||
password = binascii.b2a_base64(sha('admin').digest())[:-1]
|
||||
f.write('admin:{SHA}%s\n' % password)
|
||||
f.close()
|
||||
os.chmod('inituser', 0o644)
|
||||
# User "zope" must own this instance
|
||||
os.system('chown -R zope %s' % self.instancePath)
|
||||
print(('Zope instance created in %s.' % self.instancePath))
|
||||
os.chdir(curdir)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class NewScript:
|
||||
'''usage: %prog ploneVersion plonePath instancePath
|
||||
|
||||
"ploneVersion" can be plone25, plone30, plone3x, plone4 or zope
|
||||
(plone3x represents Plone 3.2.x, Plone 3.3.5...)
|
||||
|
||||
"plonePath" is the (absolute) path to your plone (or zope)
|
||||
installation. Plone 2.5 and 3.0 are typically installed
|
||||
in /opt/Plone-x.x.x, while Plone 3 > 3.0 is typically
|
||||
installed in in /usr/local/Plone.
|
||||
"instancePath" is the (absolute) path where you want to create your
|
||||
instance (should not already exist).'''
|
||||
ploneVersions = ('plone25', 'plone30', 'plone3x', 'plone4', 'zope')
|
||||
|
||||
def installPlone25or30Stuff(self, linksForProducts):
|
||||
'''Here, we will copy all Plone2-related stuff in the Zope instance
|
||||
we've created, to get a full Plone-ready Zope instance. If
|
||||
p_linksForProducts is True, we do not perform a real copy: we will
|
||||
create symlinks to products lying within Plone installer files.'''
|
||||
j = os.path.join
|
||||
if self.ploneVersion == 'plone25':
|
||||
sourceFolders = ('zeocluster/Products',)
|
||||
else:
|
||||
sourceFolders = ('zinstance/Products', 'zinstance/lib/python')
|
||||
for sourceFolder in sourceFolders:
|
||||
sourceBase = j(self.plonePath, sourceFolder)
|
||||
destBase = j(self.instancePath,
|
||||
sourceFolder[sourceFolder.find('/')+1:])
|
||||
for name in os.listdir(sourceBase):
|
||||
folderName = j(sourceBase, name)
|
||||
if os.path.isdir(folderName):
|
||||
destFolder = j(destBase, name)
|
||||
# This is a Plone product. Copy it to the instance.
|
||||
if linksForProducts:
|
||||
# Create a symlink to this product in the instance
|
||||
cmd = 'ln -s %s %s' % (folderName, destFolder)
|
||||
os.system(cmd)
|
||||
else:
|
||||
# Copy thre product into the instance
|
||||
copyFolder(folderName, destFolder)
|
||||
|
||||
filesToPatch = ('meta.zcml', 'configure.zcml', 'overrides.zcml')
|
||||
patchRex = re.compile('<includePlugins.*?/>', re.S)
|
||||
def patchPlone3x(self):
|
||||
'''Auto-proclaimed ugly code in z3c forces us to patch some files
|
||||
in Products.CMFPlone because these guys make the assumption that
|
||||
"plone.*" packages are within eggs when they've implemented their
|
||||
ZCML directives "includePlugins" and "includePluginsOverrides".
|
||||
So in this method, I remove every call to those directives in
|
||||
CMFPlone files. It does not seem to affect Plone behaviour. Indeed,
|
||||
these directives seem to be useful only when adding sad (ie, non
|
||||
Appy) Plone plug-ins.'''
|
||||
j = os.path.join
|
||||
ploneFolder = os.path.join(self.productsFolder, 'CMFPlone')
|
||||
# Patch files
|
||||
for fileName in self.filesToPatch:
|
||||
filePath = os.path.join(ploneFolder, fileName)
|
||||
f = file(filePath)
|
||||
fileContent = f.read()
|
||||
f.close()
|
||||
f = file(filePath, 'w')
|
||||
f.write(self.patchRex.sub('<!--Del. includePlugins-->',fileContent))
|
||||
f.close()
|
||||
|
||||
missingIncludes = ('plone.app.upgrade', 'plonetheme.sunburst',
|
||||
'plonetheme.classic')
|
||||
def patchPlone4(self, versions):
|
||||
'''Patches Plone 4 that can't live without buildout as-is.'''
|
||||
self.patchPlone3x() # We still need this for Plone 4 as well.
|
||||
# bin/zopectl
|
||||
content = zopeCtlPlone % (self.pythonPath, self.instancePath,
|
||||
self.zopePath)
|
||||
f = file('%s/bin/zopectl' % self.instancePath, 'w')
|
||||
f.write(content)
|
||||
f.close()
|
||||
# bin/runzope
|
||||
content = runZopePlone % (self.pythonPath, self.instancePath,
|
||||
self.zopePath)
|
||||
f = file('%s/bin/runzope' % self.instancePath, 'w')
|
||||
f.write(content)
|
||||
f.close()
|
||||
j = os.path.join
|
||||
# As eggs have been deleted, versions of components are lost. Reify
|
||||
# them from p_versions.
|
||||
dVersions = ['"%s":"%s"' % (n, v) for n, v in versions.items()]
|
||||
sVersions = 'appyVersions = {' + ','.join(dVersions) + '}'
|
||||
codeFile = "%s/pkg_resources.py" % self.libFolder
|
||||
f = file(codeFile)
|
||||
content = f.read().replace("raise DistributionNotFound(req)",
|
||||
"dist = getAppyVersion(req, '%s')" % self.instancePath)
|
||||
content = sVersions + '\n' + pkgResourcesPatch + '\n' + content
|
||||
f.close()
|
||||
f = file(codeFile, 'w')
|
||||
f.write(content)
|
||||
f.close()
|
||||
# Some 'include' directives must be added with our install.
|
||||
configPlone = j(self.productsFolder, 'CMFPlone', 'configure.zcml')
|
||||
f = file(configPlone)
|
||||
content = f.read()
|
||||
f.close()
|
||||
missing = ''
|
||||
for missingInclude in self.missingIncludes:
|
||||
missing += ' <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):
|
||||
'''Copy content of eggs into the Zope instance. This method also
|
||||
retrieves every egg version and returns a dict {s_egg:s_version}.'''
|
||||
j = os.path.join
|
||||
eggsFolder = j(self.plonePath, 'buildout-cache/eggs')
|
||||
res = {}
|
||||
for name in os.listdir(eggsFolder):
|
||||
if name == 'EGG-INFO': continue
|
||||
splittedName = name.split('-')
|
||||
res[splittedName[0]] = splittedName[1]
|
||||
if splittedName[0].startswith('Products.'):
|
||||
res[splittedName[0][9:]] = splittedName[1]
|
||||
absName = j(eggsFolder, name)
|
||||
# Copy every file or sub-folder into self.libFolder or
|
||||
# self.productsFolder.
|
||||
for fileName in os.listdir(absName):
|
||||
absFileName = j(absName, fileName)
|
||||
if fileName == 'Products' and not name.startswith('Zope2-'):
|
||||
# Copy every sub-folder into self.productsFolder
|
||||
for folderName in os.listdir(absFileName):
|
||||
absFolder = j(absFileName, folderName)
|
||||
if not os.path.isdir(absFolder): continue
|
||||
copyFolder(absFolder, j(self.productsFolder,folderName))
|
||||
elif os.path.isdir(absFileName):
|
||||
copyFolder(absFileName, j(self.libFolder, fileName))
|
||||
else:
|
||||
shutil.copy(absFileName, self.libFolder)
|
||||
return res
|
||||
|
||||
def createInstance(self, linksForProducts):
|
||||
'''Calls the Zope script that allows to create a Zope instance and copy
|
||||
into it all the Plone packages and products.'''
|
||||
j = os.path.join
|
||||
# Find the Python interpreter running Zope
|
||||
for elem in os.listdir(self.plonePath):
|
||||
pythonPath = None
|
||||
elemPath = j(self.plonePath, elem)
|
||||
if elem.startswith('Python-') and os.path.isdir(elemPath):
|
||||
pythonPath = elemPath + '/bin/python'
|
||||
if not os.path.exists(pythonPath):
|
||||
raise NewError(PYTHON_EXE_NOT_FOUND % pythonPath)
|
||||
break
|
||||
if not pythonPath:
|
||||
raise NewError(PYTHON_NOT_FOUND % self.plonePath)
|
||||
self.pythonPath = pythonPath
|
||||
# Find the Zope script mkzopeinstance.py and Zope itself
|
||||
makeInstancePath = None
|
||||
self.zopePath = None
|
||||
for dirname, dirs, files in os.walk(self.plonePath):
|
||||
# Find Zope
|
||||
for folderName in dirs:
|
||||
if folderName.startswith('Zope2-'):
|
||||
self.zopePath = j(dirname, folderName)
|
||||
# Find mkzopeinstance
|
||||
for fileName in files:
|
||||
if fileName == 'mkzopeinstance.py':
|
||||
if self.ploneVersion == 'plone4':
|
||||
makeInstancePath = j(dirname, fileName)
|
||||
else:
|
||||
if ('/buildout-cache/' not in dirname):
|
||||
makeInstancePath = j(dirname, fileName)
|
||||
if not makeInstancePath:
|
||||
raise NewError(MKZOPE_NOT_FOUND % self.plonePath)
|
||||
# Execute mkzopeinstance.py with the right Python interpreter.
|
||||
# For Plone4, we will call it later.
|
||||
cmd = '%s %s -d %s' % (pythonPath, makeInstancePath, self.instancePath)
|
||||
if self.ploneVersion != 'plone4':
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
# Now, make the instance Plone-ready
|
||||
action = 'Copying'
|
||||
if linksForProducts:
|
||||
action = 'Symlinking'
|
||||
print(('%s Plone stuff in the Zope instance...' % action))
|
||||
if self.ploneVersion in ('plone25', 'plone30'):
|
||||
self.installPlone25or30Stuff(linksForProducts)
|
||||
elif self.ploneVersion in ('plone3x', 'plone4'):
|
||||
versions = self.copyEggs()
|
||||
if self.ploneVersion == 'plone3x':
|
||||
self.patchPlone3x()
|
||||
elif self.ploneVersion == 'plone4':
|
||||
# Create the Zope instance
|
||||
os.environ['PYTHONPATH'] = '%s:%s' % \
|
||||
(j(self.instancePath,'Products'),
|
||||
j(self.instancePath, 'lib/python'))
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
self.patchPlone4(versions)
|
||||
# Remove .bat files under Linux
|
||||
if os.name == 'posix':
|
||||
cleanFolder(j(self.instancePath, 'bin'), exts=('.bat',))
|
||||
|
||||
def manageArgs(self, args):
|
||||
'''Ensures that the script was called with the right parameters.'''
|
||||
if len(args) != 3: raise NewError(WRONG_NB_OF_ARGS)
|
||||
self.ploneVersion, self.plonePath, self.instancePath = args
|
||||
# Add some more folder definitions
|
||||
j = os.path.join
|
||||
self.productsFolder = j(self.instancePath, 'Products')
|
||||
self.libFolder = j(self.instancePath, 'lib/python')
|
||||
# Check Plone version
|
||||
if self.ploneVersion not in self.ploneVersions:
|
||||
raise NewError(WRONG_PLONE_VERSION % str(self.ploneVersions))
|
||||
# Check Plone path
|
||||
if not os.path.exists(self.plonePath) \
|
||||
or not os.path.isdir(self.plonePath):
|
||||
raise NewError(WRONG_PLONE_PATH % self.plonePath)
|
||||
# Check instance path
|
||||
parentFolder = os.path.dirname(self.instancePath)
|
||||
if not os.path.exists(parentFolder) or not os.path.isdir(parentFolder):
|
||||
raise NewError(WRONG_INSTANCE_PATH % parentFolder)
|
||||
|
||||
def run(self):
|
||||
optParser = OptionParser(usage=NewScript.__doc__)
|
||||
optParser.add_option("-l", "--links", action="store_true",
|
||||
help="[Linux, plone25 or plone30 only] Within the created " \
|
||||
"instance, symlinks to Products lying within the Plone " \
|
||||
"installer files are created instead of copying them into " \
|
||||
"the instance. This avoids duplicating the Products source " \
|
||||
"code and is interesting if you create a lot of Zope " \
|
||||
"instances.")
|
||||
(options, args) = optParser.parse_args()
|
||||
linksForProducts = options.links
|
||||
try:
|
||||
self.manageArgs(args)
|
||||
if self.ploneVersion != 'zope':
|
||||
print(('Creating new %s instance...' % self.ploneVersion))
|
||||
self.createInstance(linksForProducts)
|
||||
else:
|
||||
ZopeInstanceCreator(self.instancePath).run()
|
||||
except NewError as ne:
|
||||
optParser.print_help()
|
||||
sys.stderr.write(str(ne))
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(ERROR_CODE)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
NewScript().run()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,76 +0,0 @@
|
|||
'''This script allows to perform a "grep" command that will be applied on files
|
||||
content.xml and styles.xml within all ODF files (odt and ods) found within a
|
||||
given folder.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import sys, os.path, zipfile, time, subprocess
|
||||
from appy.shared.utils import getOsTempFolder, FolderDeleter
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
usage = '''Usage: python odfGrep.py [file|folder] [keyword].
|
||||
|
||||
If *file* is given, it is the path to an ODF file (odt or ods). grep will be
|
||||
run on this file only.
|
||||
If *folder* is given, the grep will be run on all ODF files found in this
|
||||
folder and sub-folders.
|
||||
|
||||
*keyword* is the string to search within the file(s).
|
||||
'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class OdfGrep:
|
||||
toGrep = ('content.xml', 'styles.xml')
|
||||
toUnzip = ('.ods', '.odt')
|
||||
def __init__(self, fileOrFolder, keyword):
|
||||
self.fileOrFolder = fileOrFolder
|
||||
self.keyword = keyword
|
||||
self.tempFolder = getOsTempFolder()
|
||||
|
||||
def callGrep(self, folder):
|
||||
'''Performs a "grep" with self.keyword in p_folder.'''
|
||||
cmd = 'grep -irn "%s" %s' % (self.keyword, folder)
|
||||
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
out, err = proc.communicate()
|
||||
return bool(out)
|
||||
|
||||
def grepFile(self, fileName):
|
||||
'''Unzips the .xml files from file named p_fileName and performs a
|
||||
grep on it.'''
|
||||
# Unzip the file in the temp folder
|
||||
name = 'f%f' % time.time()
|
||||
tempFolder = os.path.join(self.tempFolder, name)
|
||||
os.mkdir(tempFolder)
|
||||
zip = zipfile.ZipFile(fileName)
|
||||
for zippedFile in zip.namelist():
|
||||
if zippedFile not in self.toGrep: continue
|
||||
destFile = os.path.join(tempFolder, zippedFile)
|
||||
f = open(destFile, 'wb')
|
||||
fileContent = zip.read(zippedFile)
|
||||
f.write(fileContent)
|
||||
f.close()
|
||||
# Run "grep" in this folder
|
||||
match = self.callGrep(tempFolder)
|
||||
if match:
|
||||
print(('Found in %s' % fileName))
|
||||
FolderDeleter.delete(tempFolder)
|
||||
|
||||
def run(self):
|
||||
if os.path.isfile(self.fileOrFolder):
|
||||
self.grepFile(self.fileOrFolder)
|
||||
elif os.path.isdir(self.fileOrFolder):
|
||||
# Grep on all files found in this folder.
|
||||
for dir, dirnames, filenames in os.walk(self.fileOrFolder):
|
||||
for name in filenames:
|
||||
if os.path.splitext(name)[1] in self.toUnzip:
|
||||
self.grepFile(os.path.join(dir, name))
|
||||
else:
|
||||
print(('%s does not exist.' % self.fileOrFolder))
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
print(usage)
|
||||
sys.exit()
|
||||
OdfGrep(sys.argv[1], sys.argv[2]).run()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,75 +0,0 @@
|
|||
'''This script allows to walk (and potentially patch) files (content.xml,
|
||||
styles.xml...) contained within a given ODF file or within all ODF files
|
||||
found in some folder.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import sys, os.path, time
|
||||
from appy.shared.zip import unzip, zip
|
||||
from appy.shared.utils import getOsTempFolder, FolderDeleter, executeCommand
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
usage = '''Usage: python odfWalk.py [file|folder] yourScript.
|
||||
|
||||
If *file* is given, it is the path to an ODF file (odt or ods). This single
|
||||
file will be walked.
|
||||
If *folder* is given, we will walk all ODF files found in this folder and
|
||||
sub-folders.
|
||||
|
||||
*yourScript* is the path to a Python script that will be run on every walked
|
||||
file. It will be called with a single arg containing the absolute path to the
|
||||
folder containing the unzipped file content (content.xml, styles.xml...).'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class OdfWalk:
|
||||
toUnzip = ('.ods', '.odt')
|
||||
def __init__(self, fileOrFolder, script):
|
||||
self.fileOrFolder = fileOrFolder
|
||||
self.script = script
|
||||
self.tempFolder = getOsTempFolder()
|
||||
|
||||
def walkFile(self, fileName):
|
||||
'''Unzip p_fileName in a temp folder, call self.script, and then re-zip
|
||||
the result.'''
|
||||
print 'Walking %s...' % fileName
|
||||
# Create a temp folder
|
||||
name = 'f%f' % time.time()
|
||||
tempFolder = os.path.join(self.tempFolder, name)
|
||||
os.mkdir(tempFolder)
|
||||
# Unzip the file in it
|
||||
unzip(fileName, tempFolder)
|
||||
# Call self.script
|
||||
py = sys.executable or 'python'
|
||||
cmd = '%s %s %s' % (py, self.script, tempFolder)
|
||||
print ' Running %s...' % cmd,
|
||||
os.system(cmd)
|
||||
# Re-zip the result
|
||||
zip(fileName, tempFolder, odf=True)
|
||||
FolderDeleter.delete(tempFolder)
|
||||
print 'done.'
|
||||
|
||||
def run(self):
|
||||
if os.path.isfile(self.fileOrFolder):
|
||||
self.walkFile(self.fileOrFolder)
|
||||
elif os.path.isdir(self.fileOrFolder):
|
||||
# Walk all files found in this folder
|
||||
for dir, dirnames, filenames in os.walk(self.fileOrFolder):
|
||||
for name in filenames:
|
||||
if os.path.splitext(name)[1] in self.toUnzip:
|
||||
self.walkFile(os.path.join(dir, name))
|
||||
else:
|
||||
print('%s does not exist.' % self.fileOrFolder)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
print(usage)
|
||||
sys.exit()
|
||||
# Warn the user.
|
||||
print 'All the files in %s will be modified. ' \
|
||||
'Are you sure? [y/N] ' % sys.argv[1],
|
||||
response = sys.stdin.readline().strip().lower()
|
||||
if response == 'y':
|
||||
OdfWalk(sys.argv[1], sys.argv[2]).run()
|
||||
else:
|
||||
print 'Canceled.'
|
||||
# ------------------------------------------------------------------------------
|
517
bin/publish.py
|
@ -1,517 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# Imports ----------------------------------------------------------------------
|
||||
import os, os.path, sys, shutil, re, zipfile, sys, ftplib, time
|
||||
import appy
|
||||
from appy.shared import appyPath
|
||||
from appy.shared.utils import FolderDeleter, LinesCounter
|
||||
from appy.shared.packaging import Debianizer
|
||||
from appy.bin.clean import Cleaner
|
||||
from appy.gen.utils import produceNiceMessage
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
versionRex = re.compile('(\d+\.\d+\.\d+)')
|
||||
distInfo = '''from distutils.core import setup
|
||||
setup(name = "appy", version = "%s",
|
||||
description = "The Appy framework",
|
||||
long_description = "Appy builds simple but complex web Python apps.",
|
||||
author = "Gaetan Delannay",
|
||||
author_email = "gaetan.delannay AT geezteem.com",
|
||||
license = "GPL", platforms="all",
|
||||
url = 'http://appyframework.org',
|
||||
packages = [%s],
|
||||
package_data = {'':["*.*"]})
|
||||
'''
|
||||
manifestInfo = '''
|
||||
recursive-include appy/bin *
|
||||
recursive-include appy/fields *
|
||||
recursive-include appy/gen *
|
||||
recursive-include appy/pod *
|
||||
recursive-include appy/shared *
|
||||
'''
|
||||
def askLogin():
|
||||
print('Login:')
|
||||
login = sys.stdin.readline().strip()
|
||||
print('Password:')
|
||||
passwd = sys.stdin.readline().strip()
|
||||
return (login, passwd)
|
||||
|
||||
class FtpFolder:
|
||||
'''Represents a folder on a FTP site.'''
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.parent = None
|
||||
self.subFolders = []
|
||||
self.files = []
|
||||
self.isComplete = False # Is True if all contained files and direct
|
||||
# subFolders were analysed.
|
||||
|
||||
def getFullName(self):
|
||||
if not self.parent:
|
||||
res = '.'
|
||||
else:
|
||||
res = '%s/%s' % (self.parent.getFullName(), self.name)
|
||||
return res
|
||||
def addSubFolder(self, subFolder):
|
||||
self.subFolders.append(subFolder)
|
||||
subFolder.parent = self
|
||||
|
||||
def isFullyComplete(self):
|
||||
res = self.isComplete
|
||||
for subFolder in self.subFolders:
|
||||
res = res and subFolder.isFullyComplete()
|
||||
return res
|
||||
|
||||
def getIncompleteSubFolders(self):
|
||||
res = []
|
||||
for subFolder in self.subFolders:
|
||||
if not subFolder.isComplete:
|
||||
res.append(subFolder)
|
||||
elif not subFolder.isFullyComplete():
|
||||
res += subFolder.getIncompleteSubFolders()
|
||||
return res
|
||||
|
||||
def __str__(self):
|
||||
res = 'Folder %s' % self.getFullName()
|
||||
if self.files:
|
||||
res += '\nFiles:\n'
|
||||
for f in self.files:
|
||||
res += '%s\n' % f
|
||||
if self.subFolders:
|
||||
res += '\nSubFolders:\n'
|
||||
for subFolder in self.subFolders:
|
||||
res += str(subFolder)
|
||||
return res
|
||||
|
||||
def clean(self, site):
|
||||
'''Cleans this folder'''
|
||||
# First, clean subFolders if they exist
|
||||
print(('Cleaning %s %d subFolders' % \
|
||||
(self.getFullName(), len(self.subFolders))))
|
||||
for subFolder in self.subFolders:
|
||||
subFolder.clean(site)
|
||||
# Remove the subFolder
|
||||
site.rmd(subFolder.getFullName())
|
||||
# Then, remove the files contained in the folder.
|
||||
for f in self.files:
|
||||
fileName = '%s/%s' % (self.getFullName(), f)
|
||||
site.delete(fileName)
|
||||
print(('%s removed.' % fileName))
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class AppySite:
|
||||
'''Represents the Appy web sie where the project is published.'''
|
||||
name = 'appyframework.org'
|
||||
textExtensions = ('.htm', '.html', '.css', '.txt')
|
||||
def __init__(self):
|
||||
# Delete the "egg" folder on not-yet-copied local site.
|
||||
eggFolder = '%s/temp/egg' % appyPath
|
||||
if os.path.isdir(eggFolder):
|
||||
FolderDeleter.delete(eggFolder)
|
||||
# Ask user id and password for FTP transfer
|
||||
userId, userPassword = askLogin()
|
||||
self.site = ftplib.FTP(self.name)
|
||||
self.site.login(userId, userPassword)
|
||||
self.rootFolder = None # Root folder of appy site ~FtpFolder~
|
||||
self.currentFolder = None # Currently visited folder ~FtpFolder~
|
||||
|
||||
def analyseFolderEntry(self, folderEntry):
|
||||
'''p_line corresponds to a 'ls' entry.'''
|
||||
elems = folderEntry.split(' ')
|
||||
elemName = elems[len(elems)-1]
|
||||
if (elemName.startswith('.') or elemName.startswith('_')) and \
|
||||
(not elemName.startswith('__init__.py')):
|
||||
return
|
||||
if elems[0].startswith('d'):
|
||||
self.currentFolder.addSubFolder(FtpFolder(elemName))
|
||||
else:
|
||||
self.currentFolder.files.append(elemName)
|
||||
|
||||
def createFolderProxies(self):
|
||||
'''Creates a representation of the FTP folders of the appy site in the
|
||||
form of FtpFolder instances.'''
|
||||
self.rootFolder = FtpFolder('.')
|
||||
self.currentFolder = self.rootFolder
|
||||
self.site.dir(self.currentFolder.getFullName(), self.analyseFolderEntry)
|
||||
self.rootFolder.isComplete = True
|
||||
while not self.rootFolder.isFullyComplete():
|
||||
incompleteFolders = self.rootFolder.getIncompleteSubFolders()
|
||||
for folder in incompleteFolders:
|
||||
self.currentFolder = folder
|
||||
self.site.dir(self.currentFolder.getFullName(),
|
||||
self.analyseFolderEntry)
|
||||
self.currentFolder.isComplete = True
|
||||
|
||||
def copyFile(self, fileName):
|
||||
'''Copies a file on the FTP server.'''
|
||||
localFile = file(fileName)
|
||||
cmd = 'STOR %s' % fileName
|
||||
fileExt = os.path.splitext(fileName)[1]
|
||||
if fileExt in self.textExtensions:
|
||||
# Make a transfer in text mode
|
||||
print(('Transfer file %s (text mode)' % fileName))
|
||||
self.site.storlines(cmd, localFile)
|
||||
else:
|
||||
# Make a transfer in binary mode
|
||||
print(('Transfer file %s (binary mode)' % fileName))
|
||||
self.site.storbinary(cmd, localFile)
|
||||
|
||||
def publish(self):
|
||||
# Delete the existing content of the distant site
|
||||
self.createFolderProxies()
|
||||
print('Removing existing data on site...')
|
||||
self.rootFolder.clean(self.site)
|
||||
curDir = os.getcwd()
|
||||
os.chdir('%s/temp' % appyPath)
|
||||
for root, dirs, files in os.walk('.'):
|
||||
for folder in dirs:
|
||||
folderName = '%s/%s' % (root, folder)
|
||||
self.site.mkd(folderName)
|
||||
for f in files:
|
||||
fileName = '%s/%s' % (root, f)
|
||||
self.copyFile(fileName)
|
||||
os.chdir(curDir)
|
||||
self.site.close()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Text2Html:
|
||||
'''Converts a text file into a HTML file.'''
|
||||
def __init__(self, txtFile, htmlFile):
|
||||
self.txtFile = file(txtFile)
|
||||
self.htmlFile = file(htmlFile, 'w')
|
||||
def retainLine(self, line):
|
||||
'''Must we dump this line in the result ?'''
|
||||
pass
|
||||
def getFirstChar(self, line):
|
||||
'''Gets the first relevant character of the line. For a TodoConverter
|
||||
this is not really the first one because lines taken into account start
|
||||
with a 'v' character.'''
|
||||
return line[self.firstChar]
|
||||
def getCleanLine(self, line, isTitle=False):
|
||||
'''Gets the line as it will be inserted in the HTML result: remove some
|
||||
leading and trailing characters.'''
|
||||
start = self.firstChar
|
||||
if not isTitle:
|
||||
start += 1
|
||||
return line[start:-1]
|
||||
def getProlog(self):
|
||||
'''If you want to write a small prolog in the HTML file, you may
|
||||
generate it here.'''
|
||||
return ''
|
||||
def run(self):
|
||||
self.htmlFile.write('<html>\n\n<head><title>%s</title></head>\n\n' \
|
||||
'<body>\n' % self.title)
|
||||
self.htmlFile.write(self.getProlog())
|
||||
inList = False
|
||||
for line in self.txtFile:
|
||||
if self.retainLine(line):
|
||||
firstChar = self.getFirstChar(line)
|
||||
if firstChar == '-':
|
||||
if not inList:
|
||||
# Begin a new bulleted list
|
||||
self.htmlFile.write('<ul>\n')
|
||||
inList = True
|
||||
self.htmlFile.write(
|
||||
'<li>%s</li>\n' % self.getCleanLine(line))
|
||||
elif firstChar == ' ':
|
||||
pass
|
||||
else:
|
||||
# It is a title
|
||||
if inList:
|
||||
self.htmlFile.write('</ul>\n')
|
||||
inList = False
|
||||
self.htmlFile.write(
|
||||
'<h1>%s</h1>\n' % self.getCleanLine(line, True))
|
||||
self.htmlFile.write('\n</ul>\n</body>\n</html>')
|
||||
self.txtFile.close()
|
||||
self.htmlFile.close()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Publisher:
|
||||
'''Publishes Appy on the web.'''
|
||||
pageBody = re.compile('<body.*?>(.*)</body>', re.S)
|
||||
|
||||
def __init__(self):
|
||||
self.genFolder = '%s/temp' % appyPath
|
||||
self.ftp = None # FTP connection to appyframework.org
|
||||
# Retrieve version-related information
|
||||
versionFileName = '%s/doc/version.txt' % appyPath
|
||||
f = file(versionFileName)
|
||||
self.versionShort = f.read().strip()
|
||||
# Long version includes release date
|
||||
self.versionLong = '%s (%s)' % (self.versionShort,
|
||||
time.strftime('%Y/%m/%d %H:%M'))
|
||||
f.close()
|
||||
# In silent mode (option -s), no question is asked, default answers are
|
||||
# automatically given.
|
||||
if (len(sys.argv) > 1) and (sys.argv[1] == '-s'):
|
||||
self.silent = True
|
||||
else:
|
||||
self.silent = False
|
||||
|
||||
def askQuestion(self, question, default='yes'):
|
||||
'''Asks a question to the user (yes/no) and returns True if the user
|
||||
answered "yes".'''
|
||||
if self.silent: return (default == 'yes')
|
||||
defaultIsYes = (default.lower() in ('y', 'yes'))
|
||||
if defaultIsYes:
|
||||
yesNo = '[Y/n]'
|
||||
else:
|
||||
yesNo = '[y/N]'
|
||||
print((question + ' ' + yesNo + ' '))
|
||||
response = sys.stdin.readline().strip().lower()
|
||||
res = False
|
||||
if response in ('y', 'yes'):
|
||||
res = True
|
||||
elif response in ('n', 'no'):
|
||||
res = False
|
||||
elif not response:
|
||||
# It depends on default value
|
||||
if defaultIsYes:
|
||||
res = True
|
||||
else:
|
||||
res = False
|
||||
return res
|
||||
|
||||
def executeCommand(self, cmd):
|
||||
'''Executes the system command p_cmd.'''
|
||||
print(('Executing %s...' % cmd))
|
||||
os.system(cmd)
|
||||
|
||||
distExcluded = ('appy/doc', 'appy/temp', 'appy/versions', 'appy/gen/test')
|
||||
def isDistExcluded(self, name):
|
||||
'''Returns True if folder named p_name must be included in the
|
||||
distribution.'''
|
||||
if '.bzr' in name: return True
|
||||
for prefix in self.distExcluded:
|
||||
if name.startswith(prefix): return True
|
||||
|
||||
def createDebianRelease(self):
|
||||
'''Creates a Debian package for Appy.'''
|
||||
j = os.path.join
|
||||
sign = self.askQuestion('Sign the Debian package?', default='no')
|
||||
Debianizer(j(self.genFolder, 'appy'), j(appyPath, 'versions'),
|
||||
appVersion=self.versionShort, depends=[], sign=sign).run()
|
||||
|
||||
def createDistRelease(self):
|
||||
'''Create the distutils package.'''
|
||||
curdir = os.getcwd()
|
||||
distFolder = '%s/dist' % self.genFolder
|
||||
# Create setup.py
|
||||
os.mkdir(distFolder)
|
||||
f = file('%s/setup.py' % distFolder, 'w')
|
||||
# List all packages to include
|
||||
packages = []
|
||||
os.chdir(os.path.dirname(appyPath))
|
||||
for dir, dirnames, filenames in os.walk('appy'):
|
||||
if self.isDistExcluded(dir): continue
|
||||
packageName = dir.replace('/', '.')
|
||||
packages.append('"%s"' % packageName)
|
||||
f.write(distInfo % (self.versionShort, ','.join(packages)))
|
||||
f.close()
|
||||
# Create MANIFEST.in
|
||||
f = file('%s/MANIFEST.in' % distFolder, 'w')
|
||||
f.write(manifestInfo)
|
||||
f.close()
|
||||
# Move appy sources within the dist folder
|
||||
os.rename('%s/appy' % self.genFolder, '%s/appy' % distFolder)
|
||||
# Create the source distribution
|
||||
os.chdir(distFolder)
|
||||
self.executeCommand('python setup.py sdist')
|
||||
# DistUtils has created the .tar.gz file. Move it to folder "versions"
|
||||
name = 'appy-%s.tar.gz' % self.versionShort
|
||||
os.rename('%s/dist/%s' % (distFolder, name),
|
||||
'%s/versions/%s' % (appyPath, name))
|
||||
os.rmdir('%s/dist' % distFolder)
|
||||
# Upload the package on Pypi?
|
||||
if self.askQuestion('Upload %s on PyPI?' % name, default='no'):
|
||||
self.executeCommand('python setup.py sdist upload')
|
||||
# Clean temp files
|
||||
os.chdir(curdir)
|
||||
# Keep the Appy source for building the Debian package afterwards
|
||||
os.rename(os.path.join(self.genFolder, 'dist', 'appy'), \
|
||||
os.path.join(self.genFolder, 'appy'))
|
||||
FolderDeleter.delete(os.path.join(self.genFolder, 'dist'))
|
||||
|
||||
def createZipRelease(self):
|
||||
'''Creates a zip file with the appy sources.'''
|
||||
newZipRelease = '%s/versions/appy-%s.zip' % (appyPath,self.versionShort)
|
||||
if os.path.exists(newZipRelease):
|
||||
if not self.askQuestion('"%s" already exists. Replace it?' % \
|
||||
newZipRelease, default='yes'):
|
||||
print('Publication canceled.')
|
||||
sys.exit(1)
|
||||
print(('Removing obsolete %s...' % newZipRelease))
|
||||
os.remove(newZipRelease)
|
||||
zipFile = zipfile.ZipFile(newZipRelease, 'w', zipfile.ZIP_DEFLATED)
|
||||
curdir = os.getcwd()
|
||||
os.chdir(self.genFolder)
|
||||
for dir, dirnames, filenames in os.walk('appy'):
|
||||
for f in filenames:
|
||||
fileName = os.path.join(dir, f)
|
||||
zipFile.write(fileName)
|
||||
# [2:] is there to avoid havin './' in the path in the zip file.
|
||||
zipFile.close()
|
||||
os.chdir(curdir)
|
||||
|
||||
def applyTemplate(self):
|
||||
'''Decorates each page with the template.'''
|
||||
# First, load the template into memory
|
||||
templateFileName = '%s/doc/template.html' % appyPath
|
||||
templateFile = open(templateFileName)
|
||||
templateContent = templateFile.read()
|
||||
templateFile.close()
|
||||
# Then, decorate each other html file
|
||||
for pageName in os.listdir(self.genFolder):
|
||||
if pageName.endswith('.html'):
|
||||
pageFileName = '%s/%s' % (self.genFolder, pageName)
|
||||
pageFile = file(pageFileName)
|
||||
pageContent = pageFile.read()
|
||||
pageFile.close()
|
||||
# Extract the page title (excepted for the main page, we don't
|
||||
# need this title, to save space.
|
||||
pageTitle = ''
|
||||
if pageName != 'index.html':
|
||||
i, j = pageContent.find('<title>'), \
|
||||
pageContent.find('</title>')
|
||||
pageTitle = '<tr><td align="center" style="padding: 10px; '\
|
||||
'font-size:150%%; border-bottom: 1px black ' \
|
||||
'dashed">%s</td></tr>' % pageContent[i+7:j]
|
||||
# Extract the body tag content from the page
|
||||
pageContent = self.pageBody.search(pageContent).group(1)
|
||||
pageFile = open(pageFileName, 'w')
|
||||
templateWithTitle = templateContent.replace('{{ title }}',
|
||||
pageTitle)
|
||||
pageFile.write(templateWithTitle.replace('{{ content }}',
|
||||
pageContent))
|
||||
pageFile.close()
|
||||
|
||||
def _getPageTitle(self, url):
|
||||
'''Returns the documentation page title from its URL.'''
|
||||
res = url.split('.')[0]
|
||||
if res not in ('pod', 'gen'):
|
||||
res = produceNiceMessage(res[3:])
|
||||
return res
|
||||
|
||||
mainToc = re.compile('<span class="doc">(.*?)</span>', re.S)
|
||||
tocLink = re.compile('<a href="(.*?)">(.*?)</a>')
|
||||
subSection = re.compile('<h1>(.*?)</h1>')
|
||||
subSectionContent = re.compile('<a name="(.*?)">.*?</a>(.*)')
|
||||
def createDocToc(self):
|
||||
res = '<table width="100%"><tr valign="top">'
|
||||
docToc = '%s/docToc.html' % self.genFolder
|
||||
# First, parse template.html to get the main TOC structure
|
||||
template = file('%s/doc/template.html' % appyPath)
|
||||
mainData = self.mainToc.search(template.read()).group(0)
|
||||
links = self.tocLink.findall(mainData)
|
||||
sectionNb = 0
|
||||
for url, title in links:
|
||||
if title in ('appy.gen', 'appy.pod'):
|
||||
tag = 'h1'
|
||||
indent = 0
|
||||
styleBegin = ''
|
||||
styleEnd = ''
|
||||
if title == 'pod':
|
||||
res += '</td>'
|
||||
res += '<td>'
|
||||
else:
|
||||
tag = 'p'
|
||||
indent = 2
|
||||
styleBegin = '<i>'
|
||||
styleEnd = '</i>'
|
||||
tabs = ' ' * indent * 2
|
||||
res += '<%s>%s%s<a href="%s">%s</a>%s</%s>\n' % \
|
||||
(tag, tabs, styleBegin, url, self._getPageTitle(url),
|
||||
styleEnd, tag)
|
||||
# Parse each HTML file and retrieve sections title that have an
|
||||
# anchor defined
|
||||
docFile = file('%s/doc/%s' % (appyPath, url))
|
||||
docContent = docFile.read()
|
||||
docFile.close()
|
||||
sections = self.subSection.findall(docContent)
|
||||
for section in sections:
|
||||
r = self.subSectionContent.search(section)
|
||||
if r:
|
||||
sectionNb += 1
|
||||
tabs = ' ' * 8
|
||||
res += '<div>%s%d. <a href="%s#%s">%s</a></div>\n' % \
|
||||
(tabs, sectionNb, url, r.group(1), r.group(2))
|
||||
res += '</td></tr></table>'
|
||||
f = file(docToc)
|
||||
toc = f.read()
|
||||
f.close()
|
||||
toc = toc.replace('{{ doc }}', res)
|
||||
f = file(docToc, 'w')
|
||||
f.write(toc)
|
||||
f.close()
|
||||
|
||||
privateScripts = ('publish.py', 'zip.py', 'startoo')
|
||||
def prepareGenFolder(self, minimalist=False):
|
||||
'''Creates the basic structure of the temp folder where the appy
|
||||
website will be generated.'''
|
||||
# Reinitialise temp folder where the generated website will be dumped
|
||||
if os.path.exists(self.genFolder):
|
||||
FolderDeleter.delete(self.genFolder)
|
||||
shutil.copytree('%s/doc' % appyPath, self.genFolder)
|
||||
# Copy appy.css from gen, with minor updates.
|
||||
f = file('%s/gen/ui/appy.css' % appyPath)
|
||||
css = f.read().replace('ui/li.gif', 'img/li.gif')
|
||||
f.close()
|
||||
f = file('%s/appy.css' % self.genFolder, 'w')
|
||||
f.write(css)
|
||||
f.close()
|
||||
shutil.copy('%s/gen/ui/li.gif' % appyPath, '%s/img' % self.genFolder)
|
||||
# Create a temp clean copy of appy sources (without .svn folders, etc)
|
||||
genSrcFolder = '%s/appy' % self.genFolder
|
||||
os.mkdir(genSrcFolder)
|
||||
for aFile in ('__init__.py',):
|
||||
shutil.copy('%s/%s' % (appyPath, aFile), genSrcFolder)
|
||||
for aFolder in ('bin', 'fields', 'gen', 'pod', 'px', 'shared'):
|
||||
shutil.copytree('%s/%s' % (appyPath, aFolder),
|
||||
'%s/%s' % (genSrcFolder, aFolder))
|
||||
# Remove some scripts from bin
|
||||
for script in self.privateScripts:
|
||||
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)'''
|
||||
print(('Publishing version %s...' % self.versionShort))
|
||||
# Dump version info in appy/version.py
|
||||
f = file('%s/version.py' % genSrcFolder, 'w')
|
||||
f.write('short = "%s"\n' % self.versionShort)
|
||||
f.write('verbose = "%s"' % self.versionLong)
|
||||
f.close()
|
||||
# Remove unwanted files
|
||||
os.remove('%s/version.txt' % self.genFolder)
|
||||
os.remove('%s/license.txt' % self.genFolder)
|
||||
os.remove('%s/template.html' % self.genFolder)
|
||||
os.remove('%s/artwork.odg' % self.genFolder)
|
||||
# Remove subversion folders
|
||||
for root, dirs, files in os.walk(self.genFolder):
|
||||
for dirName in dirs:
|
||||
if dirName == '.svn':
|
||||
FolderDeleter.delete(os.path.join(root, dirName))
|
||||
|
||||
def run(self):
|
||||
Cleaner().run(verbose=False)
|
||||
# Perform a small analysis on the Appy code
|
||||
LinesCounter(appy).run()
|
||||
print(('Generating site in %s...' % self.genFolder))
|
||||
minimalist = self.askQuestion('Minimalist (shipped without tests)?',
|
||||
default='no')
|
||||
self.prepareGenFolder(minimalist)
|
||||
self.createDocToc()
|
||||
self.applyTemplate()
|
||||
self.createZipRelease()
|
||||
self.createDistRelease()
|
||||
self.createDebianRelease()
|
||||
# Remove folder 'appy', in order to avoid copying it on the website
|
||||
FolderDeleter.delete(os.path.join(self.genFolder, 'appy'))
|
||||
if self.askQuestion('Publish on appyframework.org?', default='no'):
|
||||
AppySite().publish()
|
||||
if self.askQuestion('Delete locally generated site ?', default='no'):
|
||||
FolderDeleter.delete(self.genFolder)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
Publisher().run()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,93 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import sys, time, os, os.path
|
||||
from optparse import OptionParser
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class RestoreError(Exception): pass
|
||||
ERROR_CODE = 1
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ZodbRestorer:
|
||||
def __init__(self, storageLocation, backupFolder, options):
|
||||
self.storageLocation = storageLocation
|
||||
self.backupFolder = backupFolder
|
||||
self.repozo = options.repozo or 'repozo.py'
|
||||
self.restoreDate = options.date
|
||||
self.python = options.python
|
||||
def run(self):
|
||||
startTime = time.time()
|
||||
datePart = ''
|
||||
if self.restoreDate:
|
||||
datePart = '-D %s' % self.restoreDate
|
||||
repozoCmd = '%s %s -Rv -r %s %s -o %s' % (self.python,
|
||||
self.repozo, self.backupFolder, datePart, self.storageLocation)
|
||||
print(('Executing %s...' % repozoCmd))
|
||||
os.system(repozoCmd)
|
||||
stopTime = time.time()
|
||||
print(('Done in %d minute(s).' % ((stopTime-startTime)/60)))
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ZodbRestoreScript:
|
||||
'''usage: python restore.py storageLocation backupFolder [options]
|
||||
storageLocation is the storage that will be created at the end of the
|
||||
restore process (ie /tmp/Data.hurrah.fs);
|
||||
backupFolder is the folder used for storing storage backups
|
||||
(ie /data/zodbbackups).'''
|
||||
|
||||
def checkArgs(self, options, args):
|
||||
'''Check that the scripts arguments are correct.'''
|
||||
# Do I have the correct number of args?
|
||||
if len(args) != 2:
|
||||
raise RestoreError('Wrong number of arguments.')
|
||||
# Check that storageLocation does not exist.
|
||||
if os.path.exists(args[0]):
|
||||
raise RestoreError('"%s" exists. Please specify the name of a ' \
|
||||
'new file (in a temp folder for example); you ' \
|
||||
'will move this at the right place in a second '\
|
||||
'step.' % args[0])
|
||||
# Check backupFolder
|
||||
if not os.path.isdir(args[1]):
|
||||
raise RestoreError('"%s" does not exist or is not a folder.' % \
|
||||
args[1])
|
||||
# Try to create storageLocation to check if we have write
|
||||
# access in it.
|
||||
try:
|
||||
f = file(args[0], 'w')
|
||||
f.write('Hello.')
|
||||
f.close()
|
||||
os.remove(args[0])
|
||||
except OSError as oe:
|
||||
raise RestoreError('I do not have the right to write file ' \
|
||||
'"%s".' % args[0])
|
||||
|
||||
def run(self):
|
||||
optParser = OptionParser(usage=ZodbRestoreScript.__doc__)
|
||||
optParser.add_option("-p", "--python", dest="python",
|
||||
help="The path to the Python interpreter running "\
|
||||
"Zope",
|
||||
default='python2.4',metavar="REPOZO",type='string')
|
||||
optParser.add_option("-r", "--repozo", dest="repozo",
|
||||
help="The path to repozo.py",
|
||||
default='', metavar="REPOZO", type='string')
|
||||
optParser.add_option("-d", "--date", dest="date",
|
||||
help="Date of the image to restore (format=" \
|
||||
"YYYY-MM-DD-HH-MM-SS). It is UTC time, " \
|
||||
"not local time. If you don't specify this " \
|
||||
"option, it defaults to now. If specified, " \
|
||||
"hour, minute, and second parts are optional",
|
||||
default='', metavar="DATE", type='string')
|
||||
(options, args) = optParser.parse_args()
|
||||
try:
|
||||
self.checkArgs(options, args)
|
||||
backuper = ZodbRestorer(args[0], args[1], options)
|
||||
backuper.run()
|
||||
except RestoreError as be:
|
||||
sys.stderr.write(str(be))
|
||||
sys.stderr.write('\n')
|
||||
optParser.print_help()
|
||||
sys.exit(ERROR_CODE)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
ZodbRestoreScript().run()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
soffice "--accept=socket,host=localhost,port=2002;urp;"
|
||||
echo "Press <enter>..."
|
||||
read R
|
41
bin/zip.py
|
@ -1,41 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, zipfile, sys
|
||||
from appy.shared import appyPath
|
||||
from appy.bin.clean import Cleaner
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Zipper:
|
||||
def __init__(self):
|
||||
self.zipFileName = '%s/Desktop/appy.zip' % os.environ['HOME']
|
||||
def createZipFile(self):
|
||||
print(('Creating %s...' % self.zipFileName))
|
||||
zipFile = zipfile.ZipFile(self.zipFileName, 'w', zipfile.ZIP_DEFLATED)
|
||||
for dir, dirnames, filenames in os.walk(appyPath):
|
||||
for f in filenames:
|
||||
fileName = os.path.join(dir, f)
|
||||
arcName = fileName[fileName.find('appy/'):]
|
||||
print(('Adding %s' % fileName))
|
||||
zipFile.write(fileName, arcName)
|
||||
zipFile.close()
|
||||
|
||||
def run(self):
|
||||
# Where to put the zip file ?
|
||||
print(("Where do you want to put appy.zip ? [Default is %s] " % \
|
||||
os.path.dirname(self.zipFileName)))
|
||||
response = sys.stdin.readline().strip()
|
||||
if response:
|
||||
if os.path.exists(response) and os.path.isdir(response):
|
||||
self.zipFileName = '%s/appy.zip' % response
|
||||
else:
|
||||
print(('%s is not a folder.' % response))
|
||||
sys.exit(1)
|
||||
if os.path.exists(self.zipFileName):
|
||||
print(('Removing existing %s...' % self.zipFileName))
|
||||
os.remove(self.zipFileName)
|
||||
Cleaner().run(verbose=False)
|
||||
self.createZipFile()
|
||||
|
||||
# Main program -----------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
Zipper().run()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,24 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import sys, os, os.path
|
||||
import Zope2.Startup.zopectl as zctl
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ZopeRunner:
|
||||
'''This class allows to run a Appy/Zope instance.'''
|
||||
|
||||
def run(self):
|
||||
# Check that an arg has been given (start, stop, fg, run)
|
||||
if not sys.argv[3].strip():
|
||||
print('Argument required.')
|
||||
sys.exit(-1)
|
||||
# Identify the name of the application for which Zope must run.
|
||||
app = os.path.splitext(os.path.basename(sys.argv[2]))[0].lower()
|
||||
# Launch Zope.
|
||||
options = zctl.ZopeCtlOptions()
|
||||
options.realize(None)
|
||||
options.program = ['/usr/bin/%srun' % app]
|
||||
options.sockname = '/var/lib/%s/zopectlsock' % app
|
||||
c = zctl.ZopeCmd(options)
|
||||
c.onecmd(" ".join(options.args))
|
||||
return min(c._exitstatus, 1)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,963 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import copy, types, re
|
||||
from appy import Object
|
||||
from appy.gen.layout import Table, defaultFieldLayouts
|
||||
from appy.gen import utils as gutils
|
||||
from appy.px import Px
|
||||
from appy.shared import utils as sutils
|
||||
from .group import Group
|
||||
from .page import Page
|
||||
import collections
|
||||
|
||||
# In this file, names "list" and "dict" refer to sub-modules. To use Python
|
||||
# builtin types, use __builtins__['list'] and __builtins__['dict']
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Field:
|
||||
'''Basic abstract class for defining any field.'''
|
||||
|
||||
# Some global static variables
|
||||
nullValues = (None, '', [], {})
|
||||
validatorTypes = (types.FunctionType, types.UnboundMethodType,
|
||||
type(re.compile('')))
|
||||
labelTypes = ('label', 'descr', 'help')
|
||||
|
||||
# Those attributes can be overridden by subclasses for defining,
|
||||
# respectively, names of CSS and Javascript files that are required by this
|
||||
# field, keyed by layoutType.
|
||||
cssFiles = {}
|
||||
jsFiles = {}
|
||||
bLayouts = Table('lrv-f', width=None)
|
||||
dLayouts = 'lrv-d-f'
|
||||
hLayouts = 'lhrv-f'
|
||||
wLayouts = Table('lrv-f')
|
||||
|
||||
# Render a field. Optional vars:
|
||||
# * fieldName can be given as different as field.name for fields included
|
||||
# in List fields: in this case, fieldName includes the row
|
||||
# index.
|
||||
# * showChanges If True, a variant of the field showing successive changes
|
||||
# made to it is shown.
|
||||
pxRender = Px('''
|
||||
<x var="showChanges=showChanges|req.get('showChanges') == 'True';
|
||||
layoutType=layoutType|req.get('layoutType');
|
||||
isSearch = layoutType == 'search';
|
||||
layout=field.layouts[layoutType];
|
||||
name=fieldName|field.name;
|
||||
widgetName = isSearch and ('w_%s' % name) or name;
|
||||
outerValue=value|None;
|
||||
rawValue=not isSearch and zobj.getFieldValue(name, \
|
||||
layoutType=layoutType, outerValue=outerValue);
|
||||
value=not isSearch and \
|
||||
field.getFormattedValue(zobj,rawValue,layoutType,showChanges);
|
||||
requestValue=not isSearch and zobj.getRequestFieldValue(name);
|
||||
inRequest=field.valueIsInRequest(zobj, req, name, layoutType);
|
||||
error=req.get('%s_error' % name);
|
||||
isMultiple=(field.multiplicity[1] == None) or \
|
||||
(field.multiplicity[1] > 1);
|
||||
masterCss=field.slaves and ('master_%s' % name) or '';
|
||||
slaveCss=field.getSlaveCss();
|
||||
tagCss=tagCss|'';
|
||||
tagCss=('%s %s' % (slaveCss, tagCss)).strip();
|
||||
zobj=zobj or ztool;
|
||||
tagId='%s_%s' % (zobj.id, name);
|
||||
tagName=field.master and 'slave' or '';
|
||||
layoutTarget=field">:tool.pxLayoutedObject</x>''')
|
||||
|
||||
def doRender(self, layoutType, request, context=None, name=None):
|
||||
'''Allows to call pxRender from code, to display the content of this
|
||||
field in some specific context, for example in a Computed field.'''
|
||||
if context == None: context = {}
|
||||
context['layoutType'] = layoutType
|
||||
context['field'] = self
|
||||
context['name'] = name or self.name
|
||||
# We may be executing a PX on a given object or on a given object tied
|
||||
# through a Ref.
|
||||
ctx = request.pxContext
|
||||
if 'obj' not in context:
|
||||
context['obj'] = ('tied' in ctx) and ctx['tied'] or ctx['obj']
|
||||
context['zobj'] = context['obj'].o
|
||||
# Copy some keys from the context of the currently executed PX.
|
||||
for k in ('tool', 'ztool', 'req', '_', 'q', 'url', 'dir', 'dright',
|
||||
'dleft', 'inPopup'):
|
||||
if k in context: continue
|
||||
context[k] = ctx[k]
|
||||
return self.pxRender(context).encode('utf-8')
|
||||
|
||||
# Show the field content for some object on a list of referred objects
|
||||
pxRenderAsTied = Px('''
|
||||
<!-- The "title" field -->
|
||||
<x if="refField.name == 'title'">
|
||||
<x if="mayView">
|
||||
<x if="not field.menuUrlMethod">:field.pxObjectTitle</x>
|
||||
<a if="field.menuUrlMethod"
|
||||
var2="info=field.getMenuUrl(zobj, tied)"
|
||||
href=":info[0]" target=":info[1]">:tied.title</a>
|
||||
<x if="tied.o.mayAct()">:field.pxObjectActions</x>
|
||||
</x>
|
||||
<div if="not mayView">
|
||||
<img src=":url('fake')" style="margin-right: 5px"/>
|
||||
<x>:_('unauthorized')</x></div>
|
||||
</x>
|
||||
<!-- Any other field -->
|
||||
<x if="(refField.name != 'title') and mayView">
|
||||
<x var="zobj=tied.o; obj=tied; layoutType='cell'; field=refField"
|
||||
if="field.isShowable(zobj, 'result')">:field.pxRender</x>
|
||||
</x>''')
|
||||
|
||||
# Show the field content for some object on a list of results
|
||||
pxRenderAsResult = Px('''
|
||||
<!-- Title -->
|
||||
<x if="field.name == 'title'"
|
||||
var2="navInfo='search.%s.%s.%d.%d' % (className, searchName, \
|
||||
startNumber+currentNumber, totalNumber)">
|
||||
<x if="mayView"
|
||||
var2="titleMode=inPopup and 'select' or 'link';
|
||||
pageName=zobj.getDefaultViewPage();
|
||||
selectJs=inPopup and 'onSelectObject(%s,%s,%s)' % (q(cbId), \
|
||||
q(rootHookId), q(uiSearch.initiator.url))">
|
||||
<x var="sup=zobj.getSupTitle(navInfo)" if="sup">::sup</x>
|
||||
<x>::zobj.getListTitle(mode=titleMode, nav=navInfo, target=target, \
|
||||
page=pageName, inPopup=inPopup, selectJs=selectJs, highlight=True)</x>
|
||||
<span style=":showSubTitles and 'display:inline' or 'display:none'"
|
||||
name="subTitle" var="sub=zobj.getSubTitle()"
|
||||
if="sub">::zobj.highlight(sub)</span>
|
||||
|
||||
<!-- Actions -->
|
||||
<div if="not inPopup and uiSearch.showActions and zobj.mayAct()"
|
||||
class="objectActions" style=":'display:%s' % uiSearch.showActions"
|
||||
var2="layoutType='buttons'">
|
||||
<!-- Edit -->
|
||||
<a if="zobj.mayEdit()"
|
||||
var2="linkInPopup=inPopup or (target.target != '_self')"
|
||||
target=":target.target" onclick=":target.openPopup"
|
||||
href=":zobj.getUrl(mode='edit', page=zobj.getDefaultEditPage(), \
|
||||
nav=navInfo, inPopup=linkInPopup)">
|
||||
<img src=":url('edit')" title=":_('object_edit')"/>
|
||||
</a>
|
||||
<!-- Delete -->
|
||||
<img if="zobj.mayDelete()" class="clickable" src=":url('delete')"
|
||||
title=":_('object_delete')"
|
||||
onClick=":'onDeleteObject(%s)' % q(zobj.id)"/>
|
||||
<!-- Workflow transitions -->
|
||||
<x if="zobj.showTransitions('result')"
|
||||
var2="targetObj=zobj">:targetObj.appy().pxTransitions</x>
|
||||
<!-- Fields (actions) defined with layout "buttons" -->
|
||||
<x if="not inPopup"
|
||||
var2="fields=zobj.getAppyTypes('buttons', 'main');
|
||||
layoutType='cell'">
|
||||
<!-- Call pxCell and not pxRender to avoid having a table -->
|
||||
<x for="field in fields"
|
||||
var2="name=field.name; smallButtons=True">:field.pxCell</x>
|
||||
</x>
|
||||
</div>
|
||||
</x>
|
||||
<x if="not mayView">
|
||||
<img src=":url('fake')" style="margin-right: 5px"/>
|
||||
<x>:_('unauthorized')</x>
|
||||
</x>
|
||||
</x>
|
||||
<!-- Any other field -->
|
||||
<x if="(field.name != 'title') and mayView">
|
||||
<x var="layoutType='cell'"
|
||||
if="field.isShowable(zobj, 'result')">:field.pxRender</x>
|
||||
</x>''')
|
||||
|
||||
# Displays a field label
|
||||
pxLabel = Px('''<label if="field.hasLabel and field.renderLabel"
|
||||
lfor=":field.name">::_('label', field=field)</label>''')
|
||||
|
||||
# Displays a field description
|
||||
pxDescription = Px('''<span if="field.hasDescr"
|
||||
class="discreet">::_('descr', field=field)</span>''')
|
||||
|
||||
# Displays a field help
|
||||
pxHelp = Px('''<acronym title=":_('help', field=field)"><img
|
||||
src=":url('help')"/></acronym>''')
|
||||
|
||||
# Displays validation-error-related info about a field
|
||||
pxValidation = Px('''<x><acronym if="error" title=":error"><img
|
||||
src=":url('warning')"/></acronym><img if="not error"
|
||||
src=":url('warning_no.gif')"/></x>''')
|
||||
|
||||
# Displays the fact that a field is required
|
||||
pxRequired = Px('''<img src=":url('required.gif')"/>''')
|
||||
|
||||
# Button for showing changes to the field
|
||||
pxChanges = Px('''
|
||||
<div if="zobj.hasHistory(name)" style="margin-bottom: 5px">
|
||||
<!-- Button for showing the field version containing changes -->
|
||||
<input if="not showChanges"
|
||||
var2="label=_('changes_show');
|
||||
css=ztool.getButtonCss(label)" type="button" class=":css"
|
||||
value=":label" style=":url('changes', bg=True)"
|
||||
onclick=":'askField(%s,%s,%s,null,%s)' % \
|
||||
(q(tagId), q(obj.url), q('view'), q('True'))"/>
|
||||
<!-- Button for showing the field version without changes -->
|
||||
<input if="showChanges"
|
||||
var2="label=_('changes_hide');
|
||||
css=ztool.getButtonCss(label)" type="button" class=":css"
|
||||
value=":label" style=":url('changesNo', bg=True)"
|
||||
onclick=":'askField(%s,%s,%s,null,%s)' % \
|
||||
(q(tagId), q(obj.url), q('view'), q('False'))"/>
|
||||
</div>''')
|
||||
|
||||
def __init__(self, validator, multiplicity, default, show, page, group,
|
||||
layouts, move, indexed, mustIndex, searchable,
|
||||
specificReadPermission, specificWritePermission, width, height,
|
||||
maxChars, colspan, master, masterValue, focus, historized,
|
||||
mapping, label, sdefault, scolspan, swidth, sheight, persist,
|
||||
view, xml):
|
||||
# The validator restricts which values may be defined. It can be an
|
||||
# interval (1,None), a list of string values ['choice1', 'choice2'],
|
||||
# a regular expression, a custom function, a Selection instance, etc.
|
||||
self.validator = validator
|
||||
# Multiplicity is a 2-tuple indicating the minimum and maximum
|
||||
# occurrences of values.
|
||||
self.multiplicity = multiplicity
|
||||
# Is the field required or not ? (derived from multiplicity)
|
||||
self.required = self.multiplicity[0] > 0
|
||||
# Default value
|
||||
self.default = default
|
||||
# Must the field be visible or not?
|
||||
self.show = show
|
||||
# When displaying/editing the whole object, on what page and phase must
|
||||
# this field value appear?
|
||||
self.page = Page.get(page)
|
||||
self.pageName = self.page.name
|
||||
# Within self.page, in what group of fields must this one appear?
|
||||
self.group = Group.get(group)
|
||||
# The following attribute allows to move a field back to a previous
|
||||
# position (useful for moving fields above predefined ones).
|
||||
self.move = move
|
||||
# If indexed is True, a database index will be set on the field for
|
||||
# fast access.
|
||||
self.indexed = indexed
|
||||
# If "mustIndex", True by default, is specified, it must be a method
|
||||
# returning a boolean value. Indexation will only occur when this value
|
||||
# is True.
|
||||
self.mustIndex = mustIndex
|
||||
if not mustIndex and not callable(mustIndex):
|
||||
raise Exception('Value for param "mustIndex" must be a method.')
|
||||
# If specified "searchable", the field will be added to some global
|
||||
# index allowing to perform application-wide, keyword searches.
|
||||
self.searchable = searchable
|
||||
# Normally, permissions to read or write every attribute in a type are
|
||||
# granted if the user has the global permission to read or
|
||||
# edit instances of the whole type. If you want a given attribute
|
||||
# to be protected by specific permissions, set one or the 2 next boolean
|
||||
# values to "True". In this case, you will create a new "field-only"
|
||||
# read and/or write permission. If you need to protect several fields
|
||||
# with the same read/write permission, you can avoid defining one
|
||||
# specific permission for every field by specifying a "named"
|
||||
# permission (string) instead of assigning "True" to the following
|
||||
# arg(s). A named permission will be global to your whole Zope site, so
|
||||
# take care to the naming convention. Typically, a named permission is
|
||||
# of the form: "<yourAppName>: Write|Read ---". If, for example, I want
|
||||
# to define, for my application "MedicalFolder" a specific permission
|
||||
# for a bunch of fields that can only be modified by a doctor, I can
|
||||
# define a permission "MedicalFolder: Write medical information" and
|
||||
# assign it to the "specificWritePermission" of every impacted field.
|
||||
self.specificReadPermission = specificReadPermission
|
||||
self.specificWritePermission = specificWritePermission
|
||||
# Widget width and height
|
||||
self.width = width
|
||||
self.height = height
|
||||
# While width and height refer to widget dimensions, maxChars hereafter
|
||||
# represents the maximum number of chars that a given input field may
|
||||
# accept (corresponds to HTML "maxlength" property). "None" means
|
||||
# "unlimited".
|
||||
self.maxChars = maxChars or ''
|
||||
# If the widget is in a group with multiple columns, the following
|
||||
# attribute specifies on how many columns to span the widget.
|
||||
self.colspan = colspan or 1
|
||||
# The list of slaves of this field, if it is a master
|
||||
self.slaves = []
|
||||
# The behaviour of this field may depend on another, "master" field
|
||||
self.master = master
|
||||
if master: self.master.slaves.append(self)
|
||||
# The semantics of attribute "masterValue" below is as follows:
|
||||
# - if "masterValue" is anything but a method, the field will be shown
|
||||
# only when the master has this value, or one of it if multivalued;
|
||||
# - if "masterValue" is a method, the value(s) of the slave field will
|
||||
# be returned by this method, depending on the master value(s) that
|
||||
# are given to it, as its unique parameter.
|
||||
self.masterValue = gutils.initMasterValue(masterValue)
|
||||
# If a field must retain attention in a particular way, set focus=True.
|
||||
# It will be rendered in a special way.
|
||||
self.focus = focus
|
||||
# If we must keep track of changes performed on a field, "historized"
|
||||
# must be set to True.
|
||||
self.historized = historized
|
||||
# Mapping is a dict of contexts that, if specified, are given when
|
||||
# translating the label, descr or help related to this field.
|
||||
self.mapping = self.formatMapping(mapping)
|
||||
self.id = id(self)
|
||||
self.type = self.__class__.__name__
|
||||
self.pythonType = None # The True corresponding Python type
|
||||
# Get the layouts. Consult layout.py for more info about layouts.
|
||||
self.layouts = self.formatLayouts(layouts)
|
||||
# Can we filter this field?
|
||||
self.filterable = False
|
||||
# Can this field have values that can be edited and validated?
|
||||
self.validable = True
|
||||
# The base label for translations is normally generated automatically.
|
||||
# It is made of 2 parts: the prefix, based on class name, and the name,
|
||||
# which is the field name by default. You can change this by specifying
|
||||
# a value for param "label". If this value is a string, it will be
|
||||
# understood as a new prefix. If it is a tuple, it will represent the
|
||||
# prefix and another name. If you want to specify a new name only, and
|
||||
# not a prefix, write (None, newName).
|
||||
self.label = label
|
||||
# When you specify a default value "for search" (= "sdefault"), on a
|
||||
# search screen, in the search field corresponding to this field, this
|
||||
# default value will be present.
|
||||
self.sdefault = sdefault
|
||||
# Colspan for rendering the search widget corresponding to this field.
|
||||
self.scolspan = scolspan or 1
|
||||
# Width and height for the search widget
|
||||
self.swidth = swidth or width
|
||||
self.sheight = sheight or height
|
||||
# "persist" indicates if field content must be stored in the database.
|
||||
# For some fields it is not wanted (ie, fields used only as masters to
|
||||
# update slave's selectable values).
|
||||
self.persist = persist
|
||||
# If you want to use an alternate PX than Field.pxView, you can specify
|
||||
# it in "view".
|
||||
if view != None:
|
||||
# This instance attribute will override the class attribute
|
||||
self.pxView = view
|
||||
# Standard marshallers are provided for converting values of this field
|
||||
# into XML. If you want to customize the marshalling process, you can
|
||||
# define a method in "xml" that will accept a field value and will
|
||||
# return a possibly different value. Be careful: do not return a chunk
|
||||
# of XML here! Simply return an alternate value, that will be
|
||||
# XML-marshalled.
|
||||
self.xml = xml
|
||||
|
||||
def init(self, name, klass, appName):
|
||||
'''When the application server starts, this secondary constructor is
|
||||
called for storing the name of the Appy field (p_name) and other
|
||||
attributes that are based on the name of the Appy p_klass, and the
|
||||
application name (p_appName).'''
|
||||
if hasattr(self, 'name'): return # Already initialized
|
||||
self.name = name
|
||||
# Determine prefix for this class
|
||||
if not klass: prefix = appName
|
||||
else: prefix = gutils.getClassName(klass, appName)
|
||||
# Recompute the ID (and derived attributes) that may have changed if
|
||||
# we are in debug mode (because we recreate new Field instances).
|
||||
self.id = id(self)
|
||||
# Remember master name on every slave
|
||||
for slave in self.slaves: slave.masterName = name
|
||||
# Determine ids of i18n labels for this field
|
||||
labelName = name
|
||||
trPrefix = None
|
||||
if self.label:
|
||||
if isinstance(self.label, str): trPrefix = self.label
|
||||
else: # It is a tuple (trPrefix, name)
|
||||
if self.label[1]: labelName = self.label[1]
|
||||
if self.label[0]: trPrefix = self.label[0]
|
||||
if not trPrefix:
|
||||
trPrefix = prefix
|
||||
# Determine name to use for i18n
|
||||
self.labelId = '%s_%s' % (trPrefix, labelName)
|
||||
self.descrId = self.labelId + '_descr'
|
||||
self.helpId = self.labelId + '_help'
|
||||
# Determine read and write permissions for this field
|
||||
rp = self.specificReadPermission
|
||||
if rp and not isinstance(rp, str):
|
||||
self.readPermission = '%s: Read %s %s' % (appName, prefix, name)
|
||||
elif rp and isinstance(rp, str):
|
||||
self.readPermission = rp
|
||||
else:
|
||||
self.readPermission = 'read'
|
||||
wp = self.specificWritePermission
|
||||
if wp and not isinstance(wp, str):
|
||||
self.writePermission = '%s: Write %s %s' % (appName, prefix, name)
|
||||
elif wp and isinstance(wp, str):
|
||||
self.writePermission = wp
|
||||
else:
|
||||
self.writePermission = 'write'
|
||||
if (self.type == 'Ref') and not self.isBack:
|
||||
# We must initialise the corresponding back reference
|
||||
self.back.klass = klass
|
||||
self.back.init(self.back.attribute, self.klass, appName)
|
||||
if self.type in ('List', 'Dict'):
|
||||
for subName, subField in self.fields:
|
||||
fullName = '%s_%s' % (name, subName)
|
||||
subField.init(fullName, klass, appName)
|
||||
subField.name = '%s*%s' % (name, subName)
|
||||
|
||||
def reload(self, klass, obj):
|
||||
'''In debug mode, we want to reload layouts without restarting Zope.
|
||||
So this method will prepare a "new", reloaded version of p_self,
|
||||
that corresponds to p_self after a "reload" of its containing Python
|
||||
module has been performed.'''
|
||||
res = getattr(klass, self.name, None)
|
||||
if not res: return self
|
||||
if (self.type == 'Ref') and self.isBack: return self
|
||||
res.init(self.name, klass, obj.getProductConfig().PROJECTNAME)
|
||||
return res
|
||||
|
||||
def isMultiValued(self):
|
||||
'''Does this type definition allow to define multiple values?'''
|
||||
maxOccurs = self.multiplicity[1]
|
||||
return (maxOccurs == None) or (maxOccurs > 1)
|
||||
|
||||
def isSortable(self, usage):
|
||||
'''Can fields of this type be used for sorting purposes (when sorting
|
||||
search results (p_usage="search") or when sorting reference fields
|
||||
(p_usage="ref")?'''
|
||||
if self.name == 'state': return
|
||||
if usage == 'search':
|
||||
return self.indexed and not self.isMultiValued() and not \
|
||||
((self.type == 'String') and self.isSelection())
|
||||
elif usage == 'ref':
|
||||
if self.type in ('Integer', 'Float', 'Boolean', 'Date'): return True
|
||||
elif self.type == 'String':
|
||||
return (self.format == 0) and not self.isMultilingual(None,True)
|
||||
|
||||
def isShowable(self, obj, layoutType):
|
||||
'''When displaying p_obj on a given p_layoutType, must we show this
|
||||
field?'''
|
||||
# Check if the user has the permission to view or edit the field
|
||||
perm = (layoutType == 'edit') and self.writePermission or \
|
||||
self.readPermission
|
||||
if not obj.allows(perm): return
|
||||
# Evaluate self.show
|
||||
if isinstance(self.show, collections.Callable):
|
||||
res = self.callMethod(obj, self.show)
|
||||
else:
|
||||
res = self.show
|
||||
# Take into account possible values 'view', 'edit', 'result'...
|
||||
if type(res) in sutils.sequenceTypes:
|
||||
for r in res:
|
||||
if r == layoutType: return True
|
||||
return
|
||||
elif res in ('view', 'edit', 'result', 'buttons', 'xml'):
|
||||
return res == layoutType
|
||||
# For showing a field on layout "buttons", the "buttons" layout must
|
||||
# explicitly be returned by the show method.
|
||||
if layoutType != 'buttons': return bool(res)
|
||||
|
||||
def isRenderable(self, layoutType):
|
||||
'''In some contexts, computing m_isShowable can be a performance
|
||||
problem. For example, when showing fields of some object on layout
|
||||
"buttons", there are plenty of fields that simply can't be shown on
|
||||
this kind of layout: it is no worth computing m_isShowable for those
|
||||
fields. m_isRenderable is meant to define light conditions to
|
||||
determine, before calling m_isShowable, if some field has a chance to
|
||||
be shown or not.
|
||||
|
||||
In other words, m_isRenderable defines a "structural" condition,
|
||||
independent of any object, while m_isShowable defines a contextual
|
||||
condition, depending on some object.'''
|
||||
# Most fields are not renderable on layout "buttons"
|
||||
if layoutType == 'buttons': return
|
||||
return True
|
||||
|
||||
def isClientVisible(self, obj):
|
||||
'''This method returns True if this field is visible according to
|
||||
master/slave relationships.'''
|
||||
masterData = self.getMasterData()
|
||||
if not masterData: return True
|
||||
else:
|
||||
master, masterValue = masterData
|
||||
if masterValue and isinstance(masterValue, collections.Callable): return True
|
||||
reqValue = master.getRequestValue(obj)
|
||||
# reqValue can be a list or not
|
||||
if type(reqValue) not in sutils.sequenceTypes:
|
||||
return reqValue in masterValue
|
||||
else:
|
||||
for m in masterValue:
|
||||
for r in reqValue:
|
||||
if m == r: return True
|
||||
|
||||
def formatMapping(self, mapping):
|
||||
'''Creates a dict of mappings, one entry by label type (label, descr,
|
||||
help).'''
|
||||
if isinstance(mapping, __builtins__['dict']):
|
||||
# Is it a dict like {'label':..., 'descr':...}, or is it directly a
|
||||
# dict with a mapping?
|
||||
for k, v in mapping.items():
|
||||
if (k not in self.labelTypes) or isinstance(v, str):
|
||||
# It is already a mapping
|
||||
return {'label':mapping, 'descr':mapping, 'help':mapping}
|
||||
# If we are here, we have {'label':..., 'descr':...}. Complete
|
||||
# it if necessary.
|
||||
for labelType in self.labelTypes:
|
||||
if labelType not in mapping:
|
||||
mapping[labelType] = None # No mapping for this value.
|
||||
return mapping
|
||||
else:
|
||||
# Mapping is a method that must be applied to any i18n message
|
||||
return {'label':mapping, 'descr':mapping, 'help':mapping}
|
||||
|
||||
def formatLayouts(self, layouts):
|
||||
'''Standardizes the given p_layouts. .'''
|
||||
# First, get the layouts as a dictionary, if p_layouts is None or
|
||||
# expressed as a simple string.
|
||||
areDefault = False
|
||||
if not layouts:
|
||||
# Get the default layouts as defined by the subclass
|
||||
areDefault = True
|
||||
layouts = self.computeDefaultLayouts()
|
||||
else:
|
||||
if isinstance(layouts, str):
|
||||
# The user specified a single layoutString (the "edit" one)
|
||||
layouts = {'edit': layouts}
|
||||
elif isinstance(layouts, Table):
|
||||
# Idem, but with a Table instance
|
||||
layouts = {'edit': Table(other=layouts)}
|
||||
else:
|
||||
# Here, we make a copy of the layouts, because every layout can
|
||||
# be different, even if the user decides to reuse one from one
|
||||
# field to another. This is because we modify every layout for
|
||||
# adding master/slave-related info, focus-related info, etc,
|
||||
# which can be different from one field to the other.
|
||||
layouts = copy.deepcopy(layouts)
|
||||
if 'edit' not in layouts:
|
||||
defEditLayout = self.computeDefaultLayouts()
|
||||
if isinstance(defEditLayout, __builtins__['dict']):
|
||||
defEditLayout = defEditLayout['edit']
|
||||
layouts['edit'] = defEditLayout
|
||||
# We have now a dict of layouts in p_layouts. Ensure now that a Table
|
||||
# instance is created for every layout (=value from the dict). Indeed,
|
||||
# a layout could have been expressed as a simple layout string.
|
||||
for layoutType in layouts.keys():
|
||||
if isinstance(layouts[layoutType], str):
|
||||
layouts[layoutType] = Table(layouts[layoutType])
|
||||
# Derive "view", "search" and "cell" layouts from the "edit" layout
|
||||
# when relevant.
|
||||
if 'view' not in layouts:
|
||||
layouts['view'] = Table(other=layouts['edit'], derivedType='view')
|
||||
if 'search' not in layouts:
|
||||
layouts['search'] = Table(other=layouts['view'],
|
||||
derivedType='search')
|
||||
# Create the "cell" layout from the 'view' layout if not specified.
|
||||
if 'cell' not in layouts:
|
||||
layouts['cell'] = Table(other=layouts['view'], derivedType='cell')
|
||||
# Put the required CSS classes in the layouts
|
||||
layouts['cell'].addCssClasses('no')
|
||||
if self.focus:
|
||||
# We need to make it flashy
|
||||
layouts['view'].addCssClasses('focus')
|
||||
layouts['edit'].addCssClasses('focus')
|
||||
# If layouts are the default ones, set width=None instead of width=100%
|
||||
# for the field if it is not in a group (excepted for rich texts and
|
||||
# refs).
|
||||
if areDefault and not self.group and \
|
||||
not ((self.type == 'String') and (self.format == self.XHTML)) and \
|
||||
not (self.type == 'Ref'):
|
||||
for layoutType in layouts.keys():
|
||||
layouts[layoutType].width = ''
|
||||
# Remove letters "r" from the layouts if the field is not required.
|
||||
if not self.required:
|
||||
for layoutType in layouts.keys():
|
||||
layouts[layoutType].removeElement('r')
|
||||
# Derive some boolean values from the layouts.
|
||||
self.hasLabel = self.hasLayoutElement('l', layouts)
|
||||
# "renderLabel" indicates if the existing label (if hasLabel is True)
|
||||
# must be rendered by pxLabel. For example, if field is an action, the
|
||||
# label will be rendered within the button, not by pxLabel.
|
||||
self.renderLabel = self.hasLabel
|
||||
# If field is within a group rendered like a tab, the label will already
|
||||
# be rendered in the corresponding tab.
|
||||
if self.group and (self.group.style == 'tabs'): self.renderLabel = False
|
||||
self.hasDescr = self.hasLayoutElement('d', layouts)
|
||||
self.hasHelp = self.hasLayoutElement('h', layouts)
|
||||
return layouts
|
||||
|
||||
@staticmethod
|
||||
def copyLayouts(layouts):
|
||||
'''Create a deep copy of p_layouts.'''
|
||||
res = {}
|
||||
for k, v in layouts.iteritems():
|
||||
if isinstance(v, Table): res[k] = Table(other=v)
|
||||
else: res[k] = v
|
||||
return res
|
||||
|
||||
def hasLayoutElement(self, element, layouts):
|
||||
'''This method returns True if the given layout p_element can be found
|
||||
at least once among the various p_layouts defined for this field.'''
|
||||
for layout in layouts.values():
|
||||
if element in layout.layoutString: return True
|
||||
return False
|
||||
|
||||
def getDefaultLayouts(self):
|
||||
'''Any subclass can define this for getting a specific set of
|
||||
default layouts. If None is returned, a global set of default layouts
|
||||
will be used.'''
|
||||
|
||||
def getInputLayouts(self):
|
||||
'''Gets, as a string, the layouts as could have been specified as input
|
||||
value for the Field constructor.'''
|
||||
res = '{'
|
||||
for k, v in self.layouts.items():
|
||||
res += '"%s":"%s",' % (k, v.layoutString)
|
||||
res += '}'
|
||||
return res
|
||||
|
||||
def computeDefaultLayouts(self):
|
||||
'''This method gets the default layouts from an Appy type, or a copy
|
||||
from the global default field layouts when they are not available.'''
|
||||
res = self.getDefaultLayouts()
|
||||
if not res:
|
||||
# Get the global default layouts
|
||||
res = copy.deepcopy(defaultFieldLayouts)
|
||||
return res
|
||||
|
||||
def getCss(self, layoutType, res, config):
|
||||
'''This method completes the list p_res with the names of CSS files
|
||||
that are required for displaying widgets of self's type on a given
|
||||
p_layoutType. p_res is not a set because order of inclusion of CSS
|
||||
files may be important and may be loosed by using sets.'''
|
||||
if layoutType in self.cssFiles:
|
||||
for fileName in self.cssFiles[layoutType]:
|
||||
if fileName not in res:
|
||||
res.append(fileName)
|
||||
|
||||
def getJs(self, layoutType, res, config):
|
||||
'''This method completes the list p_res with the names of Javascript
|
||||
files that are required for displaying widgets of self's type on a
|
||||
given p_layoutType. p_res is not a set because order of inclusion of
|
||||
CSS files may be important and may be loosed by using sets.'''
|
||||
if layoutType in self.jsFiles:
|
||||
for fileName in self.jsFiles[layoutType]:
|
||||
if fileName not in res:
|
||||
res.append(fileName)
|
||||
|
||||
def getValue(self, obj):
|
||||
'''Gets, on_obj, the value conforming to self's type definition.'''
|
||||
value = getattr(obj.aq_base, self.name, None)
|
||||
if self.isEmptyValue(obj, value):
|
||||
# If there is no value, get the default value if any: return
|
||||
# self.default, of self.default() if it is a method.
|
||||
if isinstance(self.default, collections.Callable):
|
||||
try:
|
||||
# Caching a default value can lead to problems. For example,
|
||||
# the process of creating an object from another one, or
|
||||
# from some data, sometimes consists in (a) creating an
|
||||
# "empty" object, (b) initializing its values and
|
||||
# (c) reindexing it. Default values are computed in (a),
|
||||
# but it they depend on values set at (b), and are cached
|
||||
# and indexed, (c) will get the wrong, cached value.
|
||||
return self.callMethod(obj, self.default, cache=False)
|
||||
except Exception as e:
|
||||
# Already logged. Here I do not raise the exception,
|
||||
# because it can be raised as the result of reindexing
|
||||
# the object in situations that are not foreseen by
|
||||
# method in self.default.
|
||||
return
|
||||
else:
|
||||
return self.default
|
||||
return value
|
||||
|
||||
def getCopyValue(self, obj):
|
||||
'''Gets the value of this field on p_obj as with m_getValue above. But
|
||||
if this value is mutable, get a copy of it.'''
|
||||
return self.getValue(obj)
|
||||
|
||||
def getFormattedValue(self, obj, value, layoutType='view',
|
||||
showChanges=False, language=None):
|
||||
'''p_value is a real p_obj(ect) value from a field from this type. This
|
||||
method returns a pretty, string-formatted version, for displaying
|
||||
purposes. Needs to be overridden by some child classes. If
|
||||
p_showChanges is True, the result must also include the changes that
|
||||
occurred on p_value across the ages. If the formatting implies
|
||||
translating some elements, p_language will be used if given, the
|
||||
user language else.'''
|
||||
if self.isEmptyValue(obj, value): return ''
|
||||
return value
|
||||
|
||||
def getShownValue(self, obj, value, layoutType='view',
|
||||
showChanges=False, language=None):
|
||||
'''Similar to m_getFormattedValue, but in some contexts, only a part of
|
||||
p_value must be shown. For example, sometimes we need to display only
|
||||
a language-specific part of a multilingual field (see overridden
|
||||
method in string.py).'''
|
||||
return self.getFormattedValue(obj,value,layoutType,showChanges,language)
|
||||
|
||||
def getXmlValue(self, obj, value):
|
||||
'''This method allows a developer to customize the value that will be
|
||||
marshalled into XML. It makes use of attribute "xml".'''
|
||||
if not self.xml: return value
|
||||
return self.xml(obj, value)
|
||||
|
||||
def getIndexType(self):
|
||||
'''Returns the name of the technical, Zope-level index type for this
|
||||
field.'''
|
||||
# Normally, self.indexed contains a Boolean. If a string value is given,
|
||||
# we consider it to be an index type. It allows to bypass the standard
|
||||
# way to decide what index type must be used.
|
||||
if isinstance(self.indexed, str): return self.indexed
|
||||
if self.name == 'title': return 'TextIndex'
|
||||
return 'FieldIndex'
|
||||
|
||||
def getIndexValue(self, obj, forSearch=False):
|
||||
'''This method returns a version for this field value on p_obj that is
|
||||
ready for indexing purposes. Needs to be overridden by some child
|
||||
classes.
|
||||
|
||||
If p_forSearch is True, it will return a "string" version of the
|
||||
index value suitable for a global search.'''
|
||||
# Must we produce an index value?
|
||||
if not self.getAttribute(obj, 'mustIndex'): return
|
||||
# Start by getting the field value on p_obj
|
||||
res = self.getValue(obj)
|
||||
# Zope catalog does not like unicode strings
|
||||
if isinstance(value, str):
|
||||
res = value.encode('utf-8')
|
||||
if forSearch and (res != None):
|
||||
if type(res) in sutils.sequenceTypes:
|
||||
vals = []
|
||||
for v in res:
|
||||
if isinstance(v, str): vals.append(v.encode('utf-8'))
|
||||
else: vals.append(str(v))
|
||||
res = ' '.join(vals)
|
||||
else:
|
||||
res = str(res)
|
||||
return res
|
||||
|
||||
def getIndexName(self, usage='search'):
|
||||
'''Gets the name of the Zope index that corresponds to this field.
|
||||
Indexes can be used for searching (p_usage="search") or for sorting
|
||||
(usage="sort"). The method returns None if the field
|
||||
named p_fieldName can't be used for p_usage.'''
|
||||
# Manage special cases
|
||||
if self.name == 'title':
|
||||
# For field 'title', Appy has a specific index 'SortableTitle',
|
||||
# because index 'Title' is a TextIndex (for searchability) and can't
|
||||
# be used for sorting.
|
||||
return (usage == 'sort') and 'SortableTitle' or 'Title'
|
||||
elif self.name == 'state': return 'State'
|
||||
elif self.name == 'SearchableText': return 'SearchableText'
|
||||
else:
|
||||
res = 'get%s%s'% (self.name[0].upper(), self.name[1:])
|
||||
if (usage == 'sort') and self.hasSortIndex(): res += '_sort'
|
||||
return res
|
||||
|
||||
def hasSortIndex(self):
|
||||
'''Some fields have indexes that prevents sorting (ie, list indexes).
|
||||
Those fields may define a secondary index, specifically for sorting.
|
||||
This is the case of Ref fields for example.'''
|
||||
return
|
||||
|
||||
def getCatalogValue(self, obj, usage='search'):
|
||||
'''This method returns the index value that is currently stored in the
|
||||
catalog for this field on p_obj.'''
|
||||
if not self.indexed:
|
||||
raise Exception('Field %s: cannot retrieve catalog version of ' \
|
||||
'unindexed field.' % self.name)
|
||||
return obj.getTool().getCatalogValue(obj, self.getIndexName(usage))
|
||||
|
||||
def valueIsInRequest(self, obj, request, name, layoutType):
|
||||
'''Is there a value corresponding to this field in the request? p_name
|
||||
can be different from self.name (ie, if it is a field within another
|
||||
(List) field). In most cases, checking that this p_name is in the
|
||||
request is sufficient. But in some cases it may be more complex, ie
|
||||
for string multilingual fields.'''
|
||||
return request.has_key(name)
|
||||
|
||||
def getRequestValue(self, obj, requestName=None):
|
||||
'''Gets a value for this field as carried in the request object. In the
|
||||
simplest cases, the request value is a single value whose name in the
|
||||
request is the name of the field.
|
||||
|
||||
Sometimes (ie: a Date: see the overriden method in the Date class),
|
||||
several request values must be combined.
|
||||
|
||||
Sometimes (ie, a field which is a sub-field in a List), the name of
|
||||
the request value(s) representing the field value do not correspond
|
||||
to the field name (ie: the request name includes information about
|
||||
the container field). In this case, p_requestName must be used for
|
||||
searching into the request, instead of the field name (self.name).'''
|
||||
name = requestName or self.name
|
||||
return obj.REQUEST.get(name, None)
|
||||
|
||||
def getStorableValue(self, obj, value):
|
||||
'''p_value is a valid value initially computed through calling
|
||||
m_getRequestValue. So, it is a valid string (or list of strings)
|
||||
representation of the field value coming from the request.
|
||||
This method computes the real (potentially converted or manipulated
|
||||
in some other way) value as can be stored in the database.'''
|
||||
if self.isEmptyValue(obj, value): return
|
||||
return value
|
||||
|
||||
def setSlave(self, slaveField, masterValue):
|
||||
'''Sets p_slaveField as slave of this field. Normally, master/slave
|
||||
relationships are defined when a slave field is defined. At this time
|
||||
you specify parameters "master" and "masterValue" for this field and
|
||||
that's all. This method is used to add a master/slave relationship
|
||||
that was not initially foreseen.'''
|
||||
slaveField.master = self
|
||||
slaveField.masterValue = gutils.initMasterValue(masterValue)
|
||||
if slaveField not in self.slaves:
|
||||
self.slaves.append(slaveField)
|
||||
# Master's init method may not have been called yet.
|
||||
slaveField.masterName = getattr(self, 'name', None)
|
||||
|
||||
def getMasterData(self):
|
||||
'''Gets the master of this field (and masterValue) or, recursively, of
|
||||
containing groups when relevant.'''
|
||||
if self.master: return (self.master, self.masterValue)
|
||||
if self.group: return self.group.getMasterData()
|
||||
|
||||
def getSlaveCss(self):
|
||||
'''Gets the CSS class that must apply to this field in the web UI when
|
||||
this field is the slave of another field.'''
|
||||
if not self.master: return ''
|
||||
res = 'slave*%s*' % self.masterName
|
||||
if not isinstance(self.masterValue, collections.Callable):
|
||||
res += '*'.join(self.masterValue)
|
||||
else:
|
||||
res += '+'
|
||||
return res
|
||||
|
||||
def getOnChange(self, zobj, layoutType, className=None):
|
||||
'''When this field is a master, this method computes the call to the
|
||||
Javascript function that will be called when its value changes (in
|
||||
order to update slaves).'''
|
||||
if not self.slaves: return ''
|
||||
q = zobj.getTool().quote
|
||||
# When the field is on a search screen, we need p_className.
|
||||
cName = className and (',%s' % q(className)) or ''
|
||||
return 'updateSlaves(this,null,%s,%s,null,null%s)' % \
|
||||
(q(zobj.absolute_url()), q(layoutType), cName)
|
||||
|
||||
def isEmptyValue(self, obj, value):
|
||||
'''Returns True if the p_value must be considered as an empty value.'''
|
||||
return value in self.nullValues
|
||||
|
||||
def isCompleteValue(self, obj, value):
|
||||
'''Returns True if the p_value must be considered as "complete". While,
|
||||
in most cases, a "complete" value simply means a "non empty" value
|
||||
(see m_isEmptyValue above), in some special cases it is more subtle.
|
||||
For example, a multilingual string value is not empty as soon as a
|
||||
value is given for some language but will not be considered as
|
||||
complete while a value is missing for some language. Another example:
|
||||
a Date with the "hour" part required will not be considered as empty
|
||||
if the "day, month, year" part is present but will not be considered
|
||||
as complete without the "hour, minute" part.'''
|
||||
return not self.isEmptyValue(obj, value)
|
||||
|
||||
def validateValue(self, obj, value):
|
||||
'''This method may be overridden by child classes and will be called at
|
||||
the right moment by m_validate defined below for triggering
|
||||
type-specific validation. p_value is never empty.'''
|
||||
return
|
||||
|
||||
def securityCheck(self, obj, value):
|
||||
'''This method performs some security checks on the p_value that
|
||||
represents user input.'''
|
||||
if not isinstance(value, str): return
|
||||
# Search Javascript code in the value (prevent XSS attacks).
|
||||
if '<script' in value:
|
||||
obj.log('Detected Javascript in user input.', type='error')
|
||||
raise Exception('Your behaviour is considered a security ' \
|
||||
'attack. System administrator has been warned.')
|
||||
|
||||
def validate(self, obj, value):
|
||||
'''This method checks that p_value, coming from the request (p_obj is
|
||||
being created or edited) and formatted through a call to
|
||||
m_getRequestValue defined above, is valid according to this type
|
||||
definition. If it is the case, None is returned. Else, a translated
|
||||
error message is returned.'''
|
||||
# If the value is required, check that a (complete) value is present
|
||||
if not self.isCompleteValue(obj, value):
|
||||
if self.required and self.isClientVisible(obj):
|
||||
# If the field is required, but not visible according to
|
||||
# master/slave relationships, we consider it not to be required.
|
||||
return obj.translate('field_required')
|
||||
else:
|
||||
return
|
||||
# Perform security checks on p_value
|
||||
self.securityCheck(obj, value)
|
||||
# Triggers the sub-class-specific validation for this value
|
||||
message = self.validateValue(obj, value)
|
||||
if message: return message
|
||||
# Evaluate the custom validator if one has been specified
|
||||
value = self.getStorableValue(obj, value)
|
||||
if self.validator and (type(self.validator) in self.validatorTypes):
|
||||
obj = obj.appy()
|
||||
if type(self.validator) != self.validatorTypes[-1]:
|
||||
# It is a custom function: execute it
|
||||
try:
|
||||
validValue = self.validator(obj, value)
|
||||
if isinstance(validValue, str) and validValue:
|
||||
# Validation failed; and p_validValue contains an error
|
||||
# message.
|
||||
return validValue
|
||||
else:
|
||||
if not validValue:
|
||||
return obj.translate('field_invalid')
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
except:
|
||||
return obj.translate('field_invalid')
|
||||
else:
|
||||
# It is a regular expression
|
||||
if not self.validator.match(value):
|
||||
return obj.translate('field_invalid')
|
||||
|
||||
def store(self, obj, value):
|
||||
'''Stores the p_value (produced by m_getStorableValue) that complies to
|
||||
p_self type definition on p_obj.'''
|
||||
if self.persist: setattr(obj, self.name, value)
|
||||
|
||||
def callMethod(self, obj, method, cache=True):
|
||||
'''This method is used to call a p_method on p_obj. p_method is part of
|
||||
this type definition (ie a default method, the method of a Computed
|
||||
field, a method used for showing or not a field...). Normally, those
|
||||
methods are called without any arg. But one may need, within the
|
||||
method, to access the related field. This method tries to call
|
||||
p_method with no arg *or* with the field arg.'''
|
||||
obj = obj.appy()
|
||||
try:
|
||||
return gutils.callMethod(obj, method, cache=cache)
|
||||
except TypeError as te:
|
||||
# Try a version of the method that would accept self as an
|
||||
# additional parameter. In this case, we do not try to cache the
|
||||
# value (we do not call gutils.callMethod), because the value may
|
||||
# be different depending on the parameter.
|
||||
tb = sutils.Traceback.get()
|
||||
try:
|
||||
return method(obj, self)
|
||||
except Exception as e:
|
||||
obj.log(tb, type='error')
|
||||
# Raise the initial error.
|
||||
raise te
|
||||
except Exception as e:
|
||||
obj.log(sutils.Traceback.get(), type='error')
|
||||
raise e
|
||||
|
||||
def getAttribute(self, obj, name):
|
||||
'''Gets the value of attribue p_name on p_self, which can be a simple
|
||||
value or the result of a method call on p_obj.'''
|
||||
res = getattr(self, name)
|
||||
if not isinstance(res, collections.Callable): return res
|
||||
return self.callMethod(obj, res)
|
||||
|
||||
def process(self, obj):
|
||||
'''This method is a general hook allowing a field to perform some
|
||||
processing after an URL on an object has been called, of the form
|
||||
<objUrl>/onProcess.'''
|
||||
return obj.goto(obj.absolute_url())
|
||||
# ------------------------------------------------------------------------------
|
163
fields/action.py
|
@ -1,163 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import os.path
|
||||
from appy.fields import Field
|
||||
from appy.px import Px
|
||||
from appy.shared import utils as sutils
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Action(Field):
|
||||
'''An action is a Python method that can be triggered by the user on a
|
||||
given gen-class. An action is rendered as a button.'''
|
||||
|
||||
# PX for viewing the Action button
|
||||
pxView = pxCell = Px('''
|
||||
<form var="formId='%s_%s_form' % (zobj.id, name);
|
||||
label=_(field.labelId);
|
||||
descr=field.hasDescr and _(field.descrId) or None;
|
||||
smallButtons=smallButtons|False;
|
||||
css=ztool.getButtonCss(label, smallButtons);
|
||||
back=(layoutType == 'cell') and q(zobj.id) or 'null'"
|
||||
id=":formId" action=":zobj.absolute_url() + '/onExecuteAction'"
|
||||
style="display:inline">
|
||||
<input type="hidden" name="fieldName" value=":name"/>
|
||||
<input type="hidden" name="popupComment" value=""/>
|
||||
<input type="button" class=":css" title=":descr"
|
||||
var="textConfirm=field.confirm and _(field.labelId+'_confirm') or '';
|
||||
showComment=(field.confirm == 'text') and 'true' or 'false'"
|
||||
value=":label" style=":url(field.icon, bg=True)"
|
||||
onclick=":'submitForm(%s,%s,%s,%s)' % (q(formId), q(textConfirm), \
|
||||
showComment, back)"/>
|
||||
</form>''')
|
||||
|
||||
# It is not possible to edit an action, not to search it
|
||||
pxEdit = pxSearch = ''
|
||||
|
||||
def __init__(self, validator=None, multiplicity=(1,1), default=None,
|
||||
show=('view', 'result'), page='main', group=None, layouts=None,
|
||||
move=0, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
maxChars=None, colspan=1, action=None, result='computation',
|
||||
confirm=False, master=None, masterValue=None, focus=False,
|
||||
historized=False, mapping=None, label=None, icon=None,
|
||||
view=None, xml=None):
|
||||
# Can be a single method or a list/tuple of methods
|
||||
self.action = action
|
||||
# For the 'result' param:
|
||||
# * value 'computation' means that the action will simply compute
|
||||
# things and redirect the user to the same page, with some status
|
||||
# message about execution of the action;
|
||||
# * '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
|
||||
# If following field "confirm" is True, a popup will ask the user if
|
||||
# she is really sure about triggering this action.
|
||||
self.confirm = confirm
|
||||
# If no p_icon is specified, "action.png" will be used
|
||||
self.icon = icon or 'action'
|
||||
Field.__init__(self, None, (0,1), default, show, page, group, layouts,
|
||||
move, False, True, False, specificReadPermission,
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, historized, mapping, label,
|
||||
None, None, None, None, False, view, xml)
|
||||
self.validable = False
|
||||
self.renderLabel = False # Label is rendered directly within the button
|
||||
|
||||
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
|
||||
|
||||
def callAction(self, obj, method, hasParam, param):
|
||||
'''Calls p_method on p_obj. m_method can be the single action as defined
|
||||
in self.action or one of them is self.action contains several
|
||||
methods. Calling m_method can be done with a p_param (when p_hasParam
|
||||
is True), ie, when self.confirm is "text".'''
|
||||
if hasParam: return method(obj, param)
|
||||
else: return method(obj)
|
||||
|
||||
def __call__(self, obj):
|
||||
'''Calls the action on p_obj'''
|
||||
# Must we call the method(s) with a param ?
|
||||
hasParam = self.confirm == 'text'
|
||||
param = hasParam and obj.request.get('popupComment', None)
|
||||
if type(self.action) in sutils.sequenceTypes:
|
||||
# There are multiple Python methods
|
||||
res = [True, '']
|
||||
for act in self.action:
|
||||
actRes = self.callAction(obj, act, hasParam, param)
|
||||
if type(actRes) in sutils.sequenceTypes:
|
||||
res[0] = res[0] and actRes[0]
|
||||
if self.result.startswith('file'):
|
||||
res[1] = res[1] + actRes[1]
|
||||
else:
|
||||
res[1] = res[1] + '\n' + actRes[1]
|
||||
else:
|
||||
res[0] = res[0] and actRes
|
||||
else:
|
||||
# There is only one Python method
|
||||
actRes = self.callAction(obj, self.action, hasParam, param)
|
||||
if type(actRes) in sutils.sequenceTypes:
|
||||
res = list(actRes)
|
||||
else:
|
||||
res = [actRes, '']
|
||||
# If res is None (ie the user-defined action did not return anything),
|
||||
# we consider the action as successfull.
|
||||
if res[0] == None: res[0] = True
|
||||
return res
|
||||
|
||||
def isShowable(self, obj, layoutType):
|
||||
if layoutType == 'edit': return
|
||||
return Field.isShowable(self, obj, layoutType)
|
||||
|
||||
# Action fields can a priori be shown on every layout, "buttons" included
|
||||
def isRenderable(self, layoutType): return True
|
||||
|
||||
def onUiRequest(self, obj, rq):
|
||||
'''This method is called when a user triggers the execution of this
|
||||
action from the user interface.'''
|
||||
# Execute the action (method __call__)
|
||||
actionRes = self(obj.appy())
|
||||
parent = obj.getParentNode()
|
||||
parentAq = getattr(parent, 'aq_base', parent)
|
||||
if not hasattr(parentAq, obj.id):
|
||||
# The action has led to obj's deletion
|
||||
obj.reindex()
|
||||
# Unwrap action results
|
||||
successfull, msg = actionRes
|
||||
if not msg:
|
||||
# Use the default i18n messages
|
||||
suffix = successfull and 'done' or 'ko'
|
||||
msg = obj.translate('action_%s' % suffix)
|
||||
if (self.result == 'computation') or not successfull:
|
||||
# If we are called from an Ajax request, simply return msg
|
||||
if hasattr(rq, 'pxContext') and rq.pxContext['ajax']: return msg
|
||||
obj.say(msg)
|
||||
return obj.goto(obj.getUrl(rq['HTTP_REFERER']))
|
||||
elif self.result == 'file':
|
||||
# msg does not contain a message, but a file instance
|
||||
response = rq.RESPONSE
|
||||
response.setHeader('Content-Type', sutils.getMimeType(msg.name))
|
||||
response.setHeader('Content-Disposition', 'inline;filename="%s"' %\
|
||||
os.path.basename(msg.name))
|
||||
response.write(msg.read())
|
||||
msg.close()
|
||||
elif self.result == 'redirect':
|
||||
# msg does not contain a message, but the URL where to redirect
|
||||
# the user.
|
||||
return obj.goto(msg)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,137 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy.fields import Field
|
||||
from appy.px import Px
|
||||
from appy.gen.layout import Table
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Boolean(Field):
|
||||
'''Field for storing boolean values.'''
|
||||
|
||||
yesNo = {'true': 'yes', 'false': 'no', True: 'yes', False: 'no'}
|
||||
trueFalse = {True: 'true', False: 'false'}
|
||||
|
||||
# Default layout (render = "checkbox") ("b" stands for "base").
|
||||
bLayouts = {'view': 'lf', 'edit': Table('f;lrv;-', width=None),
|
||||
'search': 'l-f'}
|
||||
# Layout including a description.
|
||||
dLayouts = {'view': 'lf', 'edit': Table('flrv;=d', width=None)}
|
||||
# Centered layout, no description.
|
||||
cLayouts = {'view': 'lf|', 'edit': 'flrv|'}
|
||||
# Layout for radio buttons (render = "radios")
|
||||
rLayouts = {'edit': 'f', 'view': 'f', 'search': 'l-f'}
|
||||
rlLayouts = {'edit': 'l-f', 'view': 'lf', 'search': 'l-f'}
|
||||
|
||||
pxView = pxCell = Px('''<x>:value</x>
|
||||
<input type="hidden" if="masterCss"
|
||||
class=":masterCss" value=":rawValue" name=":name" id=":name"/>''')
|
||||
|
||||
pxEdit = Px('''<x var="isTrue=field.isTrue(zobj, rawValue)">
|
||||
<x if="field.render == 'checkbox'">
|
||||
<input type="checkbox" name=":name + '_visible'" id=":name"
|
||||
class=":masterCss" checked=":isTrue"
|
||||
onclick=":'toggleCheckbox(%s, %s); %s' % (q(name), \
|
||||
q('%s_hidden' % name), \
|
||||
field.getOnChange(zobj, layoutType))"/>
|
||||
<input type="hidden" name=":name" id=":'%s_hidden' % name"
|
||||
value=":isTrue and 'True' or 'False'"/>
|
||||
</x>
|
||||
<x if="field.render == 'radios'"
|
||||
var2="falseId='%s_false' % name;
|
||||
trueId='%s_true' % name">
|
||||
<input type="radio" name=":name" id=":falseId" class=":masterCss"
|
||||
value="False" checked=":not isTrue"/>
|
||||
<label lfor=":falseId">:_(field.labelId + '_false')</label><br/>
|
||||
<input type="radio" name=":name" id=":trueId" class=":masterCss"
|
||||
value="True" checked=":isTrue"/>
|
||||
<label lfor=":trueId">:_(field.labelId + '_true')</label>
|
||||
</x></x>''')
|
||||
|
||||
pxSearch = Px('''<x var="typedWidget='%s*bool' % widgetName">
|
||||
<x var="valueId='%s_yes' % name">
|
||||
<input type="radio" value="True" name=":typedWidget" id=":valueId"/>
|
||||
<label lfor=":valueId">:_(field.getValueLabel(True))</label>
|
||||
</x>
|
||||
<x var="valueId='%s_no' % name">
|
||||
<input type="radio" value="False" name=":typedWidget" id=":valueId"/>
|
||||
<label lfor=":valueId">:_(field.getValueLabel(False))</label>
|
||||
</x>
|
||||
<x var="valueId='%s_whatever' % name">
|
||||
<input type="radio" value="" name=":typedWidget" id=":valueId"
|
||||
checked="checked"/>
|
||||
<label lfor=":valueId">:_('whatever')</label>
|
||||
</x><br/></x>''')
|
||||
|
||||
def __init__(self, validator=None, multiplicity=(0,1), default=None,
|
||||
show=True, page='main', group=None, layouts = None, move=0,
|
||||
indexed=False, mustIndex=True, searchable=False,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=None, height=None, maxChars=None, colspan=1, master=None,
|
||||
masterValue=None, focus=False, historized=False, mapping=None,
|
||||
label=None, sdefault=False, scolspan=1, swidth=None,
|
||||
sheight=None, persist=True, render='checkbox', view=None,
|
||||
xml=None):
|
||||
# By default, a boolean is edited via a checkbox. It can also be edited
|
||||
# via 2 radio buttons (p_render="radios").
|
||||
self.render = render
|
||||
Field.__init__(self, validator, multiplicity, default, show, page,
|
||||
group, layouts, move, indexed, mustIndex, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, None, colspan, master, masterValue, focus,
|
||||
historized, mapping, label, sdefault, scolspan, swidth,
|
||||
sheight, persist, view, xml)
|
||||
self.pythonType = bool
|
||||
|
||||
def getDefaultLayouts(self):
|
||||
cp = Field.copyLayouts
|
||||
if self.render == 'radios': return cp(Boolean.rLayouts)
|
||||
return cp(Boolean.bLayouts)
|
||||
|
||||
def getValue(self, obj):
|
||||
'''Never returns "None". Returns always "True" or "False", even if
|
||||
"None" is stored in the DB.'''
|
||||
value = Field.getValue(self, obj)
|
||||
if value == None: return False
|
||||
return value
|
||||
|
||||
def getValueLabel(self, value):
|
||||
'''Returns the label for p_value (True or False): if self.render is
|
||||
"checkbox", the label is simply the translated version of "yes" or
|
||||
"no"; if self.render is "radios", there are specific labels.'''
|
||||
if self.render == 'radios':
|
||||
return '%s_%s' % (self.labelId, self.trueFalse[value])
|
||||
return self.yesNo[value]
|
||||
|
||||
def getFormattedValue(self, obj, value, layoutType='view',
|
||||
showChanges=False, language=None):
|
||||
return obj.translate(self.getValueLabel(value), language=language)
|
||||
|
||||
def getStorableValue(self, obj, value):
|
||||
if not self.isEmptyValue(obj, value):
|
||||
exec('res = %s' % value)
|
||||
return res
|
||||
|
||||
def isTrue(self, obj, dbValue):
|
||||
'''When rendering this field as a checkbox, must it be checked or
|
||||
not?'''
|
||||
rq = obj.REQUEST
|
||||
# Get the value we must compare (from request or from database)
|
||||
if self.name in rq:
|
||||
return rq.get(self.name) in ('True', 1, '1')
|
||||
return dbValue
|
||||
# ------------------------------------------------------------------------------
|
1772
fields/calendar.py
|
@ -1,104 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy.fields import Field
|
||||
from appy.px import Px
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Computed(Field):
|
||||
WRONG_METHOD = 'Wrong value "%s". Param "method" must contain a method ' \
|
||||
'or a PX.'
|
||||
pxView = pxCell = pxEdit = Px('''<x if="field.plainText">:value</x>
|
||||
<x if="not field.plainText">::value</x>''')
|
||||
|
||||
pxSearch = Px('''
|
||||
<input type="text" name=":'%s*string' % widgetName"
|
||||
maxlength=":field.maxChars" size=":field.width"
|
||||
value=":field.sdefault"/>''')
|
||||
|
||||
def __init__(self, validator=None, multiplicity=(0,1), default=None,
|
||||
show=None, page='main', group=None, layouts=None, move=0,
|
||||
indexed=False, mustIndex=True, searchable=False,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=None, height=None, maxChars=None, colspan=1, method=None,
|
||||
formatMethod=None, plainText=False, master=None,
|
||||
masterValue=None, focus=False, historized=False, mapping=None,
|
||||
label=None, sdefault='', scolspan=1, swidth=None, sheight=None,
|
||||
context=None, view=None, xml=None):
|
||||
# The Python method used for computing the field value, or a PX
|
||||
self.method = method
|
||||
# A specific method for producing the formatted value of this field.
|
||||
# This way, if, for example, the value is a DateTime instance which is
|
||||
# indexed, you can specify in m_formatMethod the way to format it in
|
||||
# the user interface while m_method computes the value stored in the
|
||||
# catalog.
|
||||
self.formatMethod = formatMethod
|
||||
if isinstance(self.method, str):
|
||||
# A legacy macro identifier. Raise an exception
|
||||
raise Exception(self.WRONG_METHOD % self.method)
|
||||
# Does field computation produce plain text or XHTML?
|
||||
self.plainText = plainText
|
||||
if isinstance(method, Px):
|
||||
# When field computation is done with a PX, the result is XHTML
|
||||
self.plainText = False
|
||||
# Determine default value for "show"
|
||||
if show == None:
|
||||
# XHTML content in a Computed field generally corresponds to some
|
||||
# custom XHTML widget. This is why, by default, we do not render it
|
||||
# in the xml layout.
|
||||
show = self.plainText and ('view', 'result', 'xml') or \
|
||||
('view', 'result')
|
||||
# If method is a PX, its context can be given in p_context
|
||||
self.context = context
|
||||
Field.__init__(self, None, multiplicity, default, show, page, group,
|
||||
layouts, move, indexed, mustIndex, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, None, colspan, master, masterValue, focus,
|
||||
historized, mapping, label, sdefault, scolspan, swidth,
|
||||
sheight, False, view, xml)
|
||||
self.validable = False
|
||||
|
||||
def getValue(self, obj):
|
||||
'''Computes the value instead of getting it in the database.'''
|
||||
if not self.method: return
|
||||
if isinstance(self.method, Px):
|
||||
obj = obj.appy()
|
||||
tool = obj.tool
|
||||
req = obj.request
|
||||
# Get the context of the currently executed PX if present
|
||||
try:
|
||||
ctx = req.pxContext
|
||||
except AttributeError:
|
||||
# Create some standard context
|
||||
ctx = {'obj': obj, 'zobj': obj.o, 'field': self,
|
||||
'req': req, 'tool': tool, 'ztool': tool.o,
|
||||
'_': tool.translate, 'url': tool.o.getIncludeUrl}
|
||||
if self.context: ctx.update(self.context)
|
||||
return self.method(ctx)
|
||||
else:
|
||||
# self.method is a method that will return the field value
|
||||
return self.callMethod(obj, self.method, cache=False)
|
||||
|
||||
def getFormattedValue(self, obj, value, layoutType='view',
|
||||
showChanges=False, language=None):
|
||||
if self.formatMethod:
|
||||
res = self.formatMethod(obj, value)
|
||||
else:
|
||||
res = value
|
||||
if not isinstance(res, str): res = str(res)
|
||||
return res
|
||||
# ------------------------------------------------------------------------------
|
301
fields/date.py
|
@ -1,301 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import time
|
||||
from appy.fields import Field
|
||||
from appy.px import Px
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
def getDateFromIndexValue(indexValue):
|
||||
'''p_indexValue is the internal representation of a date as stored in the
|
||||
zope Date index (see "_convert" method in DateIndex.py in
|
||||
Products.pluginIndexes/DateIndex). This function produces a DateTime
|
||||
based on it.'''
|
||||
# p_indexValue represents a number of minutes
|
||||
minutes = indexValue % 60
|
||||
indexValue = (indexValue-minutes) / 60 # The remaining part, in hours
|
||||
# Get hours
|
||||
hours = indexValue % 24
|
||||
indexValue = (indexValue-hours) / 24 # The remaining part, in days
|
||||
# Get days
|
||||
day = indexValue % 31
|
||||
if day == 0: day = 31
|
||||
indexValue = (indexValue-day) / 31 # The remaining part, in months
|
||||
# Get months
|
||||
month = indexValue % 12
|
||||
if month == 0: month = 12
|
||||
year = (indexValue - month) / 12
|
||||
from DateTime import DateTime
|
||||
utcDate = DateTime('%d/%d/%d %d:%d UTC' % (year,month,day,hours,minutes))
|
||||
return utcDate.toZone(utcDate.localZone())
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Date(Field):
|
||||
|
||||
pxView = pxCell = Px('''<x>:value</x>''')
|
||||
pxEdit = Px('''
|
||||
<x var="years=field.getSelectableYears()">
|
||||
<!-- Day -->
|
||||
<select var="days=range(1,32)"
|
||||
name=":'%s_day' % name" id=":'%s_day' % name">
|
||||
<option value="">-</option>
|
||||
<option for="day in days"
|
||||
var2="zDay=str(day).zfill(2)" value=":zDay"
|
||||
selected=":field.isSelected(zobj, 'day', day, \
|
||||
rawValue)">:zDay</option>
|
||||
</select>
|
||||
|
||||
<!-- Month -->
|
||||
<select var="months=range(1,13)"
|
||||
name=":'%s_month' % name" id=":'%s_month' % name">
|
||||
<option value="">-</option>
|
||||
<option for="month in months"
|
||||
var2="zMonth=str(month).zfill(2)" value=":zMonth"
|
||||
selected=":field.isSelected(zobj, 'month', month, \
|
||||
rawValue)">:zMonth</option>
|
||||
</select>
|
||||
|
||||
<!-- Year -->
|
||||
<select name=":'%s_year' % name" id=":'%s_year' % name">
|
||||
<option value="">-</option>
|
||||
<option for="year in years" value=":year"
|
||||
selected=":field.isSelected(zobj, 'year', year, \
|
||||
rawValue)">:year</option>
|
||||
</select>
|
||||
|
||||
<!-- The icon for displaying the calendar popup -->
|
||||
<x if="field.calendar">
|
||||
<input type="hidden" id=":name" name=":name"/>
|
||||
<img id=":'%s_img' % name" src=":url('calendar.gif')"/>
|
||||
<script type="text/javascript">::field.getJsInit(name, years)</script>
|
||||
</x>
|
||||
|
||||
<!-- Hour and minutes -->
|
||||
<x if="field.format == 0">
|
||||
<select var="hours=range(0,24)" name=":'%s_hour' % name"
|
||||
id=":'%s_hour' % name">
|
||||
<option value="">-</option>
|
||||
<option for="hour in hours"
|
||||
var2="zHour=str(hour).zfill(2)" value=":zHour"
|
||||
selected=":field.isSelected(zobj, 'hour', hour, \
|
||||
rawValue)">:zHour</option>
|
||||
</select> :
|
||||
<select var="minutes=range(0,60,5)" name=":'%s_minute' % name"
|
||||
id=":'%s_minute' % name">
|
||||
<option value="">-</option>
|
||||
<option for="minute in minutes"
|
||||
var2="zMinute=str(minute).zfill(2)" value=":zMinute"
|
||||
selected=":field.isSelected(zobj, 'minute', minute,\
|
||||
rawValue)">:zMinute</option>
|
||||
</select>
|
||||
</x>
|
||||
</x>''')
|
||||
|
||||
pxSearch = Px('''<table var="years=range(field.startYear, field.endYear+1)">
|
||||
<!-- From -->
|
||||
<tr var="fromName='%s_from' % name;
|
||||
dayFromName='%s_from_day' % name;
|
||||
monthFromName='%s_from_month' % name;
|
||||
yearFromName='%s*date' % widgetName">
|
||||
<td width="10px"> </td>
|
||||
<td><label>:_('search_from')</label></td>
|
||||
<td>
|
||||
<select id=":dayFromName" name=":dayFromName">
|
||||
<option value="">--</option>
|
||||
<option for="value in [str(v).zfill(2) for v in range(1, 32)]"
|
||||
value=":value">:value</option>
|
||||
</select> /
|
||||
<select id=":monthFromName" name=":monthFromName">
|
||||
<option value="">--</option>
|
||||
<option for="value in [str(v).zfill(2) for v in range(1, 13)]"
|
||||
value=":value">:value</option>
|
||||
</select> /
|
||||
<select id=":yearFromName" name=":yearFromName">
|
||||
<option value="">--</option>
|
||||
<option for="value in range(field.startYear, field.endYear+1)"
|
||||
value=":value">:value</option>
|
||||
</select>
|
||||
<!-- The icon for displaying the calendar popup -->
|
||||
<x if="field.calendar">
|
||||
<input type="hidden" id=":fromName" name=":fromName"/>
|
||||
<img id=":'%s_img' % fromName" src=":url('calendar.gif')"/>
|
||||
<script type="text/javascript">::field.getJsInit(fromName, years)
|
||||
</script>
|
||||
</x>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- To -->
|
||||
<tr var="toName='%s_to' % name;
|
||||
dayToName='%s_to_day' % name;
|
||||
monthToName='%s_to_month' % name;
|
||||
yearToName='%s_to_year' % name">
|
||||
<td></td>
|
||||
<td><label>:_('search_to')</label> </td>
|
||||
<td height="20px">
|
||||
<select id=":dayToName" name=":dayToName">
|
||||
<option value="">--</option>
|
||||
<option for="value in [str(v).zfill(2) for v in range(1, 32)]"
|
||||
value=":value">:value</option>
|
||||
</select> /
|
||||
<select id=":monthToName" name=":monthToName">
|
||||
<option value="">--</option>
|
||||
<option for="value in [str(v).zfill(2) for v in range(1, 13)]"
|
||||
value=":value">:value</option>
|
||||
</select> /
|
||||
<select id=":yearToName" name=":yearToName">
|
||||
<option value="">--</option>
|
||||
<option for="value in range(field.startYear, field.endYear+1)"
|
||||
value=":value">:value</option>
|
||||
</select>
|
||||
<!-- The icon for displaying the calendar popup -->
|
||||
<x if="field.calendar">
|
||||
<input type="hidden" id=":toName" name=":toName"/>
|
||||
<img id=":'%s_img' % toName" src=":url('calendar.gif')"/>
|
||||
<script type="text/javascript">::field.getJsInit(toName, years)
|
||||
</script>
|
||||
</x>
|
||||
</td>
|
||||
</tr>
|
||||
</table>''')
|
||||
|
||||
# Required CSS and Javascript files for this type.
|
||||
cssFiles = {'edit': ('jscalendar/calendar-blue.css',)}
|
||||
jsFiles = {'edit': ('jscalendar/calendar.js',
|
||||
'jscalendar/lang/calendar-en.js',
|
||||
'jscalendar/calendar-setup.js')}
|
||||
# Possible values for "format"
|
||||
WITH_HOUR = 0
|
||||
WITHOUT_HOUR = 1
|
||||
dateParts = ('year', 'month', 'day')
|
||||
hourParts = ('hour', 'minute')
|
||||
|
||||
def __init__(self, validator=None, multiplicity=(0,1), default=None,
|
||||
format=WITH_HOUR, calendar=True,
|
||||
startYear=time.localtime()[0]-10,
|
||||
endYear=time.localtime()[0]+10, reverseYears=False,
|
||||
show=True, page='main', group=None, layouts=None, move=0,
|
||||
indexed=False, mustIndex=True, searchable=False,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=None, height=None, maxChars=None, colspan=1, master=None,
|
||||
masterValue=None, focus=False, historized=False, mapping=None,
|
||||
label=None, sdefault=None, scolspan=1, swidth=None,
|
||||
sheight=None, persist=True, view=None, xml=None):
|
||||
self.format = format
|
||||
self.calendar = calendar
|
||||
self.startYear = startYear
|
||||
self.endYear = endYear
|
||||
# If reverseYears is True, in the selection box, available years, from
|
||||
# self.startYear to self.endYear will be listed in reverse order.
|
||||
self.reverseYears = reverseYears
|
||||
Field.__init__(self, validator, multiplicity, default, show, page,
|
||||
group, layouts, move, indexed, mustIndex, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, None, colspan, master, masterValue, focus,
|
||||
historized, mapping, label, sdefault, scolspan, swidth,
|
||||
sheight, persist, view, xml)
|
||||
|
||||
def getCss(self, layoutType, res, config):
|
||||
# CSS files are only required if the calendar must be shown
|
||||
if self.calendar: Field.getCss(self, layoutType, res, config)
|
||||
|
||||
def getJs(self, layoutType, res, config):
|
||||
# Javascript files are only required if the calendar must be shown
|
||||
if self.calendar: Field.getJs(self, layoutType, res, config)
|
||||
|
||||
def getSelectableYears(self):
|
||||
'''Gets the list of years one may select for this field.'''
|
||||
res = list(range(self.startYear, self.endYear + 1))
|
||||
if self.reverseYears: res.reverse()
|
||||
return res
|
||||
|
||||
def validateValue(self, obj, value):
|
||||
DateTime = obj.getProductConfig().DateTime
|
||||
try:
|
||||
value = DateTime(value)
|
||||
except DateTime.DateError as ValueError:
|
||||
return obj.translate('bad_date')
|
||||
|
||||
def getFormattedValue(self, obj, value, layoutType='view',
|
||||
showChanges=False, language=None):
|
||||
if self.isEmptyValue(obj, value): return ''
|
||||
tool = obj.getTool().appy()
|
||||
# A problem may occur with some extreme year values. Replace the "year"
|
||||
# part "by hand".
|
||||
dateFormat = tool.dateFormat
|
||||
if '%Y' in dateFormat:
|
||||
dateFormat = dateFormat.replace('%Y', str(value.year()))
|
||||
res = value.strftime(dateFormat)
|
||||
if self.format == Date.WITH_HOUR:
|
||||
res += ' %s' % value.strftime(tool.hourFormat)
|
||||
return res
|
||||
|
||||
def getRequestValue(self, obj, requestName=None):
|
||||
request = obj.REQUEST
|
||||
name = requestName or self.name
|
||||
# Manage the "date" part
|
||||
value = ''
|
||||
for part in self.dateParts:
|
||||
valuePart = request.get('%s_%s' % (name, part), None)
|
||||
if not valuePart: return None
|
||||
value += valuePart + '/'
|
||||
value = value[:-1]
|
||||
# Manage the "hour" part
|
||||
if self.format == self.WITH_HOUR:
|
||||
value += ' '
|
||||
for part in self.hourParts:
|
||||
valuePart = request.get('%s_%s' % (name, part), None)
|
||||
if not valuePart: return None
|
||||
value += valuePart + ':'
|
||||
value = value[:-1]
|
||||
return value
|
||||
|
||||
def getStorableValue(self, obj, value):
|
||||
if not self.isEmptyValue(obj, value):
|
||||
import DateTime
|
||||
return DateTime.DateTime(value)
|
||||
|
||||
def getIndexType(self): return 'DateIndex'
|
||||
|
||||
def isSelected(self, obj, fieldPart, dateValue, dbValue):
|
||||
'''When displaying this field, must the particular p_dateValue be
|
||||
selected in the sub-field p_fieldPart corresponding to the date
|
||||
part?'''
|
||||
# Get the value we must compare (from request or from database)
|
||||
rq = obj.REQUEST
|
||||
partName = '%s_%s' % (self.name, fieldPart)
|
||||
if partName in rq:
|
||||
compValue = rq.get(partName)
|
||||
if compValue.isdigit():
|
||||
compValue = int(compValue)
|
||||
else:
|
||||
compValue = dbValue
|
||||
if compValue:
|
||||
compValue = getattr(compValue, fieldPart)()
|
||||
# Compare the value
|
||||
return compValue == dateValue
|
||||
|
||||
def getJsInit(self, name, years):
|
||||
'''Gets the Javascript init code for displaying a calendar popup for
|
||||
this field, for an input named p_name (which can be different from
|
||||
self.name if, ie, it is a search field).'''
|
||||
# Always express the range of years in chronological order.
|
||||
years = [years[0], years[-1]]
|
||||
years.sort()
|
||||
return 'Calendar.setup({inputField: "%s", button: "%s_img", ' \
|
||||
'onSelect: onSelectDate, range:%s})' % (name, name, str(years))
|
||||
# ------------------------------------------------------------------------------
|
105
fields/dict.py
|
@ -1,105 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy import Object
|
||||
from list import List
|
||||
from appy.px import Px
|
||||
from appy.gen.layout import Table
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Dict(List):
|
||||
'''A Dict is stored as a dict of Object instances [Object]~. Keys are fixed
|
||||
and are given by a method specified in parameter "keys". Values are
|
||||
Object instances, whose attributes are determined by parameter "fields"
|
||||
that, similarly to the List field, determines sub-data for every entry in
|
||||
the dict. This field is build on top of the List field.'''
|
||||
|
||||
# PX for rendering a single row
|
||||
pxRow = Px('''
|
||||
<tr valign="top" class=":loop.row.odd and 'even' or 'odd'">
|
||||
<td class="discreet">:row[1]</td>
|
||||
<td for="info in subFields" if="info[1]" align="center"
|
||||
var2="field=info[1];
|
||||
fieldName='%s*%d' % (field.name, rowIndex);
|
||||
tagCss='no'">:field.pxRender</td>
|
||||
</tr>''')
|
||||
|
||||
# PX for rendering the dict (shared between pxView and pxEdit)
|
||||
pxTable = Px('''
|
||||
<table var="isEdit=layoutType == 'edit'" if="isEdit or value"
|
||||
id=":'list_%s' % name" class=":isEdit and 'grid' or 'list'"
|
||||
width=":field.width"
|
||||
var2="keys=field.keys(obj);
|
||||
subFields=field.getSubFields(zobj, layoutType)">
|
||||
<!-- Header -->
|
||||
<tr valign="bottom">
|
||||
<th width=":field.widths[0]"></th>
|
||||
<th for="info in subFields" if="info[1]"
|
||||
width=":field.widths[loop.info.nb+1]">::_(info[1].labelId)</th>
|
||||
</tr>
|
||||
<!-- Rows of data -->
|
||||
<x for="row in keys" var2="rowIndex=loop.row.nb">:field.pxRow</x>
|
||||
</table>''')
|
||||
|
||||
def __init__(self, keys, fields, validator=None, multiplicity=(0,1),
|
||||
default=None, show=True, page='main', group=None, layouts=None,
|
||||
move=0, specificReadPermission=False,
|
||||
specificWritePermission=False, width='', height=None,
|
||||
maxChars=None, colspan=1, master=None, masterValue=None,
|
||||
focus=False, historized=False, mapping=None, label=None,
|
||||
subLayouts=Table('frv', width=None), widths=None, view=None,
|
||||
xml=None):
|
||||
List.__init__(self, fields, validator, multiplicity, default, show, page,
|
||||
group, layouts, move, specificReadPermission,
|
||||
specificWritePermission, width, height, maxChars, colspan,
|
||||
master, masterValue, focus, historized, mapping, label,
|
||||
subLayouts, widths, view, xml)
|
||||
# Method in "keys" must return a list of tuples (key, title): "key"
|
||||
# determines the key that will be used to store the entry in the
|
||||
# database, while "title" will get the text that will be shown in the ui
|
||||
# while encoding/viewing this entry.
|
||||
self.keys = keys
|
||||
|
||||
def computeWidths(self, widths):
|
||||
'''Set given p_widths or compute default ones if not given.'''
|
||||
if not widths:
|
||||
self.widths = [''] * (len(self.fields) + 1)
|
||||
else:
|
||||
self.widths = widths
|
||||
|
||||
def getFormattedValue(self, obj, value, layoutType='view',
|
||||
showChanges=False, language=None):
|
||||
'''Formats the dict value as a list of values'''
|
||||
res = []
|
||||
for key, title in self.keys(obj.appy()):
|
||||
if value and (key in value):
|
||||
res.append(value[key])
|
||||
else:
|
||||
# There is no value for this key in the database p_value
|
||||
res.append(None)
|
||||
return res
|
||||
|
||||
def getStorableValue(self, obj, value):
|
||||
'''Gets p_value in a form that can be stored in the database'''
|
||||
res = {}
|
||||
values = List.getStorableValue(self, obj, value)
|
||||
i = -1
|
||||
for key, title in self.keys(obj.appy()):
|
||||
i += 1
|
||||
res[key] = values[i]
|
||||
return res
|
||||
# ------------------------------------------------------------------------------
|
486
fields/file.py
|
@ -1,486 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import time, os.path, mimetypes, shutil
|
||||
from cStringIO import StringIO
|
||||
from appy import Object
|
||||
from appy.fields import Field
|
||||
from appy.px import Px
|
||||
from appy.shared import utils as sutils
|
||||
from appy.shared import UnmarshalledFile, mimeTypesExts
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
|
||||
'2-tuple (fileName, fileContent) or a 3-tuple (fileName, fileContent, ' \
|
||||
'mimeType).'
|
||||
CONVERSION_ERROR = 'An error occurred. %s'
|
||||
|
||||
def guessMimeType(fileName):
|
||||
'''Try to find the MIME type of file p_fileName.'''
|
||||
return mimetypes.guess_type(fileName)[0] or File.defaultMimeType
|
||||
|
||||
def osPathJoin(*pathElems):
|
||||
'''Version of os.path.elems that takes care of path elems being empty
|
||||
strings.'''
|
||||
return os.path.join(*pathElems).rstrip(os.sep)
|
||||
|
||||
def getShownSize(size):
|
||||
'''Express p_size (a file size in bytes) in a human-readable way.'''
|
||||
# Display the size in bytes if smaller than 1024 bytes
|
||||
if size < 1024: return '%d byte(s)' % size
|
||||
size = size / 1024.0 # This is the size, in Kb
|
||||
if size < 1024: return '%s Kb' % sutils.formatNumber(size, precision=1)
|
||||
size = size / 1024.0 # This is the size, in Mb
|
||||
return '%s Mb' % sutils.formatNumber(size, precision=1)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class FileInfo:
|
||||
'''A FileInfo instance holds metadata about a file on the filesystem.
|
||||
|
||||
For every File field, we will store a FileInfo instance in the dabatase;
|
||||
the real file will be stored in the Appy/ZODB database-managed
|
||||
filesystem.
|
||||
|
||||
This is the primary usage of FileInfo instances. FileInfo instances can
|
||||
also be used every time we need to manipulate a file. For example, when
|
||||
getting the content of a Pod field, a temporary file may be generated and
|
||||
you will get a FileInfo that represents it.
|
||||
'''
|
||||
BYTES = 50000
|
||||
|
||||
def __init__(self, fsPath, inDb=True, uploadName=None):
|
||||
'''p_fsPath is the path of the file on disk.
|
||||
- If p_inDb is True, this FileInfo will be stored in the database and
|
||||
will hold metadata about a File field whose content will lie in the
|
||||
database-controlled filesystem. In this case, p_fsPath is the path
|
||||
of the file *relative* to the root DB folder. We avoid storing
|
||||
absolute paths in order to ease the transfer of databases from one
|
||||
place to the other. Moreover, p_fsPath does not include the
|
||||
filename, that will be computed later, from the field name.
|
||||
|
||||
- If p_inDb is False, this FileInfo is a simple temporary object
|
||||
representing any file on the filesystem (not necessarily in the
|
||||
db-controlled filesystem). For instance, it could represent a temp
|
||||
file generated from a Pod field in the OS temp folder. In this
|
||||
case, p_fsPath is the absolute path to the file, including the
|
||||
filename. If you manipulate such a FileInfo instance, please avoid
|
||||
using methods that are used by Appy to manipulate
|
||||
database-controlled files (like methods getFilePath, removeFile,
|
||||
writeFile or copyFile).'''
|
||||
self.fsPath = fsPath
|
||||
self.fsName = None # The name of the file in fsPath
|
||||
self.uploadName = uploadName # The name of the uploaded file
|
||||
self.size = 0 # Its size, in bytes
|
||||
self.mimeType = None # Its MIME type
|
||||
self.modified = None # The last modification date for this file
|
||||
# Complete metadata if p_inDb is False
|
||||
if not inDb:
|
||||
self.fsName = '' # Already included in self.fsPath
|
||||
# We will not store p_inDb. Checking if self.fsName is the empty
|
||||
# string is equivalent.
|
||||
fileInfo = os.stat(self.fsPath)
|
||||
self.size = fileInfo.st_size
|
||||
self.mimeType = guessMimeType(self.fsPath)
|
||||
from DateTime import DateTime
|
||||
self.modified = DateTime(fileInfo.st_mtime)
|
||||
|
||||
def getFilePath(self, obj):
|
||||
'''Returns the absolute file name of the file on disk that corresponds
|
||||
to this FileInfo instance.'''
|
||||
dbFolder, folder = obj.o.getFsFolder()
|
||||
return osPathJoin(dbFolder, folder, self.fsName)
|
||||
|
||||
def removeFile(self, dbFolder='', removeEmptyFolders=False):
|
||||
'''Removes the file from the filesystem.'''
|
||||
try:
|
||||
os.remove(osPathJoin(dbFolder, self.fsPath, self.fsName))
|
||||
except Exception as e:
|
||||
# If the current ZODB transaction is re-triggered, the file may
|
||||
# already have been deleted.
|
||||
pass
|
||||
# Don't leave empty folders on disk. So delete folder and parent folders
|
||||
# if this removal leaves them empty (unless p_removeEmptyFolders is
|
||||
# False).
|
||||
if removeEmptyFolders:
|
||||
sutils.FolderDeleter.deleteEmpty(osPathJoin(dbFolder,self.fsPath))
|
||||
|
||||
def normalizeFileName(self, name):
|
||||
'''Normalizes file p_name.'''
|
||||
return name[max(name.rfind('/'), name.rfind('\\'), name.rfind(':'))+1:]
|
||||
|
||||
def getShownSize(self): return getShownSize(self.size)
|
||||
|
||||
def replicateFile(self, src, dest):
|
||||
'''p_src and p_dest are open file handlers. This method copies content
|
||||
of p_src to p_dest and returns the file size. Note that p_src can
|
||||
also be binary data in a string.'''
|
||||
size = 0
|
||||
if isinstance(src, str): src = StringIO(src)
|
||||
while True:
|
||||
chunk = src.read(self.BYTES)
|
||||
if not chunk: break
|
||||
size += len(chunk)
|
||||
dest.write(chunk)
|
||||
return size
|
||||
|
||||
def getMimeTypeFromFileUpload(self, fileObj):
|
||||
'''Under some unknown circumstances, the MIME type received from Zope
|
||||
FileUpload instances is wrong:
|
||||
* MIME type of docx and xlsx documents may be wrongly initialised to
|
||||
"application/zip";
|
||||
* MIME type of some Excel (.xls) files have MIME type
|
||||
"application/msword".
|
||||
This method corrects it.'''
|
||||
mimeType = fileObj.headers.get('content-type')
|
||||
ext = os.path.splitext(fileObj.filename)[1]
|
||||
# If no extension is there, I cannot correct the MIME type
|
||||
if not ext: return mimeType
|
||||
# Correct xls files having MIME type "application/msword"
|
||||
if (ext == '.xls') and (mimeType == 'application/msword'):
|
||||
return 'application/vnd.ms-excel'
|
||||
# No error: return the MIME type as computed by Zope
|
||||
if not ext or (mimeType != 'application/zip') or (ext == '.zip'):
|
||||
return mimeType
|
||||
# Correct the wrong MIME type
|
||||
ext = ext[1:].lower()
|
||||
for mime, extension in mimeTypesExts.iteritems():
|
||||
if extension == ext: return mime
|
||||
# If we are here, we haven't found the correct MIME type
|
||||
return mimeType
|
||||
|
||||
def writeFile(self, fieldName, fileObj, dbFolder):
|
||||
'''Writes to the filesystem the p_fileObj file, that can be:
|
||||
- a Zope FileUpload (coming from a HTTP post);
|
||||
- a OFS.Image.File object (legacy within-ZODB file object);
|
||||
- another ("not-in-DB") FileInfo instance;
|
||||
- a tuple (fileName, fileContent, mimeType)
|
||||
(see doc in method File.store below).'''
|
||||
# Determine p_fileObj's type
|
||||
fileType = fileObj.__class__.__name__
|
||||
# Determine the MIME type and the base name of the file to store
|
||||
if fileType == 'FileUpload':
|
||||
mimeType = self.getMimeTypeFromFileUpload(fileObj)
|
||||
fileName = fileObj.filename
|
||||
elif fileType == 'File':
|
||||
mimeType = fileObj.content_type
|
||||
fileName = fileObj.filename
|
||||
elif fileType == 'FileInfo':
|
||||
mimeType = fileObj.mimeType
|
||||
fileName = fileObj.uploadName
|
||||
else:
|
||||
mimeType = fileObj[2]
|
||||
fileName = fileObj[0]
|
||||
self.mimeType = mimeType or File.defaultMimeType
|
||||
if not fileName:
|
||||
# Name it according to field name. Deduce file extension from the
|
||||
# MIME type.
|
||||
ext = (self.mimeType in mimeTypesExts) and \
|
||||
mimeTypesExts[self.mimeType] or 'bin'
|
||||
fileName = '%s.%s' % (fieldName, ext)
|
||||
# As a preamble, extract file metadata from p_fileObj and store it in
|
||||
# this FileInfo instance.
|
||||
name = self.normalizeFileName(fileName)
|
||||
self.uploadName = name
|
||||
self.fsName = '%s%s' % (fieldName, os.path.splitext(name)[1].lower())
|
||||
# Write the file on disk (and compute/get its size in bytes)
|
||||
fsName = osPathJoin(dbFolder, self.fsPath, self.fsName)
|
||||
f = file(fsName, 'wb')
|
||||
if fileType == 'FileUpload':
|
||||
# Write the FileUpload instance on disk
|
||||
self.size = self.replicateFile(fileObj, f)
|
||||
elif fileType == 'File':
|
||||
# Write the File instance on disk
|
||||
if fileObj.data.__class__.__name__ == 'Pdata':
|
||||
# The file content is splitted in several chunks
|
||||
f.write(fileObj.data.data)
|
||||
nextPart = fileObj.data.__next__
|
||||
while nextPart:
|
||||
f.write(nextPart.data)
|
||||
nextPart = nextPart.__next__
|
||||
else:
|
||||
# Only one chunk
|
||||
f.write(fileObj.data)
|
||||
self.size = fileObj.size
|
||||
elif fileType == 'FileInfo':
|
||||
src = file(fileObj.fsPath, 'rb')
|
||||
self.size = self.replicateFile(src, f)
|
||||
src.close()
|
||||
else:
|
||||
# Write fileObj[1] on disk
|
||||
if fileObj[1].__class__.__name__ == 'file':
|
||||
# It is an open file handler
|
||||
self.size = self.replicateFile(fileObj[1], f)
|
||||
else:
|
||||
# We have file content directly in fileObj[1]
|
||||
self.size = len(fileObj[1])
|
||||
f.write(fileObj[1])
|
||||
f.close()
|
||||
from DateTime import DateTime
|
||||
self.modified = DateTime()
|
||||
|
||||
def copyFile(self, fieldName, filePath, dbFolder):
|
||||
'''Copies the "external" file stored at p_filePath in the db-controlled
|
||||
file system, for storing a value for p_fieldName.'''
|
||||
# Set names for the file
|
||||
name = self.normalizeFileName(filePath)
|
||||
self.uploadName = name
|
||||
self.fsName = '%s%s' % (fieldName, os.path.splitext(name)[1])
|
||||
# Set mimeType
|
||||
self.mimeType = guessMimeType(filePath)
|
||||
# Copy the file
|
||||
fsName = osPathJoin(dbFolder, self.fsPath, self.fsName)
|
||||
shutil.copyfile(filePath, fsName)
|
||||
from DateTime import DateTime
|
||||
self.modified = DateTime()
|
||||
self.size = os.stat(fsName).st_size
|
||||
|
||||
def writeResponse(self, response, dbFolder='', disposition='attachment'):
|
||||
'''Writes this file in the HTTP p_response object.'''
|
||||
# As a preamble, initialise response headers
|
||||
header = response.setHeader
|
||||
header('Content-Disposition',
|
||||
'%s;filename="%s"' % (disposition, self.uploadName))
|
||||
header('Content-Type', self.mimeType)
|
||||
header('Content-Length', self.size)
|
||||
header('Accept-Ranges', 'bytes')
|
||||
header('Last-Modified', self.modified.rfc822())
|
||||
#sh('Cachecontrol', 'no-cache')
|
||||
#sh('Expires', 'Thu, 11 Dec 1975 12:05:05 GMT')
|
||||
# Write the file in the response
|
||||
fsName = osPathJoin(dbFolder, self.fsPath, self.fsName)
|
||||
f = file(fsName, 'rb')
|
||||
while True:
|
||||
chunk = f.read(self.BYTES)
|
||||
if not chunk: break
|
||||
response.write(chunk)
|
||||
f.close()
|
||||
|
||||
def dump(self, obj, filePath=None, format=None):
|
||||
'''Exports this file to disk (outside the db-controller filesystem).
|
||||
The tied Appy p_obj(ect) is required. 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,
|
||||
LibreOffice will be called for converting the dumped file to the
|
||||
desired format.'''
|
||||
if not filePath:
|
||||
filePath = '%s/file%f.%s' % (sutils.getOsTempFolder(), time.time(),
|
||||
self.fsName)
|
||||
# Copies the file to disk.
|
||||
shutil.copyfile(self.getFilePath(obj), filePath)
|
||||
if format:
|
||||
# Convert the dumped file using LibreOffice
|
||||
errorMessage = obj.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):
|
||||
obj.log(CONVERSION_ERROR % errorMessage, type='error')
|
||||
return
|
||||
return filePath
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class File(Field):
|
||||
|
||||
pxView = pxCell = Px('''
|
||||
<x var="downloadUrl='%s/download?name=%s' % (zobj.absolute_url(), name);
|
||||
shownSize=value and value.getShownSize() or 0">
|
||||
<x if="value and not field.isImage">
|
||||
<a href=":downloadUrl">:value.uploadName</a> -
|
||||
<i class="discreet">:shownSize</i>
|
||||
</x>
|
||||
<x if="value and field.isImage">
|
||||
<img src=":downloadUrl"
|
||||
title=":'%s, %s' % (value.uploadName, shownSize)"/></x>
|
||||
<x if="not value">-</x>
|
||||
</x>''')
|
||||
|
||||
pxEdit = Px('''
|
||||
<x var="fName=q('%s_file' % name)">
|
||||
<x if="value">:field.pxView</x><br if="value"/>
|
||||
<x if="value">
|
||||
<!-- Keep the file unchanged. -->
|
||||
<input type="radio" value="nochange"
|
||||
checked=":value and 'checked' or None"
|
||||
name=":'%s_delete' % name" id=":'%s_nochange' % name"
|
||||
onclick=":'document.getElementById(%s).disabled=true' % fName"/>
|
||||
<label lfor=":'%s_nochange' % name">:_('keep_file')</label><br/>
|
||||
<!-- Delete the file. -->
|
||||
<x if="not field.required">
|
||||
<input type="radio" value="delete"
|
||||
name=":'%s_delete' % name" id=":'%s_delete' % name"
|
||||
onclick=":'document.getElementById(%s).disabled=true' % fName"/>
|
||||
<label lfor=":'%s_delete' % name">:_('delete_file')</label><br/>
|
||||
</x>
|
||||
<!-- Replace with a new file. -->
|
||||
<input type="radio" value=""
|
||||
checked=":not value and 'checked' or None"
|
||||
name=":'%s_delete' % name" id=":'%s_upload' % name"
|
||||
onclick=":'document.getElementById(%s).disabled=false' % fName"/>
|
||||
<label lfor=":'%s_upload' % name">:_('replace_file')</label><br/>
|
||||
</x>
|
||||
<!-- The upload field. -->
|
||||
<input type="file" name=":'%s_file' % name" id=":'%s_file' % name"
|
||||
size=":field.width"/>
|
||||
<script var="isDisabled=not value and 'false' or 'true'"
|
||||
type="text/javascript">:'document.getElementById(%s).disabled=%s'%\
|
||||
(fName, isDisabled)</script></x>''')
|
||||
|
||||
pxSearch = ''
|
||||
|
||||
def __init__(self, validator=None, multiplicity=(0,1), default=None,
|
||||
show=True, page='main', group=None, layouts=None, move=0,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=None, height=None, maxChars=None, colspan=1, master=None,
|
||||
masterValue=None, focus=False, historized=False, mapping=None,
|
||||
label=None, isImage=False, sdefault='', scolspan=1,
|
||||
swidth=None, sheight=None, view=None, xml=None):
|
||||
self.isImage = isImage
|
||||
Field.__init__(self, validator, multiplicity, default, show, page,
|
||||
group, layouts, move, False, True, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, None, colspan, master, masterValue, focus,
|
||||
historized, mapping, label, sdefault, scolspan, swidth,
|
||||
sheight, True, view, xml)
|
||||
|
||||
def getRequestValue(self, obj, requestName=None):
|
||||
name = requestName or self.name
|
||||
return obj.REQUEST.get('%s_file' % name)
|
||||
|
||||
def getCopyValue(self, obj):
|
||||
'''Create a copy of the FileInfo instance stored for p_obj for this
|
||||
field. This copy will contain the absolute path to the file on the
|
||||
filesystem. This way, the file may be read independently from p_obj
|
||||
(and copied somewhere else).'''
|
||||
info = self.getValue(obj)
|
||||
if not info: return
|
||||
# Create a "not-in-DB", temporary FileInfo
|
||||
return FileInfo(info.getFilePath(obj), inDb=False,
|
||||
uploadName=info.uploadName)
|
||||
|
||||
def getDefaultLayouts(self): return {'view':'l-f','edit':'lrv-f'}
|
||||
|
||||
def isEmptyValue(self, obj, value):
|
||||
'''Must p_value be considered as empty?'''
|
||||
if value: return
|
||||
# If "nochange", the value must not be considered as empty
|
||||
return obj.REQUEST.get('%s_delete' % self.name) != 'nochange'
|
||||
|
||||
imageExts = ('.jpg', '.jpeg', '.png', '.gif')
|
||||
def validateValue(self, obj, value):
|
||||
form = obj.REQUEST.form
|
||||
action = '%s_delete' % self.name
|
||||
if (not value or not value.filename) and action in form and \
|
||||
not form[action]:
|
||||
# If this key is present but empty, it means that the user selected
|
||||
# "replace the file with a new one". So in this case he must provide
|
||||
# a new file to upload.
|
||||
return obj.translate('file_required')
|
||||
# Check that, if self.isImage, the uploaded file is really an image
|
||||
if value and value.filename and self.isImage:
|
||||
ext = os.path.splitext(value.filename)[1].lower()
|
||||
if ext not in File.imageExts:
|
||||
return obj.translate('image_required')
|
||||
|
||||
defaultMimeType = 'application/octet-stream'
|
||||
def store(self, obj, value):
|
||||
'''Stores the p_value that represents some file. p_value can be:
|
||||
a. an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In
|
||||
this case, it is file content coming from a HTTP POST;
|
||||
b. an instance of Zope class OFS.Image.File (legacy within-ZODB file
|
||||
object);
|
||||
c. an instance of appy.shared.UnmarshalledFile. In this case, the
|
||||
file comes from a peer Appy site, unmarshalled from XML content
|
||||
sent via an HTTP request;
|
||||
d. a string. In this case, the string represents the path of a file
|
||||
on disk;
|
||||
e. 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.
|
||||
f. a 3-tuple (fileName, fileContent, mimeType) where
|
||||
- fileName and fileContent have the same meaning than above;
|
||||
- mimeType is the MIME type of the file.
|
||||
g. a FileInfo instance, that must be "not-in-DB", ie, with an
|
||||
absolute path in attribute fsPath.
|
||||
'''
|
||||
zobj = obj.o
|
||||
if value:
|
||||
# There is a new value to store. Get the folder on disk where to
|
||||
# store the new file.
|
||||
dbFolder, folder = zobj.getFsFolder(create=True)
|
||||
# Remove the previous file if it existed
|
||||
info = getattr(obj.aq_base, self.name, None)
|
||||
if info:
|
||||
# The previous file can be a legacy File object in an old
|
||||
# database we are migrating.
|
||||
if isinstance(info, FileInfo): info.removeFile(dbFolder)
|
||||
else: delattr(obj, self.name)
|
||||
# Store the new file. As a preamble, create a FileInfo instance.
|
||||
info = FileInfo(folder)
|
||||
cfg = zobj.getProductConfig()
|
||||
if isinstance(value, cfg.FileUpload) or isinstance(value, cfg.File):
|
||||
# Cases a, b
|
||||
value.filename = value.filename.replace('/', '-')
|
||||
info.writeFile(self.name, value, dbFolder)
|
||||
elif isinstance(value, UnmarshalledFile):
|
||||
# Case c
|
||||
fileInfo = (value.name, value.content, value.mimeType)
|
||||
info.writeFile(self.name, fileInfo, dbFolder)
|
||||
elif isinstance(value, str):
|
||||
# Case d
|
||||
info.copyFile(self.name, value, dbFolder)
|
||||
elif isinstance(value, FileInfo):
|
||||
# Case g
|
||||
info.writeFile(self.name, value, dbFolder)
|
||||
else:
|
||||
# Cases e, f. Extract file name, content and MIME type.
|
||||
fileName = mimeType = None
|
||||
if len(value) == 2:
|
||||
fileName, fileContent = value
|
||||
elif len(value) == 3:
|
||||
fileName, fileContent, mimeType = value
|
||||
if not fileName:
|
||||
raise Exception(WRONG_FILE_TUPLE)
|
||||
mimeType = mimeType or guessMimeType(fileName)
|
||||
info.writeFile(self.name, (fileName, fileContent, mimeType),
|
||||
dbFolder)
|
||||
# Store the FileInfo instance in the database
|
||||
setattr(obj, self.name, info)
|
||||
else:
|
||||
# I store value "None", excepted if I find in the request the desire
|
||||
# to keep the file unchanged.
|
||||
action = None
|
||||
rq = getattr(zobj, 'REQUEST', None)
|
||||
if rq: action = rq.get('%s_delete' % self.name, None)
|
||||
if action != 'nochange':
|
||||
# Delete the file on disk
|
||||
info = getattr(zobj.aq_base, self.name, None)
|
||||
if info:
|
||||
info.removeFile(zobj.getDbFolder(), removeEmptyFolders=True)
|
||||
# Delete the FileInfo in the DB
|
||||
setattr(zobj, self.name, None)
|
||||
# ------------------------------------------------------------------------------
|
104
fields/float.py
|
@ -1,104 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy.fields import Field
|
||||
from appy.px import Px
|
||||
from appy.shared import utils as sutils
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Float(Field):
|
||||
allowedDecimalSeps = (',', '.')
|
||||
allowedThousandsSeps = (' ', '')
|
||||
|
||||
pxView = pxCell = Px('''
|
||||
<x><x>:value</x>
|
||||
<input type="hidden" if="masterCss" class=":masterCss" value=":value"
|
||||
name=":name" id=":name"/>
|
||||
</x>''')
|
||||
|
||||
pxEdit = Px('''
|
||||
<input id=":name" name=":name" size=":field.width"
|
||||
maxlength=":field.maxChars"
|
||||
value=":inRequest and requestValue or value" type="text"/>''')
|
||||
|
||||
pxSearch = Px('''
|
||||
<!-- From -->
|
||||
<x var="fromName='%s*float' % widgetName">
|
||||
<label lfor=":fromName">:_('search_from')</label>
|
||||
<input type="text" name=":fromName" maxlength=":field.maxChars"
|
||||
value=":field.sdefault[0]" size=":field.swidth"/>
|
||||
</x>
|
||||
<!-- To -->
|
||||
<x var="toName='%s_to' % name">
|
||||
<label lfor=":toName">:_('search_to')</label>
|
||||
<input type="text" name=":toName" maxlength=":field.maxChars"
|
||||
value=":field.sdefault[1]" size="field.swidth"/>
|
||||
</x><br/>''')
|
||||
|
||||
def __init__(self, validator=None, multiplicity=(0,1), default=None,
|
||||
show=True, page='main', group=None, layouts=None, move=0,
|
||||
indexed=False, mustIndex=True, searchable=False,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=5, height=None, maxChars=13, colspan=1, master=None,
|
||||
masterValue=None, focus=False, historized=False, mapping=None,
|
||||
label=None, sdefault=('',''), scolspan=1, swidth=None,
|
||||
sheight=None, persist=True, precision=None, sep=(',', '.'),
|
||||
tsep=' ', view=None, xml=None):
|
||||
# The precision is the number of decimal digits. This number is used
|
||||
# for rendering the float, but the internal float representation is not
|
||||
# rounded.
|
||||
self.precision = precision
|
||||
# The decimal separator can be a tuple if several are allowed, ie
|
||||
# ('.', ',')
|
||||
if type(sep) not in sutils.sequenceTypes:
|
||||
self.sep = (sep,)
|
||||
else:
|
||||
self.sep = sep
|
||||
# Check that the separator(s) are among allowed decimal separators
|
||||
for sep in self.sep:
|
||||
if sep not in Float.allowedDecimalSeps:
|
||||
raise Exception('Char "%s" is not allowed as decimal ' \
|
||||
'separator.' % sep)
|
||||
self.tsep = tsep
|
||||
Field.__init__(self, validator, multiplicity, default, show, page,
|
||||
group, layouts, move, indexed, mustIndex, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, maxChars, colspan, master, masterValue, focus,
|
||||
historized, mapping, label, sdefault, scolspan, swidth,
|
||||
sheight, persist, view, xml)
|
||||
self.pythonType = float
|
||||
|
||||
def getFormattedValue(self, obj, value, layoutType='view',
|
||||
showChanges=False, language=None):
|
||||
return sutils.formatNumber(value, sep=self.sep[0],
|
||||
precision=self.precision, tsep=self.tsep)
|
||||
|
||||
def validateValue(self, obj, value):
|
||||
# Replace used separator with the Python separator '.'
|
||||
for sep in self.sep: value = value.replace(sep, '.')
|
||||
value = value.replace(self.tsep, '')
|
||||
try:
|
||||
value = self.pythonType(value)
|
||||
except ValueError:
|
||||
return obj.translate('bad_%s' % self.pythonType.__name__)
|
||||
|
||||
def getStorableValue(self, obj, value):
|
||||
if not self.isEmptyValue(obj, value):
|
||||
for sep in self.sep: value = value.replace(sep, '.')
|
||||
value = value.replace(self.tsep, '')
|
||||
return self.pythonType(value)
|
||||
# ------------------------------------------------------------------------------
|
398
fields/group.py
|
@ -1,398 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy.px import Px
|
||||
from appy.gen import utils as gutils
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Group:
|
||||
'''Used for describing a group of fields within a page.'''
|
||||
def __init__(self, name, columns=['100%'], wide=True, style='section2',
|
||||
hasLabel=True, hasDescr=False, hasHelp=False,
|
||||
hasHeaders=False, group=None, colspan=1, align='center',
|
||||
valign='top', css_class='', master=None, masterValue=None,
|
||||
cellpadding=1, cellspacing=1, cellgap='0.6em', label=None,
|
||||
translated=None):
|
||||
self.name = name
|
||||
# In its simpler form, field "columns" below can hold a list or tuple
|
||||
# of column widths expressed as strings, that will be given as is in
|
||||
# the "width" attributes of the corresponding "td" tags. Instead of
|
||||
# strings, within this list or tuple, you may give Column instances
|
||||
# (see below).
|
||||
self.columns = columns
|
||||
self._setColumns()
|
||||
# If field "wide" below is True, the HTML table corresponding to this
|
||||
# group will have width 100%. You can also specify some string value,
|
||||
# which will be used for HTML param "width".
|
||||
if wide == True:
|
||||
self.wide = '100%'
|
||||
elif isinstance(wide, str):
|
||||
self.wide = wide
|
||||
else:
|
||||
self.wide = ''
|
||||
# If style = 'fieldset', all widgets within the group will be rendered
|
||||
# within an HTML fieldset. If style is 'section1' or 'section2', widgets
|
||||
# will be rendered after the group title.
|
||||
self.style = style
|
||||
# If hasLabel is True, the group will have a name and the corresponding
|
||||
# i18n label will be generated.
|
||||
self.hasLabel = hasLabel
|
||||
# If hasDescr is True, the group will have a description and the
|
||||
# corresponding i18n label will be generated.
|
||||
self.hasDescr = hasDescr
|
||||
# If hasHelp is True, the group will have a help text associated and the
|
||||
# corresponding i18n label will be generated.
|
||||
self.hasHelp = hasHelp
|
||||
# If hasheaders is True, group content will begin with a row of headers,
|
||||
# and a i18n label will be generated for every header.
|
||||
self.hasHeaders = hasHeaders
|
||||
self.nbOfHeaders = len(columns)
|
||||
# If this group is himself contained in another group, the following
|
||||
# attribute is filled.
|
||||
self.group = Group.get(group)
|
||||
# If the group is rendered into another group, we can specify the number
|
||||
# of columns that this group will span.
|
||||
self.colspan = colspan
|
||||
self.align = align
|
||||
self.valign = valign
|
||||
self.cellpadding = cellpadding
|
||||
self.cellspacing = cellspacing
|
||||
# Beyond standard cellpadding and cellspacing, cellgap can define an
|
||||
# additional horizontal gap between cells in a row. So this value does
|
||||
# not add space before the first cell or after the last one.
|
||||
self.cellgap = cellgap
|
||||
if style == 'tabs':
|
||||
# Group content will be rendered as tabs. In this case, some
|
||||
# param combinations have no sense.
|
||||
self.hasLabel = self.hasDescr = self.hasHelp = False
|
||||
# The rendering is forced to a single column
|
||||
self.columns = self.columns[:1]
|
||||
# Inner field/group labels will be used as tab labels.
|
||||
self.css_class = css_class
|
||||
self.master = master
|
||||
self.masterValue = gutils.initMasterValue(masterValue)
|
||||
if master: master.slaves.append(self)
|
||||
self.label = label # See similar attr of Type class.
|
||||
# If a translated name is already given here, we will use it instead of
|
||||
# trying to translate the group label.
|
||||
self.translated = translated
|
||||
|
||||
def _setColumns(self):
|
||||
'''Standardizes field "columns" as a list of Column instances. Indeed,
|
||||
the initial value for field "columns" may be a list or tuple of
|
||||
Column instances or strings.'''
|
||||
for i in range(len(self.columns)):
|
||||
columnData = self.columns[i]
|
||||
if not isinstance(columnData, Column):
|
||||
self.columns[i] = Column(self.columns[i])
|
||||
|
||||
@staticmethod
|
||||
def get(groupData):
|
||||
'''Produces a Group instance from p_groupData. User-defined p_groupData
|
||||
can be a string or a Group instance; this method returns always a
|
||||
Group instance.'''
|
||||
res = groupData
|
||||
if res and isinstance(res, str):
|
||||
# Group data is given as a string. 2 more possibilities:
|
||||
# (a) groupData is simply the name of the group;
|
||||
# (b) groupData is of the form <groupName>_<numberOfColumns>.
|
||||
groupElems = groupData.rsplit('_', 1)
|
||||
if len(groupElems) == 1:
|
||||
res = Group(groupElems[0])
|
||||
else:
|
||||
try:
|
||||
nbOfColumns = int(groupElems[1])
|
||||
except ValueError:
|
||||
nbOfColumns = 1
|
||||
width = 100.0 / nbOfColumns
|
||||
res = Group(groupElems[0], ['%.2f%%' % width] * nbOfColumns)
|
||||
return res
|
||||
|
||||
def getMasterData(self):
|
||||
'''Gets the master of this group (and masterValue) or, recursively, of
|
||||
containing groups when relevant.'''
|
||||
if self.master: return (self.master, self.masterValue)
|
||||
if self.group: return self.group.getMasterData()
|
||||
|
||||
def generateLabels(self, messages, classDescr, walkedGroups,
|
||||
content='fields'):
|
||||
'''This method allows to generate all the needed i18n labels related to
|
||||
this group. p_messages is the list of i18n p_messages (a PoMessages
|
||||
instance) that we are currently building; p_classDescr is the
|
||||
descriptor of the class where this group is defined. The type of
|
||||
content in this group is specified by p_content.'''
|
||||
# A part of the group label depends on p_content.
|
||||
gp = (content == 'searches') and 'searchgroup' or 'group'
|
||||
if self.hasLabel:
|
||||
msgId = '%s_%s_%s' % (classDescr.name, gp, self.name)
|
||||
messages.append(msgId, self.name)
|
||||
if self.hasDescr:
|
||||
msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name)
|
||||
messages.append(msgId, ' ', nice=False)
|
||||
if self.hasHelp:
|
||||
msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name)
|
||||
messages.append(msgId, ' ', nice=False)
|
||||
if self.hasHeaders:
|
||||
for i in range(self.nbOfHeaders):
|
||||
msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1)
|
||||
messages.append(msgId, ' ', nice=False)
|
||||
walkedGroups.add(self)
|
||||
if self.group and (self.group not in walkedGroups) and \
|
||||
not self.group.label:
|
||||
# We remember walked groups for avoiding infinite recursion.
|
||||
self.group.generateLabels(messages, classDescr, walkedGroups,
|
||||
content=content)
|
||||
|
||||
def insertInto(self, elems, uiGroups, page, className, content='fields'):
|
||||
'''Inserts the UiGroup instance corresponding to this Group instance
|
||||
into p_elems, the recursive structure used for displaying all
|
||||
elements in a given p_page (fields, searches, transitions...) and
|
||||
returns this UiGroup instance.'''
|
||||
# First, create the corresponding UiGroup if not already in p_uiGroups.
|
||||
if self.name not in uiGroups:
|
||||
uiGroup = uiGroups[self.name] = UiGroup(self, page, className,
|
||||
content=content)
|
||||
# Insert the group at the higher level (ie, directly in p_elems)
|
||||
# if the group is not itself in a group.
|
||||
if not self.group:
|
||||
elems.append(uiGroup)
|
||||
else:
|
||||
outerGroup = self.group.insertInto(elems, uiGroups, page,
|
||||
className, content=content)
|
||||
outerGroup.addElement(uiGroup)
|
||||
else:
|
||||
uiGroup = uiGroups[self.name]
|
||||
return uiGroup
|
||||
|
||||
class Column:
|
||||
'''Used for describing a column within a Group like defined above'''
|
||||
def __init__(self, width, align="left"):
|
||||
self.width = width
|
||||
self.align = align
|
||||
|
||||
class UiGroup:
|
||||
'''On-the-fly-generated data structure that groups all elements
|
||||
(fields, searches, transitions...) sharing the same Group instance, that
|
||||
the currently logged user can see.'''
|
||||
|
||||
# PX that renders a help icon for a group.
|
||||
pxHelp = Px('''<acronym title="obj.translate('help', field=field)"><img
|
||||
src=":url('help')"/></acronym>''')
|
||||
|
||||
# PX that renders the content of a group (which is referred as var "field").
|
||||
pxContent = Px('''
|
||||
<table var="cellgap=field.cellgap" width=":field.wide"
|
||||
align=":ztool.flipLanguageDirection(field.align, dir)"
|
||||
id=":tagId" name=":tagName" class=":groupCss"
|
||||
cellspacing=":field.cellspacing" cellpadding=":field.cellpadding">
|
||||
<!-- Display the title of the group if not rendered a fieldset. -->
|
||||
<tr if="(field.style != 'fieldset') and field.hasLabel">
|
||||
<td colspan=":len(field.columnsWidths)" class=":field.style"
|
||||
align=":dleft">
|
||||
<x>::_(field.labelId)</x><x if="field.hasHelp">:field.pxHelp</x>
|
||||
</td>
|
||||
</tr>
|
||||
<tr if="(field.style != 'fieldset') and field.hasDescr">
|
||||
<td colspan=":len(field.columnsWidths)"
|
||||
class="discreet">::_(field.descrId)</td>
|
||||
</tr>
|
||||
<!-- The column headers -->
|
||||
<tr>
|
||||
<th for="colNb in range(len(field.columnsWidths))"
|
||||
align=":ztool.flipLanguageDirection(field.columnsAligns[colNb], dir)"
|
||||
width=":field.columnsWidths[colNb]">::field.hasHeaders and \
|
||||
_('%s_col%d' % (field.labelId, (colNb+1))) or ''</th>
|
||||
</tr>
|
||||
<!-- The rows of widgets -->
|
||||
<tr valign=":field.valign" for="row in field.elements">
|
||||
<td for="field in row" colspan=":field.colspan|1"
|
||||
style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''">
|
||||
<x if="field">
|
||||
<x if="field.type == 'group'">:field.pxView</x>
|
||||
<x if="field.type != 'group'">:field.pxRender</x>
|
||||
</x>
|
||||
</td>
|
||||
</tr>
|
||||
</table>''')
|
||||
|
||||
# PX that renders a group of fields (the group is refered as var "field").
|
||||
pxView = Px('''
|
||||
<x var="tagCss=field.master and ('slave*%s*%s' % \
|
||||
(field.masterName, '*'.join(field.masterValue))) or '';
|
||||
widgetCss=field.css_class;
|
||||
groupCss=tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
|
||||
tagName=field.master and 'slave' or '';
|
||||
tagId='%s_%s' % (zobj.id, field.name)">
|
||||
|
||||
<!-- Render the group as a fieldset if required -->
|
||||
<fieldset if="field.style == 'fieldset'">
|
||||
<legend if="field.hasLabel">
|
||||
<i>::_(field.labelId)></i><x if="field.hasHelp">:field.pxHelp</x>
|
||||
</legend>
|
||||
<div if="field.hasDescr" class="discreet">::_(field.descrId)</div>
|
||||
<x>:field.pxContent</x>
|
||||
</fieldset>
|
||||
|
||||
<!-- Render the group as a section if required -->
|
||||
<x if="field.style not in ('fieldset', 'tabs')">:field.pxContent</x>
|
||||
|
||||
<!-- Render the group as tabs if required -->
|
||||
<x if="field.style == 'tabs'" var2="tabsCount=len(field.elements)">
|
||||
<table width=":field.wide" class=":groupCss" id=":tagId" name=":tagName">
|
||||
<!-- First row: the tabs. -->
|
||||
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
|
||||
<table class="tabs" cellpadding="0" cellspacing="0">
|
||||
<tr valign="middle">
|
||||
<x for="sub in field.elements"
|
||||
var2="nb = loop.sub.nb + 1;
|
||||
suffix='%s_%d_%d' % (field.name, nb, tabsCount);
|
||||
tabId='tab_%s' % suffix">
|
||||
<td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
|
||||
<td style=":url('tabBg', bg=True)" class="tab" id=":tabId">
|
||||
<a onclick=":'showTab(%s)' % q(suffix)"
|
||||
class="clickable">:_(sub.labelId)</a>
|
||||
</td>
|
||||
<td><img id=":'%s_right' % tabId" src=":url('tabRight')"/></td>
|
||||
</x>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
|
||||
<!-- Other rows: the fields -->
|
||||
<tr for="sub in field.elements"
|
||||
var2="nb=loop.sub.nb + 1"
|
||||
id=":'tabcontent_%s_%d_%d' % (field.name, nb, tabsCount)"
|
||||
style=":(nb == 1) and 'display:table-row' or 'display:none'">
|
||||
<td var="field=sub">
|
||||
<x if="field.type == 'group'">:field.pxView</x>
|
||||
<x if="field.type != 'group'">:field.pxRender</x>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<script type="text/javascript">:'initTab(%s,%s)' % \
|
||||
(q('tab_%s' % field.name), q('%s_1_%d' % (field.name, tabsCount)))
|
||||
</script>
|
||||
</x>
|
||||
</x>''')
|
||||
|
||||
# PX that renders a group of searches
|
||||
pxViewSearches = Px('''
|
||||
<x var="collapse=field.getCollapseInfo(field.labelId, req)">
|
||||
<!-- Group name, prefixed by the expand/collapse icon -->
|
||||
<div class="portletGroup"><x>:collapse.px</x>
|
||||
<x if="not field.translated">:_(field.labelId)</x>
|
||||
<x if="field.translated">:field.translated</x>
|
||||
</div>
|
||||
<!-- Group content -->
|
||||
<div id=":collapse.id" style=":'padding-left: 10px; %s' % collapse.style">
|
||||
<x for="searches in field.elements">
|
||||
<x for="elem in searches">
|
||||
<!-- An inner group within this group -->
|
||||
<x if="elem.type== 'group'" var2="field=elem">:field.pxViewSearches</x>
|
||||
<!-- A search -->
|
||||
<x if="elem.type!= 'group'" var2="search=elem">:search.pxView</x>
|
||||
</x>
|
||||
</x>
|
||||
</div></x>''')
|
||||
|
||||
# PX that renders a group of transitions.
|
||||
pxViewTransitions = Px('''
|
||||
<!-- Render a group of transitions, as a one-column table -->
|
||||
<table>
|
||||
<x for="row in uiGroup.elements">
|
||||
<x for="transition in row"><tr><td>:transition.pxView</td></tr></x>
|
||||
</x>
|
||||
</table>''')
|
||||
|
||||
# What PX to use, depending on group content?
|
||||
pxByContent = {'fields': pxView, 'searches': pxViewSearches,
|
||||
'transitions': pxViewTransitions}
|
||||
|
||||
def __init__(self, group, page, className, content='fields'):
|
||||
'''A UiGroup can group various kinds of elements: fields, searches,
|
||||
transitions..., The type of content that one may find in this group
|
||||
is given in p_content.
|
||||
* p_group is the Group instance corresponding to this UiGroup;
|
||||
* p_page is the Page instance where the group is rendered (for
|
||||
transitions, it corresponds to a virtual page
|
||||
"workflow");
|
||||
* p_className is the name of the class that holds the elements to
|
||||
group.'''
|
||||
self.type = 'group'
|
||||
# All p_group attributes become self attributes. This is required
|
||||
# because a UiGroup, in some PXs, must behave like a Field (ie, have
|
||||
# the same attributes, like "master".
|
||||
for name, value in group.__dict__.items():
|
||||
if not name.startswith('_'):
|
||||
setattr(self, name, value)
|
||||
self.group = group
|
||||
self.columnsWidths = [col.width for col in group.columns]
|
||||
self.columnsAligns = [col.align for col in group.columns]
|
||||
# Names of i18n labels for this group
|
||||
labelName = self.name
|
||||
prefix = className
|
||||
if group.label:
|
||||
if isinstance(group.label, str): prefix = group.label
|
||||
else: # It is a tuple (className, name)
|
||||
if group.label[1]: labelName = group.label[1]
|
||||
if group.label[0]: prefix = group.label[0]
|
||||
gp = (content == 'searches') and 'searchgroup' or 'group'
|
||||
self.labelId = '%s_%s_%s' % (prefix, gp, labelName)
|
||||
self.descrId = self.labelId + '_descr'
|
||||
self.helpId = self.labelId + '_help'
|
||||
# The name of the page where the group lies
|
||||
self.page = page.name
|
||||
# The elements (fields or sub-groups) contained in the group, that the
|
||||
# current user may see. They will be inserted by m_addElement below.
|
||||
if self.style != 'tabs':
|
||||
# In most cases, "elements" will be a list of lists for rendering
|
||||
# them as a table.
|
||||
self.elements = [[]]
|
||||
else:
|
||||
# If the group is a tab, elements will be stored as a simple list.
|
||||
self.elements = []
|
||||
# PX to use for rendering this group.
|
||||
self.px = self.pxByContent[content]
|
||||
|
||||
def addElement(self, element):
|
||||
'''Adds p_element into self.elements. We try first to add p_element into
|
||||
the last row. If it is not possible, we create a new row.'''
|
||||
if self.style == 'tabs':
|
||||
self.elements.append(element)
|
||||
return
|
||||
# Get the last row
|
||||
lastRow = self.elements[-1]
|
||||
numberOfColumns = len(self.columnsWidths)
|
||||
# Compute the number of columns already filled in the last row.
|
||||
filledColumns = 0
|
||||
for rowElem in lastRow: filledColumns += rowElem.colspan
|
||||
freeColumns = numberOfColumns - filledColumns
|
||||
if freeColumns >= element.colspan:
|
||||
# We can add the element in the last row.
|
||||
lastRow.append(element)
|
||||
else:
|
||||
if freeColumns:
|
||||
# Terminate the current row by appending empty cells
|
||||
for i in range(freeColumns): lastRow.append('')
|
||||
# Create a new row
|
||||
self.elements.append([element])
|
||||
|
||||
def getCollapseInfo(self, id, request):
|
||||
'''Returns a Collapsible instance, that determines if this group,
|
||||
represented as an expandable menu item, is collapsed or expanded.'''
|
||||
return gutils.Collapsible(id, request)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,39 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy.fields import Field
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Info(Field):
|
||||
'''An info is a field whose purpose is to present information
|
||||
(text, html...) to the user.'''
|
||||
# An info only displays a label. So PX for showing content are empty.
|
||||
pxView = pxEdit = pxCell = pxSearch = ''
|
||||
|
||||
def __init__(self, validator=None, multiplicity=(1,1), default=None,
|
||||
show='view', page='main', group=None, layouts=None, move=0,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=None, height=None, maxChars=None, colspan=1, master=None,
|
||||
masterValue=None, focus=False, historized=False, mapping=None,
|
||||
label=None, view=None, xml=None):
|
||||
Field.__init__(self, None, (0,1), default, show, page, group, layouts,
|
||||
move, False, True, False, specificReadPermission,
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, historized, mapping, label,
|
||||
None, None, None, None, False, view, xml)
|
||||
self.validable = False
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,78 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy.fields import Field
|
||||
from appy.px import Px
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Integer(Field):
|
||||
|
||||
pxView = pxCell = Px('''
|
||||
<x><x>:value</x>
|
||||
<input type="hidden" if="masterCss"
|
||||
class=":masterCss" value=":value" name=":name" id=":name"/>
|
||||
</x>''')
|
||||
|
||||
pxEdit = Px('''
|
||||
<input id=":name" name=":name" size=":field.width"
|
||||
maxlength=":field.maxChars"
|
||||
value=":inRequest and requestValue or value" type="text"/>''')
|
||||
|
||||
pxSearch = Px('''
|
||||
<!-- From -->
|
||||
<x var="fromName='%s*int' % widgetName">
|
||||
<label lfor=":fromName">:_('search_from')</label>
|
||||
<input type="text" name=":fromName" maxlength=":field.maxChars"
|
||||
value=":field.sdefault[0]" size=":field.swidth"/>
|
||||
</x>
|
||||
<!-- To -->
|
||||
<x var="toName='%s_to' % name">
|
||||
<label lfor=":toName">:_('search_to')</label>
|
||||
<input type="text" name=":toName" maxlength=":field.maxChars"
|
||||
value=":field.sdefault[1]" size=":field.swidth"/>
|
||||
</x><br/>''')
|
||||
|
||||
def __init__(self, validator=None, multiplicity=(0,1), default=None,
|
||||
show=True, page='main', group=None, layouts=None, move=0,
|
||||
indexed=False, mustIndex=True, searchable=False,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=5, height=None, maxChars=13, colspan=1, master=None,
|
||||
masterValue=None, focus=False, historized=False, mapping=None,
|
||||
label=None, sdefault=('',''), scolspan=1, swidth=None,
|
||||
sheight=None, persist=True, view=None, xml=None):
|
||||
Field.__init__(self, validator, multiplicity, default, show, page,
|
||||
group, layouts, move, indexed, mustIndex, searchable,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, maxChars, colspan, master, masterValue, focus,
|
||||
historized, mapping, label, sdefault, scolspan, swidth,
|
||||
sheight, persist, view, xml)
|
||||
self.pythonType = int
|
||||
|
||||
def validateValue(self, obj, value):
|
||||
try:
|
||||
value = self.pythonType(value)
|
||||
except ValueError:
|
||||
return obj.translate('bad_%s' % self.pythonType.__name__)
|
||||
|
||||
def getStorableValue(self, obj, value):
|
||||
if not self.isEmptyValue(obj, value): return self.pythonType(value)
|
||||
|
||||
def getFormattedValue(self, obj, value, layoutType='view',
|
||||
showChanges=False, language=None):
|
||||
if self.isEmptyValue(obj, value): return ''
|
||||
return str(value)
|
||||
# ------------------------------------------------------------------------------
|
202
fields/list.py
|
@ -1,202 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy import Object
|
||||
from appy.fields import Field
|
||||
from appy.px import Px
|
||||
from appy.gen.layout import Table
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class List(Field):
|
||||
'''A list, stored as a list of Object instances ~[Object]~. Every object in
|
||||
the list has attributes named according to the sub-fields defined in this
|
||||
List.'''
|
||||
|
||||
# PX for rendering a single row
|
||||
pxRow = Px('''
|
||||
<tr valign="top" style=":(rowIndex==-1) and 'display: none' or ''">
|
||||
<td for="info in subFields" if="info[1]" align="center"
|
||||
var2="field=info[1];
|
||||
fieldName='%s*%d' % (field.name, rowIndex);
|
||||
tagCss='no'">:field.pxRender</td>
|
||||
<!-- Icon for removing the row -->
|
||||
<td if="layoutType=='edit'" align=":dright">
|
||||
<img class="clickable" src=":url('delete')" title=":_('object_delete')"
|
||||
onclick=":'deleteRow(%s, this)' % q('list_%s' % name)"/>
|
||||
</td>
|
||||
</tr>''')
|
||||
|
||||
# PX for rendering the list (shared between pxView and pxEdit)
|
||||
pxTable = Px('''
|
||||
<table var="isEdit=layoutType == 'edit'" if="isEdit or value"
|
||||
id=":'list_%s' % name" class=":isEdit and 'grid' or 'list'"
|
||||
width=":field.width"
|
||||
var2="subFields=field.getSubFields(zobj, layoutType)">
|
||||
<!-- Header -->
|
||||
<tr valign="bottom">
|
||||
<th for="info in subFields" if="info[1]"
|
||||
width=":field.widths[loop.info.nb]">::_(info[1].labelId)</th>
|
||||
<!-- Icon for adding a new row. -->
|
||||
<th if="isEdit">
|
||||
<img class="clickable" src=":url('plus')" title=":_('add_ref')"
|
||||
onclick=":'insertRow(%s)' % q('list_%s' % name)"/>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<!-- Template row (edit only) -->
|
||||
<x var="rowIndex=-1" if="isEdit">:field.pxRow</x>
|
||||
<tr height="7px" if="isEdit"><td></td></tr>
|
||||
|
||||
<!-- Rows of data -->
|
||||
<x var="rows=inRequest and requestValue or value"
|
||||
for="row in rows" var2="rowIndex=loop.row.nb">:field.pxRow</x>
|
||||
</table>''')
|
||||
|
||||
pxView = pxCell = Px('''<x>:field.pxTable</x>''')
|
||||
pxEdit = Px('''<x>
|
||||
<!-- This input makes Appy aware that this field is in the request -->
|
||||
<input type="hidden" name=":name" value=""/><x>:field.pxTable</x>
|
||||
</x>''')
|
||||
|
||||
pxSearch = ''
|
||||
|
||||
def __init__(self, fields, validator=None, multiplicity=(0,1), default=None,
|
||||
show=True, page='main', group=None, layouts=None, move=0,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width='', height=None, maxChars=None, colspan=1, master=None,
|
||||
masterValue=None, focus=False, historized=False, mapping=None,
|
||||
label=None, subLayouts=Table('frv', width=None), widths=None,
|
||||
view=None, xml=None):
|
||||
Field.__init__(self, validator, multiplicity, default, show, page,
|
||||
group, layouts, move, False, True, False,
|
||||
specificReadPermission, specificWritePermission, width,
|
||||
height, None, colspan, master, masterValue, focus,
|
||||
historized, mapping, label, None, None, None, None,
|
||||
True, view, xml)
|
||||
self.validable = True
|
||||
# Tuples of (names, Field instances) determining the format of every
|
||||
# element in the list.
|
||||
self.fields = fields
|
||||
# Force some layouting for sub-fields, if subLayouts are given. So the
|
||||
# one who wants freedom on tuning layouts at the field level must
|
||||
# specify subLayouts=None.
|
||||
if subLayouts:
|
||||
for name, field in self.fields:
|
||||
field.layouts = field.formatLayouts(subLayouts)
|
||||
# One may specify the width of every column in the list. Indeed, using
|
||||
# widths and layouts of sub-fields may not be sufficient.
|
||||
self.computeWidths(widths)
|
||||
|
||||
def computeWidths(self, widths):
|
||||
'''Set given p_widths or compute default ones if not given.'''
|
||||
if not widths:
|
||||
self.widths = [''] * len(self.fields)
|
||||
else:
|
||||
self.widths = widths
|
||||
|
||||
def getField(self, name):
|
||||
'''Gets the field definition whose name is p_name.'''
|
||||
for n, field in self.fields:
|
||||
if n == name: return field
|
||||
|
||||
def getSubFields(self, obj, layoutType):
|
||||
'''Returns the sub-fields (name, Field) that are showable among
|
||||
field.fields on the given p_layoutType. Fields that cannot appear in
|
||||
the result are nevertheless present as a tuple (name, None). This
|
||||
way, it keeps a nice layouting of the table.'''
|
||||
res = []
|
||||
for n, field in self.fields:
|
||||
elem = (n, None)
|
||||
if field.isShowable(obj, layoutType):
|
||||
elem = (n, field)
|
||||
res.append(elem)
|
||||
return res
|
||||
|
||||
def getRequestValue(self, obj, requestName=None):
|
||||
'''Concatenates the list from distinct form elements in the request'''
|
||||
request = obj.REQUEST
|
||||
name = requestName or self.name # A List may be into another List (?)
|
||||
prefix = name + '*' + self.fields[0][0] + '*'
|
||||
res = {}
|
||||
for key in list(request.keys()):
|
||||
if not key.startswith(prefix): continue
|
||||
# I have found a row. Gets its index.
|
||||
row = Object()
|
||||
if '_' in key: key = key[:key.index('_')]
|
||||
rowIndex = int(key.split('*')[-1])
|
||||
if rowIndex == -1: continue # Ignore the template row.
|
||||
for subName, subField in self.fields:
|
||||
keyName = '%s*%s*%s' % (name, subName, rowIndex)
|
||||
v = subField.getRequestValue(obj, requestName=keyName)
|
||||
setattr(row, subName, v)
|
||||
res[rowIndex] = row
|
||||
# Produce a sorted list
|
||||
keys = list(res.keys())
|
||||
keys.sort()
|
||||
res = [res[key] for key in keys]
|
||||
# I store in the request this computed value. This way, when individual
|
||||
# subFields will need to get their value, they will take it from here,
|
||||
# instead of taking it from the specific request key. Indeed, specific
|
||||
# request keys contain row indexes that may be wrong after row deletions
|
||||
# by the user.
|
||||
request.set(name, res)
|
||||
return res
|
||||
|
||||
def getStorableValue(self, obj, value):
|
||||
'''Gets p_value in a form that can be stored in the database'''
|
||||
res = []
|
||||
for v in value:
|
||||
sv = Object()
|
||||
for name, field in self.fields:
|
||||
subValue = getattr(v, name)
|
||||
try:
|
||||
setattr(sv, name, field.getStorableValue(obj, subValue))
|
||||
except ValueError:
|
||||
# The value for this field for this specific row is
|
||||
# incorrect. It can happen in the process of validating the
|
||||
# whole List field (a call to m_getStorableValue occurs at
|
||||
# this time). We don't care about it, because later on we
|
||||
# will have sub-field specific validation that will also
|
||||
# detect the error and will prevent storing the wrong value
|
||||
# in the database.
|
||||
setattr(sv, name, subValue)
|
||||
res.append(sv)
|
||||
return res
|
||||
|
||||
def getInnerValue(self, obj, outerValue, name, i):
|
||||
'''Returns the value of inner field named p_name in row number p_i
|
||||
within the whole list of values p_outerValue.'''
|
||||
if i == -1: return ''
|
||||
if not outerValue: return ''
|
||||
if i >= len(outerValue): return ''
|
||||
# Return the value, or a potential default value
|
||||
value = getattr(outerValue[i], name, None)
|
||||
if value != None: return value
|
||||
value = self.getField(name).getValue(obj)
|
||||
if value != None: return value
|
||||
return ''
|
||||
|
||||
def getCss(self, layoutType, res, config):
|
||||
'''Gets the CSS required by sub-fields if any'''
|
||||
for name, field in self.fields:
|
||||
field.getCss(layoutType, res, config)
|
||||
|
||||
def getJs(self, layoutType, res, config):
|
||||
'''Gets the JS required by sub-fields if any'''
|
||||
for name, field in self.fields:
|
||||
field.getJs(layoutType, res, config)
|
||||
# ------------------------------------------------------------------------------
|
165
fields/ogone.py
|
@ -1,165 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import sha
|
||||
from appy import Object
|
||||
from appy.gen import Field
|
||||
from appy.px import Px
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class OgoneConfig:
|
||||
'''If you plan, in your app, to perform on-line payments via the Ogone (r)
|
||||
system, create an instance of this class in your app and place it in the
|
||||
'ogone' attr of your appy.gen.Config class.'''
|
||||
def __init__(self):
|
||||
# self.env refers to the Ogone environment and can be "test" or "prod".
|
||||
self.env = 'test'
|
||||
# You merchant Ogone ID
|
||||
self.PSPID = None
|
||||
# Default currency for transactions
|
||||
self.currency = 'EUR'
|
||||
# Default language
|
||||
self.language = 'en_US'
|
||||
# SHA-IN key (digest will be generated with the SHA-1 algorithm)
|
||||
self.shaInKey = ''
|
||||
# SHA-OUT key (digest will be generated with the SHA-1 algorithm)
|
||||
self.shaOutKey = ''
|
||||
|
||||
def __repr__(self): return str(self.__dict__)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Ogone(Field):
|
||||
'''This field allows to perform payments with the Ogone (r) system.'''
|
||||
urlTypes = ('accept', 'decline', 'exception', 'cancel')
|
||||
|
||||
pxView = pxCell = Px('''<x>
|
||||
<!-- var "value" is misused and contains the contact params for Ogone -->
|
||||
<!-- The form for sending the payment request to Ogone -->
|
||||
<form method="post" id="form1" name="form1" var="env=value['env']"
|
||||
action=":'https://secure.ogone.com/ncol/%s/orderstandard.asp'% env">
|
||||
<input type="hidden" for="item in value.items()" if="item[0] != 'env'"
|
||||
id=":item[0]" name=":item[0]" value=":item[1]"/>
|
||||
<!-- Submit image -->
|
||||
<input type="image" id="submit2" name="submit2" src=":url('ogone.gif')"
|
||||
title=":_('custom_pay')"/>
|
||||
</form>
|
||||
</x>''')
|
||||
|
||||
pxEdit = pxSearch = ''
|
||||
|
||||
def __init__(self, orderMethod, responseMethod, show='view', page='main',
|
||||
group=None, layouts=None, move=0, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
mapping=None, label=None, view=None, xml=None):
|
||||
Field.__init__(self, None, (0,1), None, show, page, group, layouts,
|
||||
move, False, True, False,specificReadPermission,
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, False, mapping, label, None,
|
||||
None, None, None, False, view, xml)
|
||||
# orderMethod must contain a method returning a dict containing info
|
||||
# about the order. Following keys are mandatory:
|
||||
# * orderID An identifier for the order. Don't use the object UID
|
||||
# for this, use a random number, because if the payment
|
||||
# is canceled, Ogone will not allow you to reuse the same
|
||||
# orderID for the next tentative.
|
||||
# * amount An integer representing the price for this order,
|
||||
# multiplied by 100 (no floating point value, no commas
|
||||
# are tolerated. Dont't forget to multiply the amount by
|
||||
# 100!
|
||||
self.orderMethod = orderMethod
|
||||
# responseMethod must contain a method accepting one param, let's call
|
||||
# it "response". The response method will be called when we will get
|
||||
# Ogone's response about the status of the payment. Param "response" is
|
||||
# an object whose attributes correspond to all parameters that you have
|
||||
# chosen to receive in your Ogone merchant account. After the payment,
|
||||
# the user will be redirected to the object's view page, excepted if
|
||||
# your method returns an alternatve URL.
|
||||
self.responseMethod = responseMethod
|
||||
|
||||
noShaInKeys = ('env',)
|
||||
noShaOutKeys = ('name', 'SHASIGN')
|
||||
def createShaDigest(self, values, passphrase, keysToIgnore=()):
|
||||
'''Creates an Ogone-compliant SHA-1 digest based on key-value pairs in
|
||||
dict p_values and on some p_passphrase.'''
|
||||
# Create a new dict by removing p_keysToIgnore from p_values, and by
|
||||
# upperizing all keys.
|
||||
shaRes = {}
|
||||
for k, v in values.items():
|
||||
if k in keysToIgnore: continue
|
||||
# Ogone: we must not include empty values.
|
||||
if (v == None) or (v == ''): continue
|
||||
shaRes[k.upper()] = v
|
||||
# Create a sorted list of keys
|
||||
keys = list(shaRes.keys())
|
||||
keys.sort()
|
||||
shaList = []
|
||||
for k in keys:
|
||||
shaList.append('%s=%s' % (k, shaRes[k]))
|
||||
shaObject = sha.new(passphrase.join(shaList) + passphrase)
|
||||
res = shaObject.hexdigest()
|
||||
return res
|
||||
|
||||
def getValue(self, obj):
|
||||
'''The "value" of the Ogone field is a dict that collects all the
|
||||
necessary info for making the payment.'''
|
||||
tool = obj.getTool()
|
||||
# Basic Ogone parameters were generated in the app config module.
|
||||
res = obj.getProductConfig(True).ogone.copy()
|
||||
shaKey = res['shaInKey']
|
||||
# Remove elements from the Ogone config that we must not send in the
|
||||
# payment request.
|
||||
del res['shaInKey']
|
||||
del res['shaOutKey']
|
||||
res.update(self.callMethod(obj, self.orderMethod))
|
||||
# Add user-related information
|
||||
res['CN'] = str(tool.getUserName(normalized=True))
|
||||
user = obj.appy().user
|
||||
res['EMAIL'] = user.email or user.login
|
||||
# Add standard back URLs
|
||||
siteUrl = tool.getSiteUrl()
|
||||
res['catalogurl'] = siteUrl
|
||||
res['homeurl'] = siteUrl
|
||||
# Add redirect URLs
|
||||
for t in self.urlTypes:
|
||||
res['%surl' % t] = '%s/onProcess' % obj.absolute_url()
|
||||
# Add additional parameter that we want Ogone to give use back in all
|
||||
# of its responses: the name of this Appy Ogone field. This way, Appy
|
||||
# will be able to call method m_process below, that will process
|
||||
# Ogone's response.
|
||||
res['paramplus'] = 'name=%s' % self.name
|
||||
# Ensure every value is a str
|
||||
for k in res.keys():
|
||||
if not isinstance(res[k], str):
|
||||
res[k] = str(res[k])
|
||||
# Compute a SHA-1 key as required by Ogone and add it to the res
|
||||
res['SHASign'] = self.createShaDigest(res, shaKey,
|
||||
keysToIgnore=self.noShaInKeys)
|
||||
return res
|
||||
|
||||
def ogoneResponseOk(self, obj):
|
||||
'''Returns True if the SHA-1 signature from Ogone matches retrieved
|
||||
params.'''
|
||||
response = obj.REQUEST.form
|
||||
shaKey = obj.getProductConfig(True).ogone['shaOutKey']
|
||||
digest = self.createShaDigest(response, shaKey,
|
||||
keysToIgnore=self.noShaOutKeys)
|
||||
return digest.lower() == response['SHASIGN'].lower()
|
||||
|
||||
def process(self, obj):
|
||||
'''Processes a response from Ogone.'''
|
||||
# Call the response method defined in this Ogone field.
|
||||
if not self.ogoneResponseOk(obj):
|
||||
obj.log('Ogone response SHA failed. REQUEST: %s' % \
|
||||
str(obj.REQUEST.form))
|
||||
raise Exception('Failure, possible fraud detection, an ' \
|
||||
'administrator has been contacted.')
|
||||
# Create a nice object from the form.
|
||||
response = Object()
|
||||
for k, v in obj.REQUEST.form.items():
|
||||
setattr(response, k, v)
|
||||
# Call the field method that handles the response received from Ogone.
|
||||
url = self.responseMethod(obj.appy(), response)
|
||||
# Redirect the user to the correct page. If the field method returns
|
||||
# some URL, use it. Else, use the view page of p_obj.
|
||||
if not url: url = obj.absolute_url()
|
||||
obj.goto(url)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,92 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy import Object
|
||||
import collections
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Page:
|
||||
'''Used for describing a page, its related phase, show condition, etc.'''
|
||||
subElements = ('save', 'cancel', 'previous', 'next', 'edit')
|
||||
|
||||
def __init__(self, name, phase='main', show=True, showSave=True,
|
||||
showCancel=True, showPrevious=True, showNext=True,
|
||||
showEdit=True, label=None):
|
||||
self.name = name
|
||||
self.phase = phase
|
||||
self.show = show
|
||||
# When editing the page, must I show the "save" button?
|
||||
self.showSave = showSave
|
||||
# When editing the page, must I show the "cancel" button?
|
||||
self.showCancel = showCancel
|
||||
# When editing the page, and when a previous page exists, must I show
|
||||
# the "previous" button?
|
||||
self.showPrevious = showPrevious
|
||||
# When editing the page, and when a next page exists, must I show the
|
||||
# "next" button?
|
||||
self.showNext = showNext
|
||||
# When viewing the page, must I show the "edit" button?
|
||||
self.showEdit = showEdit
|
||||
# Instead of computing a translated label, one may give p_label, a
|
||||
# fixed label which will not be translated.
|
||||
self.label = label
|
||||
|
||||
@staticmethod
|
||||
def get(pageData):
|
||||
'''Produces a Page instance from p_pageData. User-defined p_pageData
|
||||
can be:
|
||||
(a) a string containing the name of the page;
|
||||
(b) a string containing <pageName>_<phaseName>;
|
||||
(c) a Page instance.
|
||||
This method returns always a Page instance.'''
|
||||
res = pageData
|
||||
if res and isinstance(res, str):
|
||||
# Page data is given as a string.
|
||||
pageElems = pageData.rsplit('_', 1)
|
||||
if len(pageElems) == 1: # We have case (a)
|
||||
res = Page(pageData)
|
||||
else: # We have case (b)
|
||||
res = Page(pageData[0], phase=pageData[1])
|
||||
return res
|
||||
|
||||
def isShowable(self, obj, layoutType, elem='page'):
|
||||
'''Is this page showable for p_obj on p_layoutType ("view" or "edit")?
|
||||
|
||||
If p_elem is not "page", this method returns the fact that a
|
||||
sub-element is viewable or not (buttons "save", "cancel", etc).'''
|
||||
# Define what attribute to test for "showability".
|
||||
attr = (elem == 'page') and 'show' or ('show%s' % elem.capitalize())
|
||||
# Get the value of the show attribute as identified above.
|
||||
res = getattr(self, attr)
|
||||
if callable(res): res = res(obj.appy())
|
||||
if isinstance(res, str): return res == layoutType
|
||||
return res
|
||||
|
||||
def getInfo(self, obj, layoutType):
|
||||
'''Gets information about this page, for p_obj, as an object.'''
|
||||
res = Object()
|
||||
for elem in Page.subElements:
|
||||
showable = self.isShowable(obj, layoutType, elem)
|
||||
setattr(res, 'show%s' % elem.capitalize(), showable)
|
||||
return res
|
||||
|
||||
def getLabel(self, zobj):
|
||||
'''Returns the i18n label for this page, or a fixed label if self.label
|
||||
is not empty.'''
|
||||
if self.label: return self.label
|
||||
return zobj.translate('%s_page_%s' % (zobj.meta_type, self.name))
|
||||
# ------------------------------------------------------------------------------
|
224
fields/phase.py
|
@ -1,224 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy import Object
|
||||
from appy.px import Px
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Phase:
|
||||
'''A group of pages.'''
|
||||
|
||||
pxView = Px('''
|
||||
<table class="phase"
|
||||
var="singlePage=len(phase.pages) == 1;
|
||||
label='%s_phase_%s' % (zobj.meta_type, phase.name)">
|
||||
<tr valign="top">
|
||||
<!-- The page(s) within the phase -->
|
||||
<td for="aPage in phase.pages"
|
||||
var2="aPageInfo=phase.pagesInfo[aPage]"
|
||||
class=":(aPage == page) and 'currentPage' or ''">
|
||||
<!-- First line: page name and icons -->
|
||||
<span if="not (singlePhase and singlePage)">
|
||||
<x var="label=aPageInfo.page.getLabel(zobj)">
|
||||
<a if="aPageInfo.showOnView"
|
||||
href=":zobj.getUrl(page=aPage, inPopup=inPopup)">::label</a>
|
||||
<x if="not aPageInfo.showOnView">:label</x>
|
||||
</x>
|
||||
<x var="locked=zobj.isLocked(user, aPage);
|
||||
editable=mayEdit and aPageInfo.showOnEdit and \
|
||||
aPageInfo.showEdit">
|
||||
<a if="editable and not locked"
|
||||
href=":zobj.getUrl(mode='edit', page=aPage, inPopup=inPopup)">
|
||||
<img src=":url('edit')" title=":_('object_edit')"/></a>
|
||||
<a if="editable and locked">
|
||||
<img style="cursor: help"
|
||||
var="lockDate=ztool.formatDate(locked[1]);
|
||||
lockMap={'user':ztool.getUserName(locked[0]), \
|
||||
'date':lockDate};
|
||||
lockMsg=_('page_locked', mapping=lockMap)"
|
||||
src=":url('locked')" title=":lockMsg"/></a>
|
||||
<a if="editable and locked and user.has_role('Manager')">
|
||||
<img class="clickable" title=":_('page_unlock')" src=":url('unlock')"
|
||||
onclick=":'onUnlockPage(%s,%s)' % (q(zobj.id), q(aPage))"/></a>
|
||||
</x>
|
||||
</span>
|
||||
<!-- Next lines: links -->
|
||||
<x var="links=aPageInfo.links" if="links">
|
||||
<div for="link in links" class="refLink">
|
||||
<a href=":link.url">:link.title</a></div>
|
||||
</x>
|
||||
</td>
|
||||
</tr>
|
||||
</table>''')
|
||||
|
||||
# "Static" PX that displays all phases of a given object.
|
||||
pxAllPhases = Px('''
|
||||
<x var="singlePhase=len(phases)==1;
|
||||
page=req.get('page', '');
|
||||
uid=zobj.id;
|
||||
mayEdit=zobj.mayEdit()">
|
||||
<x if="singlePhase" var2="phase=phases[0]">:phase.pxView</x>
|
||||
<!-- Display several phases in tabs. -->
|
||||
<x if="not singlePhase">
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<!-- First row: the tabs. -->
|
||||
<tr><td style="border-bottom: 1px solid #ff8040; padding-bottom: 1px">
|
||||
<table cellpadding="0" cellspacing="0" class="tabs">
|
||||
<tr valign="middle">
|
||||
<x for="phase in phases"
|
||||
var2="nb=loop.phase.nb + 1;
|
||||
suffix='%s_%d_%d' % (uid, nb, len(phases));
|
||||
tabId='tab_%s' % suffix">
|
||||
<td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
|
||||
<td style=":url('tabBg',bg=True)" id=":tabId" class="tab">
|
||||
<a onclick=":'showTab(%s)' % q(suffix)"
|
||||
class="clickable">:_('%s_phase_%s' % (zobj.meta_type, \
|
||||
phase.name))</a>
|
||||
</td>
|
||||
<td><img id=":'%s_right' % tabId" src=":url('tabRight')"/></td>
|
||||
</x>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<!-- Other rows: the fields -->
|
||||
<tr for="phase in phases"
|
||||
var2="nb=loop.phase.nb + 1"
|
||||
id=":'tabcontent_%s_%d_%d' % (uid, nb, len(phases))"
|
||||
style=":(nb == 1) and 'display:table-row' or 'display:none'">
|
||||
<td>:phase.pxView</td>
|
||||
</tr>
|
||||
</table>
|
||||
<script type="text/javascript">:'initTab(%s,%s)' % \
|
||||
(q('tab_%s' % uid), q('%s_1_%d' % (uid, len(phases))))
|
||||
</script>
|
||||
</x>
|
||||
</x>''')
|
||||
|
||||
def __init__(self, name, obj):
|
||||
self.name = name
|
||||
self.obj = obj
|
||||
# The list of names of pages in this phase
|
||||
self.pages = []
|
||||
# The list of hidden pages in this phase
|
||||
self.hiddenPages = []
|
||||
# The dict below stores info about every page listed in self.pages.
|
||||
self.pagesInfo = {}
|
||||
self.totalNbOfPhases = None
|
||||
# The following attributes allows to browse, from a given page, to the
|
||||
# last page of the previous phase and to the first page of the following
|
||||
# phase if allowed by phase state.
|
||||
self.previousPhase = None
|
||||
self.nextPhase = None
|
||||
|
||||
def addPageLinks(self, field, obj):
|
||||
'''If p_field is a navigable Ref, we must add, within self.pagesInfo,
|
||||
objects linked to p_obj through this Ref as links.'''
|
||||
if field.page.name in self.hiddenPages: return
|
||||
infos = []
|
||||
for ztied in field.getValue(obj, appy=False):
|
||||
infos.append(Object(title=ztied.title, url=ztied.absolute_url()))
|
||||
self.pagesInfo[field.page.name].links = infos
|
||||
|
||||
def addPage(self, field, obj, layoutType):
|
||||
'''Adds page-related information in the phase.'''
|
||||
# If the page is already there, we have nothing more to do.
|
||||
if (field.page.name in self.pages) or \
|
||||
(field.page.name in self.hiddenPages): return
|
||||
# Add the page only if it must be shown.
|
||||
showOnView = field.page.isShowable(obj, 'view')
|
||||
showOnEdit = field.page.isShowable(obj, 'edit')
|
||||
if showOnView or showOnEdit:
|
||||
# The page must be added
|
||||
self.pages.append(field.page.name)
|
||||
# Create the dict about page information and add it in self.pageInfo
|
||||
pageInfo = Object(page=field.page, showOnView=showOnView,
|
||||
showOnEdit=showOnEdit, links=None)
|
||||
pageInfo.update(field.page.getInfo(obj, layoutType))
|
||||
self.pagesInfo[field.page.name] = pageInfo
|
||||
else:
|
||||
self.hiddenPages.append(field.page.name)
|
||||
|
||||
def computeNextPrevious(self, allPhases):
|
||||
'''This method also fills fields "previousPhase" and "nextPhase"
|
||||
if relevant, based on list of p_allPhases.'''
|
||||
# Identify previous and next phases
|
||||
for phase in allPhases:
|
||||
if phase.name == self.name:
|
||||
i = allPhases.index(phase)
|
||||
if i > 0:
|
||||
self.previousPhase = allPhases[i-1]
|
||||
if i < (len(allPhases)-1):
|
||||
self.nextPhase = allPhases[i+1]
|
||||
|
||||
def getPreviousPage(self, page):
|
||||
'''Returns the page that precedes p_page in this phase.'''
|
||||
try:
|
||||
pageIndex = self.pages.index(page)
|
||||
except ValueError:
|
||||
# The current page is probably not visible anymore. Return the
|
||||
# first available page in current phase.
|
||||
res = self.pages[0]
|
||||
return res, self.pagesInfo[res]
|
||||
if pageIndex > 0:
|
||||
# We stay on the same phase, previous page
|
||||
res = self.pages[pageIndex-1]
|
||||
return res, self.pagesInfo[res]
|
||||
else:
|
||||
if self.previousPhase:
|
||||
# We go to the last page of previous phase
|
||||
previousPhase = self.previousPhase
|
||||
res = previousPhase.pages[-1]
|
||||
return res, previousPhase.pagesInfo[res]
|
||||
else:
|
||||
return None, None
|
||||
|
||||
def getNextPage(self, page):
|
||||
'''Returns the page that follows p_page in this phase.'''
|
||||
try:
|
||||
pageIndex = self.pages.index(page)
|
||||
except ValueError:
|
||||
# The current page is probably not visible anymore. Return the
|
||||
# first available page in current phase.
|
||||
res = self.pages[0]
|
||||
return res, self.pagesInfo[res]
|
||||
if pageIndex < (len(self.pages)-1):
|
||||
# We stay on the same phase, next page
|
||||
res = self.pages[pageIndex+1]
|
||||
return res, self.pagesInfo[res]
|
||||
else:
|
||||
if self.nextPhase:
|
||||
# We go to the first page of next phase
|
||||
nextPhase = self.nextPhase
|
||||
res = nextPhase.pages[0]
|
||||
return res, nextPhase.pagesInfo[res]
|
||||
else:
|
||||
return None, None
|
||||
|
||||
def getPageInfo(self, page, layoutType):
|
||||
'''Return the page info corresponding to the given p_page. If this page
|
||||
cannot be shown on p_layoutType, this method returns page info about
|
||||
the first showable page on p_layoutType, or None if no page is
|
||||
showable at all.'''
|
||||
res = self.pagesInfo[page]
|
||||
showAttribute = 'showOn%s' % layoutType.capitalize()
|
||||
if getattr(res, showAttribute): return res
|
||||
# Find the first showable page in this phase on p_layoutType.
|
||||
for pageName in self.pages:
|
||||
if pageName == page: continue
|
||||
pageInfo = self.pagesInfo[pageName]
|
||||
if getattr(pageInfo, showAttribute): return pageInfo
|
||||
# ------------------------------------------------------------------------------
|
810
fields/pod.py
|
@ -1,810 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import time, os, os.path
|
||||
from .file import FileInfo
|
||||
from appy import Object
|
||||
from appy.fields import Field
|
||||
from appy.px import Px
|
||||
from appy.gen.layout import Table
|
||||
from appy.gen import utils as gutils
|
||||
from appy.pod import PodError
|
||||
from appy.pod.renderer import Renderer
|
||||
from appy.shared import utils as sutils
|
||||
import collections
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Mailing:
|
||||
'''Represents a mailing list as can be used by a pod field (see below).'''
|
||||
def __init__(self, name=None, logins=None, subject=None, body=None):
|
||||
# The mailing list name, as shown in the user interface
|
||||
self.name = name
|
||||
# The list of logins that will be used as recipients for sending
|
||||
# emails.
|
||||
self.logins = logins
|
||||
# The mail subject
|
||||
self.subject = subject
|
||||
# The mail body
|
||||
self.body = body
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Pod(Field):
|
||||
'''A pod is a field allowing to produce a (PDF, ODT, Word, RTF...) document
|
||||
from data contained in Appy class and linked objects or anything you
|
||||
want to put in it. It is the way gen uses pod.'''
|
||||
# Some right-aligned layouts, convenient for pod fields exporting query
|
||||
# results or multi-template pod fields.
|
||||
rLayouts = {'view': Table('fl!', css_class='podTable')} # "r"ight
|
||||
# "r"ight "m"ulti-template (where the global field label is not used
|
||||
rmLayouts = {'view': Table('f!', css_class='podTable')}
|
||||
allFormats = {'.odt': ('pdf', 'doc', 'odt'), '.ods': ('xls', 'ods')}
|
||||
# Parameters needed to perform a query for query-related pods
|
||||
queryParams = ('className', 'search', 'sortKey', 'sortOrder',
|
||||
'filterKey', 'filterValue')
|
||||
|
||||
POD_ERROR = 'An error occurred while generating the document. Please ' \
|
||||
'contact the system administrator.'
|
||||
NO_TEMPLATE = 'Please specify a pod template in field "template".'
|
||||
UNAUTHORIZED = 'You are not allow to perform this action.'
|
||||
TEMPLATE_NOT_FOUND = 'Template not found at %s.'
|
||||
FREEZE_ERROR = 'Error while trying to freeze a "%s" file in pod field ' \
|
||||
'"%s" (%s).'
|
||||
FREEZE_FATAL_ERROR = 'Server error. Please contact the administrator.'
|
||||
|
||||
# Icon allowing to generate a given template in a given format
|
||||
pxIcon = Px('''
|
||||
<img var="iconSuffix=frozen and 'Frozen' or ''"
|
||||
src=":url(fmt + iconSuffix)" class="clickable"
|
||||
title=":field.getIconTitle(obj, fmt, frozen)"
|
||||
onclick=":'generatePod(this,%s,%s,%s,%s,%s,null,%s)' % (q(uid), \
|
||||
q(name), q(info.template), q(fmt), q(field.getQueryInfo(req)), \
|
||||
gc)"/>''')
|
||||
|
||||
pxView = pxCell = Px('''
|
||||
<x var="uid=obj.uid;
|
||||
gc=field.getChecked and q(field.getChecked) or 'null'"
|
||||
for="info in field.getVisibleTemplates(obj)"
|
||||
var2="mailings=field.getVisibleMailings(obj, info.template);
|
||||
lineBreak=((loop.info.nb + 1) % field.maxPerRow) == 0">
|
||||
<x for="fmt in info.formats"
|
||||
var2="freezeAllowed=(fmt in info.freezeFormats) and \
|
||||
(field.show != 'result');
|
||||
hasMailings=mailings and (fmt in mailings);
|
||||
dropdownEnabled=freezeAllowed or hasMailings;
|
||||
frozen=field.isFrozen(obj, info.template, fmt)">
|
||||
<!-- A clickable icon if no freeze action is allowed and no mailing is
|
||||
available for this format -->
|
||||
<x if="not dropdownEnabled">:field.pxIcon</x>
|
||||
<!-- A clickable icon and a dropdown menu else -->
|
||||
<span if="dropdownEnabled" class="dropdownMenu"
|
||||
var2="dropdownId='%s_%s' % (uid, \
|
||||
field.getFreezeName(info.template, fmt, sep='_'))"
|
||||
onmouseover=":'toggleDropdown(%s)' % q(dropdownId)"
|
||||
onmouseout=":'toggleDropdown(%s,%s)' % (q(dropdownId), q('none'))">
|
||||
<x>:field.pxIcon</x>
|
||||
<!-- The dropdown menu containing freeze actions -->
|
||||
<table id=":dropdownId" class="dropdown" width="110px">
|
||||
<!-- Unfreeze -->
|
||||
<tr if="freezeAllowed and frozen" valign="top">
|
||||
<td width="95px">
|
||||
<a onclick=":'freezePod(%s,%s,%s,%s,%s)' % (q(uid), q(name), \
|
||||
q(info.template), q(fmt), q('unfreeze'))"
|
||||
class="smaller">:_('unfreezeField')</a>
|
||||
</td>
|
||||
<td width="15px"><img src=":url('unfreeze')"/></td>
|
||||
</tr>
|
||||
<!-- (Re-)freeze -->
|
||||
<tr if="freezeAllowed" valign="top">
|
||||
<td width="85px">
|
||||
<a onclick=":'freezePod(%s,%s,%s,%s,%s)' % (q(uid), q(name), \
|
||||
q(info.template), q(fmt), q('freeze'))"
|
||||
class="smaller">:_('freezeField')</a>
|
||||
</td>
|
||||
<td width="15px"><img src=":url('freeze')"/></td>
|
||||
</tr>
|
||||
<!-- (Re-)upload -->
|
||||
<tr if="freezeAllowed" valign="top">
|
||||
<td width="85px">
|
||||
<a onclick=":'uploadPod(%s,%s,%s,%s)' % (q(uid), q(name), \
|
||||
q(info.template), q(fmt))"
|
||||
class="smaller">:_('uploadField')</a>
|
||||
</td>
|
||||
<td width="15px"><img src=":url('upload')"/></td>
|
||||
</tr>
|
||||
<!-- Mailing lists -->
|
||||
<x if="hasMailings" var2="sendLabel=_('email_send')">
|
||||
<tr for="mailing in mailings[fmt]" valign="top"
|
||||
var2="mailingName=field.getMailingName(obj, mailing)">
|
||||
<td colspan="2">
|
||||
<a var="js='generatePod(this,%s,%s,%s,%s,%s,null,%s,%s)' % \
|
||||
(q(uid), q(name), q(info.template), q(fmt), \
|
||||
q(field.getQueryInfo(req)), gc, q(mailing))"
|
||||
onclick=":'askConfirm(%s,%s)' % (q('script'), q(js, False))"
|
||||
title=":sendLabel">
|
||||
<img src=":url('email')" align="left" style="margin-right: 2px"/>
|
||||
<x>:mailingName</x></a>
|
||||
</td>
|
||||
</tr>
|
||||
</x>
|
||||
</table>
|
||||
</span>
|
||||
</x>
|
||||
<!-- Show the specific template name only if there is more than one
|
||||
template. For a single template, the field label already does the
|
||||
job. -->
|
||||
<span if="len(field.template) > 1"
|
||||
class=":(not loop.info.last and not lineBreak) and 'pod smaller' \
|
||||
or 'smaller'">:field.getTemplateName(obj, info.template)</span>
|
||||
<br if="lineBreak"/>
|
||||
</x>''')
|
||||
|
||||
pxEdit = pxSearch = ''
|
||||
|
||||
def __init__(self, validator=None, default=None, show=('view', 'result'),
|
||||
page='main', group=None, layouts=None, move=0,
|
||||
specificReadPermission=False, specificWritePermission=False,
|
||||
width=None, height=None, maxChars=None, colspan=1, master=None,
|
||||
masterValue=None, focus=False, historized=False, mapping=None,
|
||||
label=None, template=None, templateName=None,
|
||||
showTemplate=None, freezeTemplate=None, maxPerRow=5,
|
||||
context=None, stylesMapping={}, formats=None, getChecked=None,
|
||||
mailing=None, mailingName=None, showMailing=None,
|
||||
mailingInfo=None, view=None, xml=None, downloadName=None):
|
||||
# Param "template" stores the path to the pod template(s). If there is
|
||||
# a single template, a string is expected. Else, a list or tuple of
|
||||
# strings is expected. Every such path must be relative to your
|
||||
# application. A pod template name Test.odt that is stored at the root
|
||||
# of your app will be referred as "Test.odt" in self.template. If it is
|
||||
# stored within sub-folder "pod", it will be referred as "pod/Test.odt".
|
||||
if not template: raise Exception(Pod.NO_TEMPLATE)
|
||||
if isinstance(template, str):
|
||||
self.template = [template]
|
||||
elif isinstance(template, tuple):
|
||||
self.template = list(template)
|
||||
else:
|
||||
self.template = template
|
||||
# Param "templateName", if specified, is a method that will be called
|
||||
# with the current template (from self.template) as single arg and must
|
||||
# return the name of this template. If self.template stores a single
|
||||
# template, you have no need to use param "templateName". Simply use the
|
||||
# field label to name the template. But if you have a multi-pod field
|
||||
# (with several templates specified as a list or tuple in param
|
||||
# "template"), you will probably choose to hide the field label and use
|
||||
# param "templateName" to give a specific name to every template. If
|
||||
# "template" contains several templates and "templateName" is None, Appy
|
||||
# will produce names from template filenames.
|
||||
self.templateName = templateName
|
||||
# "showTemplate" determines if the current user may generate documents
|
||||
# based on this pod field. More precisely, "showTemplate", if specified,
|
||||
# must be a method that will be called with the current template as
|
||||
# single arg (one among self.template) and that must return the list or
|
||||
# tuple of formats that the current user may use as output formats for
|
||||
# generating a document. If the current user is not allowed at all to
|
||||
# generate documents based on the current template, "showTemplate" must
|
||||
# return an empty tuple/list. If "showTemplate" is not specified, the
|
||||
# user will be able to generate documents based on the current template,
|
||||
# in any format from self.formats (see below).
|
||||
# "showTemplate" comes in addition to self.show. self.show dictates the
|
||||
# visibility of the whole field (ie, all templates from self.template)
|
||||
# while "showTemplate" dictates the visiblity of a specific template
|
||||
# within self.template.
|
||||
self.showTemplate = showTemplate
|
||||
# "freezeTemplate" determines if the current user may freeze documents
|
||||
# normally generated dynamically from this pod field. More precisely,
|
||||
# "freezeTemplate", if specified, must be a method that will be called
|
||||
# with the current template as single arg and must return the (possibly
|
||||
# empty) list or tuple of formats the current user may freeze. The
|
||||
# "freezing-related actions" that are granted by "freezeTemplate" are
|
||||
# the following. When no document is frozen yet for a given
|
||||
# template/format, the user may:
|
||||
# - freeze the document: pod will be called to produce a document from
|
||||
# the current database content and will store it in the database.
|
||||
# Subsequent user requests for this pod field will return the frozen
|
||||
# doc instead of generating on-the-fly documents;
|
||||
# - upload a document: the user will be able to upload a document that
|
||||
# will be stored in the database. Subsequent user requests for this
|
||||
# pod field will return this doc instead of generating on-the-fly
|
||||
# documents.
|
||||
# When a document is already frozen or uploaded for a given
|
||||
# template/format, the user may:
|
||||
# - unfreeze the document: the frozen or uploaded document will be
|
||||
# deleted from the database and subsequent user requests for the pod
|
||||
# field will again generate on-the-fly documents;
|
||||
# - re-freeze the document: the frozen or uploaded document will be
|
||||
# deleted, a new document will be generated from the current database
|
||||
# content and will be frozen as a replacement to the deleted one;
|
||||
# - upload a document: the frozen or uploaded document will be replaced
|
||||
# by a new document uploaded by the current user.
|
||||
self.freezeTemplate = freezeTemplate
|
||||
# If p_template contains more than 1 template, "maxPerRow" tells how
|
||||
# much templates must appear side by side.
|
||||
self.maxPerRow = maxPerRow
|
||||
# The context is a dict containing a specific pod context, or a method
|
||||
# that returns such a dict.
|
||||
self.context = context
|
||||
# A global styles mapping that would apply to the whole template
|
||||
self.stylesMapping = stylesMapping
|
||||
# What are the output formats when generating documents from this pod ?
|
||||
self.formats = formats
|
||||
if not formats: # Compute default ones
|
||||
self.formats = self.getAllFormats(self.template[0])
|
||||
# Parameter "getChecked" can specify the name of a Ref field belonging
|
||||
# to the same gen class. If it is the case, the context of the pod
|
||||
# template will contain an additional object, name "_checked", and
|
||||
# "_checked.<name of the Ref field>" will contain the list of the
|
||||
# objects linked via the Ref field that are currently selected in the
|
||||
# user interface.
|
||||
self.getChecked = getChecked
|
||||
# Mailing lists can be defined for this pod field. For every visible
|
||||
# mailing list, a menu item will be available in the user interface and
|
||||
# will allow to send the pod result as attachment to the mailing list
|
||||
# recipients. Attribute p_mailing stores a mailing list's id
|
||||
# (as a string) or a list of ids.
|
||||
self.mailing = mailing
|
||||
if isinstance(mailing, basestring):
|
||||
self.mailing = [mailing]
|
||||
elif isinstance(mailing, tuple):
|
||||
self.mailing = list(mailing)
|
||||
# "mailingName" returns the name of the mailing as will be shown in the
|
||||
# user interface. It must be a method accepting the mailing list id
|
||||
# (from self.mailing) as single arg and returning the mailing list's
|
||||
# name.
|
||||
self.mailingName = mailingName
|
||||
# "showMailing" below determines when the mailing list(s) must be shown.
|
||||
# It may store a method accepting a mailing list's id (among
|
||||
# self.mailing) and a template (among self.template) and returning the
|
||||
# list or tuple of formats for which the pod result can be sent to the
|
||||
# mailing list. If no such method is defined, the mailing list will be
|
||||
# available for all visible templates and formats.
|
||||
self.showMailing = showMailing
|
||||
# When it it time to send an email, "mailingInfo" gives all the
|
||||
# necessary information for this email: recipients, subject, body. It
|
||||
# must be a method whose single arg is the mailing id (from
|
||||
# self.mailing) and that returns an instance of class Mailing (above).
|
||||
self.mailingInfo = mailingInfo
|
||||
# "downloadName", if specified, is a method that will be called with
|
||||
# the current template (from self.template) as single arg and must
|
||||
# return the name of the file as the user will get it once he will
|
||||
# download the pod result from its browser. This is for people that do
|
||||
# not like the default download name. Do not specify any extension: it
|
||||
# will be appended automatically. For example, if your method returns
|
||||
# "PodResultForSomeObject", and the pod result is a pdf file, the file
|
||||
# will be named "PodResultForSomeObject.pdf". If you specify such a
|
||||
# method, you have the responsibility to produce a valid,
|
||||
# any-OS-and-any-browser-proof file name. For inspiration, see the
|
||||
# default m_getDownloadName method hereafter. If you have several
|
||||
# templates in self.template, for some of them where you are satisfied
|
||||
# with the default download name, return None.
|
||||
self.downloadName = downloadName
|
||||
Field.__init__(self, None, (0,1), default, show, page, group, layouts,
|
||||
move, False, True, False, specificReadPermission,
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, historized, mapping, label,
|
||||
None, None, None, None, False, view, xml)
|
||||
# Param "persist" is False, but actual persistence for this field is
|
||||
# determined by freezing.
|
||||
self.validable = False
|
||||
|
||||
def getExtension(self, template):
|
||||
'''Gets a p_template's extension (".odt" or ".ods"). Because a template
|
||||
can simply be a pointer to another template (ie, "Item.odt.variant"),
|
||||
the logic for getting the extension is a bit more tricky.'''
|
||||
elems = os.path.splitext(template)
|
||||
if elems[1] in Pod.allFormats: return elems[1]
|
||||
# p_template must be a pointer to another template and has one more
|
||||
# extension.
|
||||
return os.path.splitext(elems[0])[1]
|
||||
|
||||
def getAllFormats(self, template):
|
||||
'''Gets all the output formats that are available for a given
|
||||
p_template.'''
|
||||
return Pod.allFormats[self.getExtension(template)]
|
||||
|
||||
def setTemplateFolder(self, folder):
|
||||
'''This methods adds a prefix to every template name in
|
||||
self.template. This can be useful if a plug-in module needs to
|
||||
replace an application template by its own templates. Here is an
|
||||
example: imagine a base application has a pod field with:
|
||||
|
||||
self.templates = ["Item.odt", "Decision.odt"]
|
||||
|
||||
The plug-in module, named "PlugInApp", wants to replace it with its
|
||||
own templates Item.odt, Decision.odt and Other.odt, stored in its
|
||||
sub-folder "pod". Suppose the base pod field is in <podField>. The
|
||||
plug-in will write:
|
||||
|
||||
<podField>.templates = ["Item.odt", "Decision.odt", "Other.odt"]
|
||||
<podField>.setTemplateFolder('../PlugInApp/pod')
|
||||
|
||||
The following code is equivalent, will work, but is precisely the
|
||||
kind of things we want to avoid.
|
||||
|
||||
<podField>.templates = ["../PlugInApp/pod/Item.odt",
|
||||
"../PlugInApp/pod/Decision.odt",
|
||||
"../PlugInApp/pod/Other.odt"]
|
||||
'''
|
||||
for i in range(len(self.template)):
|
||||
self.template[i] = os.path.join(folder, self.template[i])
|
||||
|
||||
def getTemplateName(self, obj, fileName):
|
||||
'''Gets the name of a template given its p_fileName.'''
|
||||
res = None
|
||||
if self.templateName:
|
||||
# Use the method specified in self.templateName
|
||||
res = self.templateName(obj, fileName)
|
||||
# Else, deduce a nice name from p_fileName
|
||||
if not res:
|
||||
name = os.path.splitext(os.path.basename(fileName))[0]
|
||||
res = gutils.produceNiceMessage(name)
|
||||
return res
|
||||
|
||||
def getTemplatePath(self, diskFolder, template):
|
||||
'''Return the absolute path to some pod p_template, by prefixing it with
|
||||
the application path. p_template can be a pointer to another
|
||||
template.'''
|
||||
res = sutils.resolvePath(os.path.join(diskFolder, template))
|
||||
if not os.path.isfile(res):
|
||||
raise Exception(self.TEMPLATE_NOT_FOUND % templatePath)
|
||||
# Unwrap the path if the file is simply a pointer to another one.
|
||||
elems = os.path.splitext(res)
|
||||
if elems[1] not in Pod.allFormats:
|
||||
res = self.getTemplatePath(diskFolder, elems[0])
|
||||
return res
|
||||
|
||||
def getDownloadName(self, obj, template, format, queryRelated):
|
||||
'''Gets the name of the pod result as will be seen by the user that will
|
||||
download it. Ensure the returned name is not too long for the OS that
|
||||
will store the downloaded file with this name.'''
|
||||
# Use method self.downloadName if present and if it returns something
|
||||
# for p_template
|
||||
if self.downloadName:
|
||||
name = self.downloadName(obj, template)
|
||||
if name: return '%s.%s' % (name, format)
|
||||
# Compute the default download name
|
||||
norm = obj.tool.normalize
|
||||
fileName = norm(self.getTemplateName(obj, template))[:100]
|
||||
if not queryRelated:
|
||||
# This is a POD for a single object: personalize the file name with
|
||||
# the object title.
|
||||
title = obj.o.getShownValue('title')
|
||||
fileName = '%s-%s' % (norm(title)[:140], fileName)
|
||||
return fileName + '.' + format
|
||||
|
||||
def getVisibleTemplates(self, obj):
|
||||
'''Returns, among self.template, the template(s) that can be shown.'''
|
||||
res = []
|
||||
if not self.showTemplate:
|
||||
# Show them all in the formats specified in self.formats.
|
||||
for template in self.template:
|
||||
res.append(Object(template=template, formats=self.formats,
|
||||
freezeFormats=self.getFreezeFormats(obj, template)))
|
||||
else:
|
||||
for template in self.template:
|
||||
formats = self.showTemplate(obj, template)
|
||||
if not formats: continue
|
||||
elif isinstance(formats, bool): formats = self.formats
|
||||
elif isinstance(formats, str): formats = (formats,)
|
||||
res.append(Object(template=template, formats=formats,
|
||||
freezeFormats=self.getFreezeFormats(obj, template)))
|
||||
return res
|
||||
|
||||
def getVisibleMailings(self, obj, template):
|
||||
'''Gets, among self.mailing, the mailing(s) that can be shown for
|
||||
p_template, as a dict ~{s_format:[s_id]}~.'''
|
||||
if not self.mailing: return
|
||||
res = {}
|
||||
for mailing in self.mailing:
|
||||
# Is this mailing visible ? In which format(s) ?
|
||||
if not self.showMailing:
|
||||
# By default, the mailing is available in any format
|
||||
formats = True
|
||||
else:
|
||||
formats = self.showMailing(obj, mailing, template)
|
||||
if not formats: continue
|
||||
if isinstance(formats, bool): formats = self.formats
|
||||
elif isinstance(formats, basestring): formats = (formats,)
|
||||
# Add this mailing to the result
|
||||
for fmt in formats:
|
||||
if fmt in res: res[fmt].append(mailing)
|
||||
else: res[fmt] = [mailing]
|
||||
return res
|
||||
|
||||
def getMailingName(self, obj, mailing):
|
||||
'''Gets the name of a particular p_mailing.'''
|
||||
res = None
|
||||
if self.mailingName:
|
||||
# Use the method specified in self.mailingName
|
||||
res = self.mailingName(obj, mailing)
|
||||
if not res:
|
||||
# Deduce a nice name from p_mailing
|
||||
res = gutils.produceNiceMessage(mailing)
|
||||
return res
|
||||
|
||||
def getMailingInfo(self, obj, template, mailing):
|
||||
'''Gets the necessary information for sending an email to
|
||||
p_mailing list.'''
|
||||
res = self.mailingInfo(obj, mailing)
|
||||
subject = res.subject
|
||||
if not subject:
|
||||
# Give a predefined subject
|
||||
mapping = {'site': obj.tool.o.getSiteUrl(),
|
||||
'title': obj.o.getShownValue('title'),
|
||||
'template': self.getTemplateName(obj, template)}
|
||||
subject = obj.translate('podmail_subject', mapping=mapping)
|
||||
body = res.body
|
||||
if not body:
|
||||
# Give a predefined body
|
||||
mapping = {'site': obj.tool.o.getSiteUrl()}
|
||||
body = obj.translate('podmail_body', mapping=mapping)
|
||||
return res.logins, subject, body
|
||||
|
||||
def sendMailing(self, obj, template, mailing, attachment):
|
||||
'''Sends the emails for m_mailing.'''
|
||||
logins, subject, body = self.getMailingInfo(obj, template, mailing)
|
||||
if not logins:
|
||||
obj.log('mailing %s contains no recipient.' % mailing)
|
||||
return 'action_ko'
|
||||
tool = obj.tool
|
||||
# Collect logins corresponding to inexistent users and recipients
|
||||
missing = []
|
||||
recipients = []
|
||||
for login in logins:
|
||||
user = tool.search1('User', noSecurity=True, login=login)
|
||||
if not user:
|
||||
missing.append(login)
|
||||
continue
|
||||
else:
|
||||
recipient = user.getMailRecipient()
|
||||
if not recipient:
|
||||
missing.append(login)
|
||||
else:
|
||||
recipients.append(recipient)
|
||||
if missing:
|
||||
obj.log('mailing %s: inexistent user or no email for %s.' % \
|
||||
(mailing, str(missing)))
|
||||
if not recipients:
|
||||
obj.log('mailing %s contains no recipient (after removing wrong ' \
|
||||
'entries, see above).' % mailing)
|
||||
msg = 'action_ko'
|
||||
else:
|
||||
tool.sendMail(recipients, subject, body, [attachment])
|
||||
msg = 'action_done'
|
||||
return msg
|
||||
|
||||
def getValue(self, obj, template=None, format=None, result=None,
|
||||
queryData=None, customContext=None, noSecurity=False):
|
||||
'''For a pod field, getting its value means computing a pod document or
|
||||
returning a frozen one. A pod field differs from other field types
|
||||
because there can be several ways to produce the field value (ie:
|
||||
self.template can hold various templates; output file format can be
|
||||
odt, pdf,.... We get those precisions about the way to produce the
|
||||
file, either from params, or from default values.
|
||||
* p_template is the specific template, among self.template, that must
|
||||
be used as base for generating the document;
|
||||
* p_format is the output format of the resulting document;
|
||||
* p_result, if given, must be the absolute path of the document that
|
||||
will be computed by pod. If not given, pod will produce a doc in
|
||||
the OS temp folder;
|
||||
* if the pod document is related to a query, the query parameters
|
||||
needed to re-trigger the query are given in p_queryData;
|
||||
* dict p_customContext may be specified and will override any other
|
||||
value available in the context, including values from the
|
||||
field-specific context.
|
||||
'''
|
||||
obj = obj.appy()
|
||||
template = template or self.template[0]
|
||||
format = format or 'odt'
|
||||
# Security check
|
||||
if not noSecurity and not queryData:
|
||||
if self.showTemplate and not self.showTemplate(obj, template):
|
||||
raise Exception(self.UNAUTHORIZED)
|
||||
# Return the possibly frozen document (not applicable for query-related
|
||||
# pods).
|
||||
if not queryData:
|
||||
frozen = self.isFrozen(obj, template, format)
|
||||
if frozen:
|
||||
fileName = self.getDownloadName(obj, template, format, False)
|
||||
return FileInfo(frozen, inDb=False, uploadName=fileName)
|
||||
# We must call pod to compute a pod document from "template"
|
||||
tool = obj.tool
|
||||
diskFolder = tool.getDiskFolder()
|
||||
# Get the path to the pod template.
|
||||
templatePath = self.getTemplatePath(diskFolder, template)
|
||||
# Get or compute the specific POD context
|
||||
specificContext = None
|
||||
if isinstance(self.context, collections.Callable):
|
||||
specificContext = self.callMethod(obj, self.context)
|
||||
else:
|
||||
specificContext = self.context
|
||||
# Compute the name of the result file.
|
||||
if not result:
|
||||
result = '%s/%s_%f.%s' % (sutils.getOsTempFolder(),
|
||||
obj.uid, time.time(), format)
|
||||
# Define parameters to give to the appy.pod renderer
|
||||
podContext = {'tool': tool, 'user': obj.user, 'self': obj, 'field':self,
|
||||
'now': obj.o.getProductConfig().DateTime(),
|
||||
'_': obj.translate, 'projectFolder': diskFolder,
|
||||
'template': template, 'request': tool.request}
|
||||
# If the pod document is related to a query, re-trigger it and put the
|
||||
# result in the pod context.
|
||||
if queryData:
|
||||
# Retrieve query params
|
||||
cmd = ', '.join(Pod.queryParams)
|
||||
cmd += " = queryData.split(';')"
|
||||
exec(cmd)
|
||||
# (re-)execute the query, but without any limit on the number of
|
||||
# results; return Appy objects.
|
||||
objs = tool.o.executeQuery(obj.o.portal_type, searchName=search,
|
||||
sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey,
|
||||
filterValue=filterValue, maxResults='NO_LIMIT')
|
||||
podContext['objects'] = [o.appy() for o in objs.objects]
|
||||
podContext['queryData'] = queryData.split(';')
|
||||
# Add the field-specific and custom contexts if present.
|
||||
if specificContext: podContext.update(specificContext)
|
||||
if customContext: podContext.update(customContext)
|
||||
# Variable "_checked" can be expected by a template but absent (ie,
|
||||
# when generating frozen documents).
|
||||
if '_checked' not in podContext: podContext['_checked'] = Object()
|
||||
# Define a potential global styles mapping
|
||||
if isinstance(self.stylesMapping, collections.Callable):
|
||||
stylesMapping = self.callMethod(obj, self.stylesMapping)
|
||||
else:
|
||||
stylesMapping = self.stylesMapping
|
||||
rendererParams = {'template': templatePath, 'context': podContext,
|
||||
'result': result, 'stylesMapping': stylesMapping,
|
||||
'imageResolver': tool.o.getApp(),
|
||||
'overwriteExisting': True}
|
||||
if tool.unoEnabledPython:
|
||||
rendererParams['pythonWithUnoPath'] = tool.unoEnabledPython
|
||||
if tool.openOfficePort:
|
||||
rendererParams['ooPort'] = tool.openOfficePort
|
||||
# Launch the renderer
|
||||
try:
|
||||
renderer = Renderer(**rendererParams)
|
||||
renderer.run()
|
||||
except PodError as pe:
|
||||
if not os.path.exists(result):
|
||||
# In some (most?) cases, when OO returns an error, the result is
|
||||
# nevertheless generated.
|
||||
obj.log(str(pe).strip(), type='error')
|
||||
return Pod.POD_ERROR
|
||||
# Give a friendly name for this file
|
||||
fileName = self.getDownloadName(obj, template, format, queryData)
|
||||
# Get a FileInfo instance to manipulate the file on the filesystem.
|
||||
return FileInfo(result, inDb=False, uploadName=fileName)
|
||||
|
||||
def getBaseName(self, template=None):
|
||||
'''Gets the "base name" of p_template (or self.template[0] if not
|
||||
given). The base name is the name of the template, without path
|
||||
and extension. Moreover, if the template is a pointer to another one
|
||||
(ie Item.odt.something), the base name integrates the specific
|
||||
extension. In the example, the base name will be "ItemSomething".'''
|
||||
template = template or self.template[0]
|
||||
elems = os.path.splitext(os.path.basename(template))
|
||||
if elems[1] in ('.odt', '.ods'):
|
||||
res = elems[0]
|
||||
else:
|
||||
res = os.path.splitext(elems[0])[0] + elems[1][1:].capitalize()
|
||||
return res
|
||||
|
||||
def getFreezeName(self, template=None, format='pdf', sep='.'):
|
||||
'''Gets the name on disk on the frozen document corresponding to this
|
||||
pod field, p_template and p_format.'''
|
||||
return '%s_%s%s%s' % (self.name,self.getBaseName(template),sep,format)
|
||||
|
||||
def isFrozen(self, obj, template=None, format='pdf'):
|
||||
'''Is there a frozen document for thid pod field, on p_obj, for
|
||||
p_template in p_format? If yes, it returns the absolute path to the
|
||||
frozen doc.'''
|
||||
template = template or self.template[0]
|
||||
dbFolder, folder = obj.o.getFsFolder()
|
||||
fileName = self.getFreezeName(template, format)
|
||||
res = os.path.join(dbFolder, folder, fileName)
|
||||
if os.path.exists(res): return res
|
||||
|
||||
def freeze(self, obj, template=None, format='pdf', noSecurity=True,
|
||||
upload=None, freezeOdtOnError=True):
|
||||
'''Freezes, on p_obj, a document for this pod field, for p_template in
|
||||
p_format. If p_noSecurity is True, the security check, based on
|
||||
self.freezeTemplate, is bypassed. If no p_upload file is specified,
|
||||
we re-compute a pod document on-the-fly and we freeze this document.
|
||||
Else, we store the uploaded file.
|
||||
|
||||
If p_freezeOdtOnError is True and format is not "odt" (has only sense
|
||||
when no p_upload file is specified), if the freezing fails we try to
|
||||
freeze the odt version, which is more robust because it does not
|
||||
require calling LibreOffice.'''
|
||||
# Security check
|
||||
if not noSecurity and \
|
||||
(format not in self.getFreezeFormats(obj, template)):
|
||||
raise Exception(self.UNAUTHORIZED)
|
||||
# Compute the absolute path where to store the frozen document in the
|
||||
# database.
|
||||
dbFolder, folder = obj.o.getFsFolder(create=True)
|
||||
fileName = self.getFreezeName(template, format)
|
||||
result = os.path.join(dbFolder, folder, fileName)
|
||||
if os.path.exists(result):
|
||||
prefix = upload and 'freeze (upload)' or 'freeze'
|
||||
obj.log('%s: overwriting %s...' % (prefix, result))
|
||||
if not upload:
|
||||
# Generate the document
|
||||
doc = self.getValue(obj, template=template, format=format,
|
||||
result=result)
|
||||
if isinstance(doc, str):
|
||||
# An error occurred, the document was not generated.
|
||||
obj.log(self.FREEZE_ERROR % (format, self.name, doc),
|
||||
type='error')
|
||||
if not freezeOdtOnError or (format == 'odt'):
|
||||
raise Exception(self.FREEZE_FATAL_ERROR)
|
||||
obj.log('freezing the ODT version...')
|
||||
# Freeze the ODT version of the document, which does not require
|
||||
# to call LibreOffice: the risk of error is smaller.
|
||||
fileName = self.getFreezeName(template, 'odt')
|
||||
result = os.path.join(dbFolder, folder, fileName)
|
||||
if os.path.exists(result):
|
||||
obj.log('freeze: overwriting %s...' % result)
|
||||
doc = self.getValue(obj, template=template, format='odt',
|
||||
result=result)
|
||||
if isinstance(doc, str):
|
||||
self.log(self.FREEZE_ERROR % ('odt', self.name, doc),
|
||||
type='error')
|
||||
raise Exception(self.FREEZE_FATAL_ERROR)
|
||||
obj.log('freezed at %s.' % result)
|
||||
else:
|
||||
# Store the uploaded file in the database
|
||||
f = file(result, 'wb')
|
||||
doc = FileInfo(result, inDb=False)
|
||||
doc.replicateFile(upload, f)
|
||||
f.close()
|
||||
return doc
|
||||
|
||||
def unfreeze(self, obj, template=None, format='pdf', noSecurity=True):
|
||||
'''Unfreezes, on p_obj, the document for this pod field, for p_template
|
||||
in p_format.'''
|
||||
# Security check.
|
||||
if not noSecurity and \
|
||||
(format not in self.getFreezeFormats(obj, template)):
|
||||
raise Exception(self.UNAUTHORIZED)
|
||||
# Compute the absolute path to the frozen doc.
|
||||
dbFolder, folder = obj.o.getFsFolder()
|
||||
fileName = self.getFreezeName(template, format)
|
||||
frozenName = os.path.join(dbFolder, folder, fileName)
|
||||
if os.path.exists(frozenName):
|
||||
os.remove(frozenName)
|
||||
obj.log('removed (unfrozen) %s.' % frozenName)
|
||||
|
||||
def getFreezeFormats(self, obj, template=None):
|
||||
'''What are the formats into which the current user may freeze
|
||||
p_template?'''
|
||||
# One may have the right to edit the field to freeze anything in it.
|
||||
if not obj.o.mayEdit(self.writePermission): return ()
|
||||
# Manager can perform all freeze actions.
|
||||
template = template or self.template[0]
|
||||
isManager = obj.user.has_role('Manager')
|
||||
if isManager: return self.getAllFormats(template)
|
||||
# Others users can perform freeze actions depending on
|
||||
# self.freezeTemplate.
|
||||
if not self.freezeTemplate: return ()
|
||||
return self.freezeTemplate(obj, template)
|
||||
|
||||
def getIconTitle(self, obj, format, frozen):
|
||||
'''Get the title of the format icon.'''
|
||||
res = obj.translate(format)
|
||||
if frozen:
|
||||
res += ' (%s)' % obj.translate('frozen')
|
||||
return res
|
||||
|
||||
def getCustomContext(self, obj, rq):
|
||||
'''Before calling pod to compute a result, if specific elements must be
|
||||
added to the context, compute it here. This request-dependent method
|
||||
is not called when computing a pod field for freezing it into the
|
||||
database.'''
|
||||
res = {}
|
||||
# Get potential custom params from the request. Custom params must be
|
||||
# coded as a string containing a valid Python dict.
|
||||
customParams = rq.get('customParams')
|
||||
if customParams:
|
||||
paramsDict = eval(customParams)
|
||||
res.update(paramsDict)
|
||||
# Compute the selected linked objects if self.getChecked is specified
|
||||
# and if the user can read this Ref field.
|
||||
if self.getChecked and \
|
||||
obj.allows(obj.getField(self.getChecked).readPermission):
|
||||
# Get the UIDs specified in the request
|
||||
reqUids = rq['checkedUids'] and rq['checkedUids'].split(',') or []
|
||||
unchecked = rq['checkedSem'] == 'unchecked'
|
||||
objects = []
|
||||
tool = obj.tool
|
||||
for uid in getattr(obj.o.aq_base, self.getChecked, ()):
|
||||
if unchecked: condition = uid not in reqUids
|
||||
else: condition = uid in reqUids
|
||||
if condition:
|
||||
tied = tool.getObject(uid)
|
||||
if tied.allows('read'): objects.append(tied)
|
||||
res['_checked'] = Object()
|
||||
setattr(res['_checked'], self.getChecked, objects)
|
||||
return res
|
||||
|
||||
def getQueryInfo(self, req):
|
||||
'''This method encodes in a string all the params in the request that
|
||||
are required for re-triggering a search.'''
|
||||
if not req.has_key('search'): return ''
|
||||
return ';'.join([req.get(key,'').replace(';','') \
|
||||
for key in Pod.queryParams])
|
||||
|
||||
def onUiRequest(self, obj, rq):
|
||||
'''This method is called when an action tied to this pod field
|
||||
(generate, freeze, upload...) is triggered from the user
|
||||
interface.'''
|
||||
# What is the action to perform ?
|
||||
action = rq.get('action', 'generate')
|
||||
# Security check
|
||||
obj.o.mayView(self.readPermission, raiseError=True)
|
||||
# Perform the requested action
|
||||
tool = obj.tool.o
|
||||
template = rq.get('template')
|
||||
format = rq.get('podFormat')
|
||||
if action == 'generate':
|
||||
# Generate a (or get a frozen) document
|
||||
res = self.getValue(obj, template=template, format=format,
|
||||
queryData=rq.get('queryData'),
|
||||
customContext=self.getCustomContext(obj, rq))
|
||||
if isinstance(res, str):
|
||||
# An error has occurred, and p_res contains the error message
|
||||
obj.say(res)
|
||||
return tool.goto(rq.get('HTTP_REFERER'))
|
||||
# res contains a FileInfo instance.
|
||||
# Must we return the res to the ui or send a mail with the res as
|
||||
# attachment?
|
||||
mailing = rq.get('mailing')
|
||||
if not mailing:
|
||||
# With disposition=inline, Google Chrome and IE may launch a PDF
|
||||
# viewer that triggers one or many additional crashing HTTP GET
|
||||
# requests.
|
||||
rq.RESPONSE.setCookie('podDownload', 'true', path='/')
|
||||
res.writeResponse(rq.RESPONSE, disposition='attachment')
|
||||
return
|
||||
else:
|
||||
# Send the email(s)
|
||||
msg = self.sendMailing(obj, template, mailing, res)
|
||||
obj.say(obj.translate(msg))
|
||||
return tool.goto(rq.get('HTTP_REFERER'))
|
||||
# Performing any other action requires write access to p_obj
|
||||
obj.o.mayEdit(self.writePermission, raiseError=True)
|
||||
msg = 'action_done'
|
||||
if action == 'freeze':
|
||||
# (Re-)freeze a document in the database
|
||||
self.freeze(obj, template, format, noSecurity=False,
|
||||
freezeOdtOnError=False)
|
||||
elif action == 'unfreeze':
|
||||
# Unfreeze a document in the database
|
||||
self.unfreeze(obj, template, format, noSecurity=False)
|
||||
elif action == 'upload':
|
||||
# Ensure a file from the correct type has been uploaded
|
||||
upload = rq.get('uploadedFile')
|
||||
if not upload or not upload.filename or \
|
||||
not upload.filename.endswith('.%s' % format):
|
||||
# A wrong file has been uploaded (or no file at all)
|
||||
msg = 'upload_invalid'
|
||||
else:
|
||||
# Store the uploaded file in the database
|
||||
self.freeze(obj, template, format, noSecurity=False,
|
||||
upload=upload)
|
||||
# Return a message to the user interface
|
||||
obj.say(obj.translate(msg))
|
||||
return tool.goto(rq.get('HTTP_REFERER'))
|
||||
# ------------------------------------------------------------------------------
|
1550
fields/ref.py
443
fields/search.py
|
@ -1,443 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy.px import Px
|
||||
from appy.gen import utils as gutils
|
||||
from appy.gen.indexer import defaultIndexes
|
||||
from appy.shared import utils as sutils
|
||||
from .group import Group
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Search:
|
||||
'''Used for specifying a search for a given class.'''
|
||||
|
||||
def __init__(self, name=None, group=None, sortBy='', sortOrder='asc',
|
||||
maxPerPage=30, default=False, colspan=1, translated=None,
|
||||
show=True, showActions=True, translatedDescr=None,
|
||||
checkboxes=False, checkboxesDefault=True, **fields):
|
||||
# "name" is mandatory, excepted in some special cases (ie, when used as
|
||||
# "select" param for a Ref field).
|
||||
self.name = name
|
||||
# Searches may be visually grouped in the portlet.
|
||||
self.group = Group.get(group)
|
||||
self.sortBy = sortBy
|
||||
self.sortOrder = sortOrder
|
||||
self.maxPerPage = maxPerPage
|
||||
# If this search is the default one, it will be triggered by clicking
|
||||
# on main link.
|
||||
self.default = default
|
||||
self.colspan = colspan
|
||||
# If a translated name or description is already given here, we will
|
||||
# use it instead of trying to translate from labels.
|
||||
self.translated = translated
|
||||
self.translatedDescr = translatedDescr
|
||||
# Condition for showing or not this search
|
||||
self.show = show
|
||||
# Condition for showing or not actions on every result of this search.
|
||||
# Can be: True, False or "inline". If True, actions will appear in a
|
||||
# "div" tag, below the object title; if "inline", they will appear
|
||||
# besides it, producing a more compact list of results.
|
||||
self.showActions = showActions
|
||||
# In the dict below, keys are indexed field names or names of standard
|
||||
# indexes, and values are search values.
|
||||
self.fields = fields
|
||||
# Do we need to display checkboxes for every object of the query result?
|
||||
self.checkboxes = checkboxes
|
||||
# Default value for checkboxes
|
||||
self.checkboxesDefault = checkboxesDefault
|
||||
|
||||
@staticmethod
|
||||
def getIndexName(name, klass, usage='search'):
|
||||
'''Gets the name of the Zope index that corresponds to p_name. Indexes
|
||||
can be used for searching (p_usage="search") or for sorting
|
||||
(usage="sort"). The method returns None if the field named
|
||||
p_name can't be used for p_usage.'''
|
||||
# Manage indexes that do not have a corresponding field
|
||||
if name == 'created': return 'Created'
|
||||
elif name == 'modified': return 'Modified'
|
||||
elif name in defaultIndexes: return name
|
||||
else:
|
||||
# Manage indexes corresponding to fields
|
||||
field = getattr(klass, name, None)
|
||||
if field: return field.getIndexName(usage)
|
||||
|
||||
@staticmethod
|
||||
def getSearchValue(fieldName, fieldValue, klass):
|
||||
'''Returns a transformed p_fieldValue for producing a valid search
|
||||
value as required for searching in the index corresponding to
|
||||
p_fieldName.'''
|
||||
field = getattr(klass, fieldName, None)
|
||||
if (field and (field.getIndexType() == 'TextIndex')) or \
|
||||
(fieldName == 'SearchableText'):
|
||||
# For TextIndex indexes. We must split p_fieldValue into keywords.
|
||||
res = gutils.Keywords(fieldValue).get()
|
||||
elif isinstance(fieldValue, str) and fieldValue.endswith('*'):
|
||||
v = fieldValue[:-1]
|
||||
# Warning: 'z' is higher than 'Z'!
|
||||
res = {'query':(v,v+'z'), 'range':'min:max'}
|
||||
elif type(fieldValue) in sutils.sequenceTypes:
|
||||
if fieldValue and isinstance(fieldValue[0], str):
|
||||
# We have a list of string values (ie: we need to
|
||||
# search v1 or v2 or...)
|
||||
res = fieldValue
|
||||
else:
|
||||
# We have a range of (int, float, DateTime...) values
|
||||
minv, maxv = fieldValue
|
||||
rangev = 'minmax'
|
||||
queryv = fieldValue
|
||||
if minv == None:
|
||||
rangev = 'max'
|
||||
queryv = maxv
|
||||
elif maxv == None:
|
||||
rangev = 'min'
|
||||
queryv = minv
|
||||
res = {'query':queryv, 'range':rangev}
|
||||
else:
|
||||
res = fieldValue
|
||||
return res
|
||||
|
||||
def updateSearchCriteria(self, criteria, klass, advanced=False):
|
||||
'''This method updates dict p_criteria with all the search criteria
|
||||
corresponding to this Search instance. If p_advanced is True,
|
||||
p_criteria correspond to an advanced search, to be stored in the
|
||||
session: in this case we need to keep the Appy names for parameters
|
||||
sortBy and sortOrder (and not "resolve" them to Zope's sort_on and
|
||||
sort_order).'''
|
||||
# Put search criteria in p_criteria
|
||||
for name, value in self.fields.items():
|
||||
# Management of searches restricted to objects linked through a
|
||||
# Ref field: not implemented yet.
|
||||
if name == '_ref': continue
|
||||
# Make the correspondence between the name of the field and the
|
||||
# name of the corresponding index, excepted if advanced is True: in
|
||||
# that case, the correspondence will be done later.
|
||||
if not advanced:
|
||||
indexName = Search.getIndexName(name, klass)
|
||||
# Express the field value in the way needed by the index
|
||||
criteria[indexName] = Search.getSearchValue(name, value, klass)
|
||||
else:
|
||||
criteria[name] = value
|
||||
# Add a sort order if specified
|
||||
if self.sortBy:
|
||||
c = criteria
|
||||
if not advanced:
|
||||
c['sort_on']=Search.getIndexName(self.sortBy,klass,usage='sort')
|
||||
c['sort_order']= (self.sortOrder=='desc') and 'reverse' or None
|
||||
else:
|
||||
c['sortBy'] = self.sortBy
|
||||
c['sortOrder'] = self.sortOrder
|
||||
|
||||
def isShowable(self, klass, tool):
|
||||
'''Is this Search instance (defined in p_klass) showable?'''
|
||||
if self.show.__class__.__name__ == 'staticmethod':
|
||||
return gutils.callMethod(tool, self.show, klass=klass)
|
||||
return self.show
|
||||
|
||||
def getSessionKey(self, className, full=True):
|
||||
'''Returns the name of the key, in the session, where results for this
|
||||
search are stored when relevant. If p_full is False, only the suffix
|
||||
of the session key is returned (ie, without the leading
|
||||
"search_").'''
|
||||
res = (self.name == 'allSearch') and className or self.name
|
||||
if not full: return res
|
||||
return 'search_%s' % res
|
||||
|
||||
class UiSearch:
|
||||
'''Instances of this class are generated on-the-fly for manipulating a
|
||||
Search from the User Interface.'''
|
||||
# Default values for request parameters defining query sort and filter
|
||||
sortFilterDefaults = {'sortKey': '', 'sortOrder': 'asc',
|
||||
'filterKey': '', 'filterValue': ''}
|
||||
pxByMode = {'list': 'pxResultList', 'grid': 'pxResultGrid'}
|
||||
|
||||
# Rendering a search
|
||||
pxView = Px('''
|
||||
<div class="portletSearch">
|
||||
<a href=":'%s?className=%s&search=%s' % \
|
||||
(queryUrl, className, search.name)"
|
||||
class=":(search.name == currentSearch) and 'current' or ''"
|
||||
onclick="clickOn(this)"
|
||||
title=":search.translatedDescr">:search.translated</a>
|
||||
</div>''')
|
||||
|
||||
# Search results, as a list (used by pxResult below)
|
||||
pxResultList = Px('''
|
||||
<table class="list" width="100%">
|
||||
<!-- Headers, with filters and sort arrows -->
|
||||
<tr if="showHeaders">
|
||||
<th if="checkboxes" class="cbCell" style=":'display:%s' % cbDisplay">
|
||||
<img src=":url('checkall')" class="clickable"
|
||||
title=":_('check_uncheck')"
|
||||
onclick=":'toggleAllCbs(%s)' % q(checkboxesId)"/>
|
||||
</th>
|
||||
<th for="column in columns"
|
||||
var2="field=column.field;
|
||||
sortable=field.isSortable(usage='search');
|
||||
filterable=field.filterable"
|
||||
width=":column.width" align=":column.align">
|
||||
<x>::ztool.truncateText(_(field.labelId))</x>
|
||||
<x if="(totalNumber > 1) or filterValue">:tool.pxSortAndFilter</x>
|
||||
<x>:tool.pxShowDetails</x>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<!-- Results -->
|
||||
<tr if="not zobjects">
|
||||
<td colspan=":len(columns)+1">:_('query_no_result')</td>
|
||||
</tr>
|
||||
<x for="zobj in zobjects"
|
||||
var2="rowCss=loop.zobj.odd and 'even' or 'odd';
|
||||
@currentNumber=currentNumber + 1">:zobj.appy().pxViewAsResult</x>
|
||||
</table>
|
||||
<!-- The button for selecting objects and closing the popup -->
|
||||
<div if="inPopup and cbShown" align=":dleft">
|
||||
<input type="button"
|
||||
var="label=_('object_link_many'); css=ztool.getButtonCss(label)"
|
||||
value=":label" class=":css" style=":url('linkMany', bg=True)"
|
||||
onclick=":'onSelectObjects(%s,%s,%s,%s,%s,%s,%s)' % \
|
||||
(q(rootHookId), q(uiSearch.initiator.url), \
|
||||
q(uiSearch.initiatorMode), q(sortKey), q(sortOrder), \
|
||||
q(filterKey), q(filterValue))"/>
|
||||
</div>
|
||||
<!-- Init checkboxes if present -->
|
||||
<script if="checkboxes">:'initCbs(%s)' % q(checkboxesId)</script>
|
||||
<script>:'initFocus(%s)' % q(ajaxHookId)</script>''')
|
||||
|
||||
# Search results, as a grid (used by pxResult below)
|
||||
pxResultGrid = Px('''
|
||||
<table width="100%"
|
||||
var="modeElems=resultMode.split('_');
|
||||
cols=(len(modeElems)==2) and int(modeElems[1]) or 4;
|
||||
rows=ztool.splitList(zobjects, cols)">
|
||||
<tr for="row in rows" valign="middle">
|
||||
<td for="zobj in row" width=":'%d%%' % (100/cols)" align="center"
|
||||
style="padding-top: 25px"
|
||||
var2="obj=zobj.appy(); mayView=zobj.mayView()">
|
||||
<x var="@currentNumber=currentNumber + 1"
|
||||
for="column in columns"
|
||||
var2="field=column.field">:field.pxRenderAsResult</x>
|
||||
</td>
|
||||
</tr>
|
||||
</table>''')
|
||||
|
||||
# Render search results
|
||||
pxResult = Px('''
|
||||
<div var="ajaxHookId='queryResult';
|
||||
className=className|req['className'];
|
||||
klass=ztool.getAppyClass(className);
|
||||
searchName=field.name|req.get('search', '');
|
||||
uiSearch=field|ztool.getSearch(className, searchName, ui=True);
|
||||
resultMode=uiSearch.getResultMode(klass, req);
|
||||
customPx=resultMode not in uiSearch.pxByMode;
|
||||
maxResults=customPx and 'NO_LIMIT' or None;
|
||||
rootHookId=uiSearch.getRootHookId();
|
||||
refInfo=ztool.getRefInfo();
|
||||
refObject=refInfo[0];
|
||||
refField=refInfo[1];
|
||||
refUrlPart=refObject and ('&ref=%s:%s' % (refObject.id, \
|
||||
refField)) or '';
|
||||
startNumber=req.get('startNumber', '0');
|
||||
startNumber=int(startNumber);
|
||||
sortKey=req.get('sortKey', '');
|
||||
sortOrder=req.get('sortOrder', 'asc');
|
||||
filterKey=req.get('filterKey', '');
|
||||
filterValue=req.get('filterValue', '');
|
||||
queryResult=ztool.executeQuery(className, \
|
||||
search=uiSearch.search, startNumber=startNumber, \
|
||||
maxResults=maxResults, remember=True, sortBy=sortKey, \
|
||||
sortOrder=sortOrder, filterKey=filterKey, \
|
||||
filterValue=filterValue, refObject=refObject, \
|
||||
refField=refField);
|
||||
zobjects=queryResult.objects;
|
||||
objects=maxResults and [z.appy() for z in zobjects];
|
||||
totalNumber=queryResult.totalNumber;
|
||||
batchSize=queryResult.batchSize;
|
||||
batchNumber=len(zobjects);
|
||||
showNewSearch=showNewSearch|True;
|
||||
newSearchUrl='%s/search?className=%s%s' % \
|
||||
(ztool.absolute_url(), className, refUrlPart);
|
||||
showSubTitles=req.get('showSubTitles', 'true') == 'true';
|
||||
target=ztool.getLinksTargetInfo(klass);
|
||||
showHeaders=showHeaders|True;
|
||||
checkboxes=uiSearch.search.checkboxes;
|
||||
checkboxesId=rootHookId + '_objs';
|
||||
cbShown=uiSearch.showCheckboxes();
|
||||
cbDisplay=cbShown and 'table-cell' or 'none'"
|
||||
id=":ajaxHookId">
|
||||
<script>:uiSearch.getAjaxData(ajaxHookId, ztool, popup=inPopup, \
|
||||
checkboxes=checkboxes, checkboxesId=checkboxesId, \
|
||||
cbDisplay=cbDisplay, startNumber=startNumber, \
|
||||
totalNumber=totalNumber)</script>
|
||||
|
||||
<x if="zobjects or filterValue"> <!-- Pod templates -->
|
||||
<table var="fields=ztool.getResultPodFields(className);
|
||||
layoutType='view'"
|
||||
if="not inPopup and zobjects and fields" align=":dright">
|
||||
<tr>
|
||||
<td var="zobj=zobjects[0]; obj=zobj.appy()"
|
||||
for="field in fields"
|
||||
class=":not loop.field.last and 'pod' or ''">:field.pxRender</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- The title of the search -->
|
||||
<p if="not inPopup">
|
||||
<x>::uiSearch.translated</x> (<span class="discreet">:totalNumber</span>)
|
||||
<x if="showNewSearch and (searchName == 'customSearch')"> —
|
||||
<i><a href=":newSearchUrl">:_('search_new')</a></i>
|
||||
</x>
|
||||
</p>
|
||||
<table width="100%">
|
||||
<tr valign="top">
|
||||
<!-- Search description -->
|
||||
<td if="uiSearch.translatedDescr">
|
||||
<span class="discreet">:uiSearch.translatedDescr</span><br/>
|
||||
</td>
|
||||
<!-- (Top) navigation -->
|
||||
<td if="not customPx"
|
||||
align=":dright" width="200px">:tool.pxNavigate</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Results -->
|
||||
<x var="columnLayouts=ztool.getResultColumnsLayouts(className, refInfo);
|
||||
columns=ztool.getColumnsSpecifiers(className,columnLayouts,dir);
|
||||
currentNumber=0"><x>:uiSearch.getPx(resultMode, klass)</x></x>
|
||||
|
||||
<!-- (Bottom) navigation -->
|
||||
<x if="not customPx">:tool.pxNavigate</x>
|
||||
</x>
|
||||
|
||||
<x if="not zobjects and not filterValue">
|
||||
<x>:_('query_no_result')</x>
|
||||
<x if="showNewSearch and (searchName == 'customSearch')"><br/>
|
||||
<i class="discreet"><a href=":newSearchUrl">:_('search_new')</a></i></x>
|
||||
</x>
|
||||
</div>''')
|
||||
|
||||
def __init__(self, search, className, tool):
|
||||
self.search = search
|
||||
self.name = search.name
|
||||
self.type = 'search'
|
||||
self.colspan = search.colspan
|
||||
self.className = className
|
||||
# Property "display" of the div tag containing actions for every search
|
||||
# result.
|
||||
self.showActions = search.showActions
|
||||
if search.showActions == True: self.showActions = 'block'
|
||||
if search.translated:
|
||||
self.translated = search.translated
|
||||
self.translatedDescr = search.translatedDescr
|
||||
else:
|
||||
# The label may be specific in some special cases
|
||||
labelDescr = ''
|
||||
if search.name == 'allSearch':
|
||||
label = '%s_plural' % className
|
||||
elif search.name == 'customSearch':
|
||||
label = 'search_results'
|
||||
elif search.name == '_field_':
|
||||
label = None
|
||||
else:
|
||||
label = '%s_search_%s' % (className, search.name)
|
||||
labelDescr = label + '_descr'
|
||||
_ = tool.translate
|
||||
self.translated = label and _(label) or ''
|
||||
self.translatedDescr = labelDescr and _(labelDescr) or ''
|
||||
|
||||
def setInitiator(self, initiator, field, mode):
|
||||
'''If the search is defined in an attribute Ref.select, we receive here
|
||||
the p_initiator object, its Ref p_field and the p_mode, that can be:
|
||||
- "repl" if the objects selected in the popup will replace already
|
||||
tied objects;
|
||||
- "add" if those objects will be added to the already tied ones.
|
||||
.'''
|
||||
self.initiator = initiator
|
||||
self.initiatorField = field
|
||||
self.initiatorMode = mode
|
||||
# "initiatorHook" is the ID of the initiator field's XHTML tag.
|
||||
self.initiatorHook = '%s_%s' % (initiator.uid, field.name)
|
||||
|
||||
def getRootHookId(self):
|
||||
'''If an initiator field is there, return the initiator hook.
|
||||
Else, simply return the name of the search.'''
|
||||
return getattr(self, 'initiatorHook', self.name)
|
||||
|
||||
def getAllResultModes(self, klass):
|
||||
'''How must we show the result? As a list, grid, or a custom px?'''
|
||||
return getattr(klass, 'resultModes', ('list',))
|
||||
|
||||
def getResultMode(self, klass, req):
|
||||
'''Get the current result mode'''
|
||||
res = req.get('resultMode')
|
||||
if not res: res = self.getAllResultModes(klass)[0]
|
||||
return res
|
||||
|
||||
def getPx(self, mode, klass):
|
||||
'''What is the PX to show, according to the current result p_mode?'''
|
||||
if mode in UiSearch.pxByMode:
|
||||
return getattr(UiSearch, UiSearch.pxByMode[mode])
|
||||
# It must be a custom PX on p_klass
|
||||
return getattr(klass, mode)
|
||||
|
||||
def showCheckboxes(self):
|
||||
'''If checkboxes are enabled for this search (and if an initiator field
|
||||
is there), they must be visible only if the initiator field is
|
||||
multivalued. Indeed, if it is not the case, it has no sense to select
|
||||
multiple objects. But in this case, we still want checkboxes to be in
|
||||
the DOM because they store object UIDs.'''
|
||||
if not self.search.checkboxes: return
|
||||
return not self.initiator or self.initiatorField.isMultiValued()
|
||||
|
||||
def getCbJsInit(self, hookId):
|
||||
'''Returns the code that creates JS data structures for storing the
|
||||
status of checkboxes for every result of this search.'''
|
||||
default = self.search.checkboxesDefault and 'unchecked' or 'checked'
|
||||
return '''var node=findNode(this, '%s');
|
||||
node['_appy_objs_cbs'] = {};
|
||||
node['_appy_objs_sem'] = '%s';''' % (hookId, default)
|
||||
|
||||
def getAjaxData(self, hook, ztool, **params):
|
||||
'''Initializes an AjaxData object on the DOM node corresponding to
|
||||
p_hook = the whole search result.'''
|
||||
# Complete params with default ones and optional filter/sort params. For
|
||||
# performing a complete Ajax request, "className" and "searcName" are
|
||||
# not needed because included in the PX name. But they are requested by
|
||||
# sub-Ajax queries at the row level.
|
||||
params['className'] = self.className
|
||||
params['searchName'] = params['search'] = self.name
|
||||
req = ztool.REQUEST
|
||||
for param, default in UiSearch.sortFilterDefaults.iteritems():
|
||||
params[param] = req.get(param, default)
|
||||
# Convert params into a JS dict
|
||||
params = sutils.getStringDict(params)
|
||||
px = '%s:%s:pxResult' % (self.className, self.name)
|
||||
return "new AjaxData('%s', '%s', %s, null, '%s')" % \
|
||||
(hook, px, params, ztool.absolute_url())
|
||||
|
||||
def getAjaxDataRow(self, zobj, parentHook, **params):
|
||||
'''Initializes an AjaxData object on the DOM node corresponding to
|
||||
p_hook = a row within the list of results.'''
|
||||
hook = zobj.id
|
||||
return "new AjaxData('%s', 'pxViewAsResultFromAjax', %s, '%s', '%s')"% \
|
||||
(hook, sutils.getStringDict(params), parentHook,
|
||||
zobj.absolute_url())
|
||||
|
||||
def getModeText(self, mode, _):
|
||||
'''Gets the i18n text corresponding to p_mode'''
|
||||
if mode in UiSearch.pxByMode: return _('result_mode_%s' % mode)
|
||||
return _('custom_%s' % mode)
|
||||
# ------------------------------------------------------------------------------
|
1070
fields/string.py
|
@ -1,577 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ------------------------------------------------------------------------------
|
||||
import types
|
||||
import .string
|
||||
from .group import Group
|
||||
from appy.px import Px
|
||||
from appy.gen.utils import User
|
||||
import collections
|
||||
|
||||
# Default Appy permissions -----------------------------------------------------
|
||||
r, w, d = ('read', 'write', 'delete')
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Role:
|
||||
'''Represents a role, be it local or global.'''
|
||||
appyRoles = ('Manager', 'Owner', 'Anonymous', 'Authenticated')
|
||||
appyLocalRoles = ('Owner',)
|
||||
appyUngrantableRoles = ('Anonymous', 'Authenticated')
|
||||
def __init__(self, name, local=False, grantable=True):
|
||||
self.name = name
|
||||
self.local = local # True if it can be used as local role only.
|
||||
# It is a standard Zope role or an application-specific one?
|
||||
self.appy = name in self.appyRoles
|
||||
if self.appy and (name in self.appyLocalRoles):
|
||||
self.local = True
|
||||
self.grantable = grantable
|
||||
if self.appy and (name in self.appyUngrantableRoles):
|
||||
self.grantable = False
|
||||
# An ungrantable role is one that is, like the Anonymous or
|
||||
# Authenticated roles, automatically attributed to a user.
|
||||
|
||||
def __repr__(self):
|
||||
loc = self.local and ' (local)' or ''
|
||||
return '<%s%s>' % (self.name, loc)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class State:
|
||||
'''Represents a workflow state.'''
|
||||
def __init__(self, permissions, initial=False, phase=None, show=True):
|
||||
self.usedRoles = {}
|
||||
# The following dict ~{s_permissionName:[s_roleName|Role_role]}~
|
||||
# gives, for every permission managed by a workflow, the list of roles
|
||||
# for which the permission is granted in this state. Standard
|
||||
# permissions are 'read', 'write' and 'delete'.
|
||||
self.permissions = permissions
|
||||
self.initial = initial
|
||||
self.phase = phase
|
||||
self.show = show
|
||||
# Standardize the way roles are expressed within self.permissions
|
||||
self.standardizeRoles()
|
||||
|
||||
def getName(self, wf):
|
||||
'''Returns the name for this state in workflow p_wf.'''
|
||||
for name in dir(wf):
|
||||
value = getattr(wf, name)
|
||||
if (value == self): return name
|
||||
|
||||
def getRole(self, role):
|
||||
'''p_role can be the name of a role or a Role instance. If it is the
|
||||
name of a role, this method returns self.usedRoles[role] if it
|
||||
exists, or creates a Role instance, puts it in self.usedRoles and
|
||||
returns it else. If it is a Role instance, the method stores it in
|
||||
self.usedRoles if it is not in it yet and returns it.'''
|
||||
if isinstance(role, str):
|
||||
if role in self.usedRoles:
|
||||
return self.usedRoles[role]
|
||||
else:
|
||||
theRole = Role(role)
|
||||
self.usedRoles[role] = theRole
|
||||
return theRole
|
||||
else:
|
||||
if role.name not in self.usedRoles:
|
||||
self.usedRoles[role.name] = role
|
||||
return role
|
||||
|
||||
def standardizeRoles(self):
|
||||
'''This method converts, within self.permissions, every role to a
|
||||
Role instance. Every used role is stored in self.usedRoles.'''
|
||||
for permission, roles in self.permissions.items():
|
||||
if not roles: continue # Nobody may have this permission
|
||||
if isinstance(roles, str) or isinstance(roles, Role):
|
||||
self.permissions[permission] = [self.getRole(roles)]
|
||||
elif isinstance(roles, list):
|
||||
for i in range(len(roles)): roles[i] = self.getRole(roles[i])
|
||||
else: # A tuple
|
||||
self.permissions[permission] = [self.getRole(r) for r in roles]
|
||||
|
||||
def getUsedRoles(self): return list(self.usedRoles.values())
|
||||
|
||||
def addRoles(self, roleNames, permissions=()):
|
||||
'''Adds p_roleNames in self.permissions. If p_permissions is specified,
|
||||
roles are added to those permissions only. Else, roles are added for
|
||||
every permission within self.permissions.'''
|
||||
if isinstance(roleNames, str): roleNames = (roleNames,)
|
||||
if isinstance(permissions, str): permissions = (permissions,)
|
||||
for perm, roles in self.permissions.items():
|
||||
if permissions and (perm not in permissions): continue
|
||||
for roleName in roleNames:
|
||||
# Do nothing if p_roleName is already almong roles.
|
||||
alreadyThere = False
|
||||
for role in roles:
|
||||
if role.name == roleName:
|
||||
alreadyThere = True
|
||||
break
|
||||
if alreadyThere: break
|
||||
# Add the role for this permission. Here, I think we don't mind
|
||||
# if the role is local but not noted as it in this Role
|
||||
# instance.
|
||||
roles.append(self.getRole(roleName))
|
||||
|
||||
def removeRoles(self, roleNames, permissions=()):
|
||||
'''Removes p_roleNames within dict self.permissions. If p_permissions is
|
||||
specified, removal is restricted to those permissions. Else, removal
|
||||
occurs throughout the whole dict self.permissions.'''
|
||||
if isinstance(roleNames, str): roleNames = (roleNames,)
|
||||
if isinstance(permissions, str): permissions = (permissions,)
|
||||
for perm, roles in self.permissions.items():
|
||||
if permissions and (perm not in permissions): continue
|
||||
for roleName in roleNames:
|
||||
# Remove this role if present in roles for this permission.
|
||||
for role in roles:
|
||||
if role.name == roleName:
|
||||
roles.remove(role)
|
||||
break
|
||||
|
||||
def setRoles(self, roleNames, permissions=()):
|
||||
'''Sets p_rolesNames for p_permissions if not empty, for every
|
||||
permission in self.permissions else.'''
|
||||
if isinstance(roleNames, str): roleNames = (roleNames,)
|
||||
if isinstance(permissions, str): permissions = (permissions,)
|
||||
for perm in self.permissions.keys():
|
||||
if permissions and (perm not in permissions): continue
|
||||
roles = self.permissions[perm] = []
|
||||
for roleName in roleNames:
|
||||
roles.append(self.getRole(roleName))
|
||||
|
||||
def replaceRole(self, oldRoleName, newRoleName, permissions=()):
|
||||
'''Replaces p_oldRoleName by p_newRoleName. If p_permissions is
|
||||
specified, the replacement is restricted to those permissions. Else,
|
||||
replacements apply to the whole dict self.permissions.'''
|
||||
if isinstance(permissions, str): permissions = (permissions,)
|
||||
for perm, roles in self.permissions.items():
|
||||
if permissions and (perm not in permissions): continue
|
||||
# Find and delete p_oldRoleName
|
||||
for role in roles:
|
||||
if role.name == oldRoleName:
|
||||
# Remove p_oldRoleName
|
||||
roles.remove(role)
|
||||
# Add p_newRoleName
|
||||
roles.append(self.getRole(newRoleName))
|
||||
break
|
||||
|
||||
def isIsolated(self, wf):
|
||||
'''Returns True if, from this state, we cannot reach another state. The
|
||||
workflow class is given in p_wf. Modifying a workflow for getting a
|
||||
state with auto-transitions only is a common technique for disabling
|
||||
a state in a workflow. Note that if this state is in a single-state
|
||||
worklflow, this method will always return True (I mean: in this case,
|
||||
having an isolated state does not mean the state has been
|
||||
deactivated).'''
|
||||
for tr in wf.__dict__.values():
|
||||
if not isinstance(tr, Transition): continue
|
||||
if not tr.hasState(self, True): continue
|
||||
# Transition "tr" has this state as start state. If the end state is
|
||||
# different from the start state, it means that the state is not
|
||||
# isolated.
|
||||
if tr.isSingle():
|
||||
if tr.states[1] != self: return
|
||||
else:
|
||||
for start, end in tr.states:
|
||||
# Bypass (start, end) pairs that have nothing to do with
|
||||
# self.
|
||||
if start != self: continue
|
||||
if end != self: return
|
||||
# If we are here, either there was no transition starting from self,
|
||||
# either all transitions were auto-transitions: self is then isolated.
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Transition:
|
||||
'''Represents a workflow transition.'''
|
||||
def __init__(self, states, condition=True, action=None, show=True,
|
||||
confirm=False, group=None, icon=None):
|
||||
# In its simpler form, "states" is a list of 2 states:
|
||||
# (fromState, toState). But it can also be a list of several
|
||||
# (fromState, toState) sub-lists. This way, you may define only 1
|
||||
# transition at several places in the state-transition diagram. It may
|
||||
# be useful for "undo" transitions, for example.
|
||||
self.states = self.standardiseStates(states)
|
||||
self.condition = condition
|
||||
if isinstance(condition, str):
|
||||
# The condition specifies the name of a role.
|
||||
self.condition = Role(condition)
|
||||
self.action = action
|
||||
self.show = show # If False, the end user will not be able to trigger
|
||||
# the transition. It will only be possible by code.
|
||||
self.confirm = confirm # If True, a confirm popup will show up.
|
||||
self.group = Group.get(group)
|
||||
# The user may specify a specific icon to show for this transition.
|
||||
self.icon = icon or 'transition'
|
||||
|
||||
def standardiseStates(self, states):
|
||||
'''Get p_states as a list or a list of lists. Indeed, the user may also
|
||||
specify p_states as a tuple or tuple of tuples. Having lists allows
|
||||
us to easily perform changes in states if required.'''
|
||||
if isinstance(states[0], State):
|
||||
if isinstance(states, tuple): return list(states)
|
||||
return states
|
||||
return [[start, end] for start, end in states]
|
||||
|
||||
def getName(self, wf):
|
||||
'''Returns the name for this state in workflow p_wf.'''
|
||||
for name in dir(wf):
|
||||
value = getattr(wf, name)
|
||||
if (value == self): return name
|
||||
|
||||
def getUsedRoles(self):
|
||||
'''self.condition can specify a role.'''
|
||||
res = []
|
||||
if isinstance(self.condition, Role):
|
||||
res.append(self.condition)
|
||||
return res
|
||||
|
||||
def isSingle(self):
|
||||
'''If this transition is only defined between 2 states, returns True.
|
||||
Else, returns False.'''
|
||||
return isinstance(self.states[0], State)
|
||||
|
||||
def _replaceStateIn(self, oldState, newState, states):
|
||||
'''Replace p_oldState by p_newState in p_states.'''
|
||||
if oldState not in states: return
|
||||
i = states.index(oldState)
|
||||
del states[i]
|
||||
states.insert(i, newState)
|
||||
|
||||
def replaceState(self, oldState, newState):
|
||||
'''Replace p_oldState by p_newState in self.states.'''
|
||||
if self.isSingle():
|
||||
self._replaceStateIn(oldState, newState, self.states)
|
||||
else:
|
||||
for i in range(len(self.states)):
|
||||
self._replaceStateIn(oldState, newState, self.states[i])
|
||||
|
||||
def removeState(self, state):
|
||||
'''For a multi-state transition, this method removes every state pair
|
||||
containing p_state.'''
|
||||
if self.isSingle(): raise Exception('To use for multi-transitions only')
|
||||
i = len(self.states) - 1
|
||||
while i >= 0:
|
||||
if state in self.states[i]:
|
||||
del self.states[i]
|
||||
i -= 1
|
||||
# This transition may become a single-state-pair transition.
|
||||
if len(self.states) == 1:
|
||||
self.states = self.states[0]
|
||||
|
||||
def setState(self, state):
|
||||
'''Configure this transition as being an auto-transition on p_state.
|
||||
This can be useful if, when changing a workflow, one wants to remove
|
||||
a state by isolating him from the rest of the state diagram and
|
||||
disable some transitions by making them auto-transitions of this
|
||||
disabled state.'''
|
||||
self.states = [state, state]
|
||||
|
||||
def isShowable(self, workflow, obj):
|
||||
'''Is this transition showable?'''
|
||||
if isinstance(self.show, collections.Callable):
|
||||
return self.show(workflow, obj.appy())
|
||||
else:
|
||||
return self.show
|
||||
|
||||
def hasState(self, state, isFrom):
|
||||
'''If p_isFrom is True, this method returns True if p_state is a
|
||||
starting state for p_self. If p_isFrom is False, this method returns
|
||||
True if p_state is an ending state for p_self.'''
|
||||
stateIndex = 1
|
||||
if isFrom:
|
||||
stateIndex = 0
|
||||
if self.isSingle():
|
||||
res = state == self.states[stateIndex]
|
||||
else:
|
||||
res = False
|
||||
for states in self.states:
|
||||
if states[stateIndex] == state:
|
||||
res = True
|
||||
break
|
||||
return res
|
||||
|
||||
def isTriggerable(self, obj, wf, noSecurity=False):
|
||||
'''Can this transition be triggered on p_obj?'''
|
||||
wf = wf.__instance__ # We need the prototypical instance here.
|
||||
# Checks that the current state of the object is a start state for this
|
||||
# transition.
|
||||
objState = obj.State(name=False)
|
||||
if self.isSingle():
|
||||
if objState != self.states[0]: return False
|
||||
else:
|
||||
startFound = False
|
||||
for startState, stopState in self.states:
|
||||
if startState == objState:
|
||||
startFound = True
|
||||
break
|
||||
if not startFound: return False
|
||||
# Check that the condition is met, excepted if noSecurity is True.
|
||||
if noSecurity: return True
|
||||
user = obj.getTool().getUser()
|
||||
if isinstance(self.condition, Role):
|
||||
# Condition is a role. Transition may be triggered if the user has
|
||||
# this role.
|
||||
return user.has_role(self.condition.name, obj)
|
||||
elif callable(self.condition):
|
||||
return self.condition(wf, obj.appy())
|
||||
elif type(self.condition) in (tuple, list):
|
||||
# It is a list of roles and/or functions. Transition may be
|
||||
# triggered if user has at least one of those roles and if all
|
||||
# functions return True.
|
||||
hasRole = None
|
||||
for condition in self.condition:
|
||||
# "Unwrap" role names from Role instances
|
||||
if isinstance(condition, Role): condition = condition.name
|
||||
if isinstance(condition, str): # It is a role
|
||||
if hasRole == None:
|
||||
hasRole = False
|
||||
if user.has_role(condition, obj):
|
||||
hasRole = True
|
||||
else: # It is a method
|
||||
res = condition(wf, obj.appy())
|
||||
if not res: return res # False or a No instance
|
||||
if hasRole != False:
|
||||
return True
|
||||
|
||||
def executeAction(self, obj, wf):
|
||||
'''Executes the action related to this transition.'''
|
||||
msg = ''
|
||||
obj = obj.appy()
|
||||
wf = wf.__instance__ # We need the prototypical instance here
|
||||
if type(self.action) in (tuple, list):
|
||||
# We need to execute a list of actions
|
||||
for act in self.action:
|
||||
msgPart = act(wf, obj)
|
||||
if msgPart: msg += msgPart
|
||||
else: # We execute a single action only
|
||||
msgPart = self.action(wf, obj)
|
||||
if msgPart: msg += msgPart
|
||||
return msg
|
||||
|
||||
def executeCommonAction(self, obj, name, wf, fromState):
|
||||
'''Executes the action that is common to any transition, named
|
||||
"onTrigger" on the workflow class by convention. The common action is
|
||||
executed before the transition-specific action (if any).'''
|
||||
obj = obj.appy()
|
||||
wf = wf.__instance__ # We need the prototypical instance here
|
||||
wf.onTrigger(obj, name, fromState)
|
||||
|
||||
def trigger(self, name, obj, wf, comment, doAction=True, doHistory=True,
|
||||
doSay=True, reindex=True, noSecurity=False):
|
||||
'''This method triggers this transition (named p_name) on p_obj. If
|
||||
p_doAction is False, the action that must normally be executed after
|
||||
the transition has been triggered will not be executed. If
|
||||
p_doHistory is False, there will be no trace from this transition
|
||||
triggering in the workflow history. If p_doSay is False, we consider
|
||||
the transition is triggered programmatically, and no message is
|
||||
returned to the user. If p_reindex is False, object reindexing will
|
||||
be performed by the calling method.'''
|
||||
# "Triggerability" and security checks
|
||||
if (name != '_init_') and \
|
||||
not self.isTriggerable(obj, wf, noSecurity=noSecurity):
|
||||
raise Exception('Transition "%s" can\'t be triggered.' % name)
|
||||
# Create the workflow_history dict if it does not exist
|
||||
if not hasattr(obj.aq_base, 'workflow_history'):
|
||||
from persistent.mapping import PersistentMapping
|
||||
obj.workflow_history = PersistentMapping()
|
||||
# Create the event list if it does not exist in the dict. The
|
||||
# overstructure (a dict with a key 'appy') is only there for historical
|
||||
# reasons and will change in Appy 1.0
|
||||
if not obj.workflow_history: obj.workflow_history['appy'] = ()
|
||||
# Identify the target state for this transition
|
||||
if self.isSingle():
|
||||
targetState = self.states[1]
|
||||
targetStateName = targetState.getName(wf)
|
||||
else:
|
||||
startState = obj.State(name=False)
|
||||
for sState, tState in self.states:
|
||||
if startState == sState:
|
||||
targetState = tState
|
||||
targetStateName = targetState.getName(wf)
|
||||
break
|
||||
# Create the event and add it in the object history
|
||||
action = name
|
||||
if name == '_init_':
|
||||
action = None
|
||||
fromState = None
|
||||
else:
|
||||
fromState = obj.State() # Remember the "from" (=start) state
|
||||
if not doHistory: comment = '_invisible_'
|
||||
obj.addHistoryEvent(action, review_state=targetStateName,
|
||||
comments=comment)
|
||||
# Execute the action that is common to all transitions, if defined
|
||||
if doAction and hasattr(wf, 'onTrigger'):
|
||||
self.executeCommonAction(obj, name, wf, fromState)
|
||||
# Execute the related action if needed
|
||||
msg = ''
|
||||
if doAction and self.action: msg = self.executeAction(obj, wf)
|
||||
# Reindex the object if required. Not only security-related indexes
|
||||
# (Allowed, State) need to be updated here.
|
||||
if reindex and not obj.isTemporary(): obj.reindex()
|
||||
# Return a message to the user if needed
|
||||
if not doSay: return
|
||||
if not msg: msg = obj.translate('object_saved')
|
||||
return msg
|
||||
|
||||
def onUiRequest(self, obj, wf, name, rq):
|
||||
'''Executed when a user wants to trigger this transition from the UI.'''
|
||||
tool = obj.getTool()
|
||||
# Trigger the transition
|
||||
msg = self.trigger(name, obj, wf, rq.get('popupComment', ''),
|
||||
reindex=False)
|
||||
# Reindex obj if required
|
||||
if not obj.isTemporary(): obj.reindex()
|
||||
# If we are called from an Ajax request, simply return msg
|
||||
if hasattr(rq, 'pxContext') and rq.pxContext['ajax']: return msg
|
||||
# If we are viewing the object and if the logged user looses the
|
||||
# permission to view it, redirect the user to its home page.
|
||||
if msg: obj.say(msg)
|
||||
if not obj.mayView() and \
|
||||
(obj.absolute_url_path() in rq['HTTP_REFERER']):
|
||||
back = tool.getHomePage()
|
||||
else:
|
||||
back = obj.getUrl(rq['HTTP_REFERER'])
|
||||
return tool.goto(back)
|
||||
|
||||
@staticmethod
|
||||
def getBack(workflow, transition):
|
||||
'''Returns the name of the transition (in p_workflow) that "cancels" the
|
||||
triggering of p_transition and allows to go back to p_transition's
|
||||
start state.'''
|
||||
# Get the end state(s) of p_transition
|
||||
transition = getattr(workflow, transition)
|
||||
# Browse all transitions and find the one starting at p_transition's end
|
||||
# state and coming back to p_transition's start state.
|
||||
for trName, tr in workflow.__dict__.items():
|
||||
if not isinstance(tr, Transition) or (tr == transition): continue
|
||||
if transition.isSingle():
|
||||
if tr.hasState(transition.states[1], True) and \
|
||||
tr.hasState(transition.states[0], False): return trName
|
||||
else:
|
||||
startOk = False
|
||||
endOk = False
|
||||
for start, end in transition.states:
|
||||
if (not startOk) and tr.hasState(end, True):
|
||||
startOk = True
|
||||
if (not endOk) and tr.hasState(start, False):
|
||||
endOk = True
|
||||
if startOk and endOk: return trName
|
||||
|
||||
class UiTransition:
|
||||
'''Represents a widget that displays a transition'''
|
||||
pxView = Px('''
|
||||
<x var="label=transition.title;
|
||||
inButtons=layoutType == 'buttons';
|
||||
css=ztool.getButtonCss(label, inButtons)">
|
||||
<!-- Real button -->
|
||||
<input if="transition.mayTrigger" type="button" class=":css"
|
||||
var="back=inButtons and q(zobj.id) or 'null'" id=":transition.name"
|
||||
style=":url(transition.icon, bg=True)" value=":label"
|
||||
onclick=":'triggerTransition(%s,this,%s,%s)' % \
|
||||
(q(formId), q(transition.confirm), back)"/>
|
||||
|
||||
<!-- Fake button, explaining why the transition can't be triggered -->
|
||||
<input if="not transition.mayTrigger" type="button"
|
||||
class=":'fake %s' % css" style=":url('fake', bg=True)"
|
||||
value=":label" title=":transition.reason"/></x>''')
|
||||
|
||||
def __init__(self, name, transition, obj, mayTrigger):
|
||||
self.name = name
|
||||
self.transition = transition
|
||||
self.type = 'transition'
|
||||
self.icon = transition.icon
|
||||
label = obj.getWorkflowLabel(name)
|
||||
self.title = obj.translate(label)
|
||||
if transition.confirm:
|
||||
self.confirm = obj.translate('%s_confirm' % label)
|
||||
else:
|
||||
self.confirm = ''
|
||||
# May this transition be triggered via the UI?
|
||||
self.mayTrigger = True
|
||||
self.reason = ''
|
||||
if not mayTrigger:
|
||||
self.mayTrigger = False
|
||||
self.reason = mayTrigger.msg
|
||||
# Required by the UiGroup
|
||||
self.colspan = 1
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Permission:
|
||||
'''If you need to define a specific read or write permission for some field
|
||||
on a gen-class, you use the specific boolean attrs
|
||||
"specificReadPermission" or "specificWritePermission". When you want to
|
||||
refer to those specific read or write permissions when
|
||||
defining a workflow, for example, you need to use instances of
|
||||
"ReadPermission" and "WritePermission", the 2 children classes of this
|
||||
class. For example, if you need to refer to write permission of
|
||||
attribute "t1" of class A, write: WritePermission("A.t1") or
|
||||
WritePermission("x.y.A.t1") if class A is not in the same module as
|
||||
where you instantiate the class.
|
||||
|
||||
Note that this holds only if you use attributes "specificReadPermission"
|
||||
and "specificWritePermission" as booleans. When defining named
|
||||
(string) permissions, for referring to it you simply use those strings,
|
||||
you do not create instances of ReadPermission or WritePermission.'''
|
||||
|
||||
def __init__(self, fieldDescriptor):
|
||||
self.fieldDescriptor = fieldDescriptor
|
||||
|
||||
def getName(self, wf, appName):
|
||||
'''Returns the name of this permission.'''
|
||||
className, fieldName = self.fieldDescriptor.rsplit('.', 1)
|
||||
if className.find('.') == -1:
|
||||
# The related class resides in the same module as the workflow
|
||||
fullClassName= '%s_%s' % (wf.__module__.replace('.', '_'),className)
|
||||
else:
|
||||
# className contains the full package name of the class
|
||||
fullClassName = className.replace('.', '_')
|
||||
# Read or Write ?
|
||||
if self.__class__.__name__ == 'ReadPermission': access = 'Read'
|
||||
else: access = 'Write'
|
||||
return '%s: %s %s %s' % (appName, access, fullClassName, fieldName)
|
||||
|
||||
class ReadPermission(Permission): pass
|
||||
class WritePermission(Permission): pass
|
||||
|
||||
# Standard workflows -----------------------------------------------------------
|
||||
class WorkflowAnonymous:
|
||||
'''One-state workflow allowing anyone to consult and Manager to edit.'''
|
||||
ma = 'Manager'
|
||||
o = 'Owner'
|
||||
everyone = (ma, 'Anonymous', 'Authenticated')
|
||||
active = State({r:everyone, w:(ma, o), d:(ma, o)}, initial=True)
|
||||
|
||||
class WorkflowAuthenticated:
|
||||
'''One-state workflow allowing authenticated users to consult and Manager
|
||||
to edit.'''
|
||||
ma = 'Manager'
|
||||
o = 'Owner'
|
||||
authenticated = (ma, 'Authenticated')
|
||||
active = State({r:authenticated, w:(ma, o), d:(ma, o)}, initial=True)
|
||||
|
||||
class WorkflowOwner:
|
||||
'''Workflow allowing only manager and owner to consult and edit.'''
|
||||
ma = 'Manager'
|
||||
o = 'Owner'
|
||||
# States
|
||||
active = State({r:(ma, o), w:(ma, o), d:ma}, initial=True)
|
||||
inactive = State({r:(ma, o), w:ma, d:ma})
|
||||
# Transitions
|
||||
def doDeactivate(self, obj):
|
||||
'''Prevent user "admin" from being deactivated.'''
|
||||
if isinstance(obj, User) and (obj.login == 'admin'):
|
||||
raise Exception('Cannot deactivate admin.')
|
||||
deactivate = Transition( (active, inactive), condition=ma,
|
||||
action=doDeactivate)
|
||||
reactivate = Transition( (inactive, active), condition=ma)
|
||||
# ------------------------------------------------------------------------------
|
122
gen/__init__.py
|
@ -1,122 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This file is part of Appy, a framework for building applications in the Python
|
||||
# language. Copyright (C) 2007 Gaetan Delannay
|
||||
|
||||
# Appy is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Import stuff from appy.fields (and from a few other places too).
|
||||
# This way, when an app gets "from appy.gen import *", everything is available.
|
||||
from appy import Object
|
||||
from appy.px import Px
|
||||
from appy.fields import Field
|
||||
from appy.fields.action import Action
|
||||
from appy.fields.boolean import Boolean
|
||||
from appy.fields.computed import Computed
|
||||
from appy.fields.date import Date
|
||||
from appy.fields.file import File
|
||||
from appy.fields.float import Float
|
||||
from appy.fields.info import Info
|
||||
from appy.fields.integer import Integer
|
||||
from appy.fields.list import List
|
||||
from appy.fields.dict import Dict
|
||||
from appy.fields.pod import Pod
|
||||
from appy.fields.ref import Ref, autoref
|
||||
from appy.fields.string import String, Selection
|
||||
from appy.fields.search import Search, UiSearch
|
||||
from appy.fields.group import Group, Column
|
||||
from appy.fields.page import Page
|
||||
from appy.fields.phase import Phase
|
||||
from appy.fields.workflow import *
|
||||
from appy.gen.layout import Table
|
||||
from appy.gen.utils import No, Tool, User
|
||||
|
||||
# Make the following classes available here: people may need to override some
|
||||
# of their PXs (defined as static attributes).
|
||||
from appy.gen.wrappers import AbstractWrapper as BaseObject
|
||||
from appy.gen.wrappers.ToolWrapper import ToolWrapper as BaseTool
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Config:
|
||||
'''If you want to specify some configuration parameters for appy.gen and
|
||||
your application, please create a class named "Config" in the __init__.py
|
||||
file of your application and override some of the attributes defined
|
||||
here, ie:
|
||||
|
||||
import appy.gen
|
||||
class Config(appy.gen.Config):
|
||||
langages = ('en', 'fr')
|
||||
'''
|
||||
# What skin to use for the web interface? Appy has 2 skins: the default
|
||||
# one (with a fixed width) and the "wide" skin (takes the whole page width).
|
||||
skin = None # None means: the default one. Could be "wide".
|
||||
# For every language code that you specify in this list, appy.gen will
|
||||
# produce and maintain translation files.
|
||||
languages = ['en']
|
||||
# If languageSelector is True, on every page, a language selector will
|
||||
# allow to switch between languages defined in self.languages. Else,
|
||||
# the browser-defined language will be used for choosing the language
|
||||
# of returned pages.
|
||||
languageSelector = False
|
||||
# Show the link to the user profile in the user strip
|
||||
userLink = True
|
||||
# People having one of these roles will be able to create instances
|
||||
# of classes defined in your application.
|
||||
defaultCreators = ['Manager']
|
||||
# The "root" classes are those that will get their menu in the user
|
||||
# interface. Put their names in the list below. If you leave the list empty,
|
||||
# all gen-classes will be considered root classes (the default). If
|
||||
# rootClasses is None, no class will be considered as root.
|
||||
rootClasses = []
|
||||
# Number of translations for every page on a Translation object
|
||||
translationsPerPage = 30
|
||||
# Language that will be used as a basis for translating to other
|
||||
# languages.
|
||||
sourceLanguage = 'en'
|
||||
# Activate or not the button on home page for asking a new password
|
||||
activateForgotPassword = True
|
||||
# Enable session timeout?
|
||||
enableSessionTimeout = False
|
||||
# If the following field is True, the login/password widget will be
|
||||
# discreet. This is for sites where authentication is not foreseen for
|
||||
# the majority of visitors (just for some administrators).
|
||||
discreetLogin = False
|
||||
# When using Ogone, place an instance of appy.gen.ogone.OgoneConfig in
|
||||
# the field below.
|
||||
ogone = None
|
||||
# When using Google analytics, specify here the Analytics ID
|
||||
googleAnalyticsId = None
|
||||
# Create a group for every global role?
|
||||
groupsForGlobalRoles = False
|
||||
# When using a LDAP for authenticating users, place an instance of class
|
||||
# appy.shared.ldap.LdapConfig in the field below.
|
||||
ldap = None
|
||||
# When using a SMTP mail server for sending emails from your app, place an
|
||||
# instance of class appy.gen.mail.MailConfig in the field below.
|
||||
mail = None
|
||||
# For an app, the default folder where to look for static content for the
|
||||
# user interface (CSS, Javascript and image files) is folder "ui" within
|
||||
# this app.
|
||||
uiFolders = ['ui']
|
||||
# CK editor configuration. Appy integrates CK editor via CDN (see
|
||||
# http://cdn.ckeditor.com). Do not change "ckVersion" hereafter, excepted
|
||||
# if you are sure that the customized configuration files config.js,
|
||||
# contents.css and styles.js stored in appy/gen/ui/ckeditor will be
|
||||
# compatible with the version you want to use.
|
||||
ckVersion = '4.4.7'
|
||||
# ckDistribution can be "basic", "standard", "standard-all", "full" or
|
||||
# "full-all" (see doc in http://cdn.ckeditor.com).
|
||||
ckDistribution = 'standard'
|
||||
# CK toolbars are not configurable yet. So toolbar "Appy", defined in
|
||||
# appy/gen/ui/ckeditor/config.js, will always be used.
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,534 +0,0 @@
|
|||
'''Descriptor classes defined in this file are "intermediary" classes that
|
||||
gather, from the user application, information about found gen- or workflow-
|
||||
classes.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import types, copy
|
||||
import appy.gen as gen
|
||||
from . import po
|
||||
from .model import ModelClass
|
||||
from .utils import produceNiceMessage, getClassName
|
||||
TABS = 4 # Number of blanks in a Python indentation.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Descriptor: # Abstract
|
||||
def __init__(self, klass, orderedAttributes, generator):
|
||||
# The corresponding Python class
|
||||
self.klass = klass
|
||||
# The names of the static appy-compliant attributes declared in
|
||||
# self.klass
|
||||
self.orderedAttributes = orderedAttributes
|
||||
# A reference to the code generator.
|
||||
self.generator = generator
|
||||
|
||||
def __repr__(self): return '<Class %s>' % self.klass.__name__
|
||||
|
||||
class ClassDescriptor(Descriptor):
|
||||
'''This class gives information about an Appy class.'''
|
||||
|
||||
def __init__(self, klass, orderedAttributes, generator):
|
||||
Descriptor.__init__(self, klass, orderedAttributes, generator)
|
||||
self.methods = '' # Needed method definitions will be generated here
|
||||
self.name = getClassName(self.klass, generator.applicationName)
|
||||
self.predefined = False
|
||||
self.customized = False
|
||||
# Phase and page names will be calculated later, when first required
|
||||
self.phases = None
|
||||
self.pages = None
|
||||
|
||||
def getOrderedAppyAttributes(self, condition=None):
|
||||
'''Returns the appy types for all attributes of this class and parent
|
||||
class(es). If a p_condition is specified, ony Appy types matching
|
||||
the condition will be returned. p_condition must be a string
|
||||
containing an expression that will be evaluated with, in its context,
|
||||
"self" being this ClassDescriptor and "attrValue" being the current
|
||||
Type instance.
|
||||
|
||||
Order of returned attributes already takes into account type's
|
||||
"move" attributes.'''
|
||||
attrs = []
|
||||
# First, get the attributes for the current class
|
||||
for attrName in self.orderedAttributes:
|
||||
try:
|
||||
attrValue = getattr(self.klass, attrName)
|
||||
hookClass = self.klass
|
||||
except AttributeError:
|
||||
attrValue = getattr(self.modelClass, attrName)
|
||||
hookClass = self.modelClass
|
||||
if isinstance(attrValue, gen.Field):
|
||||
if not condition or eval(condition):
|
||||
attrs.append( (attrName, attrValue, hookClass) )
|
||||
# Then, add attributes from parent classes
|
||||
for baseClass in self.klass.__bases__:
|
||||
# Find the classDescr that corresponds to baseClass
|
||||
baseClassDescr = None
|
||||
for classDescr in self.generator.classes:
|
||||
if classDescr.klass == baseClass:
|
||||
baseClassDescr = classDescr
|
||||
break
|
||||
if baseClassDescr:
|
||||
attrs = baseClassDescr.getOrderedAppyAttributes() + attrs
|
||||
# Modify attributes order by using "move" attributes
|
||||
res = []
|
||||
for name, appyType, klass in attrs:
|
||||
if appyType.move:
|
||||
newPosition = len(res) - abs(appyType.move)
|
||||
if newPosition <= 0:
|
||||
newPosition = 0
|
||||
res.insert(newPosition, (name, appyType, klass))
|
||||
else:
|
||||
res.append((name, appyType, klass))
|
||||
return res
|
||||
|
||||
def getChildren(self):
|
||||
'''Returns, among p_allClasses, the classes that inherit from p_self.'''
|
||||
res = []
|
||||
for classDescr in self.generator.classes:
|
||||
if (classDescr.klass != self.klass) and \
|
||||
issubclass(classDescr.klass, self.klass):
|
||||
res.append(classDescr)
|
||||
return res
|
||||
|
||||
def getPhases(self):
|
||||
'''Lazy-gets the phases defined on fields of this class.'''
|
||||
if not hasattr(self, 'phases') or (self.phases == None):
|
||||
self.phases = []
|
||||
for fieldName, appyType, klass in self.getOrderedAppyAttributes():
|
||||
if appyType.page.phase in self.phases: continue
|
||||
self.phases.append(appyType.page.phase)
|
||||
return self.phases
|
||||
|
||||
def getPages(self):
|
||||
'''Lazy-gets the page names defined on fields of this class.'''
|
||||
if not hasattr(self, 'pages') or (self.pages == None):
|
||||
self.pages = []
|
||||
for fieldName, appyType, klass in self.getOrderedAppyAttributes():
|
||||
if appyType.page.name in self.pages: continue
|
||||
self.pages.append(appyType.page.name)
|
||||
return self.pages
|
||||
|
||||
def getParents(self, allClasses):
|
||||
parentWrapper = 'AbstractWrapper'
|
||||
parentClass = '%s.%s' % (self.klass.__module__, self.klass.__name__)
|
||||
if self.klass.__bases__:
|
||||
baseClassName = self.klass.__bases__[0].__name__
|
||||
for k in allClasses:
|
||||
if self.klass.__name__ == baseClassName:
|
||||
parentWrapper = '%s_Wrapper' % k.name
|
||||
return (parentWrapper, parentClass)
|
||||
|
||||
def generateSchema(self):
|
||||
'''Generates i18n and other related stuff for this class.'''
|
||||
for attrName in self.orderedAttributes:
|
||||
try:
|
||||
attrValue = getattr(self.klass, attrName)
|
||||
except AttributeError:
|
||||
attrValue = getattr(self.modelClass, attrName)
|
||||
if not isinstance(attrValue, gen.Field): continue
|
||||
FieldDescriptor(attrName, attrValue, self).generate()
|
||||
|
||||
def isAbstract(self):
|
||||
'''Is self.klass abstract?'''
|
||||
res = False
|
||||
if 'abstract' in self.klass.__dict__:
|
||||
res = self.klass.__dict__['abstract']
|
||||
return res
|
||||
|
||||
def isRoot(self):
|
||||
'''Is self.klass root? A root class represents some kind of major
|
||||
concept into the application. For example, creating instances
|
||||
of such classes will be easy from the user interface.'''
|
||||
res = False
|
||||
if 'root' in self.klass.__dict__:
|
||||
res = self.klass.__dict__['root']
|
||||
return res
|
||||
|
||||
def isFolder(self, klass=None):
|
||||
'''Must self.klass be a folder? If klass is not None, this method tests
|
||||
it on p_klass instead of self.klass.'''
|
||||
res = False
|
||||
theClass = self.klass
|
||||
if klass:
|
||||
theClass = klass
|
||||
if 'folder' in theClass.__dict__:
|
||||
res = theClass.__dict__['folder']
|
||||
else:
|
||||
if theClass.__bases__:
|
||||
res = self.isFolder(theClass.__bases__[0])
|
||||
return res
|
||||
|
||||
def getCreators(self):
|
||||
'''Gets the specific creators defined for this class, excepted if
|
||||
attribute "creators" does not contain a list or roles.'''
|
||||
res = []
|
||||
if not hasattr(self.klass, 'creators'): return res
|
||||
if not isinstance(self.klass.creators, list): return res
|
||||
for creator in self.klass.creators:
|
||||
if isinstance(creator, gen.Role):
|
||||
if creator.local:
|
||||
raise 'Local role "%s" cannot be used as a creator.' % \
|
||||
creator.name
|
||||
res.append(creator)
|
||||
else:
|
||||
res.append(gen.Role(creator))
|
||||
return res
|
||||
|
||||
def getCreateMean(self, type='Import'):
|
||||
'''Returns the mean for this class that corresponds to p_type, or
|
||||
None if the class does not support this create mean.'''
|
||||
if 'create' not in self.klass.__dict__: return
|
||||
else:
|
||||
means = self.klass.create
|
||||
if not means: return
|
||||
if not isinstance(means, tuple) and not isinstance(means, list):
|
||||
means = [means]
|
||||
for mean in means:
|
||||
exec('found = isinstance(mean, %s)' % type)
|
||||
if found: return mean
|
||||
|
||||
@staticmethod
|
||||
def getSearches(klass, tool=None):
|
||||
'''Returns the list of searches that are defined on this class. If
|
||||
p_tool is given, we are at execution time (not a generation time),
|
||||
and we may potentially execute search.show methods that allow to
|
||||
conditionnaly include a search or not.'''
|
||||
if 'search' in klass.__dict__:
|
||||
searches = klass.__dict__['search']
|
||||
if not tool: return searches
|
||||
# Evaluate attributes "show" for every search.
|
||||
return [s for s in searches if s.isShowable(klass, tool)]
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def getSearch(klass, searchName):
|
||||
'''Gets the search named p_searchName.'''
|
||||
for search in ClassDescriptor.getSearches(klass):
|
||||
if search.name == searchName:
|
||||
return search
|
||||
|
||||
def addIndexMethod(self, field, secondary=False):
|
||||
'''For indexed p_field, this method generates a method that allows to
|
||||
get the value of the field as must be copied into the corresponding
|
||||
index. Some fields have a secondary index for sorting purposes. If
|
||||
p_secondary is True, this method generates the method for this
|
||||
secondary index.'''
|
||||
m = self.methods
|
||||
spaces = TABS
|
||||
n = field.fieldName
|
||||
suffix = secondary and '_sort' or ''
|
||||
m += '\n' + ' '*spaces + 'def get%s%s%s(self):\n' % \
|
||||
(n[0].upper(), n[1:], suffix)
|
||||
spaces += TABS
|
||||
m += ' '*spaces + "'''Gets indexable value of field \"%s\".'''\n" % n
|
||||
suffix = secondary and ', True' or ''
|
||||
m += ' '*spaces + 'return self.getAppyType("%s").getIndexValue(' \
|
||||
'self%s)\n' % (n, suffix)
|
||||
self.methods = m
|
||||
if not secondary and field.appyType.hasSortIndex():
|
||||
self.addIndexMethod(field, secondary=True)
|
||||
|
||||
def addField(self, fieldName, fieldType):
|
||||
'''Adds a new field to the Tool.'''
|
||||
exec("self.modelClass.%s = fieldType" % fieldName)
|
||||
if fieldName in self.modelClass._appy_attributes:
|
||||
print(('Warning, field "%s" is already existing on class "%s"' % \
|
||||
(fieldName, self.modelClass.__name__)))
|
||||
return
|
||||
self.modelClass._appy_attributes.append(fieldName)
|
||||
self.orderedAttributes.append(fieldName)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class WorkflowDescriptor(Descriptor):
|
||||
'''This class gives information about an Appy workflow.'''
|
||||
@staticmethod
|
||||
def getWorkflowName(klass):
|
||||
'''Returns the name of this workflow.'''
|
||||
res = klass.__module__.replace('.', '_') + '_' + klass.__name__
|
||||
return res.lower()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class FieldDescriptor:
|
||||
'''This class gathers information about a specific typed attribute defined
|
||||
in a gen-class.'''
|
||||
|
||||
def __init__(self, fieldName, appyType, classDescriptor):
|
||||
self.appyType = appyType
|
||||
self.classDescr = classDescriptor
|
||||
self.generator = classDescriptor.generator
|
||||
self.applicationName = classDescriptor.generator.applicationName
|
||||
self.fieldName = fieldName
|
||||
self.fieldParams = {'name': fieldName}
|
||||
self.widgetParams = {}
|
||||
self.fieldType = None
|
||||
self.widgetType = None
|
||||
|
||||
def i18n(self, id, default, nice=True):
|
||||
'''Shorthand for adding a new message into self.generator.labels.'''
|
||||
self.generator.labels.append(id, default, nice=nice)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Field %s, %s>' % (self.fieldName, self.classDescr)
|
||||
|
||||
def produceMessage(self, msgId, isLabel=True):
|
||||
'''Gets the default label, description or help (depending on p_msgType)
|
||||
for i18n message p_msgId.'''
|
||||
default = ' '
|
||||
niceDefault = False
|
||||
if isLabel:
|
||||
niceDefault = True
|
||||
default = self.fieldName
|
||||
return msgId, default, niceDefault
|
||||
|
||||
def walkString(self):
|
||||
'''Generates String-specific i18n labels.'''
|
||||
if self.appyType.isSelect and \
|
||||
(type(self.appyType.validator) in (list, tuple)):
|
||||
# Generate i18n messages for every possible value if the list
|
||||
# of values is fixed.
|
||||
for value in self.appyType.validator:
|
||||
label = '%s_%s_list_%s' % (self.classDescr.name,
|
||||
self.fieldName, value)
|
||||
self.i18n(label, value)
|
||||
|
||||
def walkBoolean(self):
|
||||
'''Generates Boolean-specific i18n labels.'''
|
||||
if self.appyType.render == 'radios':
|
||||
for v in ('true', 'false'):
|
||||
label = '%s_%s_%s' % (self.classDescr.name, self.fieldName, v)
|
||||
self.i18n(label, self.appyType.yesNo[v])
|
||||
|
||||
def walkAction(self):
|
||||
'''Generates Action-specific i18n labels.'''
|
||||
if self.appyType.confirm:
|
||||
label = '%s_%s_confirm' % (self.classDescr.name, self.fieldName)
|
||||
self.i18n(label, po.CONFIRM, nice=False)
|
||||
|
||||
def walkRef(self):
|
||||
'''How to generate a Ref?'''
|
||||
# Add the label for the confirm message if relevant
|
||||
if self.appyType.addConfirm:
|
||||
label = '%s_%s_addConfirm' % (self.classDescr.name, self.fieldName)
|
||||
self.i18n(label, po.CONFIRM, nice=False)
|
||||
|
||||
def walkList(self):
|
||||
# Add i18n-specific messages
|
||||
for name, field in self.appyType.fields:
|
||||
label = '%s_%s_%s' % (self.classDescr.name, self.fieldName, name)
|
||||
self.i18n(label, name)
|
||||
if field.hasDescr:
|
||||
self.i18n('%s_descr' % label, ' ')
|
||||
if field.hasHelp:
|
||||
self.i18n('%s_help' % label, ' ')
|
||||
walkDict = walkList # Same i18n labels for a dict
|
||||
|
||||
def walkCalendar(self):
|
||||
# Add i18n-specific messages
|
||||
eTypes = self.appyType.eventTypes
|
||||
if not isinstance(eTypes, list) and not isinstance(eTypes, tuple):return
|
||||
for et in self.appyType.eventTypes:
|
||||
label = '%s_%s_event_%s' % (self.classDescr.name,self.fieldName,et)
|
||||
self.i18n(label, et)
|
||||
|
||||
def walkAppyType(self):
|
||||
'''Walks into the Appy type definition and gathers data about the
|
||||
i18n labels.'''
|
||||
# Manage things common to all Appy types
|
||||
# Put an index on this field?
|
||||
if self.appyType.indexed and (self.fieldName != 'title'):
|
||||
self.classDescr.addIndexMethod(self)
|
||||
# i18n labels
|
||||
if not self.appyType.label:
|
||||
# Create labels for generating them in i18n files, only if required.
|
||||
i18nPrefix = '%s_%s' % (self.classDescr.name, self.fieldName)
|
||||
if self.appyType.hasLabel:
|
||||
self.i18n(*self.produceMessage(i18nPrefix))
|
||||
if self.appyType.hasDescr:
|
||||
descrId = i18nPrefix + '_descr'
|
||||
self.i18n(*self.produceMessage(descrId,isLabel=False))
|
||||
if self.appyType.hasHelp:
|
||||
helpId = i18nPrefix + '_help'
|
||||
self.i18n(*self.produceMessage(helpId, isLabel=False))
|
||||
# Create i18n messages linked to pages and phases, only if there is more
|
||||
# than one page/phase for the class.
|
||||
if len(self.classDescr.getPhases()) > 1:
|
||||
# Create the message for the name of the phase
|
||||
phaseName = self.appyType.page.phase
|
||||
msgId = '%s_phase_%s' % (self.classDescr.name, phaseName)
|
||||
self.i18n(msgId, phaseName)
|
||||
if len(self.classDescr.getPages()) > 1:
|
||||
# Create the message for the name of the page
|
||||
pageName = self.appyType.page.name
|
||||
msgId = '%s_page_%s' % (self.classDescr.name, pageName)
|
||||
self.i18n(msgId, pageName)
|
||||
# Create i18n messages linked to groups
|
||||
group = self.appyType.group
|
||||
if group and not group.label:
|
||||
group.generateLabels(self.generator.labels, self.classDescr, set())
|
||||
# Generate type-specific i18n labels
|
||||
if not self.appyType.label:
|
||||
method = 'walk%s' % self.appyType.type
|
||||
if hasattr(self, method): getattr(self, method)()
|
||||
|
||||
def generate(self):
|
||||
'''Generates the i18n labels for this type.'''
|
||||
self.walkAppyType()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ToolClassDescriptor(ClassDescriptor):
|
||||
'''Represents the POD-specific fields that must be added to the tool.'''
|
||||
def __init__(self, klass, generator):
|
||||
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
|
||||
self.modelClass = self.klass
|
||||
self.predefined = True
|
||||
self.customized = False
|
||||
|
||||
def getParents(self, allClasses=()):
|
||||
res = ['Tool']
|
||||
if self.customized:
|
||||
res.append('%s.%s' % (self.klass.__module__, self.klass.__name__))
|
||||
return res
|
||||
|
||||
def update(self, klass, attributes):
|
||||
'''This method is called by the generator when he finds a custom tool
|
||||
definition. We must then add the custom tool elements in this default
|
||||
Tool descriptor.'''
|
||||
self.orderedAttributes += attributes
|
||||
self.klass = klass
|
||||
self.customized = True
|
||||
|
||||
def isFolder(self, klass=None): return True
|
||||
def isRoot(self): return False
|
||||
|
||||
def addImportRelatedFields(self, classDescr):
|
||||
'''Adds, for class p_classDescr, attributes related to the import
|
||||
functionality for class p_classDescr.'''
|
||||
className = classDescr.name
|
||||
# Field that defines the path of the files to import.
|
||||
fieldName = 'importPathFor%s' % className
|
||||
defValue = classDescr.getCreateMean('Import').path
|
||||
fieldType = gen.String(page='data', multiplicity=(1,1),
|
||||
default=defValue,group=classDescr.klass.__name__)
|
||||
self.addField(fieldName, fieldType)
|
||||
|
||||
class UserClassDescriptor(ClassDescriptor):
|
||||
'''Appy-specific class for representing a user.'''
|
||||
def __init__(self, klass, generator):
|
||||
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
|
||||
self.modelClass = self.klass
|
||||
self.predefined = True
|
||||
self.customized = False
|
||||
def getParents(self, allClasses=()):
|
||||
res = ['User']
|
||||
if self.customized:
|
||||
res.append('%s.%s' % (self.klass.__module__, self.klass.__name__))
|
||||
return res
|
||||
def update(self, klass, attributes):
|
||||
'''This method is called by the generator when he finds a custom user
|
||||
definition. We must then add the custom user elements in this
|
||||
default User descriptor.'''
|
||||
self.orderedAttributes += attributes
|
||||
self.klass = klass
|
||||
self.customized = True
|
||||
def isFolder(self, klass=None): return False
|
||||
|
||||
class GroupClassDescriptor(ClassDescriptor):
|
||||
'''Represents the class that corresponds to the Group for the generated
|
||||
application.'''
|
||||
def __init__(self, klass, generator):
|
||||
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
|
||||
self.modelClass = self.klass
|
||||
self.predefined = True
|
||||
self.customized = False
|
||||
def getParents(self, allClasses=()):
|
||||
res = ['Group']
|
||||
if self.customized:
|
||||
res.append('%s.%s' % (self.klass.__module__, self.klass.__name__))
|
||||
return res
|
||||
def update(self, klass, attributes):
|
||||
'''This method is called by the generator when he finds a custom group
|
||||
definition. We must then add the custom group elements in this
|
||||
default Group descriptor.
|
||||
|
||||
NOTE: currently, it is not possible to define a custom Group
|
||||
class.'''
|
||||
self.orderedAttributes += attributes
|
||||
self.klass = klass
|
||||
self.customized = True
|
||||
def isFolder(self, klass=None): return False
|
||||
|
||||
class TranslationClassDescriptor(ClassDescriptor):
|
||||
'''Represents the set of translation ids for a gen-application.'''
|
||||
|
||||
def __init__(self, klass, generator):
|
||||
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
|
||||
self.modelClass = self.klass
|
||||
self.predefined = True
|
||||
self.customized = False
|
||||
|
||||
def getParents(self, allClasses=()): return ('Translation',)
|
||||
def isFolder(self, klass=None): return False
|
||||
|
||||
def addLabelField(self, messageId, page):
|
||||
'''Adds a Computed field that will display, in the source language, the
|
||||
content of the text to translate.'''
|
||||
field = gen.Computed(method=self.modelClass.label, plainText=False,
|
||||
page=page, show=self.modelClass.show, layouts='f')
|
||||
self.addField('%s_label' % messageId, field)
|
||||
|
||||
def addMessageField(self, messageId, page, i18nFiles):
|
||||
'''Adds a message field corresponding to p_messageId to the Translation
|
||||
class, on a given p_page. We need i18n files p_i18nFiles for
|
||||
fine-tuning the String type to generate for this field (one-line?
|
||||
several lines?...)'''
|
||||
params = {'page':page, 'layouts':'f', 'show': self.modelClass.show}
|
||||
appName = self.generator.applicationName
|
||||
# Scan all messages corresponding to p_messageId from all translation
|
||||
# files. We will define field length from the longer found message
|
||||
# content.
|
||||
maxLine = 100 # We suppose a line is 100 characters long.
|
||||
width = 0
|
||||
height = 0
|
||||
for fileName, poFile in i18nFiles.items():
|
||||
if not fileName.startswith('%s-' % appName) or \
|
||||
messageId not in i18nFiles[fileName].messagesDict:
|
||||
# In this case this is not one of our Appy-managed translation
|
||||
# files.
|
||||
continue
|
||||
msgContent = i18nFiles[fileName].messagesDict[messageId].msg
|
||||
# Compute width
|
||||
width = max(width, len(msgContent))
|
||||
# Compute height (a "\n" counts for one line)
|
||||
mHeight = int(len(msgContent)/maxLine) + msgContent.count('<br/>')
|
||||
height = max(height, mHeight)
|
||||
if height < 1:
|
||||
# This is a one-line field
|
||||
params['width'] = width
|
||||
else:
|
||||
# This is a multi-line field, or a very-long-single-lined field
|
||||
params['format'] = gen.String.TEXT
|
||||
params['height'] = height
|
||||
self.addField(messageId, gen.String(**params))
|
||||
|
||||
class PageClassDescriptor(ClassDescriptor):
|
||||
'''Represents the class that corresponds to a Page.'''
|
||||
def __init__(self, klass, generator):
|
||||
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
|
||||
self.modelClass = self.klass
|
||||
self.predefined = True
|
||||
self.customized = False
|
||||
def getParents(self, allClasses=()):
|
||||
res = ['Page']
|
||||
if self.customized:
|
||||
res.append('%s.%s' % (self.klass.__module__, self.klass.__name__))
|
||||
return res
|
||||
def update(self, klass, attributes):
|
||||
'''This method is called by the generator when he finds a custom page
|
||||
definition. We must then add the custom page elements in this
|
||||
default Page descriptor.
|
||||
|
||||
NOTE: currently, it is not possible to define a custom Page class.'''
|
||||
self.orderedAttributes += attributes
|
||||
self.klass = klass
|
||||
self.customized = True
|
||||
def isFolder(self, klass=None): return True
|
||||
# ------------------------------------------------------------------------------
|
792
gen/generator.py
|
@ -1,792 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, re, sys, parser, symbol, token, types
|
||||
import appy, appy.pod.renderer
|
||||
from appy.shared.utils import FolderDeleter
|
||||
import appy.gen as gen
|
||||
from . import po
|
||||
from .descriptors import *
|
||||
from .utils import getClassName
|
||||
from .model import ModelClass, User, Group, Tool, Translation, Page
|
||||
import collections
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class GeneratorError(Exception): pass
|
||||
|
||||
# I need the following classes to parse Python classes and find in which
|
||||
# order the attributes are defined. --------------------------------------------
|
||||
class AstMatcher:
|
||||
'''Allows to find a given pattern within an ast (part).'''
|
||||
def _match(pattern, node):
|
||||
res = None
|
||||
if pattern[0] == node[0]:
|
||||
# This level matches
|
||||
if len(pattern) == 1:
|
||||
return node
|
||||
else:
|
||||
if type(node[1]) == tuple:
|
||||
return AstMatcher._match(pattern[1:], node[1])
|
||||
return res
|
||||
_match = staticmethod(_match)
|
||||
def match(pattern, node):
|
||||
res = []
|
||||
for subNode in node[1:]:
|
||||
# Do I find the pattern among the subnodes ?
|
||||
occurrence = AstMatcher._match(pattern, subNode)
|
||||
if occurrence:
|
||||
res.append(occurrence)
|
||||
return res
|
||||
match = staticmethod(match)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class AstClass:
|
||||
'''Python class.'''
|
||||
def __init__(self, node):
|
||||
# Link to the Python ast node
|
||||
self.node = node
|
||||
self.name = node[2][1]
|
||||
self.attributes = [] # We are only interested in parsing static
|
||||
# attributes to now their order
|
||||
if sys.version_info[:2] >= (2,5):
|
||||
self.statementPattern = (
|
||||
symbol.stmt, symbol.simple_stmt, symbol.small_stmt,
|
||||
symbol.expr_stmt, symbol.testlist, symbol.test, symbol.or_test,
|
||||
symbol.and_test, symbol.not_test, symbol.comparison, symbol.expr,
|
||||
symbol.xor_expr, symbol.and_expr, symbol.shift_expr,
|
||||
symbol.arith_expr, symbol.term, symbol.factor, symbol.power)
|
||||
else:
|
||||
self.statementPattern = (
|
||||
symbol.stmt, symbol.simple_stmt, symbol.small_stmt,
|
||||
symbol.expr_stmt, symbol.testlist, symbol.test, symbol.and_test,
|
||||
symbol.not_test, symbol.comparison, symbol.expr, symbol.xor_expr,
|
||||
symbol.and_expr, symbol.shift_expr, symbol.arith_expr,
|
||||
symbol.term, symbol.factor, symbol.power)
|
||||
for subNode in node[1:]:
|
||||
if subNode[0] == symbol.suite:
|
||||
# We are in the class body
|
||||
self.getStaticAttributes(subNode)
|
||||
|
||||
def getStaticAttributes(self, classBody):
|
||||
statements = AstMatcher.match(self.statementPattern, classBody)
|
||||
for statement in statements:
|
||||
if len(statement) == 2 and statement[1][0] == symbol.atom and \
|
||||
statement[1][1][0] == token.NAME:
|
||||
attrName = statement[1][1][1]
|
||||
self.attributes.append(attrName)
|
||||
|
||||
def __repr__(self):
|
||||
return '<class %s has attrs %s>' % (self.name, str(self.attributes))
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Ast:
|
||||
'''Python AST.'''
|
||||
classPattern = (symbol.stmt, symbol.compound_stmt, symbol.classdef)
|
||||
utf8prologue = '# -*- coding: utf-8 -*-'
|
||||
def __init__(self, pyFile):
|
||||
f = file(pyFile)
|
||||
fContent = f.read()
|
||||
f.close()
|
||||
# For some unknown reason, when an UTF-8 encoding is declared, parsing
|
||||
# does not work.
|
||||
if fContent.startswith(self.utf8prologue):
|
||||
fContent = fContent[len(self.utf8prologue):]
|
||||
fContent = fContent.replace('\r', '')
|
||||
ast = parser.suite(fContent).totuple()
|
||||
# Get all the classes defined within this module.
|
||||
self.classes = {}
|
||||
classNodes = AstMatcher.match(self.classPattern, ast)
|
||||
for node in classNodes:
|
||||
astClass = AstClass(node)
|
||||
self.classes[astClass.name] = astClass
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
CODE_HEADER = '''# -*- coding: utf-8 -*-
|
||||
#
|
||||
# GNU General Public License (GPL)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
'''
|
||||
class Generator:
|
||||
'''Abstract base class for building a generator.'''
|
||||
def __init__(self, application, options):
|
||||
self.application = application
|
||||
# Determine application name
|
||||
self.applicationName = os.path.basename(application)
|
||||
# Determine output folder (where to store the generated product)
|
||||
self.outputFolder = os.path.join(application, 'zope',
|
||||
self.applicationName)
|
||||
self.options = options
|
||||
# Determine templates folder
|
||||
genFolder = os.path.dirname(__file__)
|
||||
self.templatesFolder = os.path.join(genFolder, 'templates')
|
||||
# Default descriptor classes
|
||||
self.descriptorClasses = {
|
||||
'class': ClassDescriptor, 'tool': ClassDescriptor,
|
||||
'user': ClassDescriptor, 'workflow': WorkflowDescriptor}
|
||||
# The following dict contains a series of replacements that need to be
|
||||
# applied to file templates to generate files.
|
||||
self.repls = {'applicationName': self.applicationName,
|
||||
'applicationPath': os.path.dirname(self.application),
|
||||
'codeHeader': CODE_HEADER}
|
||||
# List of Appy classes and workflows found in the application
|
||||
self.classes = []
|
||||
self.tool = None
|
||||
self.user = None
|
||||
self.workflows = []
|
||||
self.initialize()
|
||||
self.config = gen.Config
|
||||
self.modulesWithTests = set()
|
||||
self.totalNumberOfTests = 0
|
||||
|
||||
def determineGenType(self, klass):
|
||||
'''If p_klass is:
|
||||
* a gen-class, this method returns "class";
|
||||
* a gen-workflow, this method it "workflow";
|
||||
* none of it, this method returns None.
|
||||
|
||||
If p_klass declares at least one static attribute that is a
|
||||
appy.fields.Field, it will be considered a gen-class. If p_klass
|
||||
declares at least one static attribute that is a appy.gen.State,
|
||||
it will be considered a gen-workflow.'''
|
||||
for attr in klass.__dict__.values():
|
||||
if isinstance(attr, gen.Field): return 'class'
|
||||
elif isinstance(attr, gen.State): return 'workflow'
|
||||
|
||||
def containsTests(self, moduleOrClass):
|
||||
'''Returns True if p_moduleOrClass contains doctests. This method also
|
||||
counts tests and updates self.totalNumberOfTests.'''
|
||||
res = False
|
||||
docString = moduleOrClass.__doc__
|
||||
if docString and (docString.find('>>>') != -1):
|
||||
self.totalNumberOfTests += 1
|
||||
res = True
|
||||
# Count also docstring in methods
|
||||
if type(moduleOrClass) == type:
|
||||
for name, elem in moduleOrClass.__dict__.items():
|
||||
if type(elem) in (staticmethod, classmethod):
|
||||
elem = elem.__get__(name)
|
||||
if isinstance(elem, collections.Callable) and (type(elem) != type) and \
|
||||
hasattr(elem, '__doc__') and elem.__doc__ and \
|
||||
(elem.__doc__.find('>>>') != -1):
|
||||
res = True
|
||||
self.totalNumberOfTests += 1
|
||||
return res
|
||||
|
||||
def walkModule(self, moduleName, module):
|
||||
'''Visits a given module of the application.'''
|
||||
# Create the AST for this module. Producing an AST allows us to retrieve
|
||||
# class attributes in the order of their definition, which is not
|
||||
# possible by introspecting dict-based class objects.
|
||||
moduleFile = module.__file__
|
||||
if moduleFile.endswith('.pyc'):
|
||||
moduleFile = moduleFile[:-1]
|
||||
astClasses = Ast(moduleFile).classes
|
||||
# Check if tests are present in this module
|
||||
if self.containsTests(module):
|
||||
self.modulesWithTests.add(module.__name__)
|
||||
classType = type(Generator)
|
||||
# Find all classes in this module
|
||||
for name in list(module.__dict__.keys()):
|
||||
exec('moduleElem = module.%s' % name)
|
||||
# Ignore non-classes module elements or classes that were imported
|
||||
# from other modules.
|
||||
if (type(moduleElem) != classType) or \
|
||||
(moduleElem.__module__ != module.__name__): continue
|
||||
# Ignore classes that are not gen-classes or gen-workflows.
|
||||
genType = self.determineGenType(moduleElem)
|
||||
if not genType: continue
|
||||
# Produce a list of static class attributes (in the order
|
||||
# of their definition).
|
||||
attrs = astClasses[moduleElem.__name__].attributes
|
||||
# Collect non-parsable attrs = back references added
|
||||
# programmatically
|
||||
moreAttrs = []
|
||||
for eName, eValue in moduleElem.__dict__.items():
|
||||
if isinstance(eValue, gen.Field) and (eName not in attrs):
|
||||
moreAttrs.append(eName)
|
||||
# Sort them in alphabetical order: else, order would be random
|
||||
moreAttrs.sort()
|
||||
if moreAttrs: attrs += moreAttrs
|
||||
# Add attributes added as back references
|
||||
if genType == 'class':
|
||||
# Determine the class type (standard, tool, user...)
|
||||
if issubclass(moduleElem, gen.Tool):
|
||||
if not self.tool:
|
||||
klass = self.descriptorClasses['tool']
|
||||
self.tool = klass(moduleElem, attrs, self)
|
||||
else:
|
||||
self.tool.update(moduleElem, attrs)
|
||||
elif issubclass(moduleElem, gen.User):
|
||||
if not self.user:
|
||||
klass = self.descriptorClasses['user']
|
||||
self.user = klass(moduleElem, attrs, self)
|
||||
else:
|
||||
self.user.update(moduleElem, attrs)
|
||||
else:
|
||||
descriptorClass = self.descriptorClasses['class']
|
||||
descriptor = descriptorClass(moduleElem,attrs, self)
|
||||
self.classes.append(descriptor)
|
||||
# Manage classes containing tests
|
||||
if self.containsTests(moduleElem):
|
||||
self.modulesWithTests.add(module.__name__)
|
||||
elif genType == 'workflow':
|
||||
descriptorClass = self.descriptorClasses['workflow']
|
||||
descriptor = descriptorClass(moduleElem, attrs, self)
|
||||
self.workflows.append(descriptor)
|
||||
if self.containsTests(moduleElem):
|
||||
self.modulesWithTests.add(module.__name__)
|
||||
|
||||
def walkApplication(self):
|
||||
'''This method walks into the application and creates the corresponding
|
||||
meta-classes in self.classes, self.workflows, etc.'''
|
||||
# Where is the application located ?
|
||||
containingFolder = os.path.dirname(self.application)
|
||||
sys.path.append(containingFolder)
|
||||
# What is the name of the application ?
|
||||
appName = os.path.basename(self.application)
|
||||
# Get the app-specific config if any
|
||||
exec('import %s as appModule' % appName)
|
||||
if hasattr (appModule, 'Config'):
|
||||
self.config = appModule.Config
|
||||
if not issubclass(self.config, gen.Config):
|
||||
raise Exception('Your Config class must subclass ' \
|
||||
'appy.gen.Config.')
|
||||
# Collect modules (only a the first level) in this application. Import
|
||||
# them all, to be sure class definitions are complete (ie, back
|
||||
# references are set from one class to the other). Moreover, potential
|
||||
# syntax or import errors will raise an exception and abort the
|
||||
# generation process before we do any undoable action.
|
||||
modules = []
|
||||
for fileName in os.listdir(self.application):
|
||||
# Ignore non Python files
|
||||
if not fileName.endswith('.py'): continue
|
||||
moduleName = '%s.%s' % (appName, os.path.splitext(fileName)[0])
|
||||
exec('import %s' % moduleName)
|
||||
modules.append(eval(moduleName))
|
||||
# Parse imported modules
|
||||
for module in modules:
|
||||
self.walkModule(moduleName, module)
|
||||
sys.path.pop()
|
||||
|
||||
def generateClass(self, classDescr):
|
||||
'''This method is called whenever a Python class declaring Appy type
|
||||
definition(s) is encountered within the application.'''
|
||||
|
||||
def generateWorkflow(self, workflowDescr):
|
||||
'''This method is called whenever a Python class declaring states and
|
||||
transitions is encountered within the application.'''
|
||||
|
||||
def initialize(self):
|
||||
'''Called before the old product is removed (if any), in __init__.'''
|
||||
|
||||
def finalize(self):
|
||||
'''Called at the end of the generation process.'''
|
||||
|
||||
def copyFile(self, fileName, replacements, destName=None, destFolder=None,
|
||||
isPod=False):
|
||||
'''This method will copy p_fileName from self.templatesFolder to
|
||||
self.outputFolder (or in a subFolder if p_destFolder is given)
|
||||
after having replaced all p_replacements. If p_isPod is True,
|
||||
p_fileName is a POD template and the copied file is the result of
|
||||
applying p_fileName with context p_replacements.'''
|
||||
# Get the path of the template file to copy
|
||||
templatePath = os.path.join(self.templatesFolder, fileName)
|
||||
# Get (or create if needed) the path of the result file
|
||||
destFile = fileName
|
||||
if destName: destFile = destName
|
||||
if destFolder: destFile = '%s/%s' % (destFolder, destFile)
|
||||
absDestFolder = self.outputFolder
|
||||
if destFolder:
|
||||
absDestFolder = os.path.join(self.outputFolder, destFolder)
|
||||
if not os.path.exists(absDestFolder):
|
||||
os.makedirs(absDestFolder)
|
||||
resultPath = os.path.join(self.outputFolder, destFile)
|
||||
if os.path.exists(resultPath): os.remove(resultPath)
|
||||
if not isPod:
|
||||
# Copy the template file to result file after having performed some
|
||||
# replacements
|
||||
f = file(templatePath)
|
||||
fileContent = f.read()
|
||||
f.close()
|
||||
if not fileName.endswith('.png'):
|
||||
for rKey, rValue in replacements.items():
|
||||
fileContent = fileContent.replace(
|
||||
'<!%s!>' % rKey, str(rValue))
|
||||
f = file(resultPath, 'w')
|
||||
f.write(fileContent)
|
||||
f.close()
|
||||
else:
|
||||
# Call the POD renderer to produce the result
|
||||
rendererParams = {'template': templatePath,
|
||||
'context': replacements,
|
||||
'result': resultPath}
|
||||
renderer = appy.pod.renderer.Renderer(**rendererParams)
|
||||
renderer.run()
|
||||
|
||||
def run(self):
|
||||
self.walkApplication()
|
||||
for descriptor in self.classes: self.generateClass(descriptor)
|
||||
for descriptor in self.workflows: self.generateWorkflow(descriptor)
|
||||
self.finalize()
|
||||
msg = ''
|
||||
if self.totalNumberOfTests:
|
||||
msg = ' (number of tests found: %d)' % self.totalNumberOfTests
|
||||
print(('Done%s.' % msg))
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ZopeGenerator(Generator):
|
||||
'''This generator generates a Zope-compliant product from a given Appy
|
||||
application.'''
|
||||
poExtensions = ('.po', '.pot')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Tool._appy_clean()
|
||||
Generator.__init__(self, *args, **kwargs)
|
||||
# Set our own Descriptor classes
|
||||
self.descriptorClasses['class'] = ClassDescriptor
|
||||
# Create Tool, User, Group, Translation and Page instances.
|
||||
self.tool = ToolClassDescriptor(Tool, self)
|
||||
self.user = UserClassDescriptor(User, self)
|
||||
self.group = GroupClassDescriptor(Group, self)
|
||||
self.translation = TranslationClassDescriptor(Translation, self)
|
||||
self.page = PageClassDescriptor(Page, self)
|
||||
# i18n labels to generate
|
||||
self.labels = po.PoMessages()
|
||||
|
||||
def i18n(self, id, default, nice=True):
|
||||
'''Shorthand for adding a new message into self.labels.'''
|
||||
self.labels.append(id, default, nice=nice)
|
||||
|
||||
versionRex = re.compile('(.*?\s+build)\s+(\d+)')
|
||||
def initialize(self):
|
||||
# Determine version number
|
||||
self.version = '0.1.0 build 1'
|
||||
versionTxt = os.path.join(self.outputFolder, 'version.txt')
|
||||
if os.path.exists(versionTxt):
|
||||
f = file(versionTxt)
|
||||
oldVersion = f.read().strip()
|
||||
f.close()
|
||||
res = self.versionRex.search(oldVersion)
|
||||
self.version = res.group(1) + ' ' + str(int(res.group(2))+1)
|
||||
# Existing i18n files
|
||||
self.i18nFiles = {} #~{p_fileName: PoFile}~
|
||||
# Retrieve existing i18n files if any
|
||||
i18nFolder = os.path.join(self.application, 'tr')
|
||||
if os.path.exists(i18nFolder):
|
||||
for fileName in os.listdir(i18nFolder):
|
||||
name, ext = os.path.splitext(fileName)
|
||||
if ext in self.poExtensions:
|
||||
poParser = po.PoParser(os.path.join(i18nFolder, fileName))
|
||||
self.i18nFiles[fileName] = poParser.parse()
|
||||
|
||||
def finalize(self):
|
||||
# Add a label for the application name
|
||||
self.i18n(self.applicationName, self.applicationName)
|
||||
# Add a i18n message for every role.
|
||||
for role in self.getUsedRoles(appy=False):
|
||||
self.i18n('role_%s' % role.name, role.name)
|
||||
# Create basic files (config.py, etc)
|
||||
self.generateTool()
|
||||
self.generateInit()
|
||||
self.generateTests()
|
||||
# Create version.txt
|
||||
f = open(os.path.join(self.outputFolder, 'version.txt'), 'w')
|
||||
f.write(self.version)
|
||||
f.close()
|
||||
# Make folder "tests" a Python package
|
||||
initFile = '%s/tests/__init__.py' % self.outputFolder
|
||||
if not os.path.isfile(initFile):
|
||||
f = open(initFile, 'w')
|
||||
f.write('')
|
||||
f.close()
|
||||
# Generate i18n pot file
|
||||
potFileName = '%s.pot' % self.applicationName
|
||||
if potFileName in self.i18nFiles:
|
||||
potFile = self.i18nFiles[potFileName]
|
||||
else:
|
||||
fullName = os.path.join(self.application, 'tr', potFileName)
|
||||
potFile = po.PoFile(fullName)
|
||||
self.i18nFiles[potFileName] = potFile
|
||||
# Update the pot file with (a) standard Appy labels and (b) the list of
|
||||
# generated application labels.
|
||||
appyPotFileName = os.path.join(appy.getPath(), 'gen', 'tr', 'Appy.pot')
|
||||
appyLabels = po.PoParser(appyPotFileName).parse().messages
|
||||
removedLabels = potFile.update(appyLabels + self.labels.get(),
|
||||
self.options.i18nClean, keepExistingOrder=False)
|
||||
potFile.generate()
|
||||
if removedLabels:
|
||||
print(('Warning: %d messages were removed from translation ' \
|
||||
'files: %s' % (len(removedLabels), str(removedLabels))))
|
||||
# Generate i18n po files
|
||||
for language in self.config.languages:
|
||||
# I must generate (or update) a po file for the language(s)
|
||||
# specified in the configuration.
|
||||
poFileName = potFile.getPoFileName(language)
|
||||
if poFileName in self.i18nFiles:
|
||||
poFile = self.i18nFiles[poFileName]
|
||||
else:
|
||||
fullName = os.path.join(self.application, 'tr', poFileName)
|
||||
poFile = po.PoFile(fullName)
|
||||
self.i18nFiles[poFileName] = poFile
|
||||
# If we have default Appy messages translated for this language,
|
||||
# get it. Else, use appyLabels from the pot file as default empty
|
||||
# labels.
|
||||
appyPoFileName = os.path.join(appy.getPath(), 'gen', 'tr',
|
||||
'%s.po' % language)
|
||||
if os.path.exists(appyPoFileName):
|
||||
baseLabels = po.PoParser(appyPoFileName).parse().messages
|
||||
else:
|
||||
baseLabels = appyLabels
|
||||
poFile.update(baseLabels + self.labels.get() + \
|
||||
potFile.getCustomMessages(), self.options.i18nClean,
|
||||
keepExistingOrder=False)
|
||||
poFile.generate()
|
||||
# Generate corresponding fields on the Translation class
|
||||
page = '1'
|
||||
i = 0
|
||||
for message in potFile.messages:
|
||||
i += 1
|
||||
# A computed field is used for displaying the text to translate.
|
||||
self.translation.addLabelField(message.id, page)
|
||||
# A String field will hold the translation in itself.
|
||||
self.translation.addMessageField(message.id, page, self.i18nFiles)
|
||||
if (i % self.config.translationsPerPage) == 0:
|
||||
# A new page must be defined.
|
||||
page = str(int(page)+1)
|
||||
self.generateWrappers()
|
||||
self.generateConfig()
|
||||
|
||||
def getUsedRoles(self, appy=None, local=None, grantable=None):
|
||||
'''Produces a list of all the roles used within all workflows and
|
||||
classes defined in this application.
|
||||
|
||||
If p_appy is True, it keeps only Appy standard roles; if p_appy
|
||||
is False, it keeps only roles which are specific to this application;
|
||||
if p_appy is None it has no effect (so it keeps both roles).
|
||||
|
||||
If p_local is True, it keeps only local roles (ie, roles that can
|
||||
only be granted locally); if p_local is False, it keeps only "global"
|
||||
roles; if p_local is None it has no effect (so it keeps both roles).
|
||||
|
||||
If p_grantable is True, it keeps only roles that the admin can
|
||||
grant; if p_grantable is False, if keeps only ungrantable roles (ie
|
||||
those that are implicitly granted by the system like role
|
||||
"Authenticated"); if p_grantable is None it keeps both roles.'''
|
||||
allRoles = {} # ~{s_roleName:Role_role}~
|
||||
# Gather roles from workflow states and transitions
|
||||
for wfDescr in self.workflows:
|
||||
for attr in dir(wfDescr.klass):
|
||||
attrValue = getattr(wfDescr.klass, attr)
|
||||
if isinstance(attrValue, gen.State) or \
|
||||
isinstance(attrValue, gen.Transition):
|
||||
for role in attrValue.getUsedRoles():
|
||||
if role.name not in allRoles:
|
||||
allRoles[role.name] = role
|
||||
# Gather roles from "creators" attributes from every class
|
||||
for cDescr in self.getClasses(include='all'):
|
||||
creators = cDescr.getCreators()
|
||||
if not creators: continue
|
||||
for role in creators:
|
||||
if role.name not in allRoles:
|
||||
allRoles[role.name] = role
|
||||
res = list(allRoles.values())
|
||||
# Filter the result according to parameters
|
||||
for p in ('appy', 'local', 'grantable'):
|
||||
if eval(p) != None:
|
||||
res = [r for r in res if eval('r.%s == %s' % (p, p))]
|
||||
return res
|
||||
|
||||
def getAppyTypePath(self, name, appyType, klass, isBack=False):
|
||||
'''Gets the path to the p_appyType when a direct reference to an
|
||||
appyType must be generated in a Python file.'''
|
||||
if issubclass(klass, ModelClass):
|
||||
res = 'wrappers.%s.%s' % (klass.__name__, name)
|
||||
else:
|
||||
res = '%s.%s.%s' % (klass.__module__, klass.__name__, name)
|
||||
if isBack: res += '.back'
|
||||
return res
|
||||
|
||||
def getClasses(self, include=None):
|
||||
'''Returns the descriptors for all the classes in the generated
|
||||
gen-application. If p_include is:
|
||||
* "all" it includes the descriptors for the config-related
|
||||
classes (tool, user, group, translation, page)
|
||||
* "allButTool" it includes the same descriptors, the tool excepted
|
||||
* "custom" it includes descriptors for the config-related classes
|
||||
for which the user has created a sub-class.'''
|
||||
if not include: return self.classes
|
||||
res = self.classes[:]
|
||||
configClasses = [self.tool, self.user, self.group, self.translation,
|
||||
self.page]
|
||||
if include == 'all':
|
||||
res += configClasses
|
||||
elif include == 'allButTool':
|
||||
res += configClasses[1:]
|
||||
elif include == 'custom':
|
||||
res += [c for c in configClasses if c.customized]
|
||||
elif include == 'predefined':
|
||||
res = configClasses
|
||||
return res
|
||||
|
||||
def generateConfig(self):
|
||||
repls = self.repls.copy()
|
||||
# Get some lists of classes
|
||||
classes = self.getClasses()
|
||||
classesWithCustom = self.getClasses(include='custom')
|
||||
classesButTool = self.getClasses(include='allButTool')
|
||||
classesAll = self.getClasses(include='all')
|
||||
# Compute imports
|
||||
imports = ['import %s' % self.applicationName]
|
||||
for classDescr in (classesWithCustom + self.workflows):
|
||||
theImport = 'import %s' % classDescr.klass.__module__
|
||||
if theImport not in imports:
|
||||
imports.append(theImport)
|
||||
repls['imports'] = '\n'.join(imports)
|
||||
# Compute list of class definitions
|
||||
repls['appClasses'] = ','.join(['%s.%s' % (c.klass.__module__, \
|
||||
c.klass.__name__) for c in classes])
|
||||
# Compute lists of class names
|
||||
repls['appClassNames'] = ','.join(['"%s"' % c.name \
|
||||
for c in classes])
|
||||
repls['allClassNames'] = ','.join(['"%s"' % c.name \
|
||||
for c in classesButTool])
|
||||
allShortClassNames = ['"%s":"%s"' % (c.name.split('_')[-1], c.name) \
|
||||
for c in classesAll]
|
||||
repls['allShortClassNames'] = ','.join(allShortClassNames)
|
||||
# Compute the list of ordered attributes (forward and backward,
|
||||
# inherited included) for every Appy class.
|
||||
attributes = []
|
||||
for classDescr in classesAll:
|
||||
titleFound = False
|
||||
names = []
|
||||
for name, appyType, klass in classDescr.getOrderedAppyAttributes():
|
||||
names.append(name)
|
||||
if name == 'title': titleFound = True
|
||||
# Add the 'title' mandatory field if not found
|
||||
if not titleFound: names.insert(0, 'title')
|
||||
# Add the 'state' and 'SearchableText' attributes
|
||||
names += ['state', 'SearchableText']
|
||||
qNames = ['"%s"' % name for name in names]
|
||||
attributes.append('"%s":[%s]' % (classDescr.name, ','.join(qNames)))
|
||||
repls['attributes'] = ',\n '.join(attributes)
|
||||
# Compute list of used roles for registering them if needed
|
||||
specificRoles = self.getUsedRoles(appy=False)
|
||||
repls['roles'] = ','.join(['"%s"' % r.name for r in specificRoles])
|
||||
globalRoles = self.getUsedRoles(appy=False, local=False)
|
||||
repls['gRoles'] = ','.join(['"%s"' % r.name for r in globalRoles])
|
||||
grantableRoles = self.getUsedRoles(local=False, grantable=True)
|
||||
repls['grRoles'] = ','.join(['"%s"' % r.name for r in grantableRoles])
|
||||
self.copyFile('config.pyt', repls, destName='config.py')
|
||||
|
||||
def generateInit(self):
|
||||
# Compute imports
|
||||
imports = []
|
||||
classNames = []
|
||||
for c in self.getClasses(include='all'):
|
||||
importDef = ' import %s' % c.name
|
||||
if importDef not in imports:
|
||||
imports.append(importDef)
|
||||
classNames.append("%s.%s" % (c.name, c.name))
|
||||
repls = self.repls.copy()
|
||||
repls['imports'] = '\n'.join(imports)
|
||||
repls['classes'] = ','.join(classNames)
|
||||
repls['totalNumberOfTests'] = self.totalNumberOfTests
|
||||
self.copyFile('__init__.pyt', repls, destName='__init__.py')
|
||||
|
||||
def getClassesInOrder(self, allClasses):
|
||||
'''When generating wrappers, classes mut be dumped in order (else, it
|
||||
generates forward references in the Python file, that does not
|
||||
compile).'''
|
||||
res = [] # Appy class descriptors
|
||||
resClasses = [] # Corresponding real Python classes
|
||||
for classDescr in allClasses:
|
||||
klass = classDescr.klass
|
||||
if not klass.__bases__ or \
|
||||
(klass.__bases__[0].__name__ == 'ModelClass'):
|
||||
# This is a root class. We dump it at the begin of the file.
|
||||
res.insert(0, classDescr)
|
||||
resClasses.insert(0, klass)
|
||||
else:
|
||||
# If a child of this class is already present, we must insert
|
||||
# this klass before it.
|
||||
lowestChildIndex = sys.maxsize
|
||||
for resClass in resClasses:
|
||||
if klass in resClass.__bases__:
|
||||
lowestChildIndex = min(lowestChildIndex,
|
||||
resClasses.index(resClass))
|
||||
if lowestChildIndex != sys.maxsize:
|
||||
res.insert(lowestChildIndex, classDescr)
|
||||
resClasses.insert(lowestChildIndex, klass)
|
||||
else:
|
||||
res.append(classDescr)
|
||||
resClasses.append(klass)
|
||||
return res
|
||||
|
||||
def generateWrappers(self):
|
||||
# We must generate imports and wrapper definitions
|
||||
imports = []
|
||||
wrappers = []
|
||||
allClasses = self.getClasses(include='all')
|
||||
for c in self.getClassesInOrder(allClasses):
|
||||
if not c.predefined or c.customized:
|
||||
moduleImport = 'import %s' % c.klass.__module__
|
||||
if moduleImport not in imports:
|
||||
imports.append(moduleImport)
|
||||
# Determine parent wrapper and class
|
||||
parentClasses = c.getParents(allClasses)
|
||||
wrapperDef = 'class %s_Wrapper(%s):\n' % \
|
||||
(c.name, ','.join(parentClasses))
|
||||
wrapperDef += ' security = ClassSecurityInfo()\n'
|
||||
if c.customized:
|
||||
# For custom tool, add a call to a method that allows to
|
||||
# customize elements from the base class.
|
||||
wrapperDef += " if hasattr(%s, 'update'):\n " \
|
||||
"%s.update(%s)\n" % (parentClasses[1], parentClasses[1],
|
||||
parentClasses[0])
|
||||
# For custom tool, add security declaration that will allow to
|
||||
# call their methods from ZPTs.
|
||||
for parentClass in parentClasses:
|
||||
wrapperDef += " for elem in dir(%s):\n " \
|
||||
"if not elem.startswith('_'): security.declarePublic" \
|
||||
"(elem)\n" % (parentClass)
|
||||
# Register the class in Zope.
|
||||
wrapperDef += 'InitializeClass(%s_Wrapper)\n' % c.name
|
||||
wrappers.append(wrapperDef)
|
||||
repls = self.repls.copy()
|
||||
repls['imports'] = '\n'.join(imports)
|
||||
repls['wrappers'] = '\n'.join(wrappers)
|
||||
for klass in self.getClasses(include='predefined'):
|
||||
modelClass = klass.modelClass
|
||||
repls['%s' % modelClass.__name__] = modelClass._appy_getBody()
|
||||
self.copyFile('wrappers.pyt', repls, destName='wrappers.py')
|
||||
|
||||
def generateTests(self):
|
||||
'''Generates the file needed for executing tests.'''
|
||||
repls = self.repls.copy()
|
||||
modules = self.modulesWithTests
|
||||
repls['imports'] = '\n'.join(['import %s' % m for m in modules])
|
||||
repls['modulesWithTests'] = ','.join(modules)
|
||||
self.copyFile('testAll.pyt', repls, destName='testAll.py',
|
||||
destFolder='tests')
|
||||
|
||||
def generateTool(self):
|
||||
'''Generates the tool that corresponds to this application.'''
|
||||
# Create Tool-related i18n-related messages
|
||||
self.i18n(self.tool.name, po.CONFIG % self.applicationName, nice=False)
|
||||
# Tune the Ref field between Tool->User and Group->User
|
||||
Tool.users.klass = User
|
||||
if self.user.customized:
|
||||
Tool.users.klass = self.user.klass
|
||||
Group.users.klass = self.user.klass
|
||||
|
||||
# Generate the Tool-related classes (User, Group, Translation, Page)
|
||||
for klass in (self.user, self.group, self.translation, self.page):
|
||||
klassType = klass.name[len(self.applicationName):]
|
||||
klass.generateSchema()
|
||||
self.i18n(klass.name, klassType, nice=False)
|
||||
self.i18n('%s_plural' % klass.name, klass.name+'s', nice=False)
|
||||
self.generateSearches(klass)
|
||||
repls = self.repls.copy()
|
||||
if klass.isFolder():
|
||||
parents = 'BaseMixin, Folder'
|
||||
icon = 'folder.gif'
|
||||
else:
|
||||
parents = 'BaseMixin, SimpleItem'
|
||||
icon = 'object.gif'
|
||||
repls.update({'methods': klass.methods, 'genClassName': klass.name,
|
||||
'baseMixin':'BaseMixin', 'parents': parents,
|
||||
'classDoc': 'Standard Appy class', 'icon': icon})
|
||||
self.copyFile('Class.pyt', repls, destName='%s.py' % klass.name)
|
||||
|
||||
# Before generating the Tool class, finalize it with search-related and
|
||||
# import-related fields.
|
||||
for classDescr in self.getClasses(include='allButTool'):
|
||||
if not classDescr.isRoot(): continue
|
||||
importMean = classDescr.getCreateMean('Import')
|
||||
if importMean:
|
||||
self.tool.addImportRelatedFields(classDescr)
|
||||
self.tool.generateSchema()
|
||||
|
||||
# Generate the Tool class
|
||||
repls = self.repls.copy()
|
||||
repls.update({'methods': self.tool.methods,
|
||||
'genClassName': self.tool.name, 'baseMixin':'ToolMixin',
|
||||
'parents': 'ToolMixin, Folder', 'icon': 'folder.gif',
|
||||
'classDoc': 'Tool class for %s' % self.applicationName})
|
||||
self.copyFile('Class.pyt', repls, destName='%s.py' % self.tool.name)
|
||||
|
||||
def generateSearches(self, classDescr):
|
||||
'''Generates i18n labels for searches defined on p_classDescr.'''
|
||||
for search in classDescr.getSearches(classDescr.klass):
|
||||
if not search.name:
|
||||
className = classDescr.klass.__name__
|
||||
raise Exception('Search defined on %s has no name.' % className)
|
||||
label = '%s_search_%s' % (classDescr.name, search.name)
|
||||
self.i18n(label, search.name)
|
||||
self.i18n('%s_descr' % label, ' ', nice=False)
|
||||
# Generate labels for groups of searches
|
||||
if search.group and not search.group.label:
|
||||
search.group.generateLabels(self.labels, classDescr, set(),
|
||||
content='searches')
|
||||
|
||||
def generateClass(self, classDescr):
|
||||
'''Is called each time an Appy class is found in the application, for
|
||||
generating the corresponding Archetype class.'''
|
||||
k = classDescr.klass
|
||||
print(('Generating %s.%s (gen-class)...' % (k.__module__, k.__name__)))
|
||||
# Determine base Zope class
|
||||
isFolder = classDescr.isFolder()
|
||||
baseClass = isFolder and 'Folder' or 'SimpleItem'
|
||||
icon = isFolder and 'folder.gif' or 'object.gif'
|
||||
parents = 'BaseMixin, %s' % baseClass
|
||||
classDoc = k.__doc__ or 'Appy class.'
|
||||
repls = self.repls.copy()
|
||||
classDescr.generateSchema()
|
||||
repls.update({
|
||||
'parents': parents, 'className': k.__name__,
|
||||
'genClassName': classDescr.name, 'baseMixin':'BaseMixin',
|
||||
'classDoc': classDoc, 'applicationName': self.applicationName,
|
||||
'methods': classDescr.methods, 'icon':icon})
|
||||
fileName = '%s.py' % classDescr.name
|
||||
# Create i18n labels (class name and plural form)
|
||||
self.i18n(classDescr.name, k.__name__)
|
||||
self.i18n('%s_plural' % classDescr.name, k.__name__+'s')
|
||||
# Create i18n labels for searches
|
||||
self.generateSearches(classDescr)
|
||||
# Generate the resulting Zope class.
|
||||
self.copyFile('Class.pyt', repls, destName=fileName)
|
||||
|
||||
def generateWorkflow(self, wfDescr):
|
||||
'''This method creates the i18n labels related to the workflow described
|
||||
by p_wfDescr.'''
|
||||
k = wfDescr.klass
|
||||
print(('Generating %s.%s (gen-workflow)...' % (k.__module__, k.__name__)))
|
||||
# Identify workflow name
|
||||
wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass)
|
||||
# Add i18n messages for states
|
||||
for name in dir(wfDescr.klass):
|
||||
if not isinstance(getattr(wfDescr.klass, name), gen.State): continue
|
||||
self.i18n('%s_%s' % (wfName, name), name)
|
||||
# Add i18n messages for transitions
|
||||
for name in dir(wfDescr.klass):
|
||||
transition = getattr(wfDescr.klass, name)
|
||||
if not isinstance(transition, gen.Transition): continue
|
||||
self.i18n('%s_%s' % (wfName, name), name)
|
||||
if transition.show and transition.confirm:
|
||||
# We need to generate a label for the message that will be shown
|
||||
# in the confirm popup.
|
||||
self.i18n('%s_%s_confirm'%(wfName, name),po.CONFIRM, nice=False)
|
||||
# ------------------------------------------------------------------------------
|
121
gen/indexer.py
|
@ -1,121 +0,0 @@
|
|||
'''This file defines code for extracting, from field values, the text to be
|
||||
indexed.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
from appy.gen.utils import splitIntoWords
|
||||
from appy.shared.xml_parser import XmlParser
|
||||
from appy.shared.utils import normalizeText
|
||||
|
||||
# Default Appy indexes ---------------------------------------------------------
|
||||
defaultIndexes = {
|
||||
'State': 'ListIndex', 'UID': 'FieldIndex', 'Title': 'TextIndex',
|
||||
'SortableTitle': 'FieldIndex', 'SearchableText': 'TextIndex',
|
||||
'Creator': 'FieldIndex', 'Created': 'DateIndex', 'Modified': 'DateIndex',
|
||||
'ClassName': 'FieldIndex', 'Allowed': 'KeywordIndex'}
|
||||
|
||||
# Stuff for creating or updating the indexes -----------------------------------
|
||||
class TextIndexInfo:
|
||||
'''Parameters for a text ZCTextIndex.'''
|
||||
lexicon_id = "text_lexicon"
|
||||
index_type = 'Okapi BM25 Rank'
|
||||
|
||||
class XhtmlIndexInfo:
|
||||
'''Parameters for a html ZCTextIndex.'''
|
||||
lexicon_id = "xhtml_lexicon"
|
||||
index_type = 'Okapi BM25 Rank'
|
||||
|
||||
class ListIndexInfo:
|
||||
'''Parameters for a list ZCTextIndex.'''
|
||||
lexicon_id = "list_lexicon"
|
||||
index_type = 'Okapi BM25 Rank'
|
||||
|
||||
def updateIndexes(installer, indexInfo):
|
||||
'''This function updates the indexes defined in the catalog.'''
|
||||
catalog = installer.app.catalog
|
||||
logger = installer.logger
|
||||
for indexName, indexType in indexInfo.items():
|
||||
indexRealType = indexType
|
||||
if indexType in ('XhtmlIndex', 'TextIndex', 'ListIndex'):
|
||||
indexRealType = 'ZCTextIndex'
|
||||
# If this index already exists but with a different type (or with a
|
||||
# deprecated lexicon), remove it.
|
||||
if indexName in catalog.indexes():
|
||||
indexObject = catalog.Indexes[indexName]
|
||||
oldType = indexObject.__class__.__name__
|
||||
toDelete = False
|
||||
if (oldType != indexRealType):
|
||||
toDelete = True
|
||||
info = indexRealType
|
||||
elif (oldType == 'ZCTextIndex') and \
|
||||
(indexObject.lexicon_id == 'lexicon'):
|
||||
toDelete = True
|
||||
info = '%s (%s)' % (oldType, indexType)
|
||||
if toDelete:
|
||||
catalog.delIndex(indexName)
|
||||
logger.info('Index %s (%s) to replace as %s.' % \
|
||||
(indexName, oldType, info))
|
||||
if indexName not in catalog.indexes():
|
||||
# We need to (re-)create this index.
|
||||
if indexType == 'TextIndex':
|
||||
catalog.addIndex(indexName, indexRealType, extra=TextIndexInfo)
|
||||
elif indexType == 'XhtmlIndex':
|
||||
catalog.addIndex(indexName, indexRealType, extra=XhtmlIndexInfo)
|
||||
elif indexType == 'ListIndex':
|
||||
catalog.addIndex(indexName, indexRealType, extra=ListIndexInfo)
|
||||
else:
|
||||
catalog.addIndex(indexName, indexType)
|
||||
# Indexing database content based on this index.
|
||||
logger.info('Reindexing %s (%s)...' % (indexName, indexType))
|
||||
catalog.reindexIndex(indexName, installer.app.REQUEST)
|
||||
logger.info('Done.')
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class XhtmlTextExtractor(XmlParser):
|
||||
'''Extracts text from XHTML.'''
|
||||
def startDocument(self):
|
||||
XmlParser.startDocument(self)
|
||||
self.res = []
|
||||
|
||||
def endDocument(self):
|
||||
self.res = ' '.join(self.res)
|
||||
return XmlParser.endDocument(self)
|
||||
|
||||
def characters(self, content):
|
||||
c = normalizeText(content)
|
||||
if len(c) > 1: self.res.append(c)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class XhtmlIndexer:
|
||||
'''Extracts, from XHTML field values, the text to index.'''
|
||||
def process(self, texts):
|
||||
res = set()
|
||||
for text in texts:
|
||||
extractor = XhtmlTextExtractor(raiseOnError=False)
|
||||
cleanText = extractor.parse('<p>%s</p>' % text)
|
||||
res = res.union(splitIntoWords(cleanText))
|
||||
return list(res)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class TextIndexer:
|
||||
'''Extracts, from text field values, a normalized value to index.'''
|
||||
def process(self, texts):
|
||||
res = set()
|
||||
for text in texts:
|
||||
cleanText = normalizeText(text)
|
||||
res = res.union(splitIntoWords(cleanText))
|
||||
return list(res)
|
||||
|
||||
class ListIndexer:
|
||||
'''This lexicon does nothing: list of values must be indexed as is.'''
|
||||
def process(self, texts): return texts
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
try:
|
||||
from Products.ZCTextIndex.PipelineFactory import element_factory as ef
|
||||
ef.registerFactory('XHTML indexer', 'XHTML indexer', XhtmlIndexer)
|
||||
ef.registerFactory('Text indexer', 'Text indexer', TextIndexer)
|
||||
ef.registerFactory('List indexer', 'List indexer', ListIndexer)
|
||||
except ImportError:
|
||||
# May occur at generation time
|
||||
pass
|
||||
# ------------------------------------------------------------------------------
|
382
gen/installer.py
|
@ -1,382 +0,0 @@
|
|||
'''This package contains stuff used at run-time for installing a generated
|
||||
Zope product.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import os, os.path
|
||||
import appy
|
||||
import appy.version
|
||||
import appy.gen as gen
|
||||
from appy.gen.po import PoParser
|
||||
from appy.gen.indexer import defaultIndexes, updateIndexes
|
||||
from appy.gen.migrator import Migrator
|
||||
from appy.gen import utils as gutils
|
||||
from appy.shared.data import languages
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
homePage = '<tal:h define="dummy python: request.RESPONSE.redirect(' \
|
||||
'context.config.getHomePage())"/>'
|
||||
|
||||
# Zope hacks -------------------------------------------------------------------
|
||||
class FakeXmlrpc:
|
||||
'''Fake class that behaves like Zope's xmlrpc module. This cheat disables
|
||||
Zope's XMLRPC.'''
|
||||
def parse_input(self, value): return None, ()
|
||||
def response(self, response): return response
|
||||
|
||||
class FakeZCatalog:
|
||||
# This prevents Zope frop crashing when, while updating the catalog, an
|
||||
# entry in the catalog corresponds to a missing object.
|
||||
def resolve_url(self, path, REQUEST):
|
||||
'''Cheat: "hasattr" test has been added in first line'''
|
||||
if REQUEST and hasattr(REQUEST, 'script'):
|
||||
script=REQUEST.script
|
||||
if path.find(script) != 0:
|
||||
path='%s/%s' % (script, path)
|
||||
try: return REQUEST.resolve_url(path)
|
||||
except: pass
|
||||
try:
|
||||
from Products.ZCatalog.ZCatalog import ZCatalog
|
||||
ZCatalog.resolve_url = resolve_url
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def onDelSession(sessionObject, container):
|
||||
'''This function is called when a session expires.'''
|
||||
rq = container.REQUEST
|
||||
if '_appy_' in rq.cookies and '_ZopeId' in rq.cookies and \
|
||||
(rq['_ZopeId'] == sessionObject.token):
|
||||
# The request comes from a guy whose session has expired.
|
||||
resp = rq.RESPONSE
|
||||
resp.expireCookie('_appy_', path='/')
|
||||
resp.setHeader('Content-Type', 'text/html')
|
||||
resp.write('<center>For security reasons, your session has ' \
|
||||
'expired.</center>')
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ZopeInstaller:
|
||||
'''This Zope installer runs every time Zope starts and encounters this
|
||||
generated Zope product.'''
|
||||
# Info about the default users that are always present.
|
||||
defaultUsers = {'admin': ('Manager',), 'system': ('Manager',), 'anon': ()}
|
||||
|
||||
def __init__(self, zopeContext, config, classes):
|
||||
self.zopeContext = zopeContext
|
||||
self.app = zopeContext._ProductContext__app # The root of the Zope tree
|
||||
self.config = config
|
||||
self.classes = classes
|
||||
# Unwrap some useful config variables
|
||||
self.productName = config.PROJECTNAME
|
||||
self.languages = config.appConfig.languages
|
||||
self.logger = config.logger
|
||||
|
||||
def installUi(self):
|
||||
'''Installs the user interface.'''
|
||||
# Some useful imports.
|
||||
from OFS.Folder import manage_addFolder
|
||||
from OFS.Image import manage_addImage, manage_addFile
|
||||
# Delete the existing folder if it existed.
|
||||
zopeContent = self.app.objectIds()
|
||||
if 'ui' in zopeContent: self.app.manage_delObjects(['ui'])
|
||||
manage_addFolder(self.app, 'ui')
|
||||
# Browse the physical ui folders (the Appy one and an app-specific, if
|
||||
# the app defines one) and create the corresponding objects in the Zope
|
||||
# folder. In the case of files having the same name in both folders,
|
||||
# the one from the app-specific folder is chosen.
|
||||
j = os.path.join
|
||||
uiFolders = [j(j(appy.getPath(), 'gen'), 'ui')]
|
||||
for uiFolder in self.config.appConfig.uiFolders:
|
||||
if uiFolder.startswith('..'):
|
||||
folder = j(os.path.dirname(self.config.diskFolder),uiFolder[3:])
|
||||
else:
|
||||
folder = j(self.config.diskFolder, uiFolder)
|
||||
if os.path.exists(folder):
|
||||
uiFolders.insert(0, folder)
|
||||
for ui in uiFolders:
|
||||
for root, dirs, files in os.walk(ui):
|
||||
folderName = root[len(ui):]
|
||||
# Get the Zope folder that corresponds to this name
|
||||
zopeFolder = self.app.ui
|
||||
if folderName:
|
||||
for name in folderName.strip(os.sep).split(os.sep):
|
||||
zopeFolder = zopeFolder._getOb(name)
|
||||
# Create sub-folders at this level
|
||||
for name in dirs:
|
||||
if not hasattr(zopeFolder.aq_base, name):
|
||||
manage_addFolder(zopeFolder, name)
|
||||
# Create files at this level
|
||||
for name in files:
|
||||
ext = os.path.splitext(name)[1]
|
||||
if hasattr(zopeFolder.aq_base, name): continue
|
||||
f = file(j(root, name))
|
||||
if name == 'favicon.ico':
|
||||
if not hasattr(self.app, name):
|
||||
# Copy it at the root. Else, IE won't notice it.
|
||||
manage_addImage(self.app, name, f)
|
||||
elif ext in gen.File.imageExts:
|
||||
manage_addImage(zopeFolder, name, f)
|
||||
else:
|
||||
manage_addFile(zopeFolder, name, f)
|
||||
f.close()
|
||||
# Update the home page
|
||||
if 'index_html' in zopeContent:
|
||||
self.app.manage_delObjects(['index_html'])
|
||||
from Products.PageTemplates.ZopePageTemplate import \
|
||||
manage_addPageTemplate
|
||||
manage_addPageTemplate(self.app, 'index_html', '', homePage)
|
||||
# Remove the error page.
|
||||
if 'standard_error_message' in zopeContent:
|
||||
self.app.manage_delObjects(['standard_error_message'])
|
||||
|
||||
def installCatalog(self):
|
||||
'''Create the catalog at the root of Zope if id does not exist.'''
|
||||
if 'catalog' not in self.app.objectIds():
|
||||
# Create the catalog
|
||||
from Products.ZCatalog.ZCatalog import manage_addZCatalog
|
||||
manage_addZCatalog(self.app, 'catalog', '')
|
||||
self.logger.info('Appy catalog created.')
|
||||
|
||||
# Create lexicons for ZCTextIndexes
|
||||
catalog = self.app.catalog
|
||||
lexicons = catalog.objectIds()
|
||||
from Products.ZCTextIndex.ZCTextIndex import manage_addLexicon
|
||||
if 'xhtml_lexicon' not in lexicons:
|
||||
lex = appy.Object(group='XHTML indexer', name='XHTML indexer')
|
||||
manage_addLexicon(catalog, 'xhtml_lexicon', elements=[lex])
|
||||
if 'text_lexicon' not in lexicons:
|
||||
lex = appy.Object(group='Text indexer', name='Text indexer')
|
||||
manage_addLexicon(catalog, 'text_lexicon', elements=[lex])
|
||||
if 'list_lexicon' not in lexicons:
|
||||
lex = appy.Object(group='List indexer', name='List indexer')
|
||||
manage_addLexicon(catalog, 'list_lexicon', elements=[lex])
|
||||
|
||||
# Delete the deprecated one if it exists
|
||||
if 'lexicon' in lexicons: catalog.manage_delObjects(['lexicon'])
|
||||
|
||||
# Create or update Appy-wide indexes and field-related indexes
|
||||
indexInfo = defaultIndexes.copy()
|
||||
tool = self.app.config
|
||||
for className in self.config.attributes.keys():
|
||||
wrapperClass = tool.getAppyClass(className, wrapper=True)
|
||||
indexInfo.update(wrapperClass.getIndexes(includeDefaults=False))
|
||||
updateIndexes(self, indexInfo)
|
||||
# Re-index index "SearchableText", wrongly defined for Appy < 0.8.3
|
||||
stIndex = catalog.Indexes['SearchableText']
|
||||
if stIndex.indexSize() == 0:
|
||||
self.logger.info('reindexing SearchableText...')
|
||||
catalog.reindexIndex('SearchableText', self.app.REQUEST)
|
||||
self.logger.info('done.')
|
||||
|
||||
def installBaseObjects(self):
|
||||
'''Creates the tool and the base data folder if they do not exist.'''
|
||||
# Create the tool.
|
||||
zopeContent = self.app.objectIds()
|
||||
from OFS.Folder import manage_addFolder
|
||||
|
||||
if 'config' not in zopeContent:
|
||||
toolName = '%sTool' % self.productName
|
||||
gutils.createObject(self.app, 'config', toolName, self.productName,
|
||||
wf=False, noSecurity=True)
|
||||
# Create the base data folder.
|
||||
if 'data' not in zopeContent: manage_addFolder(self.app, 'data')
|
||||
|
||||
# Remove some default objects created by Zope but not useful to Appy
|
||||
for name in ('standard_html_footer', 'standard_html_header',\
|
||||
'standard_template.pt'):
|
||||
if name in zopeContent: self.app.manage_delObjects([name])
|
||||
|
||||
def installTool(self):
|
||||
'''Updates the tool (now that the catalog is created) and updates its
|
||||
inner objects (users, groups, translations, documents).'''
|
||||
tool = self.app.config
|
||||
tool.createOrUpdate(True, None)
|
||||
appyTool = tool.appy()
|
||||
appyTool.log('Appy version is "%s".' % appy.version.short)
|
||||
|
||||
# Execute custom pre-installation code if any.
|
||||
if hasattr(appyTool, 'beforeInstall'): appyTool.beforeInstall()
|
||||
|
||||
# Create the default users if they do not exist.
|
||||
for login, roles in self.defaultUsers.items():
|
||||
if not appyTool.count('User', noSecurity=True, login=login):
|
||||
appyTool.create('users', noSecurity=True, id=login, login=login,
|
||||
password3=login, password4=login,
|
||||
email='%s@appyframework.org'%login, roles=roles)
|
||||
appyTool.log('User "%s" created.' % login)
|
||||
|
||||
# Create group "admins" if it does not exist
|
||||
if not appyTool.count('Group', noSecurity=True, login='admins'):
|
||||
appyTool.create('groups', noSecurity=True, login='admins',
|
||||
title='Administrators', roles=['Manager'])
|
||||
appyTool.log('Group "admins" created.')
|
||||
|
||||
# Create a group for every global role defined in the application
|
||||
# (if required).
|
||||
if self.config.appConfig.groupsForGlobalRoles:
|
||||
for role in self.config.applicationGlobalRoles:
|
||||
groupId = role.lower()
|
||||
if appyTool.count('Group', noSecurity=True, login=groupId):
|
||||
continue
|
||||
appyTool.create('groups', noSecurity=True, login=groupId,
|
||||
title=role, roles=[role])
|
||||
appyTool.log('Group "%s", related to global role "%s", was ' \
|
||||
'created.' % (groupId, role))
|
||||
|
||||
# Create or update Translation objects
|
||||
translations = [t.o.id for t in appyTool.translations]
|
||||
# We browse the languages supported by this application and check
|
||||
# whether we need to create the corresponding Translation objects.
|
||||
done = []
|
||||
for language in self.languages:
|
||||
if language in translations: continue
|
||||
# We will create, in the tool, the translation object for this
|
||||
# language. Determine first its title.
|
||||
langId, langEn, langNat = languages.get(language)
|
||||
if langEn != langNat:
|
||||
title = '%s (%s)' % (langEn, langNat)
|
||||
else:
|
||||
title = langEn
|
||||
appyTool.create('translations', noSecurity=True,
|
||||
id=language, title=title)
|
||||
done.append(language)
|
||||
if done: appyTool.log('Translations created for %s.' % ', '.join(done))
|
||||
|
||||
# Synchronizes, if required, every Translation object with the
|
||||
# corresponding "po" file on disk.
|
||||
if appyTool.loadTranslationsAtStartup:
|
||||
appFolder = self.config.diskFolder
|
||||
appName = self.config.PROJECTNAME
|
||||
i18nFolder = os.path.join(appFolder, 'tr')
|
||||
done = []
|
||||
for translation in appyTool.translations:
|
||||
# Get the "po" file
|
||||
poName = '%s-%s.po' % (appName, translation.id)
|
||||
poFile = PoParser(os.path.join(i18nFolder, poName)).parse()
|
||||
for message in poFile.messages:
|
||||
setattr(translation, message.id, message.getMessage())
|
||||
done.append(translation.id)
|
||||
appyTool.log('translation(s) %s updated (%s messages).' % \
|
||||
(', '.join(done), len(poFile.messages)))
|
||||
|
||||
# Execute custom installation code if any.
|
||||
if hasattr(appyTool, 'onInstall'): appyTool.onInstall()
|
||||
|
||||
def configureSessions(self):
|
||||
'''Configure the session machinery.'''
|
||||
# Register a function warning us when a session object is deleted. When
|
||||
# launching Zope in test mode, the temp folder does not exist.
|
||||
if not hasattr(self.app, 'temp_folder'): return
|
||||
sessionData = self.app.temp_folder.session_data
|
||||
if self.config.appConfig.enableSessionTimeout:
|
||||
sessionData.setDelNotificationTarget(onDelSession)
|
||||
else:
|
||||
sessionData.setDelNotificationTarget(None)
|
||||
|
||||
def installZopeClasses(self):
|
||||
'''Zope-level class registration.'''
|
||||
for klass in self.classes:
|
||||
name = klass.__name__
|
||||
module = klass.__module__
|
||||
wrapper = klass.wrapperClass
|
||||
exec('from %s import manage_add%s as ctor' % (module, name))
|
||||
self.zopeContext.registerClass(meta_type=name,
|
||||
constructors = (ctor,), permission = None)
|
||||
# Create workflow prototypical instances in __instance__ attributes
|
||||
wf = wrapper.getWorkflow()
|
||||
if not hasattr(wf, '__instance__'): wf.__instance__ = wf()
|
||||
|
||||
def installAppyTypes(self):
|
||||
'''We complete here the initialisation process of every Appy type of
|
||||
every gen-class of the application.'''
|
||||
appName = self.productName
|
||||
for klass in self.classes:
|
||||
# Store on wrapper class the ordered list of Appy types
|
||||
wrapperClass = klass.wrapperClass
|
||||
if not hasattr(wrapperClass, 'title'):
|
||||
# Special field "type" is mandatory for every class.
|
||||
title = gen.String(multiplicity=(1,1), show='edit',
|
||||
indexed=True, searchable=True)
|
||||
title.init('title', None, 'appy')
|
||||
setattr(wrapperClass, 'title', title)
|
||||
# Special field "state" must be added for every class. It must be a
|
||||
# "select" field, because it will be necessary for displaying the
|
||||
# translated state name.
|
||||
state = gen.String(validator=gen.Selection('listStates'),
|
||||
show='result', persist=False, indexed=True, height=5)
|
||||
state.init('state', None, 'workflow')
|
||||
setattr(wrapperClass, 'state', state)
|
||||
# Special field "SearchableText" must be added fot every class and
|
||||
# will allow to display a search widget for entering keywords for
|
||||
# searhing in index "SearchableText".
|
||||
searchable = gen.String(show=False, persist=False, indexed=True)
|
||||
searchable.init('SearchableText', None, 'appy')
|
||||
setattr(wrapperClass, 'SearchableText', searchable)
|
||||
# Set field "__fields__" on the wrapper class
|
||||
names = self.config.attributes[wrapperClass.__name__[:-8]]
|
||||
wrapperClass.__fields__ = [getattr(wrapperClass, n) for n in names]
|
||||
# Post-initialise every Appy type
|
||||
for baseClass in klass.wrapperClass.__bases__:
|
||||
if baseClass.__name__ == 'AbstractWrapper': continue
|
||||
for name, appyType in baseClass.__dict__.items():
|
||||
if not isinstance(appyType, gen.Field) or \
|
||||
(isinstance(appyType, gen.Ref) and appyType.isBack):
|
||||
continue # Back refs are initialised within fw refs
|
||||
appyType.init(name, baseClass, appName)
|
||||
|
||||
def installRoles(self):
|
||||
'''Installs the application-specific roles if not already done.'''
|
||||
roles = list(self.app.__ac_roles__)
|
||||
for role in self.config.applicationRoles:
|
||||
if role not in roles: roles.append(role)
|
||||
self.app.__ac_roles__ = tuple(roles)
|
||||
|
||||
def patchZope(self):
|
||||
'''Patches some arts of Zope'''
|
||||
# Disable XMLRPC. This way, Zope can transmit HTTP POSTs containing
|
||||
# XML to Appy without trying to recognize it himself as XMLRPC requests.
|
||||
import ZPublisher.HTTPRequest
|
||||
ZPublisher.HTTPRequest.xmlrpc = FakeXmlrpc()
|
||||
|
||||
def installDependencies(self):
|
||||
'''Zope products are installed in alphabetical order. But here, we need
|
||||
ZCTextIndex to be installed before our Appy application. So, we cheat
|
||||
and force Zope to install it now.'''
|
||||
from OFS.Application import install_product
|
||||
import Products
|
||||
install_product(self.app, Products.__path__[1], 'ZCTextIndex', [], {})
|
||||
|
||||
def logConnectedServers(self):
|
||||
'''Simply log the names of servers (LDAP, mail...) this app wants to
|
||||
connnect to.'''
|
||||
cfg = self.config.appConfig
|
||||
servers = []
|
||||
# Are we connected to a LDAP server for authenticating our users?
|
||||
for sv in ('ldap', 'mail'):
|
||||
if not getattr(cfg, sv): continue
|
||||
svConfig = getattr(cfg, sv)
|
||||
enabled = svConfig.enabled and 'enabled' or 'disabled'
|
||||
servers.append('%s (%s, %s)' % (svConfig, sv, enabled))
|
||||
if servers:
|
||||
self.logger.info('server(s) %s configured.' % ', '.join(servers))
|
||||
|
||||
def install(self):
|
||||
self.installDependencies()
|
||||
self.patchZope()
|
||||
self.installRoles()
|
||||
self.installAppyTypes()
|
||||
self.installZopeClasses()
|
||||
self.configureSessions()
|
||||
self.installBaseObjects()
|
||||
# The following line cleans and rebuilds the catalog entirely.
|
||||
#self.app.config.appy().refreshCatalog()
|
||||
self.installCatalog()
|
||||
self.installTool()
|
||||
self.installUi()
|
||||
# Log connections to external servers (ldap, mail...)
|
||||
self.logConnectedServers()
|
||||
# Perform migrations if required
|
||||
Migrator(self).run()
|
||||
# Update Appy version in the database
|
||||
self.app.config.appy().appyVersion = appy.version.short
|
||||
# Empty the fake REQUEST object, only used at Zope startup.
|
||||
del self.app.config.getProductConfig().fakeRequest.wrappers
|
||||
# ------------------------------------------------------------------------------
|
253
gen/layout.py
|
@ -1,253 +0,0 @@
|
|||
'''This module contains classes used for layouting graphical elements
|
||||
(fields, widgets, groups, ...).'''
|
||||
|
||||
# A layout defines how a given field is rendered in a given context. Several
|
||||
# contexts exist:
|
||||
# "view" represents a given page for a given Appy class, in read-only mode.
|
||||
# "edit" represents a given page for a given Appy class, in edit mode.
|
||||
# "cell" represents a cell in a table, like when we need to render a field
|
||||
# value in a query result or in a reference table.
|
||||
# "search" represents an advanced search screen.
|
||||
|
||||
# Layout elements for a class or page ------------------------------------------
|
||||
# s - The page summary, containing summarized information about the page or
|
||||
# class, workflow information and object history.
|
||||
# w - The widgets of the current page/class
|
||||
# n - The navigation panel (inter-objects navigation)
|
||||
# b - The range of buttons (intra-object navigation, save, edit, delete...)
|
||||
|
||||
# Layout elements for a field --------------------------------------------------
|
||||
# l - "label" The field label
|
||||
# d - "description" The field description (a description is always visible)
|
||||
# h - "help" Help for the field (typically rendered as an icon,
|
||||
# clicking on it shows a popup with online help
|
||||
# v - "validation" The icon that is shown when a validation error occurs
|
||||
# (typically only used on "edit" layouts)
|
||||
# r - "required" The icon that specified that the field is required (if
|
||||
# relevant; typically only used on "edit" layouts)
|
||||
# f - "field" The field value, or input for entering a value.
|
||||
# c - "changes" The button for displaying changes to a field
|
||||
|
||||
# For every field of a Appy class, you can define, for every layout context,
|
||||
# what field-related information will appear, and how it will be rendered.
|
||||
# Variables defaultPageLayouts and defaultFieldLayouts defined below give the
|
||||
# default layouts for pages and fields respectively.
|
||||
#
|
||||
# How to express a layout? You simply define a string that is made of the
|
||||
# letters corresponding to the field elements you want to render. The order of
|
||||
# elements correspond to the order into which they will be rendered.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
rowDelimiters = {'-':'middle', '=':'top', '_':'bottom'}
|
||||
rowDelms = ''.join(list(rowDelimiters.keys()))
|
||||
cellDelimiters = {'|': 'center', ';': 'left', '!': 'right'}
|
||||
cellDelms = ''.join(list(cellDelimiters.keys()))
|
||||
|
||||
pxDict = {
|
||||
# Page-related elements
|
||||
's': 'pxHeader', 'w': 'pxFields', 'n': 'pxNavigationStrip', 'b': 'pxButtons',
|
||||
# Field-related elements
|
||||
'l': 'pxLabel', 'd': 'pxDescription', 'h': 'pxHelp', 'v': 'pxValidation',
|
||||
'r': 'pxRequired', 'c': 'pxChanges'}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Cell:
|
||||
'''Represents a cell in a row in a table.'''
|
||||
def __init__(self, content, align, isHeader=False):
|
||||
self.align = align
|
||||
self.width = None
|
||||
self.content = None
|
||||
self.colspan = 1
|
||||
if isHeader:
|
||||
self.width = content
|
||||
else:
|
||||
self.content = [] # The list of widgets to render in the cell
|
||||
self.decodeContent(content)
|
||||
|
||||
def decodeContent(self, content):
|
||||
digits = '' # We collect the digits that will give the colspan
|
||||
for char in content:
|
||||
if char.isdigit():
|
||||
digits += char
|
||||
else:
|
||||
# It is a letter corresponding to a macro
|
||||
if char in pxDict:
|
||||
self.content.append(pxDict[char])
|
||||
elif char == 'f':
|
||||
# The exact macro to call will be known at render-time
|
||||
self.content.append('?')
|
||||
# Manage the colspan
|
||||
if digits:
|
||||
self.colspan = int(digits)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Row:
|
||||
'''Represents a row in a table.'''
|
||||
def __init__(self, content, valign, isHeader=False):
|
||||
self.valign = valign
|
||||
self.cells = []
|
||||
self.decodeCells(content, isHeader)
|
||||
# Compute the row length
|
||||
length = 0
|
||||
for cell in self.cells:
|
||||
length += cell.colspan
|
||||
self.length = length
|
||||
|
||||
def decodeCells(self, content, isHeader):
|
||||
'''Decodes the given chunk of layout string p_content containing
|
||||
column-related information (if p_isHeader is True) or cell content
|
||||
(if p_isHeader is False) and produces a list of Cell instances.'''
|
||||
cellContent = ''
|
||||
for char in content:
|
||||
if char in cellDelimiters:
|
||||
align = cellDelimiters[char]
|
||||
self.cells.append(Cell(cellContent, align, isHeader))
|
||||
cellContent = ''
|
||||
else:
|
||||
cellContent += char
|
||||
# Manage the last cell if any
|
||||
if cellContent:
|
||||
self.cells.append(Cell(cellContent, 'left', isHeader))
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Table:
|
||||
'''Represents a table where to dispose graphical elements.'''
|
||||
simpleParams = ('style', 'css_class', 'cellpadding', 'cellspacing', 'width',
|
||||
'align')
|
||||
derivedRepls = {'view': 'hrvd', 'search': '', 'cell': 'ldc'}
|
||||
def __init__(self, layoutString=None, style=None, css_class='',
|
||||
cellpadding=0, cellspacing=0, width='100%', align='left',
|
||||
other=None, derivedType=None):
|
||||
if other:
|
||||
# We need to create a Table instance from another Table instance,
|
||||
# given in p_other. In this case, we ignore previous params.
|
||||
if derivedType != None:
|
||||
# We will not simply mimic p_other. If p_derivedType is:
|
||||
# - "view", p_derivedFrom is an "edit" layout, and we must
|
||||
# create the corresponding "view" layout;
|
||||
# - "cell", p_derivedFrom is a "view" layout, and we must
|
||||
# create the corresponding "cell" layout;
|
||||
self.layoutString = Table.deriveLayout(other.layoutString,
|
||||
derivedType)
|
||||
else:
|
||||
self.layoutString = other.layoutString
|
||||
source = 'other.'
|
||||
else:
|
||||
source = ''
|
||||
self.layoutString = layoutString
|
||||
# Initialise simple params, either from the true params, either from
|
||||
# the p_other Table instance.
|
||||
for param in Table.simpleParams:
|
||||
exec('self.%s = %s%s' % (param, source, param))
|
||||
# The following attribute will store a special Row instance used for
|
||||
# defining column properties.
|
||||
self.headerRow = None
|
||||
# The content rows will be stored hereafter.
|
||||
self.rows = []
|
||||
self.decodeRows(self.layoutString)
|
||||
|
||||
@staticmethod
|
||||
def deriveLayout(layout, derivedType):
|
||||
'''Returns a layout derived from p_layout.'''
|
||||
res = layout
|
||||
for letter in Table.derivedRepls[derivedType]:
|
||||
res = res.replace(letter, '')
|
||||
# Strip the derived layout
|
||||
res = res.lstrip(rowDelms); res = res.lstrip(cellDelms)
|
||||
if derivedType == 'cell':
|
||||
res = res.rstrip(rowDelms); res = res.rstrip(cellDelms)
|
||||
return res
|
||||
|
||||
def addCssClasses(self, css_class):
|
||||
'''Adds a single or a group of p_css_class.'''
|
||||
if not self.css_class: self.css_class = css_class
|
||||
else:
|
||||
self.css_class += ' ' + css_class
|
||||
# Ensures that every class appears once
|
||||
self.css_class = ' '.join(set(self.css_class.split()))
|
||||
|
||||
def isHeaderRow(self, rowContent):
|
||||
'''Determines if p_rowContent specified the table header row or a
|
||||
content row.'''
|
||||
# Find the first char that is a number or a letter
|
||||
for char in rowContent:
|
||||
if char not in cellDelimiters:
|
||||
if char.isdigit(): return True
|
||||
else: return False
|
||||
return True
|
||||
|
||||
def decodeRows(self, layoutString):
|
||||
'''Decodes the given p_layoutString and produces a list of Row
|
||||
instances.'''
|
||||
# Split the p_layoutString with the row delimiters
|
||||
rowContent = ''
|
||||
for char in layoutString:
|
||||
if char in rowDelimiters:
|
||||
valign = rowDelimiters[char]
|
||||
if self.isHeaderRow(rowContent):
|
||||
if not self.headerRow:
|
||||
self.headerRow = Row(rowContent, valign, isHeader=True)
|
||||
else:
|
||||
self.rows.append(Row(rowContent, valign))
|
||||
rowContent = ''
|
||||
else:
|
||||
rowContent += char
|
||||
# Manage the last row if any
|
||||
if rowContent:
|
||||
self.rows.append(Row(rowContent, 'middle'))
|
||||
|
||||
def removeElement(self, elem):
|
||||
'''Removes given p_elem from myself.'''
|
||||
macroToRemove = pxDict[elem]
|
||||
for row in self.rows:
|
||||
for cell in row.cells:
|
||||
if macroToRemove in cell.content:
|
||||
cell.content.remove(macroToRemove)
|
||||
|
||||
# Some base layouts to use, for fields and pages -------------------------------
|
||||
# The default layouts for pages.
|
||||
defaultPageLayouts = {
|
||||
'view': Table('w-b'), 'edit': Table('w-b', width=None)}
|
||||
# A layout for pages, containing the page summary.
|
||||
summaryPageLayouts = {'view': Table('s-w-b'), 'edit': Table('w-b', width=None)}
|
||||
widePageLayouts = {'view': Table('w-b'), 'edit': Table('w-b')}
|
||||
centeredPageLayouts = {
|
||||
'view': Table('w|-b|', align="center"),
|
||||
'edit': Table('w|-b|', width=None, align='center')
|
||||
}
|
||||
|
||||
# The default layout for fields. Alternative layouts may exist and are declared
|
||||
# as static attributes of the concerned Type subclass.
|
||||
defaultFieldLayouts = {'edit': 'lrv-f', 'search': 'l-f'}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ColumnLayout:
|
||||
'''A "column layout" dictates the way a table column must be rendered. Such
|
||||
a layout is of the form: <name>[*width][,|!|`|`]
|
||||
* "name" is the name of the field whose content must be shown in
|
||||
column's cells;
|
||||
* "width" is the width of the column. Any valid value for the "width"
|
||||
attribute of the "td" HTML tag is accepted;
|
||||
* , | or ! indicates column alignment: respectively, left, centered or
|
||||
right.
|
||||
'''
|
||||
def __init__(self, layoutString):
|
||||
self.layoutString = layoutString
|
||||
def get(self):
|
||||
'''Returns a list containing the separate elements that are within
|
||||
self.layoutString.'''
|
||||
consumed = self.layoutString
|
||||
# Determine column alignment
|
||||
align = 'left'
|
||||
lastChar = consumed[-1]
|
||||
if lastChar in cellDelimiters:
|
||||
align = cellDelimiters[lastChar]
|
||||
consumed = consumed[:-1]
|
||||
# Determine name and width
|
||||
if '*' in consumed:
|
||||
name, width = consumed.rsplit('*', 1)
|
||||
else:
|
||||
name = consumed
|
||||
width = ''
|
||||
return name, width, align
|
||||
# ------------------------------------------------------------------------------
|
129
gen/mail.py
|
@ -1,129 +0,0 @@
|
|||
'''This package contains functions for sending email notifications.'''
|
||||
import smtplib, socket
|
||||
from email.MIMEMultipart import MIMEMultipart
|
||||
from email.MIMEBase import MIMEBase
|
||||
from email.MIMEText import MIMEText
|
||||
from email import Encoders
|
||||
from email.Header import Header
|
||||
from appy.shared.utils import sequenceTypes
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class MailConfig:
|
||||
'''Parameters for conneting to a SMTP server'''
|
||||
def __init__(self, fromName=None, fromEmail='info@appyframework.org',
|
||||
server='localhost', port=25, login=None, password=None,
|
||||
enabled=True):
|
||||
# The name that will appear in the "from" part of the messages
|
||||
self.fromName = fromName
|
||||
# The email that will appear in the "from" part of the messages
|
||||
self.fromEmail = fromEmail
|
||||
# The SMTP server address and port
|
||||
if ':' in server:
|
||||
self.server, port = server.split(':')
|
||||
self.port = int(port)
|
||||
else:
|
||||
self.server = server
|
||||
self.port = int(port) # That way, people can specify an int or str
|
||||
# Optional credentials to the SMTP server
|
||||
self.login = login
|
||||
self.password = password
|
||||
# Is this server connection enabled ?
|
||||
self.enabled = enabled
|
||||
|
||||
def getFrom(self):
|
||||
'''Gets the "from" part of the messages to send.'''
|
||||
if self.fromName: return '%s <%s>' % (self.fromName, self.fromEmail)
|
||||
return self.fromEmail
|
||||
|
||||
def __repr__(self):
|
||||
'''Short string representation of this mail config, for logging and
|
||||
debugging purposes.'''
|
||||
res = '%s:%d' % (self.server, self.port)
|
||||
if self.login: res += ' (login as %s)' % self.login
|
||||
return res
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
def sendMail(config, to, subject, body, attachments=None, log=None):
|
||||
'''Sends a mail, via the smtp server defined in the p_config (an instance of
|
||||
appy.gen.mail.MailConfig above), to p_to (a single email recipient or a
|
||||
list of recipients). Every (string) recipient can be an email address or
|
||||
a string of the form "[name] <[email]>".
|
||||
|
||||
p_attachment must be a list or tuple whose elements can have 2 forms:
|
||||
1. a tuple (fileName, fileContent): "fileName" is the name of the file
|
||||
as a string; "fileContent" is the file content, also as a string;
|
||||
2. a appy.fields.file.FileInfo instance.
|
||||
|
||||
p_log can be a function/method accepting a single string arg.
|
||||
'''
|
||||
if isinstance(to, str): to = [to]
|
||||
if not config:
|
||||
if log: log('Must send mail but no smtp server configured.')
|
||||
return
|
||||
# Just log things if mail is disabled
|
||||
fromAddress = config.getFrom()
|
||||
if not config.enabled or not config.server:
|
||||
if not config.server:
|
||||
msg = ' (no mailhost defined)'
|
||||
else:
|
||||
msg = ''
|
||||
if log:
|
||||
log('mail disabled%s: should send mail from %s to %d ' \
|
||||
'recipient(s): %s.' % (msg, fromAddress, len(to), str(to)))
|
||||
log('subject: %s' % subject)
|
||||
log('body: %s' % body)
|
||||
if attachments and log: log('%d attachment(s).' % len(attachments))
|
||||
return
|
||||
if log: log('sending mail from %s to %s (subject: %s).' % \
|
||||
(fromAddress, str(to), subject))
|
||||
# Create the base MIME message
|
||||
body = MIMEText(body, 'plain', 'utf-8')
|
||||
if attachments:
|
||||
msg = MIMEMultipart()
|
||||
msg.attach(body)
|
||||
else:
|
||||
msg = body
|
||||
# Add the header values
|
||||
msg['Subject'] = Header(subject, 'utf-8')
|
||||
msg['From'] = fromAddress
|
||||
if len(to) == 1:
|
||||
msg['To'] = to[0]
|
||||
else:
|
||||
msg['To'] = fromAddress
|
||||
msg['Bcc'] = ', '.join(to)
|
||||
to = fromAddress
|
||||
# Add attachments
|
||||
if attachments:
|
||||
for attachment in attachments:
|
||||
# 2 possible forms for an attachment
|
||||
if isinstance(attachment, tuple) or isinstance(attachment, list):
|
||||
fileName, fileContent = attachment
|
||||
else:
|
||||
# a FileInfo instance
|
||||
fileName = attachment.uploadName
|
||||
f = file(attachment.fsPath, 'rb')
|
||||
fileContent = f.read()
|
||||
f.close()
|
||||
part = MIMEBase('application', 'octet-stream')
|
||||
part.set_payload(fileContent)
|
||||
Encoders.encode_base64(part)
|
||||
part.add_header('Content-Disposition',
|
||||
'attachment; filename="%s"' % fileName)
|
||||
msg.attach(part)
|
||||
# Send the email
|
||||
try:
|
||||
smtpServer = smtplib.SMTP(config.server, port=config.port)
|
||||
if config.login:
|
||||
smtpServer.login(config.login, config.password)
|
||||
res = smtpServer.sendmail(fromAddress, to, msg.as_string())
|
||||
smtpServer.quit()
|
||||
if res and log:
|
||||
log('could not send mail to some recipients. %s' % str(res),
|
||||
type='warning')
|
||||
except smtplib.SMTPException as e:
|
||||
if log:
|
||||
log('%s: mail sending failed (%s)' % (config, str(e)), type='error')
|
||||
except socket.error as se:
|
||||
if log:
|
||||
log('%s: mail sending failed (%s)' % (config, str(se)),type='error')
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,83 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os.path, time
|
||||
from appy.fields.file import FileInfo
|
||||
from appy.shared import utils as sutils
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Migrator:
|
||||
'''This class is responsible for performing migrations, when, on
|
||||
installation, we've detected a new Appy version.'''
|
||||
def __init__(self, installer):
|
||||
self.installer = installer
|
||||
self.logger = installer.logger
|
||||
self.app = installer.app
|
||||
self.tool = self.app.config.appy()
|
||||
|
||||
@staticmethod
|
||||
def migrateBinaryFields(obj):
|
||||
'''Ensures all file and frozen pod fields on p_obj are FileInfo
|
||||
instances.'''
|
||||
migrated = 0 # Count the number of migrated fields
|
||||
for field in obj.fields:
|
||||
if field.type == 'File':
|
||||
oldValue = getattr(obj, field.name)
|
||||
if oldValue and not isinstance(oldValue, FileInfo):
|
||||
# A legacy File object. Convert it to a FileInfo instance
|
||||
# and extract the binary to the filesystem.
|
||||
setattr(obj, field.name, oldValue)
|
||||
migrated += 1
|
||||
elif field.type == 'Pod':
|
||||
frozen = getattr(obj.o, field.name, None)
|
||||
if frozen:
|
||||
# Dump this file on disk.
|
||||
tempFolder = sutils.getOsTempFolder()
|
||||
fmt = os.path.splitext(frozen.filename)[1][1:]
|
||||
fileName = os.path.join(tempFolder,
|
||||
'%f.%s' % (time.time(), fmt))
|
||||
f = file(fileName, 'wb')
|
||||
if frozen.data.__class__.__name__ == 'Pdata':
|
||||
# The file content is splitted in several chunks.
|
||||
f.write(frozen.data.data)
|
||||
nextPart = frozen.data.__next__
|
||||
while nextPart:
|
||||
f.write(nextPart.data)
|
||||
nextPart = nextPart.__next__
|
||||
else:
|
||||
# Only one chunk
|
||||
f.write(frozen.data)
|
||||
f.close()
|
||||
f = file(fileName)
|
||||
field.freeze(obj, template=field.template[0], format=fmt,
|
||||
noSecurity=True, upload=f,
|
||||
freezeOdtOnError=False)
|
||||
f.close()
|
||||
# Remove the legacy in-zodb file object
|
||||
setattr(obj.o, field.name, None)
|
||||
migrated += 1
|
||||
return migrated
|
||||
|
||||
def migrateTo_0_9_0(self):
|
||||
'''Migrates this DB to Appy 0.9.x.'''
|
||||
# Put all binaries to the filesystem
|
||||
tool = self.tool
|
||||
tool.log('Migrating binary fields...')
|
||||
context = {'migrate': self.migrateBinaryFields, 'nb': 0}
|
||||
for className in tool.o.getAllClassNames():
|
||||
tool.compute(className, context=context, noSecurity=True,
|
||||
expression="ctx['nb'] += ctx['migrate'](obj)")
|
||||
tool.log('Migrated %d binary field(s).' % context['nb'])
|
||||
|
||||
def run(self, force=False):
|
||||
'''Executes a migration when relevant, or do it for sure if p_force is
|
||||
True.'''
|
||||
appyVersion = self.tool.appyVersion
|
||||
# appyVersion being None simply means that we are creating a new DB.
|
||||
if force or (appyVersion and (appyVersion < '0.9.0')):
|
||||
# Migration is required.
|
||||
self.logger.info('Appy version (DB) is %s' % appyVersion)
|
||||
startTime = time.time()
|
||||
self.migrateTo_0_9_0()
|
||||
stopTime = time.time()
|
||||
elapsed = (stopTime-startTime) / 60.0
|
||||
self.logger.info('Migration done in %d minute(s).' % elapsed)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,85 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, sys
|
||||
try:
|
||||
from AccessControl.SecurityManagement import \
|
||||
newSecurityManager, noSecurityManager
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class TestMixin:
|
||||
'''This class is mixed in with any ZopeTestCase.'''
|
||||
def getNonEmptySubModules(self, moduleName):
|
||||
'''Returns the list of 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 as ie:
|
||||
return res
|
||||
except SyntaxError as 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__.keys():
|
||||
if not elem.startswith('__'):
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def getCovFolder():
|
||||
'''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
|
||||
def _setup(self): pass
|
||||
|
||||
# Functions executed before and after every test -------------------------------
|
||||
def beforeTest(test):
|
||||
'''Is executed before every test.'''
|
||||
g = test.globs
|
||||
g['tool'] = test.app.config.appy()
|
||||
cfg = g['tool'].o.getProductConfig()
|
||||
g['appFolder'] = cfg.diskFolder
|
||||
moduleOrClassName = g['test'].name # Not used yet.
|
||||
# Initialize the test
|
||||
g['t'] = g['test']
|
||||
|
||||
def afterTest(test):
|
||||
'''Is executed after every test.'''
|
||||
g = test.globs
|
||||
appName = g['tool'].o.getAppName()
|
||||
exec('from Products.%s import cov, covFolder, totalNumberOfTests, ' \
|
||||
'countTest' % appName)
|
||||
countTest()
|
||||
exec('from Products.%s import numberOfExecutedTests' % appName)
|
||||
if cov and (numberOfExecutedTests == totalNumberOfTests):
|
||||
cov.stop()
|
||||
appModules = test.getNonEmptySubModules(appName)
|
||||
# Dumps the coverage report
|
||||
# HTML version
|
||||
cov.html_report(directory=covFolder, morfs=appModules)
|
||||
# Summary in a text file
|
||||
f = file('%s/summary.txt' % covFolder, 'w')
|
||||
cov.report(file=f, morfs=appModules)
|
||||
f.close()
|
||||
# Annotated modules
|
||||
cov.annotate(directory=covFolder, morfs=appModules)
|
||||
# ------------------------------------------------------------------------------
|
317
gen/model.py
|
@ -1,317 +0,0 @@
|
|||
'''This file contains basic classes that will be added into any user
|
||||
application for creating the basic structure of the application "Tool" which
|
||||
is the set of web pages used for configuring the application.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import types
|
||||
import appy.gen as gen
|
||||
import collections
|
||||
|
||||
# Prototypical instances of every type -----------------------------------------
|
||||
class Protos:
|
||||
protos = {}
|
||||
# List of attributes that can't be given to a Type constructor
|
||||
notInit = ('id', 'type', 'pythonType', 'slaves', 'isSelect', 'hasLabel',
|
||||
'hasDescr', 'hasHelp', 'required', 'filterable', 'validable',
|
||||
'isBack', 'pageName', 'masterName', 'renderLabel')
|
||||
@classmethod
|
||||
def get(self, appyType):
|
||||
'''Returns a prototype instance for p_appyType.'''
|
||||
className = appyType.__class__.__name__
|
||||
isString = (className == 'String')
|
||||
if isString:
|
||||
# For Strings, we create one prototype per format, because default
|
||||
# values may change according to format.
|
||||
className += str(appyType.format)
|
||||
if className in self.protos: return self.protos[className]
|
||||
# The prototype does not exist yet: create it
|
||||
if isString:
|
||||
proto = appyType.__class__(format=appyType.format)
|
||||
# Now, we fake to be able to detect default values
|
||||
proto.format = 0
|
||||
else:
|
||||
proto = appyType.__class__()
|
||||
self.protos[className] = proto
|
||||
return proto
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class ModelClass:
|
||||
'''This class is the abstract class of all predefined application classes
|
||||
used in the Appy model: Tool, User, etc. All methods and attributes of
|
||||
those classes are part of the Appy machinery and are prefixed with _appy_
|
||||
in order to avoid name conflicts with user-defined parts of the
|
||||
application model.'''
|
||||
# In any ModelClass subclass we need to declare attributes in the following
|
||||
# list (including back attributes), to keep track of attributes order.
|
||||
_appy_attributes = []
|
||||
folder = False
|
||||
@classmethod
|
||||
def _appy_getTypeBody(klass, appyType, wrapperName):
|
||||
'''This method returns the code declaration for p_appyType.'''
|
||||
typeArgs = ''
|
||||
proto = Protos.get(appyType)
|
||||
for name, value in appyType.__dict__.items():
|
||||
# Some attrs can't be given to the constructor
|
||||
if name in Protos.notInit: continue
|
||||
# If the given value corresponds to the default value, don't give it
|
||||
if value == getattr(proto, name): continue
|
||||
if name == 'layouts':
|
||||
# For Tool attributes we do not copy layout info. Indeed, most
|
||||
# fields added to the Tool are config-related attributes whose
|
||||
# layouts must be standard.
|
||||
if klass.__name__ == 'Tool': continue
|
||||
layouts = appyType.getInputLayouts()
|
||||
# For the Translation class that has potentially thousands of
|
||||
# attributes, the most used layout is cached in a global var in
|
||||
# named "tfw" in wrappers.py.
|
||||
if (klass.__name__ == 'Translation') and \
|
||||
(layouts == '{"edit":"f","cell":"f","view":"f",}'):
|
||||
value = 'tfw'
|
||||
else:
|
||||
value = appyType.getInputLayouts()
|
||||
elif (name == 'klass') and value and (value == klass):
|
||||
# This is a auto-Ref (a Ref that references the klass itself).
|
||||
# At this time, we can't reference the class that is still being
|
||||
# defined. So we initialize it to None. The post-init of the
|
||||
# field must be done manually in wrappers.py.
|
||||
value = 'None'
|
||||
elif isinstance(value, str):
|
||||
value = '"%s"' % value
|
||||
elif isinstance(value, gen.Ref):
|
||||
if not value.isBack: continue
|
||||
value = klass._appy_getTypeBody(value, wrapperName)
|
||||
elif type(value) == type(ModelClass):
|
||||
moduleName = value.__module__
|
||||
if moduleName.startswith('appy.gen'):
|
||||
value = value.__name__
|
||||
else:
|
||||
value = '%s.%s' % (moduleName, value.__name__)
|
||||
elif isinstance(value, gen.Selection):
|
||||
value = 'Selection("%s")' % value.methodName
|
||||
elif isinstance(value, gen.Group):
|
||||
value = 'Grp("%s")' % value.name
|
||||
elif isinstance(value, gen.Page):
|
||||
value = 'pges["%s"]' % value.name
|
||||
elif isinstance(value, collections.Callable):
|
||||
className = wrapperName
|
||||
if (appyType.type == 'Ref') and appyType.isBack:
|
||||
className = value.__self__.__class__.__name__
|
||||
value = '%s.%s' % (className, value.__name__)
|
||||
typeArgs += '%s=%s,' % (name, value)
|
||||
return '%s(%s)' % (appyType.__class__.__name__, typeArgs)
|
||||
|
||||
@classmethod
|
||||
def _appy_getBody(klass):
|
||||
'''This method returns the code declaration of this class. We will dump
|
||||
this in wrappers.py in the Zope product.'''
|
||||
className = klass.__name__
|
||||
# Determine the name of the class and its wrapper. Because so much
|
||||
# attributes can be generated on a TranslationWrapper, shortcutting it
|
||||
# to 'TW' may reduce the generated file from several kilobytes.
|
||||
if className == 'Translation': wrapperName = 'WT'
|
||||
else: wrapperName = 'W%s' % className
|
||||
res = 'class %s(%s):\n' % (className, wrapperName)
|
||||
# Tool must be folderish
|
||||
if klass.folder: res += ' folder=True\n'
|
||||
# First, scan all attributes, determine all used pages and create a
|
||||
# dict with it. It will prevent us from creating a new Page instance
|
||||
# for every field.
|
||||
pages = {}
|
||||
layouts = []
|
||||
for name in klass._appy_attributes:
|
||||
exec('appyType = klass.%s' % name)
|
||||
if appyType.page.name not in pages:
|
||||
pages[appyType.page.name] = appyType.page
|
||||
res += ' pges = {'
|
||||
for page in pages.values():
|
||||
# Determine page "show" attributes
|
||||
pShow = ''
|
||||
for attr in ('',) + page.subElements:
|
||||
attrName = 'show%s' % attr.capitalize()
|
||||
pageShow = getattr(page, attrName)
|
||||
if isinstance(pageShow, str): pageShow='"%s"' % pageShow
|
||||
elif callable(pageShow):
|
||||
pageShow = '%s.%s' % (wrapperName, pageShow.__name__)
|
||||
if pageShow != True:
|
||||
pShow += ', %s=%s' % (attrName, pageShow)
|
||||
# For translation pages, fixed labels are used.
|
||||
label = ''
|
||||
if className == 'Translation':
|
||||
name = (page.name == 'main') and 'Options' or page.name
|
||||
label = ', label="%s"' % name
|
||||
res += '"%s":Pge("%s"%s%s),' % (page.name, page.name, pShow, label)
|
||||
res += '}\n'
|
||||
# Secondly, dump every (not Ref.isBack) attribute
|
||||
for name in klass._appy_attributes:
|
||||
exec('appyType = klass.%s' % name)
|
||||
if (appyType.type == 'Ref') and appyType.isBack: continue
|
||||
typeBody = klass._appy_getTypeBody(appyType, wrapperName)
|
||||
res += ' %s=%s\n' % (name, typeBody)
|
||||
return res
|
||||
|
||||
# The User class ---------------------------------------------------------------
|
||||
class User(ModelClass):
|
||||
_appy_attributes = ['password1', 'password2', 'title', 'name', 'firstName',
|
||||
'source', 'login', 'password3', 'password4', 'email',
|
||||
'roles', 'groups', 'toTool']
|
||||
# All methods defined below are fake. Real versions are in the wrapper.
|
||||
# Passwords are on a specific page.
|
||||
def showPassword12(self): pass
|
||||
def showPassword34(self): pass
|
||||
def validatePassword(self): pass
|
||||
pp = {'page': gen.Page('passwords', showNext=False, show=showPassword12),
|
||||
'width': 34, 'multiplicity': (1,1), 'format': gen.String.PASSWORD,
|
||||
'show': showPassword12}
|
||||
password1 = gen.String(validator=validatePassword, **pp)
|
||||
password2 = gen.String(**pp)
|
||||
|
||||
# Fields "password3" and "password4" are only shown when creating a user.
|
||||
# After user creation, those fields are not used anymore; fields "password1"
|
||||
# and "password2" above are then used to modify the password on a separate
|
||||
# page.
|
||||
pm = {'page': gen.Page('main', showPrevious=False), 'group': 'main',
|
||||
'width': 34}
|
||||
title = gen.String(show=False, indexed=True, **pm)
|
||||
def showName(self): pass
|
||||
name = gen.String(show=showName, **pm)
|
||||
firstName = gen.String(show=showName, **pm)
|
||||
# Where is this user stored? By default, in the ZODB. But the user can be
|
||||
# stored in an external LDAP (source='ldap').
|
||||
source = gen.String(show=False, default='zodb', layouts='f', **pm)
|
||||
pm['multiplicity'] = (1,1)
|
||||
def showLogin(self): pass
|
||||
def validateLogin(self): pass
|
||||
login = gen.String(show=showLogin, validator=validateLogin,
|
||||
indexed=True, **pm)
|
||||
password3 = gen.String(validator=validatePassword, show=showPassword34,
|
||||
format=gen.String.PASSWORD,
|
||||
label=(None, 'password1'), **pm)
|
||||
password4 = gen.String(show=showPassword34, format=gen.String.PASSWORD,
|
||||
label=(None, 'password2'), **pm)
|
||||
def showEmail(self): pass
|
||||
email = gen.String(show=showEmail, **pm)
|
||||
pm['multiplicity'] = (0, None)
|
||||
def showRoles(self): pass
|
||||
roles = gen.String(show=showRoles, indexed=True,
|
||||
validator=gen.Selection('getGrantableRoles'), **pm)
|
||||
|
||||
# The Group class --------------------------------------------------------------
|
||||
class Group(ModelClass):
|
||||
_appy_attributes = ['title', 'login', 'roles', 'users', 'toTool2']
|
||||
# All methods defined below are fake. Real versions are in the wrapper.
|
||||
m = {'group': 'main', 'width': 25, 'indexed': True}
|
||||
title = gen.String(multiplicity=(1,1), **m)
|
||||
def showLogin(self): pass
|
||||
def validateLogin(self): pass
|
||||
login = gen.String(show=showLogin, validator=validateLogin,
|
||||
multiplicity=(1,1), **m)
|
||||
roles = gen.String(validator=gen.Selection('getGrantableRoles'),
|
||||
multiplicity=(0,None), **m)
|
||||
def getSelectableUsers(self): pass
|
||||
users = gen.Ref(User, multiplicity=(0,None), add=False, link=True,
|
||||
back=gen.Ref(attribute='groups', show=User.showRoles,
|
||||
multiplicity=(0,None)),
|
||||
select=getSelectableUsers, height=15,
|
||||
showHeaders=True, shownInfo=('title', 'login'),
|
||||
showActions='inline')
|
||||
|
||||
# The Translation class --------------------------------------------------------
|
||||
class Translation(ModelClass):
|
||||
_appy_attributes = ['po', 'title', 'sourceLanguage', 'trToTool']
|
||||
# All methods defined below are fake. Real versions are in the wrapper.
|
||||
title = gen.String(show=False, indexed=True,
|
||||
page=gen.Page('main',label='Main'))
|
||||
def getPoFile(self): pass
|
||||
po = gen.Action(action=getPoFile, result='file')
|
||||
sourceLanguage = gen.String(width=4)
|
||||
def label(self): pass
|
||||
def show(self, name): pass
|
||||
|
||||
# The Page class ---------------------------------------------------------------
|
||||
class Page(ModelClass):
|
||||
_appy_attributes = ['title', 'content', 'pages', 'parent', 'toTool3']
|
||||
folder = True
|
||||
title = gen.String(show='edit', multiplicity=(1,1), indexed=True)
|
||||
content = gen.String(format=gen.String.XHTML, layouts='f')
|
||||
# Pages can contain other pages.
|
||||
def showSubPages(self): pass
|
||||
pages = gen.Ref(None, multiplicity=(0,None), add=True, link=False,
|
||||
back=gen.Ref(attribute='parent', show=False),
|
||||
show=showSubPages, navigable=True)
|
||||
Page.pages.klass = Page
|
||||
setattr(Page, Page.pages.back.attribute, Page.pages.back)
|
||||
|
||||
# The Tool class ---------------------------------------------------------------
|
||||
# Prefixes of the fields generated on the Tool.
|
||||
defaultToolFields = ('title', 'mailHost', 'mailEnabled', 'mailFrom',
|
||||
'appyVersion', 'dateFormat', 'hourFormat',
|
||||
'unoEnabledPython', 'openOfficePort',
|
||||
'numberOfResultsPerPage', 'users',
|
||||
'connectedUsers', 'synchronizeExternalUsers', 'groups',
|
||||
'translations', 'loadTranslationsAtStartup', 'pages')
|
||||
|
||||
class Tool(ModelClass):
|
||||
# In a ModelClass we need to declare attributes in the following list
|
||||
_appy_attributes = list(defaultToolFields)
|
||||
folder = True
|
||||
|
||||
# Tool attributes
|
||||
def isManager(self): pass
|
||||
def isManagerEdit(self): pass
|
||||
lf = {'layouts':'f'}
|
||||
title = gen.String(show=False, page=gen.Page('main', show=False),
|
||||
default='Configuration', **lf)
|
||||
mailHost = gen.String(default='localhost:25', **lf)
|
||||
mailEnabled = gen.Boolean(default=False, **lf)
|
||||
mailFrom = gen.String(default='info@appyframework.org', **lf)
|
||||
appyVersion = gen.String(**lf)
|
||||
dateFormat = gen.String(default='%d/%m/%Y', **lf)
|
||||
hourFormat = gen.String(default='%H:%M', **lf)
|
||||
unoEnabledPython = gen.String(default='/usr/bin/python', **lf)
|
||||
openOfficePort = gen.Integer(default=2002, **lf)
|
||||
numberOfResultsPerPage = gen.Integer(default=30, **lf)
|
||||
|
||||
# Ref(User) will maybe be transformed into Ref(CustomUserClass)
|
||||
userPage = gen.Page('users', show=isManager)
|
||||
users = gen.Ref(User, multiplicity=(0,None), add=True, link=False,
|
||||
back=gen.Ref(attribute='toTool', show=False), page=userPage,
|
||||
queryable=True, queryFields=('title', 'login'),
|
||||
show=isManager, showHeaders=True, showActions='inline',
|
||||
shownInfo=('title', 'login*120px', 'roles*120px'))
|
||||
|
||||
def computeConnectedUsers(self): pass
|
||||
connectedUsers = gen.Computed(method=computeConnectedUsers, page=userPage,
|
||||
plainText=False, show=isManager)
|
||||
def doSynchronizeExternalUsers(self): pass
|
||||
def showSynchronizeUsers(self): pass
|
||||
synchronizeExternalUsers = gen.Action(action=doSynchronizeExternalUsers,
|
||||
show=showSynchronizeUsers, confirm=True, page=userPage)
|
||||
|
||||
groups = gen.Ref(Group, multiplicity=(0,None), add=True, link=False,
|
||||
back=gen.Ref(attribute='toTool2', show=False),
|
||||
page=gen.Page('groups', show=isManager), show=isManager,
|
||||
queryable=True, queryFields=('title', 'login'),
|
||||
showHeaders=True, showActions='inline',
|
||||
shownInfo=('title', 'login*120px', 'roles*120px'))
|
||||
pt = gen.Page('translations', show=isManager)
|
||||
translations = gen.Ref(Translation, multiplicity=(0,None), add=False,
|
||||
link=False, show='view', page=pt,
|
||||
back=gen.Ref(attribute='trToTool', show=False))
|
||||
loadTranslationsAtStartup = gen.Boolean(default=True, show=False, page=pt,
|
||||
layouts='f')
|
||||
pages = gen.Ref(Page, multiplicity=(0,None), add=True, link=False,
|
||||
show='view', back=gen.Ref(attribute='toTool3', show=False),
|
||||
showActions='inline', page=gen.Page('pages',show=isManager))
|
||||
|
||||
@classmethod
|
||||
def _appy_clean(klass):
|
||||
toClean = []
|
||||
for k, v in klass.__dict__.items():
|
||||
if not k.startswith('__') and (not k.startswith('_appy_')):
|
||||
if k not in defaultToolFields:
|
||||
toClean.append(k)
|
||||
for k in toClean:
|
||||
exec('del klass.%s' % k)
|
||||
klass._appy_attributes = list(defaultToolFields)
|
||||
klass.folder = True
|
||||
# ------------------------------------------------------------------------------
|
220
gen/navigate.py
|
@ -1,220 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.px import Px
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Siblings:
|
||||
'''Abstract class containing information for navigating from one object to
|
||||
its siblings.'''
|
||||
siblingTypes = ('previous', 'next', 'first', 'last')
|
||||
|
||||
# Buttons for going to siblings of the current object.
|
||||
pxNavigate = Px('''
|
||||
<!-- Go to the source URL (search or referred object) -->
|
||||
<a if="not inPopup" href=":self.sourceUrl"><img
|
||||
var="goBack='%s - %s' % (self.getBackText(), _('goto_source'))"
|
||||
src=":url('gotoSource')" title=":goBack"/></a>
|
||||
|
||||
<!-- Go to the first or previous page -->
|
||||
<a if="self.firstUrl" href=":self.firstUrl"><img title=":_('goto_first')"
|
||||
src=":url('arrowsLeft')"/></a><a
|
||||
if="self.previousUrl" href=":self.previousUrl"><img
|
||||
title=":_('goto_previous')" src=":url('arrowLeft')"/></a>
|
||||
|
||||
<!-- Explain which element is currently shown -->
|
||||
<span class="discreet">
|
||||
<x>:self.number</x> <b>//</b>
|
||||
<x>:self.total</x> </span>
|
||||
|
||||
<!-- Go to the next or last page -->
|
||||
<a if="self.nextUrl" href=":self.nextUrl"><img title=":_('goto_next')"
|
||||
src=":url('arrowRight')"/></a><a
|
||||
if="self.lastUrl" href=":self.lastUrl"><img title=":_('goto_last')"
|
||||
src=":url('arrowsRight')"/></a>
|
||||
|
||||
<!-- Go to the element number... -->
|
||||
<x if="self.showGotoNumber()"
|
||||
var2="field=self.field; sourceUrl=self.sourceObject.absolute_url();
|
||||
totalNumber=self.total"><br/><x>:obj.pxGotoNumber</x></x>''')
|
||||
|
||||
@staticmethod
|
||||
def get(nav, tool, inPopup):
|
||||
'''This method analyses the navigation info p_nav and returns the
|
||||
corresponding concrete Siblings instance.'''
|
||||
elems = nav.split('.')
|
||||
params = elems[1:]
|
||||
if elems[0] == 'ref': return RefSiblings(tool, inPopup, *params)
|
||||
elif elems[0] == 'search': return SearchSiblings(tool, inPopup, *params)
|
||||
|
||||
def computeStartNumber(self):
|
||||
'''Returns the start number of the batch where the current element
|
||||
lies.'''
|
||||
# First index starts at O, so we calibrate self.number
|
||||
number = self.number - 1
|
||||
batchSize = self.getBatchSize()
|
||||
res = 0
|
||||
while (res < self.total):
|
||||
if (number < res + batchSize): return res
|
||||
res += batchSize
|
||||
return res
|
||||
|
||||
def __init__(self, tool, inPopup, number, total):
|
||||
self.tool = tool
|
||||
self.request = tool.REQUEST
|
||||
# Are we in a popup window or not?
|
||||
self.inPopup = inPopup
|
||||
# The number of the current element
|
||||
self.number = int(number)
|
||||
# The total number of siblings
|
||||
self.total = int(total)
|
||||
# Do I need to navigate to first, previous, next and/or last sibling ?
|
||||
self.previousNeeded = False # Previous ?
|
||||
self.previousIndex = self.number - 2
|
||||
if (self.previousIndex > -1) and (self.total > self.previousIndex):
|
||||
self.previousNeeded = True
|
||||
self.nextNeeded = False # Next ?
|
||||
self.nextIndex = self.number
|
||||
if self.nextIndex < self.total: self.nextNeeded = True
|
||||
self.firstNeeded = False # First ?
|
||||
self.firstIndex = 0
|
||||
if self.previousIndex > 0: self.firstNeeded = True
|
||||
self.lastNeeded = False # Last ?
|
||||
self.lastIndex = self.total - 1
|
||||
if (self.nextIndex < self.lastIndex): self.lastNeeded = True
|
||||
# Compute the UIDs of the siblings of the current object
|
||||
self.siblings = self.getSiblings()
|
||||
# Compute back URL and URLs to siblings
|
||||
self.sourceUrl = self.getSourceUrl()
|
||||
siblingNav = self.getNavKey()
|
||||
siblingPage = self.request.get('page', 'main')
|
||||
for urlType in self.siblingTypes:
|
||||
exec 'needIt = self.%sNeeded' % urlType
|
||||
urlKey = '%sUrl' % urlType
|
||||
setattr(self, urlKey, None)
|
||||
if not needIt: continue
|
||||
exec 'index = self.%sIndex' % urlType
|
||||
uid = None
|
||||
try:
|
||||
# self.siblings can be a list (ref) or a dict (search)
|
||||
uid = self.siblings[index]
|
||||
except KeyError: continue
|
||||
except IndexError: continue
|
||||
if not uid: continue
|
||||
sibling = self.tool.getObject(uid)
|
||||
if not sibling: continue
|
||||
setattr(self, urlKey, sibling.getUrl(nav=siblingNav % (index + 1),
|
||||
page=siblingPage, inPopup=inPopup))
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class RefSiblings(Siblings):
|
||||
'''Class containing information for navigating from one object to another
|
||||
within tied objects from a Ref field.'''
|
||||
prefix = 'ref'
|
||||
|
||||
def __init__(self, tool, inPopup, sourceUid, fieldName, number, total):
|
||||
# The source object of the Ref field
|
||||
self.sourceObject = tool.getObject(sourceUid)
|
||||
# The Ref field in itself
|
||||
self.field = self.sourceObject.getAppyType(fieldName)
|
||||
# Call the base constructor
|
||||
Siblings.__init__(self, tool, inPopup, number, total)
|
||||
|
||||
def getNavKey(self):
|
||||
'''Returns the general navigation key for navigating to another
|
||||
sibling.'''
|
||||
return self.field.getNavInfo(self.sourceObject, None, self.total)
|
||||
|
||||
def getBackText(self):
|
||||
'''Computes the text to display when the user want to navigate back to
|
||||
the list of tied objects.'''
|
||||
_ = self.tool.translate
|
||||
return '%s - %s' % (self.sourceObject.Title(), _(self.field.labelId))
|
||||
|
||||
def getBatchSize(self):
|
||||
'''Returns the maximum number of shown objects at a time for this
|
||||
ref.'''
|
||||
return self.field.maxPerPage
|
||||
|
||||
def getSiblings(self):
|
||||
'''Returns the siblings of the current object.'''
|
||||
return getattr(self.sourceObject, self.field.name, ())
|
||||
|
||||
def getSourceUrl(self):
|
||||
'''Computes the URL allowing to go back to self.sourceObject's page
|
||||
where self.field lies and shows the list of tied objects, at the
|
||||
batch where the current object lies.'''
|
||||
# Allow to go back to the batch where the current object lies
|
||||
field = self.field
|
||||
startNumberKey = '%s_%s_objs_startNumber' % \
|
||||
(self.sourceObject.id,field.name)
|
||||
startNumber = str(self.computeStartNumber())
|
||||
return self.sourceObject.getUrl(**{startNumberKey:startNumber,
|
||||
'page':field.pageName, 'nav':''})
|
||||
|
||||
def showGotoNumber(self):
|
||||
'''Show "goto number" if the Ref field is numbered.'''
|
||||
return self.field.isNumbered(self.sourceObject)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class SearchSiblings(Siblings):
|
||||
'''Class containing information for navigating from one object to another
|
||||
within results of a search.'''
|
||||
prefix = 'search'
|
||||
|
||||
def __init__(self, tool, inPopup, className, searchName, number, total):
|
||||
# The class determining the type of searched objects
|
||||
self.className = className
|
||||
# Get the search object
|
||||
self.searchName = searchName
|
||||
self.uiSearch = tool.getSearch(className, searchName, ui=True)
|
||||
self.search = self.uiSearch.search
|
||||
Siblings.__init__(self, tool, inPopup, number, total)
|
||||
|
||||
def getNavKey(self):
|
||||
'''Returns the general navigation key for navigating to another
|
||||
sibling.'''
|
||||
return 'search.%s.%s.%%d.%d' % (self.className, self.searchName,
|
||||
self.total)
|
||||
|
||||
def getBackText(self):
|
||||
'''Computes the text to display when the user want to navigate back to
|
||||
the list of searched objects.'''
|
||||
return self.uiSearch.translated
|
||||
|
||||
def getBatchSize(self):
|
||||
'''Returns the maximum number of shown objects at a time for this
|
||||
search.'''
|
||||
return self.search.maxPerPage
|
||||
|
||||
def getSiblings(self):
|
||||
'''Returns the siblings of the current object. For performance reasons,
|
||||
only a part of the is stored, in the session object.'''
|
||||
session = self.request.SESSION
|
||||
searchKey = self.search.getSessionKey(self.className)
|
||||
if session.has_key(searchKey): res = session[searchKey]
|
||||
else: res = {}
|
||||
if (self.previousNeeded and not res.has_key(self.previousIndex)) or \
|
||||
(self.nextNeeded and not res.has_key(self.nextIndex)):
|
||||
# The needed sibling UID is not in session. We will need to
|
||||
# retrigger the query by querying all objects surrounding this one.
|
||||
newStartNumber = (self.number-1) - (self.search.maxPerPage / 2)
|
||||
if newStartNumber < 0: newStartNumber = 0
|
||||
self.tool.executeQuery(self.className, search=self.search,
|
||||
startNumber=newStartNumber, remember=True)
|
||||
res = session[searchKey]
|
||||
# For the moment, for first and last, we get them only if we have them
|
||||
# in session.
|
||||
if not res.has_key(0): self.firstNeeded = False
|
||||
if not res.has_key(self.lastIndex): self.lastNeeded = False
|
||||
return res
|
||||
|
||||
def getSourceUrl(self):
|
||||
'''Computes the (non-Ajax) URL allowing to go back to the search
|
||||
results, at the batch where the current object lies.'''
|
||||
params = 'className=%s&search=%s&startNumber=%d' % \
|
||||
(self.className, self.searchName, self.computeStartNumber())
|
||||
ref = self.request.get('ref', None)
|
||||
if ref: params += '&ref=%s' % ref
|
||||
return '%s/query?%s' % (self.tool.absolute_url(), params)
|
||||
|
||||
def showGotoNumber(self): return
|
||||
# ------------------------------------------------------------------------------
|
337
gen/po.py
|
@ -1,337 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, re, time, copy
|
||||
from .utils import produceNiceMessage
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
poHeader = '''msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: %s\\n"
|
||||
"POT-Creation-Date: %s\\n"
|
||||
"MIME-Version: 1.0\\n"
|
||||
"Content-Type: text/plain; charset=utf-8\\n"
|
||||
"Content-Transfer-Encoding: 8bit\\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\\n"
|
||||
"Language-code: %s\\n"
|
||||
"Language-name: %s\\n"
|
||||
"Preferred-encodings: utf-8 latin1\\n"
|
||||
"Domain: %s\\n"
|
||||
%s
|
||||
|
||||
'''
|
||||
fallbacks = {'en': 'en-us en-ca',
|
||||
'fr': 'fr-be fr-ca fr-lu fr-mc fr-ch fr-fr'}
|
||||
|
||||
# Default values for i18n labels whose ids are not fixed.
|
||||
CONFIG = "Configuration panel for product '%s'"
|
||||
CONFIRM = 'Are you sure ?'
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class PoMessage:
|
||||
'''Represents a i18n message (po format).'''
|
||||
def __init__(self, id, msg, default, fuzzy=False, comments=[],
|
||||
niceDefault=False):
|
||||
self.id = id
|
||||
self.msg = msg
|
||||
self.default = default
|
||||
if niceDefault: self.produceNiceDefault()
|
||||
self.fuzzy = fuzzy # True if the default value has changed in the pot
|
||||
# file: the msg in the po file needs to be translated again.
|
||||
self.comments = comments
|
||||
|
||||
def update(self, newMsg, isPot, language):
|
||||
'''Updates me with new values from p_newMsg. If p_isPot is True (which
|
||||
means that the current file is a pot file), I do not care about
|
||||
filling self.msg.'''
|
||||
if isPot:
|
||||
self.msg = ""
|
||||
if not self.default:
|
||||
self.default = newMsg.default
|
||||
# It means that if the default message has changed, we will not
|
||||
# update it in the pot file. We will always keep the one that
|
||||
# the user may have changed in the pot file. We will write a
|
||||
# default message only when no default message is defined.
|
||||
else:
|
||||
# newMsg comes from a pot file or from a base po file (like a
|
||||
# standard Appy po file). We must update the corresponding
|
||||
# message in the current po file.
|
||||
oldDefault = self.default
|
||||
if self.default != newMsg.default:
|
||||
# The default value has changed in the pot file
|
||||
oldDefault = self.default
|
||||
self.default = newMsg.default
|
||||
self.fuzzy = False
|
||||
if self.msg.strip():
|
||||
self.fuzzy = True
|
||||
# We mark the message as "fuzzy" (=may need to be rewritten
|
||||
# because the default value has changed) only if the user
|
||||
# has already entered a message. Else, this has no sense to
|
||||
# rewrite the empty message.
|
||||
if not oldDefault.strip():
|
||||
# This is a strange case: the old default value did not
|
||||
# exist. Maybe was this PO file generated from some
|
||||
# tool, but simply without any default value. So in
|
||||
# this case, we do not consider the label as fuzzy.
|
||||
self.fuzzy = False
|
||||
# If p_newMsg contains a message, and no message is defined for
|
||||
# self, copy it.
|
||||
if newMsg.msg and not self.msg:
|
||||
self.msg = newMsg.msg
|
||||
# For english, the the default value from a pot file can be used as
|
||||
# value for the po file.
|
||||
if (language == 'en'):
|
||||
if not self.msg:
|
||||
# Put the default message into msg for english
|
||||
self.msg = self.default
|
||||
if self.fuzzy and (self.msg == oldDefault):
|
||||
# The message was equal to the old default value. It means
|
||||
# that the user did not change it, because for English we
|
||||
# fill by default the message with the default value (see
|
||||
# code just above). So in this case, the message was not
|
||||
# really fuzzy.
|
||||
self.fuzzy = False
|
||||
self.msg = self.default
|
||||
|
||||
def produceNiceDefault(self):
|
||||
'''Transforms self.default into a nice msg.'''
|
||||
self.default = produceNiceMessage(self.default)
|
||||
|
||||
def generate(self):
|
||||
'''Produces myself as I must appear in a po(t) file.'''
|
||||
res = ''
|
||||
for comment in self.comments:
|
||||
res += comment + '\n'
|
||||
if self.default != None:
|
||||
res = '#. Default: "%s"\n' % self.default
|
||||
if self.fuzzy:
|
||||
res += '#, fuzzy\n'
|
||||
res += 'msgid "%s"\n' % self.id
|
||||
res += 'msgstr "%s"\n' % self.msg
|
||||
return res
|
||||
|
||||
def __repr__(self):
|
||||
return '<i18n msg id="%s", msg="%s", default="%s">' % \
|
||||
(self.id, self.msg, self.default)
|
||||
|
||||
def getMessage(self):
|
||||
'''Returns self.msg, but with some replacements.'''
|
||||
return self.msg.replace('<br/>', '\n').replace('\\"', '"')
|
||||
|
||||
class PoMessages:
|
||||
'''A list of po messages under construction.'''
|
||||
def __init__(self):
|
||||
# The list of messages
|
||||
self.messages = []
|
||||
# A dict of message ids, useful for efficiently checking if an id is
|
||||
# already in the list or not.
|
||||
self.ids = {}
|
||||
|
||||
def append(self, id, default, nice=True):
|
||||
'''Creates a new PoMessage and adds it to self.messages. If p_nice is
|
||||
True, it produces a nice default value for the message.'''
|
||||
# Avoir creating duplicate ids
|
||||
if id in self.ids: return
|
||||
message = PoMessage(id, '', default, niceDefault=nice)
|
||||
self.messages.append(message)
|
||||
self.ids[id] = True
|
||||
|
||||
def get(self): return self.messages
|
||||
|
||||
class PoHeader:
|
||||
def __init__(self, name, value):
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def generate(self):
|
||||
'''Generates the representation of myself into a po(t) file.'''
|
||||
return '"%s: %s\\n"\n' % (self.name, self.value)
|
||||
|
||||
class PoFile:
|
||||
'''Represents a i18n file.'''
|
||||
def __init__(self, fileName):
|
||||
self.fileName = fileName
|
||||
self.isPot = fileName.endswith('.pot')
|
||||
self.messages = [] # Ordered list of messages
|
||||
self.messagesDict = {} # Dict of the same messages, indexed by msgid
|
||||
self.headers = []
|
||||
self.headersDict = {}
|
||||
# Get application name, domain name and language from fileName
|
||||
self.applicationName = ''
|
||||
self.language = ''
|
||||
self.domain = ''
|
||||
baseName = os.path.splitext(os.path.basename(fileName))[0]
|
||||
elems = baseName.split('-')
|
||||
if self.isPot:
|
||||
if len(elems) == 1:
|
||||
self.applicationName = self.domain = elems[0]
|
||||
else:
|
||||
self.applicationName, self.domain = elems
|
||||
else:
|
||||
if len(elems) == 1:
|
||||
self.applicationName = self.domain = ''
|
||||
self.language = elems[0]
|
||||
elif len(elems) == 2:
|
||||
self.applicationName = self.domain = elems[0]
|
||||
self.language = elems[1]
|
||||
else:
|
||||
self.applicationName, self.domain, self.language = elems
|
||||
self.generated = False # Will be True during the generation process,
|
||||
# once this file will have been generated.
|
||||
|
||||
def addMessage(self, newMsg, needsCopy=True):
|
||||
if needsCopy:
|
||||
res = copy.copy(newMsg)
|
||||
else:
|
||||
res = newMsg
|
||||
self.messages.append(res)
|
||||
self.messagesDict[res.id] = res
|
||||
return res
|
||||
|
||||
def addHeader(self, newHeader):
|
||||
self.headers.append(newHeader)
|
||||
self.headersDict[newHeader.name] = newHeader
|
||||
|
||||
def update(self, newMessages, removeNotNewMessages=False,
|
||||
keepExistingOrder=True):
|
||||
'''Updates the existing messages with p_newMessages.
|
||||
If p_removeNotNewMessages is True, all messages in self.messages
|
||||
that are not in newMessages will be removed, excepted if they start
|
||||
with "custom_". If p_keepExistingOrder is False, self.messages will
|
||||
be sorted according to p_newMessages. Else, newMessages that are not
|
||||
yet in self.messages will be appended to the end of self.messages.'''
|
||||
# First, remove not new messages if necessary
|
||||
newIds = [m.id for m in newMessages]
|
||||
removedIds = []
|
||||
if removeNotNewMessages:
|
||||
i = len(self.messages)-1
|
||||
while i >= 0:
|
||||
oldId = self.messages[i].id
|
||||
if not oldId.startswith('custom_') and (oldId not in newIds):
|
||||
del self.messages[i]
|
||||
del self.messagesDict[oldId]
|
||||
removedIds.append(oldId)
|
||||
i -= 1
|
||||
if keepExistingOrder:
|
||||
# Update existing messages and add inexistent messages to the end.
|
||||
for newMsg in newMessages:
|
||||
if newMsg.id in self.messagesDict:
|
||||
msg = self.messagesDict[newMsg.id]
|
||||
else:
|
||||
msg = self.addMessage(newMsg)
|
||||
msg.update(newMsg, self.isPot, self.language)
|
||||
else:
|
||||
# Keep the list of all old messages not being in new messages.
|
||||
# We will append them at the end of the new messages.
|
||||
notNewMessages = [m for m in self.messages if m.id not in newIds]
|
||||
del self.messages[:]
|
||||
for newMsg in newMessages:
|
||||
if newMsg.id in self.messagesDict:
|
||||
msg = self.messagesDict[newMsg.id]
|
||||
self.messages.append(msg)
|
||||
else:
|
||||
msg = self.addMessage(newMsg)
|
||||
msg.update(newMsg, self.isPot, self.language)
|
||||
# Append the list of old messages to the end
|
||||
self.messages += notNewMessages
|
||||
return removedIds
|
||||
|
||||
def generateHeaders(self, f):
|
||||
if not self.headers:
|
||||
creationTime = time.strftime("%Y-%m-%d %H:%M-%S", time.localtime())
|
||||
fb = ''
|
||||
if not self.isPot:
|
||||
# I must add fallbacks
|
||||
if self.language in fallbacks:
|
||||
fb = '"X-is-fallback-for: %s\\n"' % fallbacks[self.language]
|
||||
f.write(poHeader % (self.applicationName, creationTime,
|
||||
self.language, self.language, self.domain, fb))
|
||||
else:
|
||||
# Some headers were already found, we dump them as is
|
||||
f.write('msgid ""\nmsgstr ""\n')
|
||||
for header in self.headers:
|
||||
f.write(header.generate())
|
||||
f.write('\n')
|
||||
|
||||
def generate(self):
|
||||
'''Generates the corresponding po or pot file.'''
|
||||
folderName = os.path.dirname(self.fileName)
|
||||
if not os.path.exists(folderName):
|
||||
os.makedirs(folderName)
|
||||
f = file(self.fileName, 'w')
|
||||
self.generateHeaders(f)
|
||||
for msg in self.messages:
|
||||
f.write(msg.generate())
|
||||
f.write('\n')
|
||||
f.close()
|
||||
self.generated = True
|
||||
|
||||
def getPoFileName(self, language):
|
||||
'''Gets the name of the po file that corresponds to this pot file and
|
||||
the given p_language.'''
|
||||
if self.applicationName == self.domain:
|
||||
res = '%s-%s.po' % (self.applicationName, language)
|
||||
else:
|
||||
res = '%s-%s-%s.po' % (self.applicationName, self.domain, language)
|
||||
return res
|
||||
|
||||
def getCustomMessages(self):
|
||||
'''Returns, the list of messages from self.messages whose ID starts with
|
||||
"custom_".'''
|
||||
return [m for m in self.messages if m.id.startswith('custom_')]
|
||||
|
||||
class PoParser:
|
||||
'''Allows to parse a i18n file. The result is produced in self.res as a
|
||||
PoFile instance.'''
|
||||
def __init__(self, fileName):
|
||||
self.res = PoFile(fileName)
|
||||
|
||||
# Regular expressions for msgIds, msgStrs and default values.
|
||||
re_default = re.compile('#\.\s+Default\s*:\s*"(.*)"')
|
||||
re_fuzzy = re.compile('#,\s+fuzzy')
|
||||
re_id = re.compile('msgid\s+"(.*)"')
|
||||
re_msg = re.compile('msgstr\s+"(.*)"')
|
||||
|
||||
def parse(self):
|
||||
'''Parses all i18n messages in the file, stores it in
|
||||
self.res.messages and returns self.res.'''
|
||||
f = file(self.res.fileName)
|
||||
# Currently parsed values
|
||||
msgDefault = msgFuzzy = msgId = msgStr = None
|
||||
comments = []
|
||||
# Walk every line of the po(t) file
|
||||
for line in f:
|
||||
lineContent = line.strip()
|
||||
if lineContent and (not lineContent.startswith('"')):
|
||||
r = self.re_default.match(lineContent)
|
||||
if r:
|
||||
msgDefault = r.group(1)
|
||||
else:
|
||||
r = self.re_fuzzy.match(lineContent)
|
||||
if r:
|
||||
msgFuzzy = True
|
||||
else:
|
||||
r = self.re_id.match(lineContent)
|
||||
if r:
|
||||
msgId = r.group(1)
|
||||
else:
|
||||
r = self.re_msg.match(lineContent)
|
||||
if r:
|
||||
msgStr = r.group(1)
|
||||
else:
|
||||
if lineContent.startswith('#'):
|
||||
comments.append(lineContent.strip())
|
||||
if msgStr != None:
|
||||
if not ((msgId == '') and (msgStr == '')):
|
||||
poMsg = PoMessage(msgId, msgStr, msgDefault, msgFuzzy,
|
||||
comments)
|
||||
self.res.addMessage(poMsg)
|
||||
msgDefault = msgFuzzy = msgId = msgStr = None
|
||||
comments = []
|
||||
if lineContent.startswith('"'):
|
||||
# It is a header value
|
||||
name, value = lineContent.strip('"').split(':', 1)
|
||||
if value.endswith('\\n'):
|
||||
value = value[:-2]
|
||||
self.res.addHeader(PoHeader(name.strip(), value.strip()))
|
||||
f.close()
|
||||
return self.res
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,33 +0,0 @@
|
|||
<!codeHeader!>
|
||||
from OFS.SimpleItem import SimpleItem
|
||||
from OFS.Folder import Folder
|
||||
from appy.gen.utils import createObject
|
||||
from AccessControl import ClassSecurityInfo
|
||||
import Products.<!applicationName!>.config as cfg
|
||||
from appy.gen.mixins import BaseMixin
|
||||
from appy.gen.mixins.ToolMixin import ToolMixin
|
||||
from wrappers import <!genClassName!>_Wrapper as Wrapper
|
||||
|
||||
def manage_add<!genClassName!>(self, id, title='', REQUEST=None):
|
||||
'''Creates instances of this class.'''
|
||||
createObject(self, id, '<!genClassName!>', '<!applicationName!>')
|
||||
if REQUEST is not None: return self.manage_main(self, REQUEST)
|
||||
|
||||
class <!genClassName!>(<!parents!>):
|
||||
'''<!classDoc!>'''
|
||||
security = ClassSecurityInfo()
|
||||
meta_type = '<!genClassName!>'
|
||||
portal_type = '<!genClassName!>'
|
||||
allowed_content_types = ()
|
||||
filter_content_types = 0
|
||||
global_allow = 1
|
||||
icon = "ui/<!icon!>"
|
||||
wrapperClass = Wrapper
|
||||
config = cfg
|
||||
def do(self):
|
||||
'''BaseMixin.do can't be traversed by Zope if this class is the tool.
|
||||
So here, we redefine this method.'''
|
||||
return BaseMixin.do(self)
|
||||
for elem in dir(<!baseMixin!>):
|
||||
if not elem.startswith('__'): security.declarePublic(elem)
|
||||
<!methods!>
|
|
@ -1,38 +0,0 @@
|
|||
<!codeHeader!>
|
||||
# Test coverage-related stuff --------------------------------------------------
|
||||
import sys
|
||||
from appy.gen.mixins.TestMixin import TestMixin
|
||||
covFolder = TestMixin.getCovFolder()
|
||||
# The previous method checks in sys.argv whether Zope was lauched for performing
|
||||
# coverage tests or not.
|
||||
cov = None # The main Coverage instance as created by the coverage program.
|
||||
totalNumberOfTests = <!totalNumberOfTests!>
|
||||
numberOfExecutedTests = 0
|
||||
if covFolder:
|
||||
try:
|
||||
import coverage
|
||||
from coverage import coverage
|
||||
cov = coverage()
|
||||
cov.start()
|
||||
except ImportError:
|
||||
print('COVERAGE KO! The "coverage" program is not installed. You can ' \
|
||||
'download it from http://nedbatchelder.com/code/coverage.' \
|
||||
'\nHit <enter> to execute the test suite without coverage.')
|
||||
sys.stdin.readline()
|
||||
|
||||
def countTest():
|
||||
global numberOfExecutedTests
|
||||
numberOfExecutedTests += 1
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import config
|
||||
from appy.gen.installer import ZopeInstaller
|
||||
|
||||
# Zope-level installation of the generated product. ----------------------------
|
||||
def initialize(context):
|
||||
<!imports!>
|
||||
# I need to do those imports here; else, types and add permissions will not
|
||||
# be registered.
|
||||
classes = [<!classes!>]
|
||||
ZopeInstaller(context, config, classes).install()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,45 +0,0 @@
|
|||
<!codeHeader!>
|
||||
import os, os.path, sys, copy
|
||||
import appy
|
||||
import appy.gen
|
||||
import wrappers
|
||||
<!imports!>
|
||||
|
||||
# The following imports are here for allowing mixin classes to access those
|
||||
# elements without being statically dependent on Zope packages.
|
||||
from persistent.list import PersistentList
|
||||
from OFS.Image import File
|
||||
from ZPublisher.HTTPRequest import FileUpload
|
||||
from DateTime import DateTime
|
||||
import appy.gen
|
||||
import logging
|
||||
logger = logging.getLogger('<!applicationName!>')
|
||||
|
||||
# Some global variables --------------------------------------------------------
|
||||
PROJECTNAME = '<!applicationName!>'
|
||||
diskFolder = os.path.dirname(<!applicationName!>.__file__)
|
||||
|
||||
# Applications classes, in various formats
|
||||
appClasses = [<!appClasses!>]
|
||||
appClassNames = [<!appClassNames!>]
|
||||
allClassNames = [<!allClassNames!>]
|
||||
allShortClassNames = {<!allShortClassNames!>}
|
||||
|
||||
# In the following dict, we store, for every Appy class, the ordered list of
|
||||
# fields.
|
||||
attributes = {<!attributes!>}
|
||||
|
||||
# Application roles
|
||||
applicationRoles = [<!roles!>]
|
||||
applicationGlobalRoles = [<!gRoles!>]
|
||||
grantableRoles = [<!grRoles!>]
|
||||
|
||||
try:
|
||||
appConfig = <!applicationName!>.Config
|
||||
except AttributeError:
|
||||
appConfig = appy.gen.Config
|
||||
|
||||
# When Zope is starting or runs in test mode, there is no request object. We
|
||||
# create here a fake one for storing Appy wrappers.
|
||||
fakeRequest = appy.Object()
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,23 +0,0 @@
|
|||
<!codeHeader!>
|
||||
|
||||
from unittest import TestSuite
|
||||
from Testing import ZopeTestCase
|
||||
from Testing.ZopeTestCase import ZopeDocTestSuite
|
||||
from appy.gen.mixins.TestMixin import TestMixin, beforeTest, afterTest
|
||||
<!imports!>
|
||||
|
||||
# Initialize the Zope test system ----------------------------------------------
|
||||
ZopeTestCase.installProduct('<!applicationName!>')
|
||||
|
||||
class Test(TestMixin, ZopeTestCase.ZopeTestCase):
|
||||
'''Base test class for <!applicationName!> test cases.'''
|
||||
|
||||
# Data needed for defining the tests -------------------------------------------
|
||||
data = {'test_class': Test, 'setUp': beforeTest, 'tearDown': afterTest,
|
||||
'globs': {'appName': '<!applicationName!>'}}
|
||||
modulesWithTests = [<!modulesWithTests!>]
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
def test_suite():
|
||||
return TestSuite([ZopeDocTestSuite(m, **data) for m in modulesWithTests])
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,25 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
from appy.gen import *
|
||||
Grp = Group # Avoid name clashes with the Group class below and appy.gen.Group
|
||||
Pge = Page # Avoid name clashes with the Page class below and appy.gen.Page
|
||||
from appy.gen.wrappers import AbstractWrapper
|
||||
from appy.gen.wrappers.ToolWrapper import ToolWrapper as WTool
|
||||
from appy.gen.wrappers.UserWrapper import UserWrapper as WUser
|
||||
from appy.gen.wrappers.GroupWrapper import GroupWrapper as WGroup
|
||||
from appy.gen.wrappers.TranslationWrapper import TranslationWrapper as WT
|
||||
from appy.gen.wrappers.PageWrapper import PageWrapper as WPage
|
||||
from Globals import InitializeClass
|
||||
from AccessControl import ClassSecurityInfo
|
||||
# Layouts for Translation fields
|
||||
tfw = {"edit":"f","cell":"f","view":"f","search":"f"}
|
||||
<!imports!>
|
||||
|
||||
<!User!>
|
||||
<!Group!>
|
||||
<!Translation!>
|
||||
<!Page!>
|
||||
autoref(Page, Page.pages)
|
||||
|
||||
<!Tool!>
|
||||
<!wrappers!>
|
||||
# ------------------------------------------------------------------------------
|
804
gen/tr/Appy.pot
|
@ -1,804 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Appy\n"
|
||||
"POT-Creation-Date: 2013-10-19 22:35-36\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"Language-code: \n"
|
||||
"Language-name: \n"
|
||||
"Preferred-encodings: utf-8 latin1\n"
|
||||
"Domain: Appy\n"
|
||||
|
||||
#. Default: "Appy"
|
||||
msgid "app_name"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "state"
|
||||
msgid "workflow_state"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Optional comment"
|
||||
msgid "workflow_comment"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Manager"
|
||||
msgid "role_Manager"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Anonymous"
|
||||
msgid "role_Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Authenticated"
|
||||
msgid "role_Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Owner"
|
||||
msgid "role_Owner"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Title"
|
||||
msgid "appy_title"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Ok"
|
||||
msgid "appy_ok"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Keywords"
|
||||
msgid "appy_SearchableText"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Data change"
|
||||
msgid "data_change"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Modified field"
|
||||
msgid "modified_field"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Previous value or modification"
|
||||
msgid "previous_value"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "phase"
|
||||
msgid "phase"
|
||||
msgstr ""
|
||||
|
||||
#. Default: " - "
|
||||
msgid "choose_a_value"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "[ Documents ]"
|
||||
msgid "choose_a_doc"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must choose more elements here."
|
||||
msgid "min_ref_violated"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Too much elements are selected here."
|
||||
msgid "max_ref_violated"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "-"
|
||||
msgid "no_ref"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Add"
|
||||
msgid "add_ref"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Available elements"
|
||||
msgid "selectable_objects"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted elements"
|
||||
msgid "selected_objects"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move up"
|
||||
msgid "move_up"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move down"
|
||||
msgid "move_down"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move to top"
|
||||
msgid "move_top"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move to bottom"
|
||||
msgid "move_bottom"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Update the hereabove number and click here to move this element to a new position."
|
||||
msgid "move_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "create"
|
||||
msgid "query_create"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Nothing to see for the moment."
|
||||
msgid "query_no_result"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "consult all"
|
||||
msgid "query_consult_all"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Advanced search"
|
||||
msgid "search_title"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Search"
|
||||
msgid "search_button"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Search results"
|
||||
msgid "search_results"
|
||||
msgstr ""
|
||||
|
||||
#. Default: " "
|
||||
msgid "search_results_descr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All results"
|
||||
msgid "search_results_all"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "New search"
|
||||
msgid "search_new"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "From"
|
||||
msgid "search_from"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "to"
|
||||
msgid "search_to"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "or"
|
||||
msgid "search_or"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "and"
|
||||
msgid "search_and"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "No move occurred: please specify a valid number."
|
||||
msgid "ref_invalid_index"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "An integer value is expected; do not enter any space."
|
||||
msgid "bad_long"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
|
||||
msgid "bad_float"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please specify a valid date."
|
||||
msgid "bad_date"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The value is not among possible values for this field."
|
||||
msgid "bad_select_value"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Un)select all"
|
||||
msgid "select_delesect"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Automatic (de)selection"
|
||||
msgid "select_auto"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must select at least one element."
|
||||
msgid "no_elem_selected"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Edit"
|
||||
msgid "object_edit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Delete"
|
||||
msgid "object_delete"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Delete selection"
|
||||
msgid "object_delete_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Remove"
|
||||
msgid "object_unlink"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Remove selection"
|
||||
msgid "object_unlink_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert"
|
||||
msgid "object_link"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert selection"
|
||||
msgid "object_link_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Un)check everything"
|
||||
msgid "check_uncheck"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unlock"
|
||||
msgid "page_unlock"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Keep the file unchanged"
|
||||
msgid "keep_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Delete the file"
|
||||
msgid "delete_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Replace it with a new file"
|
||||
msgid "replace_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure?"
|
||||
msgid "action_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The action has been performed."
|
||||
msgid "action_done"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "A problem occurred while executing the action."
|
||||
msgid "action_ko"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action could not be performed on ${nb} element(s)."
|
||||
msgid "action_partial"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action had no effect."
|
||||
msgid "action_null"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure you want to apply this change?"
|
||||
msgid "save_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure? You are going to permanently change the order of these elements, for all users."
|
||||
msgid "sort_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to top"
|
||||
msgid "goto_first"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to previous"
|
||||
msgid "goto_previous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to next"
|
||||
msgid "goto_next"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to end"
|
||||
msgid "goto_last"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go back"
|
||||
msgid "goto_source"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to number"
|
||||
msgid "goto_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Whatever"
|
||||
msgid "whatever"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Yes"
|
||||
msgid "yes"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "No"
|
||||
msgid "no"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please fill this field."
|
||||
msgid "field_required"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please fill or correct this."
|
||||
msgid "field_invalid"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please select a file."
|
||||
msgid "file_required"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The uploaded file must be an image."
|
||||
msgid "image_required"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "ODT"
|
||||
msgid "odt"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "PDF"
|
||||
msgid "pdf"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "DOC"
|
||||
msgid "doc"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "ODS"
|
||||
msgid "ods"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "XLS"
|
||||
msgid "xls"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "frozen"
|
||||
msgid "frozen"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Re-)freeze"
|
||||
msgid "freezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unfreeze"
|
||||
msgid "unfreezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Upload a new file"
|
||||
msgid "uploadField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please upload a file of the same type."
|
||||
msgid "upload_invalid"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Welcome to this Appy-powered site."
|
||||
msgid "front_page_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please type \"${text}\" (without the double quotes) in the field besides, but without the character at position ${number}."
|
||||
msgid "captcha_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The code was not correct. Please try again."
|
||||
msgid "bad_captcha"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Login"
|
||||
msgid "app_login"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Log in"
|
||||
msgid "app_connect"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Logout"
|
||||
msgid "app_logout"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Password"
|
||||
msgid "app_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Home"
|
||||
msgid "app_home"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "This login is reserved."
|
||||
msgid "login_reserved"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "This login is already in use."
|
||||
msgid "login_in_use"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Login failed."
|
||||
msgid "login_ko"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Welcome! You are now logged in."
|
||||
msgid "login_ok"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Passwords must contain at least ${nb} characters."
|
||||
msgid "password_too_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Passwords do not match."
|
||||
msgid "passwords_mismatch"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Save"
|
||||
msgid "object_save"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Changes saved."
|
||||
msgid "object_saved"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please correct the indicated errors."
|
||||
msgid "validation_error"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Cancel"
|
||||
msgid "object_cancel"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Changes canceled."
|
||||
msgid "object_canceled"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must enable cookies before you can log in."
|
||||
msgid "enable_cookies"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Previous page"
|
||||
msgid "page_previous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Next page"
|
||||
msgid "page_next"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Forgot password?"
|
||||
msgid "forgot_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Ask new password"
|
||||
msgid "ask_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Something went wrong. First possibility: you have already clicked on the link (maybe have you double-clicked?) and your password has already been re-initialized. Please check that you haven't received your new password in another email. Second possibility: the link that you received in your mailer was splitted on several lines. In this case, please re-type the link in one single line and retry. Third possibility: you have waited too long and your request has expired, or a technical error occurred. In this case, please try again to ask a new password from the start."
|
||||
msgid "wrong_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "A mail has been sent to you. Please follow the instructions from this email."
|
||||
msgid "reinit_mail_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Password re-initialisation"
|
||||
msgid "reinit_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>A password re-initialisation has been requested, tied to this email address, for the website ${siteUrl}. If you are not at the origin of this request, please ignore this email. Else, click on the link below to receive a new password.<br/><br/>${url}"
|
||||
msgid "reinit_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password"
|
||||
msgid "new_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>The new password you have requested for website ${siteUrl} is ${password}<br/><br/>Best regards."
|
||||
msgid "new_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password has been sent to you by email."
|
||||
msgid "new_password_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last access"
|
||||
msgid "last_user_access"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "History"
|
||||
msgid "object_history"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "By"
|
||||
msgid "object_created_by"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "On"
|
||||
msgid "object_created_on"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last updated on"
|
||||
msgid "object_modified_on"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action"
|
||||
msgid "object_action"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Author"
|
||||
msgid "object_author"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Date"
|
||||
msgid "action_date"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Comment"
|
||||
msgid "action_comment"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Create from another object"
|
||||
msgid "create_from_predecessor"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mon"
|
||||
msgid "day_Mon_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tue"
|
||||
msgid "day_Tue_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wed"
|
||||
msgid "day_Wed_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thu"
|
||||
msgid "day_Thu_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Fri"
|
||||
msgid "day_Fri_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sat"
|
||||
msgid "day_Sat_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sun"
|
||||
msgid "day_Sun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Off"
|
||||
msgid "day_Off_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Monday"
|
||||
msgid "day_Mon"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tuesday"
|
||||
msgid "day_Tue"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wednesday"
|
||||
msgid "day_Wed"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thursday"
|
||||
msgid "day_Thu"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Friday"
|
||||
msgid "day_Fri"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Saturday"
|
||||
msgid "day_Sat"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sunday"
|
||||
msgid "day_Sun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Day off"
|
||||
msgid "day_Off"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "AM"
|
||||
msgid "ampm_am"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "PM"
|
||||
msgid "ampm_pm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jan"
|
||||
msgid "month_Jan_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Feb"
|
||||
msgid "month_Feb_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mar"
|
||||
msgid "month_Mar_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Apr"
|
||||
msgid "month_Apr_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jun"
|
||||
msgid "month_Jun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jul"
|
||||
msgid "month_Jul_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Aug"
|
||||
msgid "month_Aug_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sep"
|
||||
msgid "month_Sep_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Oct"
|
||||
msgid "month_Oct_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Nov"
|
||||
msgid "month_Nov_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Dec"
|
||||
msgid "month_Dec_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "January"
|
||||
msgid "month_Jan"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "February"
|
||||
msgid "month_Feb"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "March"
|
||||
msgid "month_Mar"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "April"
|
||||
msgid "month_Apr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "June"
|
||||
msgid "month_Jun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "July"
|
||||
msgid "month_Jul"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Augustus"
|
||||
msgid "month_Aug"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "September"
|
||||
msgid "month_Sep"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "October"
|
||||
msgid "month_Oct"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "November"
|
||||
msgid "month_Nov"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "December"
|
||||
msgid "month_Dec"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Today"
|
||||
msgid "today"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Which event type would you like to create?"
|
||||
msgid "which_event"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Extend the event on the following number of days (leave blank to create an event on the current day only):"
|
||||
msgid "event_span"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Also delete successive events of the same type."
|
||||
msgid "del_next_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Several events"
|
||||
msgid "several_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Timeslot"
|
||||
msgid "timeslot"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All day"
|
||||
msgid "timeslot_main"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Cannot create such an event in the ${slot} slot."
|
||||
msgid "timeslot_misfit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Validate events"
|
||||
msgid "validate_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
|
||||
msgid "validate_events_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
|
||||
msgid "validate_events_done"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted by ${userName}"
|
||||
msgid "history_insert"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Deleted by ${userName}"
|
||||
msgid "history_delete"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Show changes"
|
||||
msgid "changes_show"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hide changes"
|
||||
msgid "changes_hide"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "an anonymous user"
|
||||
msgid "anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${date} - This page is locked by ${user}."
|
||||
msgid "page_locked"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "In some situations, by leaving this page this way, you may lose encoded data or prevent other users from editing it afterwards. Please use buttons instead."
|
||||
msgid "warn_leave_form"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You are not allowed to consult this."
|
||||
msgid "unauthorized"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Microsoft Internet Explorer ${version} is not supported. Please upgrade your browser to version ${min} or above."
|
||||
msgid "wrong_browser"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Send by email"
|
||||
msgid "email_send"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${site} - ${title} - ${template}"
|
||||
msgid "podmail_subject"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello, this email that was sent to you via ${site}. Please consult the attached file(s)."
|
||||
msgid "podmail_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "List"
|
||||
msgid "result_mode_list"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Grid"
|
||||
msgid "result_mode_grid"
|
||||
msgstr ""
|
804
gen/tr/ar.po
|
@ -1,804 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Appy\n"
|
||||
"POT-Creation-Date: 2013-10-19 22:57-25\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"Language-code: ar\n"
|
||||
"Language-name: ar\n"
|
||||
"Preferred-encodings: utf-8 latin1\n"
|
||||
"Domain: Appy\n"
|
||||
|
||||
#. Default: "Appy"
|
||||
msgid "app_name"
|
||||
msgstr "Appy"
|
||||
|
||||
#. Default: "state"
|
||||
msgid "workflow_state"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Optional comment"
|
||||
msgid "workflow_comment"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Manager"
|
||||
msgid "role_Manager"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Anonymous"
|
||||
msgid "role_Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Authenticated"
|
||||
msgid "role_Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Owner"
|
||||
msgid "role_Owner"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Title"
|
||||
msgid "appy_title"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Ok"
|
||||
msgid "appy_ok"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Keywords"
|
||||
msgid "appy_SearchableText"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Data change"
|
||||
msgid "data_change"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Modified field"
|
||||
msgid "modified_field"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Previous value or modification"
|
||||
msgid "previous_value"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "phase"
|
||||
msgid "phase"
|
||||
msgstr ""
|
||||
|
||||
#. Default: " - "
|
||||
msgid "choose_a_value"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "[ Documents ]"
|
||||
msgid "choose_a_doc"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must choose more elements here."
|
||||
msgid "min_ref_violated"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Too much elements are selected here."
|
||||
msgid "max_ref_violated"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "-"
|
||||
msgid "no_ref"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Add"
|
||||
msgid "add_ref"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Available elements"
|
||||
msgid "selectable_objects"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted elements"
|
||||
msgid "selected_objects"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move up"
|
||||
msgid "move_up"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move down"
|
||||
msgid "move_down"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move to top"
|
||||
msgid "move_top"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move to bottom"
|
||||
msgid "move_bottom"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Update the hereabove number and click here to move this element to a new position."
|
||||
msgid "move_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "create"
|
||||
msgid "query_create"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Nothing to see for the moment."
|
||||
msgid "query_no_result"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "consult all"
|
||||
msgid "query_consult_all"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Advanced search"
|
||||
msgid "search_title"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Search"
|
||||
msgid "search_button"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Search results"
|
||||
msgid "search_results"
|
||||
msgstr ""
|
||||
|
||||
#. Default: " "
|
||||
msgid "search_results_descr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All results"
|
||||
msgid "search_results_all"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "New search"
|
||||
msgid "search_new"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "From"
|
||||
msgid "search_from"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "to"
|
||||
msgid "search_to"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "or"
|
||||
msgid "search_or"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "and"
|
||||
msgid "search_and"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "No move occurred: please specify a valid number."
|
||||
msgid "ref_invalid_index"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "An integer value is expected; do not enter any space."
|
||||
msgid "bad_long"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
|
||||
msgid "bad_float"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please specify a valid date."
|
||||
msgid "bad_date"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The value is not among possible values for this field."
|
||||
msgid "bad_select_value"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Un)select all"
|
||||
msgid "select_delesect"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Automatic (de)selection"
|
||||
msgid "select_auto"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must select at least one element."
|
||||
msgid "no_elem_selected"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Edit"
|
||||
msgid "object_edit"
|
||||
msgstr "تعديل"
|
||||
|
||||
#. Default: "Delete"
|
||||
msgid "object_delete"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Delete selection"
|
||||
msgid "object_delete_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Remove"
|
||||
msgid "object_unlink"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Remove selection"
|
||||
msgid "object_unlink_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert"
|
||||
msgid "object_link"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert selection"
|
||||
msgid "object_link_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Un)check everything"
|
||||
msgid "check_uncheck"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unlock"
|
||||
msgid "page_unlock"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Keep the file unchanged"
|
||||
msgid "keep_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Delete the file"
|
||||
msgid "delete_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Replace it with a new file"
|
||||
msgid "replace_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure?"
|
||||
msgid "action_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The action has been performed."
|
||||
msgid "action_done"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "A problem occurred while executing the action."
|
||||
msgid "action_ko"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action could not be performed on ${nb} element(s)."
|
||||
msgid "action_partial"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action had no effect."
|
||||
msgid "action_null"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure you want to apply this change?"
|
||||
msgid "save_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure? You are going to permanently change the order of these elements, for all users."
|
||||
msgid "sort_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to top"
|
||||
msgid "goto_first"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to previous"
|
||||
msgid "goto_previous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to next"
|
||||
msgid "goto_next"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to end"
|
||||
msgid "goto_last"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go back"
|
||||
msgid "goto_source"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to number"
|
||||
msgid "goto_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Whatever"
|
||||
msgid "whatever"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Yes"
|
||||
msgid "yes"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "No"
|
||||
msgid "no"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please fill this field."
|
||||
msgid "field_required"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please fill or correct this."
|
||||
msgid "field_invalid"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please select a file."
|
||||
msgid "file_required"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The uploaded file must be an image."
|
||||
msgid "image_required"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "ODT"
|
||||
msgid "odt"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "PDF"
|
||||
msgid "pdf"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "DOC"
|
||||
msgid "doc"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "ODS"
|
||||
msgid "ods"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "XLS"
|
||||
msgid "xls"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "frozen"
|
||||
msgid "frozen"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Re-)freeze"
|
||||
msgid "freezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unfreeze"
|
||||
msgid "unfreezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Upload a new file"
|
||||
msgid "uploadField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please upload a file of the same type."
|
||||
msgid "upload_invalid"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Welcome to this Appy-powered site."
|
||||
msgid "front_page_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please type \"${text}\" (without the double quotes) in the field besides, but without the character at position ${number}."
|
||||
msgid "captcha_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The code was not correct. Please try again."
|
||||
msgid "bad_captcha"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Login"
|
||||
msgid "app_login"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Log in"
|
||||
msgid "app_connect"
|
||||
msgstr "تسجيل الدّخول"
|
||||
|
||||
#. Default: "Logout"
|
||||
msgid "app_logout"
|
||||
msgstr "تسجيل الخروج"
|
||||
|
||||
#. Default: "Password"
|
||||
msgid "app_password"
|
||||
msgstr "كلمة السر"
|
||||
|
||||
#. Default: "Home"
|
||||
msgid "app_home"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "This login is reserved."
|
||||
msgid "login_reserved"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "This login is already in use."
|
||||
msgid "login_in_use"
|
||||
msgstr "اسم الدخول الذي اخترته مستخدمٌ مسبقاً أو ليس صالحاً. يرجى اختيار اسمٍ آخر."
|
||||
|
||||
#. Default: "Login failed."
|
||||
msgid "login_ko"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Welcome! You are now logged in."
|
||||
msgid "login_ok"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Passwords must contain at least ${nb} characters."
|
||||
msgid "password_too_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Passwords do not match."
|
||||
msgid "passwords_mismatch"
|
||||
msgstr "كلمتا المرور غير متطابقتين."
|
||||
|
||||
#. Default: "Save"
|
||||
msgid "object_save"
|
||||
msgstr "حفظ"
|
||||
|
||||
#. Default: "Changes saved."
|
||||
msgid "object_saved"
|
||||
msgstr "التغييرات قد سجلت."
|
||||
|
||||
#. Default: "Please correct the indicated errors."
|
||||
msgid "validation_error"
|
||||
msgstr "يرجى تصحيح الأخطاء المبيّنة."
|
||||
|
||||
#. Default: "Cancel"
|
||||
msgid "object_cancel"
|
||||
msgstr "إلغاء"
|
||||
|
||||
#. Default: "Changes canceled."
|
||||
msgid "object_canceled"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must enable cookies before you can log in."
|
||||
msgid "enable_cookies"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Previous page"
|
||||
msgid "page_previous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Next page"
|
||||
msgid "page_next"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Forgot password?"
|
||||
msgid "forgot_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Ask new password"
|
||||
msgid "ask_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Something went wrong. First possibility: you have already clicked on the link (maybe have you double-clicked?) and your password has already been re-initialized. Please check that you haven't received your new password in another email. Second possibility: the link that you received in your mailer was splitted on several lines. In this case, please re-type the link in one single line and retry. Third possibility: you have waited too long and your request has expired, or a technical error occurred. In this case, please try again to ask a new password from the start."
|
||||
msgid "wrong_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "A mail has been sent to you. Please follow the instructions from this email."
|
||||
msgid "reinit_mail_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Password re-initialisation"
|
||||
msgid "reinit_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>A password re-initialisation has been requested, tied to this email address, for the website ${siteUrl}. If you are not at the origin of this request, please ignore this email. Else, click on the link below to receive a new password.<br/><br/>${url}"
|
||||
msgid "reinit_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password"
|
||||
msgid "new_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>The new password you have requested for website ${siteUrl} is ${password}<br/><br/>Best regards."
|
||||
msgid "new_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password has been sent to you by email."
|
||||
msgid "new_password_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last access"
|
||||
msgid "last_user_access"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "History"
|
||||
msgid "object_history"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "By"
|
||||
msgid "object_created_by"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "On"
|
||||
msgid "object_created_on"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last updated on"
|
||||
msgid "object_modified_on"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action"
|
||||
msgid "object_action"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Author"
|
||||
msgid "object_author"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Date"
|
||||
msgid "action_date"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Comment"
|
||||
msgid "action_comment"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Create from another object"
|
||||
msgid "create_from_predecessor"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mon"
|
||||
msgid "day_Mon_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tue"
|
||||
msgid "day_Tue_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wed"
|
||||
msgid "day_Wed_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thu"
|
||||
msgid "day_Thu_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Fri"
|
||||
msgid "day_Fri_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sat"
|
||||
msgid "day_Sat_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sun"
|
||||
msgid "day_Sun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Off"
|
||||
msgid "day_Off_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Monday"
|
||||
msgid "day_Mon"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tuesday"
|
||||
msgid "day_Tue"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wednesday"
|
||||
msgid "day_Wed"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thursday"
|
||||
msgid "day_Thu"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Friday"
|
||||
msgid "day_Fri"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Saturday"
|
||||
msgid "day_Sat"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sunday"
|
||||
msgid "day_Sun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Day off"
|
||||
msgid "day_Off"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "AM"
|
||||
msgid "ampm_am"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "PM"
|
||||
msgid "ampm_pm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jan"
|
||||
msgid "month_Jan_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Feb"
|
||||
msgid "month_Feb_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mar"
|
||||
msgid "month_Mar_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Apr"
|
||||
msgid "month_Apr_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jun"
|
||||
msgid "month_Jun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jul"
|
||||
msgid "month_Jul_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Aug"
|
||||
msgid "month_Aug_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sep"
|
||||
msgid "month_Sep_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Oct"
|
||||
msgid "month_Oct_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Nov"
|
||||
msgid "month_Nov_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Dec"
|
||||
msgid "month_Dec_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "January"
|
||||
msgid "month_Jan"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "February"
|
||||
msgid "month_Feb"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "March"
|
||||
msgid "month_Mar"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "April"
|
||||
msgid "month_Apr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "June"
|
||||
msgid "month_Jun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "July"
|
||||
msgid "month_Jul"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Augustus"
|
||||
msgid "month_Aug"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "September"
|
||||
msgid "month_Sep"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "October"
|
||||
msgid "month_Oct"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "November"
|
||||
msgid "month_Nov"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "December"
|
||||
msgid "month_Dec"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Today"
|
||||
msgid "today"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Which event type would you like to create?"
|
||||
msgid "which_event"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Extend the event on the following number of days (leave blank to create an event on the current day only):"
|
||||
msgid "event_span"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Also delete successive events of the same type."
|
||||
msgid "del_next_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Several events"
|
||||
msgid "several_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Timeslot"
|
||||
msgid "timeslot"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All day"
|
||||
msgid "timeslot_main"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Cannot create such an event in the ${slot} slot."
|
||||
msgid "timeslot_misfit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Validate events"
|
||||
msgid "validate_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
|
||||
msgid "validate_events_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
|
||||
msgid "validate_events_done"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted by ${userName}"
|
||||
msgid "history_insert"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Deleted by ${userName}"
|
||||
msgid "history_delete"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Show changes"
|
||||
msgid "changes_show"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hide changes"
|
||||
msgid "changes_hide"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "an anonymous user"
|
||||
msgid "anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${date} - This page is locked by ${user}."
|
||||
msgid "page_locked"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "In some situations, by leaving this page this way, you may lose encoded data or prevent other users from editing it afterwards. Please use buttons instead."
|
||||
msgid "warn_leave_form"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You are not allowed to consult this."
|
||||
msgid "unauthorized"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Microsoft Internet Explorer ${version} is not supported. Please upgrade your browser to version ${min} or above."
|
||||
msgid "wrong_browser"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Send by email"
|
||||
msgid "email_send"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${site} - ${title} - ${template}"
|
||||
msgid "podmail_subject"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello, this email that was sent to you via ${site}. Please consult the attached file(s)."
|
||||
msgid "podmail_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "List"
|
||||
msgid "result_mode_list"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Grid"
|
||||
msgid "result_mode_grid"
|
||||
msgstr ""
|
804
gen/tr/de.po
|
@ -1,804 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Appy\n"
|
||||
"POT-Creation-Date: 2013-07-23 10:41-28\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"Language-code: de\n"
|
||||
"Language-name: de\n"
|
||||
"Preferred-encodings: utf-8 latin1\n"
|
||||
"Domain: Appy\n"
|
||||
|
||||
#. Default: "Appy"
|
||||
msgid "app_name"
|
||||
msgstr "Appy"
|
||||
|
||||
#. Default: "state"
|
||||
msgid "workflow_state"
|
||||
msgstr "Status"
|
||||
|
||||
#. Default: "Optional comment"
|
||||
msgid "workflow_comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
#. Default: "Manager"
|
||||
msgid "role_Manager"
|
||||
msgstr "Systemmanager"
|
||||
|
||||
#. Default: "Anonymous"
|
||||
msgid "role_Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Authenticated"
|
||||
msgid "role_Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Owner"
|
||||
msgid "role_Owner"
|
||||
msgstr "Eigentümer"
|
||||
|
||||
#. Default: "Title"
|
||||
msgid "appy_title"
|
||||
msgstr "Titel"
|
||||
|
||||
#. Default: "Ok"
|
||||
msgid "appy_ok"
|
||||
msgstr "OK"
|
||||
|
||||
#. Default: "Keywords"
|
||||
msgid "appy_SearchableText"
|
||||
msgstr "Stichwörter"
|
||||
|
||||
#. Default: "Data change"
|
||||
msgid "data_change"
|
||||
msgstr "Änderung der Angaben"
|
||||
|
||||
#. Default: "Modified field"
|
||||
msgid "modified_field"
|
||||
msgstr "Feld verändert"
|
||||
|
||||
#. Default: "Previous value or modification"
|
||||
msgid "previous_value"
|
||||
msgstr "Voriger Wert"
|
||||
|
||||
#. Default: "phase"
|
||||
msgid "phase"
|
||||
msgstr "Phase"
|
||||
|
||||
#. Default: " - "
|
||||
msgid "choose_a_value"
|
||||
msgstr "[Wählen]"
|
||||
|
||||
#. Default: "[ Documents ]"
|
||||
msgid "choose_a_doc"
|
||||
msgstr "[Dokumente]"
|
||||
|
||||
#. Default: "You must choose more elements here."
|
||||
msgid "min_ref_violated"
|
||||
msgstr "Hier müssen Sie Elemente auswählen."
|
||||
|
||||
#. Default: "Too much elements are selected here."
|
||||
msgid "max_ref_violated"
|
||||
msgstr "Sie haben zuviele Elemente ausgewählt."
|
||||
|
||||
#. Default: "-"
|
||||
msgid "no_ref"
|
||||
msgstr "Kein Element"
|
||||
|
||||
#. Default: "Add"
|
||||
msgid "add_ref"
|
||||
msgstr "Hinzufügen"
|
||||
|
||||
#. Default: "Available elements"
|
||||
msgid "selectable_objects"
|
||||
msgstr "Verfügbare Elemente"
|
||||
|
||||
#. Default: "Inserted elements"
|
||||
msgid "selected_objects"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move up"
|
||||
msgid "move_up"
|
||||
msgstr "Nach oben verschieben"
|
||||
|
||||
#. Default: "Move down"
|
||||
msgid "move_down"
|
||||
msgstr "Nach unten verschieben"
|
||||
|
||||
#. Default: "Move to top"
|
||||
msgid "move_top"
|
||||
msgstr "Verschiebe an den Anfang"
|
||||
|
||||
#. Default: "Move to bottom"
|
||||
msgid "move_bottom"
|
||||
msgstr "Verschiebe ans Ende"
|
||||
|
||||
#. Default: "Update the hereabove number and click here to move this element to a new position."
|
||||
msgid "move_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "create"
|
||||
msgid "query_create"
|
||||
msgstr "Anfertigen"
|
||||
|
||||
#. Default: "Nothing to see for the moment."
|
||||
msgid "query_no_result"
|
||||
msgstr "Kein Resultat"
|
||||
|
||||
#. Default: "consult all"
|
||||
msgid "query_consult_all"
|
||||
msgstr "Alles untersuchen"
|
||||
|
||||
#. Default: "Advanced search"
|
||||
msgid "search_title"
|
||||
msgstr "Fortgeschrittene Suche"
|
||||
|
||||
#. Default: "Search"
|
||||
msgid "search_button"
|
||||
msgstr "Suchen"
|
||||
|
||||
#. Default: "Search results"
|
||||
msgid "search_results"
|
||||
msgstr "Resultate suchen"
|
||||
|
||||
#. Default: " "
|
||||
msgid "search_results_descr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All results"
|
||||
msgid "search_results_all"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "New search"
|
||||
msgid "search_new"
|
||||
msgstr "Neues Suchfeld"
|
||||
|
||||
#. Default: "From"
|
||||
msgid "search_from"
|
||||
msgstr "von"
|
||||
|
||||
#. Default: "to"
|
||||
msgid "search_to"
|
||||
msgstr "bis"
|
||||
|
||||
#. Default: "or"
|
||||
msgid "search_or"
|
||||
msgstr "odr"
|
||||
|
||||
#. Default: "and"
|
||||
msgid "search_and"
|
||||
msgstr "und"
|
||||
|
||||
#. Default: "No move occurred: please specify a valid number."
|
||||
msgid "ref_invalid_index"
|
||||
msgstr "Es hat keine Verschiebung stattgefunden: Präzisieren Sie eine gültige Anzahl."
|
||||
|
||||
#. Default: "An integer value is expected; do not enter any space."
|
||||
msgid "bad_long"
|
||||
msgstr "Füllen Sie einen ganzen Wert ein."
|
||||
|
||||
#. Default: "A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
|
||||
msgid "bad_float"
|
||||
msgstr "Füllen Sie einen reellen Wert ein."
|
||||
|
||||
#. Default: "Please specify a valid date."
|
||||
msgid "bad_date"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The value is not among possible values for this field."
|
||||
msgid "bad_select_value"
|
||||
msgstr "Dieser Wert wird in diesem Feld nicht akzeptiert."
|
||||
|
||||
#. Default: "(Un)select all"
|
||||
msgid "select_delesect"
|
||||
msgstr "Alle aus- oder abwählen"
|
||||
|
||||
#. Default: "Automatic (de)selection"
|
||||
msgid "select_auto"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must select at least one element."
|
||||
msgid "no_elem_selected"
|
||||
msgstr "Sie müssen mindestens ein Element auswählen."
|
||||
|
||||
#. Default: "Edit"
|
||||
msgid "object_edit"
|
||||
msgstr "Bearbeiten"
|
||||
|
||||
#. Default: "Delete"
|
||||
msgid "object_delete"
|
||||
msgstr "Löschen"
|
||||
|
||||
#. Default: "Delete selection"
|
||||
msgid "object_delete_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Remove"
|
||||
msgid "object_unlink"
|
||||
msgstr "Zurückziehen"
|
||||
|
||||
#. Default: "Remove selection"
|
||||
msgid "object_unlink_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert"
|
||||
msgid "object_link"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert selection"
|
||||
msgid "object_link_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Un)check everything"
|
||||
msgid "check_uncheck"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unlock"
|
||||
msgid "page_unlock"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Keep the file unchanged"
|
||||
msgid "keep_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Delete the file"
|
||||
msgid "delete_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Replace it with a new file"
|
||||
msgid "replace_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure?"
|
||||
msgid "action_confirm"
|
||||
msgstr "Sind Sie sicher?"
|
||||
|
||||
#. Default: "The action has been performed."
|
||||
msgid "action_done"
|
||||
msgstr "Der Auftrag wird ausgeführt."
|
||||
|
||||
#. Default: "A problem occurred while executing the action."
|
||||
msgid "action_ko"
|
||||
msgstr "Ein Problem ist aufgetreten."
|
||||
|
||||
#. Default: "Action could not be performed on ${nb} element(s)."
|
||||
msgid "action_partial"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action had no effect."
|
||||
msgid "action_null"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure you want to apply this change?"
|
||||
msgid "save_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure? You are going to permanently change the order of these elements, for all users."
|
||||
msgid "sort_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to top"
|
||||
msgid "goto_first"
|
||||
msgstr "Zurück zum Anfang"
|
||||
|
||||
#. Default: "Go to previous"
|
||||
msgid "goto_previous"
|
||||
msgstr "Gehen Sie zum vorherigen Element"
|
||||
|
||||
#. Default: "Go to next"
|
||||
msgid "goto_next"
|
||||
msgstr "Gehen Sie zum folgenden Element"
|
||||
|
||||
#. Default: "Go to end"
|
||||
msgid "goto_last"
|
||||
msgstr "Gehen Sie zum Ende"
|
||||
|
||||
#. Default: "Go back"
|
||||
msgid "goto_source"
|
||||
msgstr "Zurück"
|
||||
|
||||
#. Default: "Go to number"
|
||||
msgid "goto_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Whatever"
|
||||
msgid "whatever"
|
||||
msgstr "Ohne Bedeutung"
|
||||
|
||||
#. Default: "Yes"
|
||||
msgid "yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#. Default: "No"
|
||||
msgid "no"
|
||||
msgstr "Nein"
|
||||
|
||||
#. Default: "Please fill this field."
|
||||
msgid "field_required"
|
||||
msgstr "Dieses Feld bitte ausfüllen."
|
||||
|
||||
#. Default: "Please fill or correct this."
|
||||
msgid "field_invalid"
|
||||
msgstr "Dieses Feld bitte ausfüllen oder verbessern."
|
||||
|
||||
#. Default: "Please select a file."
|
||||
msgid "file_required"
|
||||
msgstr "Wählen Sie eine Datei aus."
|
||||
|
||||
#. Default: "The uploaded file must be an image."
|
||||
msgid "image_required"
|
||||
msgstr "Die zum Hochladen bestimmte Datei muss ein Foto sein."
|
||||
|
||||
#. Default: "ODT"
|
||||
msgid "odt"
|
||||
msgstr "ODT (LibreOffice Writer)"
|
||||
|
||||
#. Default: "PDF"
|
||||
msgid "pdf"
|
||||
msgstr "PDF"
|
||||
|
||||
#. Default: "DOC"
|
||||
msgid "doc"
|
||||
msgstr "DOC (Microsoft Word)"
|
||||
|
||||
#. Default: "ODS"
|
||||
msgid "ods"
|
||||
msgstr "ODS (LibreOffice Calc)"
|
||||
|
||||
#. Default: "XLS"
|
||||
msgid "xls"
|
||||
msgstr "XLS (Microsoft Excel)"
|
||||
|
||||
#. Default: "frozen"
|
||||
msgid "frozen"
|
||||
msgstr "Gesperrt"
|
||||
|
||||
#. Default: "(Re-)freeze"
|
||||
msgid "freezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unfreeze"
|
||||
msgid "unfreezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Upload a new file"
|
||||
msgid "uploadField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please upload a file of the same type."
|
||||
msgid "upload_invalid"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Welcome to this Appy-powered site."
|
||||
msgid "front_page_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please type \"${text}\" (without the double quotes) in the field besides, but without the character at position ${number}."
|
||||
msgid "captcha_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The code was not correct. Please try again."
|
||||
msgid "bad_captcha"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Login"
|
||||
msgid "app_login"
|
||||
msgstr "Login"
|
||||
|
||||
#. Default: "Log in"
|
||||
msgid "app_connect"
|
||||
msgstr "Anmelden"
|
||||
|
||||
#. Default: "Logout"
|
||||
msgid "app_logout"
|
||||
msgstr "Abmelden"
|
||||
|
||||
#. Default: "Password"
|
||||
msgid "app_password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#. Default: "Home"
|
||||
msgid "app_home"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "This login is reserved."
|
||||
msgid "login_reserved"
|
||||
msgstr "Dieses Login ist reserviert"
|
||||
|
||||
#. Default: "This login is already in use."
|
||||
msgid "login_in_use"
|
||||
msgstr "Der Benutzername, den Sie ausgewählt haben, ist schon vergeben oder ungültig. Bitte wählen Sie einen anderen."
|
||||
|
||||
#. Default: "Login failed."
|
||||
msgid "login_ko"
|
||||
msgstr "Anmeldung fehlgeschlagen."
|
||||
|
||||
#. Default: "Welcome! You are now logged in."
|
||||
msgid "login_ok"
|
||||
msgstr "Willkommen! Sie sind jetzt angemeldet."
|
||||
|
||||
#. Default: "Passwords must contain at least ${nb} characters."
|
||||
msgid "password_too_short"
|
||||
msgstr "Das Passwort muss mindestens ${nb} Zeichen enthalten."
|
||||
|
||||
#. Default: "Passwords do not match."
|
||||
msgid "passwords_mismatch"
|
||||
msgstr "Die Passwörter müssen übereinstimmen."
|
||||
|
||||
#. Default: "Save"
|
||||
msgid "object_save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#. Default: "Changes saved."
|
||||
msgid "object_saved"
|
||||
msgstr "Änderungen wurden gespeichert."
|
||||
|
||||
#. Default: "Please correct the indicated errors."
|
||||
msgid "validation_error"
|
||||
msgstr "Bitte korrigieren Sie die angezeigten Fehler."
|
||||
|
||||
#. Default: "Cancel"
|
||||
msgid "object_cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#. Default: "Changes canceled."
|
||||
msgid "object_canceled"
|
||||
msgstr "Änderungen abgebrochen."
|
||||
|
||||
#. Default: "You must enable cookies before you can log in."
|
||||
msgid "enable_cookies"
|
||||
msgstr "Sie müssen Cookies erlauben, bevor Sie sich anmelden können."
|
||||
|
||||
#. Default: "Previous page"
|
||||
msgid "page_previous"
|
||||
msgstr "Vorheriger Eintrag"
|
||||
|
||||
#. Default: "Next page"
|
||||
msgid "page_next"
|
||||
msgstr "Nächster"
|
||||
|
||||
#. Default: "Forgot password?"
|
||||
msgid "forgot_password"
|
||||
msgstr "Passwort vergessen?"
|
||||
|
||||
#. Default: "Ask new password"
|
||||
msgid "ask_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Something went wrong. First possibility: you have already clicked on the link (maybe have you double-clicked?) and your password has already been re-initialized. Please check that you haven't received your new password in another email. Second possibility: the link that you received in your mailer was splitted on several lines. In this case, please re-type the link in one single line and retry. Third possibility: you have waited too long and your request has expired, or a technical error occurred. In this case, please try again to ask a new password from the start."
|
||||
msgid "wrong_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "A mail has been sent to you. Please follow the instructions from this email."
|
||||
msgid "reinit_mail_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Password re-initialisation"
|
||||
msgid "reinit_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>A password re-initialisation has been requested, tied to this email address, for the website ${siteUrl}. If you are not at the origin of this request, please ignore this email. Else, click on the link below to receive a new password.<br/><br/>${url}"
|
||||
msgid "reinit_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password"
|
||||
msgid "new_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>The new password you have requested for website ${siteUrl} is ${password}<br/><br/>Best regards."
|
||||
msgid "new_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password has been sent to you by email."
|
||||
msgid "new_password_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last access"
|
||||
msgid "last_user_access"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "History"
|
||||
msgid "object_history"
|
||||
msgstr "History"
|
||||
|
||||
#. Default: "By"
|
||||
msgid "object_created_by"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "On"
|
||||
msgid "object_created_on"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last updated on"
|
||||
msgid "object_modified_on"
|
||||
msgstr "Letzte Änderungen"
|
||||
|
||||
#. Default: "Action"
|
||||
msgid "object_action"
|
||||
msgstr "Aktion"
|
||||
|
||||
#. Default: "Author"
|
||||
msgid "object_author"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Date"
|
||||
msgid "action_date"
|
||||
msgstr "Datum"
|
||||
|
||||
#. Default: "Comment"
|
||||
msgid "action_comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
#. Default: "Create from another object"
|
||||
msgid "create_from_predecessor"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mon"
|
||||
msgid "day_Mon_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tue"
|
||||
msgid "day_Tue_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wed"
|
||||
msgid "day_Wed_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thu"
|
||||
msgid "day_Thu_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Fri"
|
||||
msgid "day_Fri_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sat"
|
||||
msgid "day_Sat_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sun"
|
||||
msgid "day_Sun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Off"
|
||||
msgid "day_Off_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Monday"
|
||||
msgid "day_Mon"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tuesday"
|
||||
msgid "day_Tue"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wednesday"
|
||||
msgid "day_Wed"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thursday"
|
||||
msgid "day_Thu"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Friday"
|
||||
msgid "day_Fri"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Saturday"
|
||||
msgid "day_Sat"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sunday"
|
||||
msgid "day_Sun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Day off"
|
||||
msgid "day_Off"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "AM"
|
||||
msgid "ampm_am"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "PM"
|
||||
msgid "ampm_pm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jan"
|
||||
msgid "month_Jan_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Feb"
|
||||
msgid "month_Feb_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mar"
|
||||
msgid "month_Mar_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Apr"
|
||||
msgid "month_Apr_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jun"
|
||||
msgid "month_Jun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jul"
|
||||
msgid "month_Jul_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Aug"
|
||||
msgid "month_Aug_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sep"
|
||||
msgid "month_Sep_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Oct"
|
||||
msgid "month_Oct_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Nov"
|
||||
msgid "month_Nov_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Dec"
|
||||
msgid "month_Dec_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "January"
|
||||
msgid "month_Jan"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "February"
|
||||
msgid "month_Feb"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "March"
|
||||
msgid "month_Mar"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "April"
|
||||
msgid "month_Apr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "June"
|
||||
msgid "month_Jun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "July"
|
||||
msgid "month_Jul"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Augustus"
|
||||
msgid "month_Aug"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "September"
|
||||
msgid "month_Sep"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "October"
|
||||
msgid "month_Oct"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "November"
|
||||
msgid "month_Nov"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "December"
|
||||
msgid "month_Dec"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Today"
|
||||
msgid "today"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Which event type would you like to create?"
|
||||
msgid "which_event"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Extend the event on the following number of days (leave blank to create an event on the current day only):"
|
||||
msgid "event_span"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Also delete successive events of the same type."
|
||||
msgid "del_next_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Several events"
|
||||
msgid "several_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Timeslot"
|
||||
msgid "timeslot"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All day"
|
||||
msgid "timeslot_main"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Cannot create such an event in the ${slot} slot."
|
||||
msgid "timeslot_misfit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Validate events"
|
||||
msgid "validate_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
|
||||
msgid "validate_events_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
|
||||
msgid "validate_events_done"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted by ${userName}"
|
||||
msgid "history_insert"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Deleted by ${userName}"
|
||||
msgid "history_delete"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Show changes"
|
||||
msgid "changes_show"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hide changes"
|
||||
msgid "changes_hide"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "an anonymous user"
|
||||
msgid "anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${date} - This page is locked by ${user}."
|
||||
msgid "page_locked"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "In some situations, by leaving this page this way, you may lose encoded data or prevent other users from editing it afterwards. Please use buttons instead."
|
||||
msgid "warn_leave_form"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You are not allowed to consult this."
|
||||
msgid "unauthorized"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Microsoft Internet Explorer ${version} is not supported. Please upgrade your browser to version ${min} or above."
|
||||
msgid "wrong_browser"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Send by email"
|
||||
msgid "email_send"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${site} - ${title} - ${template}"
|
||||
msgid "podmail_subject"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello, this email that was sent to you via ${site}. Please consult the attached file(s)."
|
||||
msgid "podmail_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "List"
|
||||
msgid "result_mode_list"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Grid"
|
||||
msgid "result_mode_grid"
|
||||
msgstr ""
|
805
gen/tr/en.po
|
@ -1,805 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Appy\n"
|
||||
"POT-Creation-Date: 2013-07-23 10:41-28\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"Language-code: en\n"
|
||||
"Language-name: en\n"
|
||||
"Preferred-encodings: utf-8 latin1\n"
|
||||
"Domain: Appy\n"
|
||||
"X-is-fallback-for: en-us en-ca\n"
|
||||
|
||||
#. Default: "Appy"
|
||||
msgid "app_name"
|
||||
msgstr "Appy"
|
||||
|
||||
#. Default: "state"
|
||||
msgid "workflow_state"
|
||||
msgstr "state"
|
||||
|
||||
#. Default: "Optional comment"
|
||||
msgid "workflow_comment"
|
||||
msgstr "Optional comment"
|
||||
|
||||
#. Default: "Manager"
|
||||
msgid "role_Manager"
|
||||
msgstr "Manager"
|
||||
|
||||
#. Default: "Anonymous"
|
||||
msgid "role_Anonymous"
|
||||
msgstr "Anonymous"
|
||||
|
||||
#. Default: "Authenticated"
|
||||
msgid "role_Authenticated"
|
||||
msgstr "Authenticated"
|
||||
|
||||
#. Default: "Owner"
|
||||
msgid "role_Owner"
|
||||
msgstr "Owner"
|
||||
|
||||
#. Default: "Title"
|
||||
msgid "appy_title"
|
||||
msgstr "Title"
|
||||
|
||||
#. Default: "Ok"
|
||||
msgid "appy_ok"
|
||||
msgstr "Ok"
|
||||
|
||||
#. Default: "Keywords"
|
||||
msgid "appy_SearchableText"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Data change"
|
||||
msgid "data_change"
|
||||
msgstr "Data change"
|
||||
|
||||
#. Default: "Modified field"
|
||||
msgid "modified_field"
|
||||
msgstr "Modified field"
|
||||
|
||||
#. Default: "Previous value or modification"
|
||||
msgid "previous_value"
|
||||
msgstr "Previous value or modification"
|
||||
|
||||
#. Default: "phase"
|
||||
msgid "phase"
|
||||
msgstr "phase"
|
||||
|
||||
#. Default: " - "
|
||||
msgid "choose_a_value"
|
||||
msgstr " - "
|
||||
|
||||
#. Default: "[ Documents ]"
|
||||
msgid "choose_a_doc"
|
||||
msgstr "[ Documents ]"
|
||||
|
||||
#. Default: "You must choose more elements here."
|
||||
msgid "min_ref_violated"
|
||||
msgstr "You must choose more elements here."
|
||||
|
||||
#. Default: "Too much elements are selected here."
|
||||
msgid "max_ref_violated"
|
||||
msgstr "Too much elements are selected here."
|
||||
|
||||
#. Default: "-"
|
||||
msgid "no_ref"
|
||||
msgstr "No object."
|
||||
|
||||
#. Default: "Add"
|
||||
msgid "add_ref"
|
||||
msgstr "Add"
|
||||
|
||||
#. Default: "Available elements"
|
||||
msgid "selectable_objects"
|
||||
msgstr "Selectable elements"
|
||||
|
||||
#. Default: "Inserted elements"
|
||||
msgid "selected_objects"
|
||||
msgstr "Inserted elements"
|
||||
|
||||
#. Default: "Move up"
|
||||
msgid "move_up"
|
||||
msgstr "Move up"
|
||||
|
||||
#. Default: "Move down"
|
||||
msgid "move_down"
|
||||
msgstr "Move down"
|
||||
|
||||
#. Default: "Move to top"
|
||||
msgid "move_top"
|
||||
msgstr "Move to top"
|
||||
|
||||
#. Default: "Move to bottom"
|
||||
msgid "move_bottom"
|
||||
msgstr "Move to bottom"
|
||||
|
||||
#. Default: "Update the hereabove number and click here to move this element to a new position."
|
||||
msgid "move_number"
|
||||
msgstr "Update the hereabove number and click here to move this element to a new position."
|
||||
|
||||
#. Default: "create"
|
||||
msgid "query_create"
|
||||
msgstr "create"
|
||||
|
||||
#. Default: "Nothing to see for the moment."
|
||||
msgid "query_no_result"
|
||||
msgstr "Nothing to see for the moment."
|
||||
|
||||
#. Default: "consult all"
|
||||
msgid "query_consult_all"
|
||||
msgstr "consult all"
|
||||
|
||||
#. Default: "Advanced search"
|
||||
msgid "search_title"
|
||||
msgstr "Advanced search"
|
||||
|
||||
#. Default: "Search"
|
||||
msgid "search_button"
|
||||
msgstr "Search"
|
||||
|
||||
#. Default: "Search results"
|
||||
msgid "search_results"
|
||||
msgstr "Search results"
|
||||
|
||||
#. Default: " "
|
||||
msgid "search_results_descr"
|
||||
msgstr " "
|
||||
|
||||
#. Default: "All results"
|
||||
msgid "search_results_all"
|
||||
msgstr "All results"
|
||||
|
||||
#. Default: "New search"
|
||||
msgid "search_new"
|
||||
msgstr "New search"
|
||||
|
||||
#. Default: "From"
|
||||
msgid "search_from"
|
||||
msgstr "From"
|
||||
|
||||
#. Default: "to"
|
||||
msgid "search_to"
|
||||
msgstr "to"
|
||||
|
||||
#. Default: "or"
|
||||
msgid "search_or"
|
||||
msgstr "or"
|
||||
|
||||
#. Default: "and"
|
||||
msgid "search_and"
|
||||
msgstr "and"
|
||||
|
||||
#. Default: "No move occurred: please specify a valid number."
|
||||
msgid "ref_invalid_index"
|
||||
msgstr "No move occurred: please specify a valid number."
|
||||
|
||||
#. Default: "An integer value is expected; do not enter any space."
|
||||
msgid "bad_long"
|
||||
msgstr "An integer value is expected; do not enter any space."
|
||||
|
||||
#. Default: "A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
|
||||
msgid "bad_float"
|
||||
msgstr "A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
|
||||
|
||||
#. Default: "Please specify a valid date."
|
||||
msgid "bad_date"
|
||||
msgstr "Please specify a valid date."
|
||||
|
||||
#. Default: "The value is not among possible values for this field."
|
||||
msgid "bad_select_value"
|
||||
msgstr "The value is not among possible values for this field."
|
||||
|
||||
#. Default: "(Un)select all"
|
||||
msgid "select_delesect"
|
||||
msgstr "(Un)select all"
|
||||
|
||||
#. Default: "Automatic (de)selection"
|
||||
msgid "select_auto"
|
||||
msgstr "Automatic (de)selection"
|
||||
|
||||
#. Default: "You must select at least one element."
|
||||
msgid "no_elem_selected"
|
||||
msgstr "You must select at least one element."
|
||||
|
||||
#. Default: "Edit"
|
||||
msgid "object_edit"
|
||||
msgstr "Edit"
|
||||
|
||||
#. Default: "Delete"
|
||||
msgid "object_delete"
|
||||
msgstr "Delete"
|
||||
|
||||
#. Default: "Delete selection"
|
||||
msgid "object_delete_many"
|
||||
msgstr "Delete selection"
|
||||
|
||||
#. Default: "Remove"
|
||||
msgid "object_unlink"
|
||||
msgstr "Unlink"
|
||||
|
||||
#. Default: "Remove selection"
|
||||
msgid "object_unlink_many"
|
||||
msgstr "Remove selection"
|
||||
|
||||
#. Default: "Insert"
|
||||
msgid "object_link"
|
||||
msgstr "Insert"
|
||||
|
||||
#. Default: "Insert selection"
|
||||
msgid "object_link_many"
|
||||
msgstr "Insert selection"
|
||||
|
||||
#. Default: "(Un)check everything"
|
||||
msgid "check_uncheck"
|
||||
msgstr "(Un)check everything"
|
||||
|
||||
#. Default: "Unlock"
|
||||
msgid "page_unlock"
|
||||
msgstr "Unlock"
|
||||
|
||||
#. Default: "Keep the file unchanged"
|
||||
msgid "keep_file"
|
||||
msgstr "Keep the file unchanged"
|
||||
|
||||
#. Default: "Delete the file"
|
||||
msgid "delete_file"
|
||||
msgstr "Delete the file"
|
||||
|
||||
#. Default: "Replace it with a new file"
|
||||
msgid "replace_file"
|
||||
msgstr "Replace it with a new file"
|
||||
|
||||
#. Default: "Are you sure?"
|
||||
msgid "action_confirm"
|
||||
msgstr "Are you sure?"
|
||||
|
||||
#. Default: "The action has been performed."
|
||||
msgid "action_done"
|
||||
msgstr "The action has been performed."
|
||||
|
||||
#. Default: "A problem occurred while executing the action."
|
||||
msgid "action_ko"
|
||||
msgstr "A problem occurred while executing the action."
|
||||
|
||||
#. Default: "Action could not be performed on ${nb} element(s)."
|
||||
msgid "action_partial"
|
||||
msgstr "Action could not be performed on ${nb} element(s)."
|
||||
|
||||
#. Default: "Action had no effect."
|
||||
msgid "action_null"
|
||||
msgstr "Action had no effect."
|
||||
|
||||
#. Default: "Are you sure you want to apply this change?"
|
||||
msgid "save_confirm"
|
||||
msgstr "Are you sure you want to apply this change?"
|
||||
|
||||
#. Default: "Are you sure? You are going to permanently change the order of these elements, for all users."
|
||||
msgid "sort_confirm"
|
||||
msgstr "Are you sure? You are going to permanently change the order of these elements, for all users."
|
||||
|
||||
#. Default: "Go to top"
|
||||
msgid "goto_first"
|
||||
msgstr "Go to top"
|
||||
|
||||
#. Default: "Go to previous"
|
||||
msgid "goto_previous"
|
||||
msgstr "Go to previous"
|
||||
|
||||
#. Default: "Go to next"
|
||||
msgid "goto_next"
|
||||
msgstr "Go to next"
|
||||
|
||||
#. Default: "Go to end"
|
||||
msgid "goto_last"
|
||||
msgstr "Go to end"
|
||||
|
||||
#. Default: "Go back"
|
||||
msgid "goto_source"
|
||||
msgstr "Go back"
|
||||
|
||||
#. Default: "Go to number"
|
||||
msgid "goto_number"
|
||||
msgstr "Go to number"
|
||||
|
||||
#. Default: "Whatever"
|
||||
msgid "whatever"
|
||||
msgstr "Whatever"
|
||||
|
||||
#. Default: "Yes"
|
||||
msgid "yes"
|
||||
msgstr "Yes"
|
||||
|
||||
#. Default: "No"
|
||||
msgid "no"
|
||||
msgstr "No"
|
||||
|
||||
#. Default: "Please fill this field."
|
||||
msgid "field_required"
|
||||
msgstr "Please fill this field."
|
||||
|
||||
#. Default: "Please fill or correct this."
|
||||
msgid "field_invalid"
|
||||
msgstr "Please fill or correct this."
|
||||
|
||||
#. Default: "Please select a file."
|
||||
msgid "file_required"
|
||||
msgstr "Please select a file."
|
||||
|
||||
#. Default: "The uploaded file must be an image."
|
||||
msgid "image_required"
|
||||
msgstr "The uploaded file must be an image."
|
||||
|
||||
#. Default: "ODT"
|
||||
msgid "odt"
|
||||
msgstr "ODT"
|
||||
|
||||
#. Default: "PDF"
|
||||
msgid "pdf"
|
||||
msgstr "PDF"
|
||||
|
||||
#. Default: "DOC"
|
||||
msgid "doc"
|
||||
msgstr "DOC"
|
||||
|
||||
#. Default: "ODS"
|
||||
msgid "ods"
|
||||
msgstr "ODS"
|
||||
|
||||
#. Default: "XLS"
|
||||
msgid "xls"
|
||||
msgstr "XLS"
|
||||
|
||||
#. Default: "frozen"
|
||||
msgid "frozen"
|
||||
msgstr "frozen"
|
||||
|
||||
#. Default: "(Re-)freeze"
|
||||
msgid "freezeField"
|
||||
msgstr "(Re-)freeze"
|
||||
|
||||
#. Default: "Unfreeze"
|
||||
msgid "unfreezeField"
|
||||
msgstr "Unfreeze"
|
||||
|
||||
#. Default: "Upload a new file"
|
||||
msgid "uploadField"
|
||||
msgstr "Upload a new file"
|
||||
|
||||
#. Default: "Please upload a file of the same type."
|
||||
msgid "upload_invalid"
|
||||
msgstr "Please upload a file of the same type."
|
||||
|
||||
#. Default: "Welcome to this Appy-powered site."
|
||||
msgid "front_page_text"
|
||||
msgstr "Welcome to this Appy-powered site."
|
||||
|
||||
#. Default: "Please type \"${text}\" (without the double quotes) in the field besides, but without the character at position ${number}."
|
||||
msgid "captcha_text"
|
||||
msgstr "Please type \"${text}\" (without the double quotes) in the field besides, but without the character at position ${number}."
|
||||
|
||||
#. Default: "The code was not correct. Please try again."
|
||||
msgid "bad_captcha"
|
||||
msgstr "The code was not correct. Please try again."
|
||||
|
||||
#. Default: "Login"
|
||||
msgid "app_login"
|
||||
msgstr "Login"
|
||||
|
||||
#. Default: "Log in"
|
||||
msgid "app_connect"
|
||||
msgstr "Log in"
|
||||
|
||||
#. Default: "Logout"
|
||||
msgid "app_logout"
|
||||
msgstr "Logout"
|
||||
|
||||
#. Default: "Password"
|
||||
msgid "app_password"
|
||||
msgstr "Password"
|
||||
|
||||
#. Default: "Home"
|
||||
msgid "app_home"
|
||||
msgstr "Home"
|
||||
|
||||
#. Default: "This login is reserved."
|
||||
msgid "login_reserved"
|
||||
msgstr "This login is reserved."
|
||||
|
||||
#. Default: "This login is already in use."
|
||||
msgid "login_in_use"
|
||||
msgstr "This login is already in use."
|
||||
|
||||
#. Default: "Login failed."
|
||||
msgid "login_ko"
|
||||
msgstr "Login failed."
|
||||
|
||||
#. Default: "Welcome! You are now logged in."
|
||||
msgid "login_ok"
|
||||
msgstr "Welcome! You are now logged in."
|
||||
|
||||
#. Default: "Passwords must contain at least ${nb} characters."
|
||||
msgid "password_too_short"
|
||||
msgstr "Passwords must contain at least ${nb} characters."
|
||||
|
||||
#. Default: "Passwords do not match."
|
||||
msgid "passwords_mismatch"
|
||||
msgstr "Passwords do not match."
|
||||
|
||||
#. Default: "Save"
|
||||
msgid "object_save"
|
||||
msgstr "Save"
|
||||
|
||||
#. Default: "Changes saved."
|
||||
msgid "object_saved"
|
||||
msgstr "Changes saved."
|
||||
|
||||
#. Default: "Please correct the indicated errors."
|
||||
msgid "validation_error"
|
||||
msgstr "Please correct the indicated errors."
|
||||
|
||||
#. Default: "Cancel"
|
||||
msgid "object_cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#. Default: "Changes canceled."
|
||||
msgid "object_canceled"
|
||||
msgstr "Changes canceled."
|
||||
|
||||
#. Default: "You must enable cookies before you can log in."
|
||||
msgid "enable_cookies"
|
||||
msgstr "You must enable cookies before you can log in."
|
||||
|
||||
#. Default: "Previous page"
|
||||
msgid "page_previous"
|
||||
msgstr "Previous page"
|
||||
|
||||
#. Default: "Next page"
|
||||
msgid "page_next"
|
||||
msgstr "Next page"
|
||||
|
||||
#. Default: "Forgot password?"
|
||||
msgid "forgot_password"
|
||||
msgstr "Forgot password?"
|
||||
|
||||
#. Default: "Ask new password"
|
||||
msgid "ask_password_reinit"
|
||||
msgstr "Ask new password"
|
||||
|
||||
#. Default: "Something went wrong. First possibility: you have already clicked on the link (maybe have you double-clicked?) and your password has already been re-initialized. Please check that you haven't received your new password in another email. Second possibility: the link that you received in your mailer was splitted on several lines. In this case, please re-type the link in one single line and retry. Third possibility: you have waited too long and your request has expired, or a technical error occurred. In this case, please try again to ask a new password from the start."
|
||||
msgid "wrong_password_reinit"
|
||||
msgstr "Something went wrong. First possibility: you have already clicked on the link (maybe have you double-clicked?) and your password has already been re-initialized. Please check that you haven't received your new password in another email. Second possibility: the link that you received in your mailer was splitted on several lines. In this case, please re-type the link in one single line and retry. Third possibility: you have waited too long and your request has expired, or a technical error occurred. In this case, please try again to ask a new password from the start."
|
||||
|
||||
#. Default: "A mail has been sent to you. Please follow the instructions from this email."
|
||||
msgid "reinit_mail_sent"
|
||||
msgstr "A mail has been sent to you. Please follow the instructions from this email."
|
||||
|
||||
#. Default: "Password re-initialisation"
|
||||
msgid "reinit_password"
|
||||
msgstr "Password re-initialisation"
|
||||
|
||||
#. Default: "Hello,<br/><br/>A password re-initialisation has been requested, tied to this email address, for the website ${siteUrl}. If you are not at the origin of this request, please ignore this email. Else, click on the link below to receive a new password.<br/><br/>${url}"
|
||||
msgid "reinit_password_body"
|
||||
msgstr "Hello,<br/><br/>A password re-initialisation has been requested, tied to this email address, for the website ${siteUrl}. If you are not at the origin of this request, please ignore this email. Else, click on the link below to receive a new password.<br/><br/>${url}"
|
||||
|
||||
#. Default: "Your new password"
|
||||
msgid "new_password"
|
||||
msgstr "Your new password"
|
||||
|
||||
#. Default: "Hello,<br/><br/>The new password you have requested for website ${siteUrl} is ${password}<br/><br/>Best regards."
|
||||
msgid "new_password_body"
|
||||
msgstr "Hello,<br/><br/>The new password you have requested for website ${siteUrl} is ${password}<br/><br/>Best regards."
|
||||
|
||||
#. Default: "Your new password has been sent to you by email."
|
||||
msgid "new_password_sent"
|
||||
msgstr "Your new password has been sent to you by email."
|
||||
|
||||
#. Default: "Last access"
|
||||
msgid "last_user_access"
|
||||
msgstr "Last access"
|
||||
|
||||
#. Default: "History"
|
||||
msgid "object_history"
|
||||
msgstr "History"
|
||||
|
||||
#. Default: "By"
|
||||
msgid "object_created_by"
|
||||
msgstr "By"
|
||||
|
||||
#. Default: "On"
|
||||
msgid "object_created_on"
|
||||
msgstr "On"
|
||||
|
||||
#. Default: "Last updated on"
|
||||
msgid "object_modified_on"
|
||||
msgstr "Last updated on"
|
||||
|
||||
#. Default: "Action"
|
||||
msgid "object_action"
|
||||
msgstr "Action"
|
||||
|
||||
#. Default: "Author"
|
||||
msgid "object_author"
|
||||
msgstr "Author"
|
||||
|
||||
#. Default: "Date"
|
||||
msgid "action_date"
|
||||
msgstr "Date"
|
||||
|
||||
#. Default: "Comment"
|
||||
msgid "action_comment"
|
||||
msgstr "Comment"
|
||||
|
||||
#. Default: "Create from another object"
|
||||
msgid "create_from_predecessor"
|
||||
msgstr "Create from another object"
|
||||
|
||||
#. Default: "Mon"
|
||||
msgid "day_Mon_short"
|
||||
msgstr "Mon"
|
||||
|
||||
#. Default: "Tue"
|
||||
msgid "day_Tue_short"
|
||||
msgstr "Tue"
|
||||
|
||||
#. Default: "Wed"
|
||||
msgid "day_Wed_short"
|
||||
msgstr "Wed"
|
||||
|
||||
#. Default: "Thu"
|
||||
msgid "day_Thu_short"
|
||||
msgstr "Thu"
|
||||
|
||||
#. Default: "Fri"
|
||||
msgid "day_Fri_short"
|
||||
msgstr "Fri"
|
||||
|
||||
#. Default: "Sat"
|
||||
msgid "day_Sat_short"
|
||||
msgstr "Sat"
|
||||
|
||||
#. Default: "Sun"
|
||||
msgid "day_Sun_short"
|
||||
msgstr "Sun"
|
||||
|
||||
#. Default: "Off"
|
||||
msgid "day_Off_short"
|
||||
msgstr "Off"
|
||||
|
||||
#. Default: "Monday"
|
||||
msgid "day_Mon"
|
||||
msgstr "Monday"
|
||||
|
||||
#. Default: "Tuesday"
|
||||
msgid "day_Tue"
|
||||
msgstr "Tuesday"
|
||||
|
||||
#. Default: "Wednesday"
|
||||
msgid "day_Wed"
|
||||
msgstr "Wednesday"
|
||||
|
||||
#. Default: "Thursday"
|
||||
msgid "day_Thu"
|
||||
msgstr "Thursday"
|
||||
|
||||
#. Default: "Friday"
|
||||
msgid "day_Fri"
|
||||
msgstr "Friday"
|
||||
|
||||
#. Default: "Saturday"
|
||||
msgid "day_Sat"
|
||||
msgstr "Saturday"
|
||||
|
||||
#. Default: "Sunday"
|
||||
msgid "day_Sun"
|
||||
msgstr "Sunday"
|
||||
|
||||
#. Default: "Day off"
|
||||
msgid "day_Off"
|
||||
msgstr "Day off"
|
||||
|
||||
#. Default: "AM"
|
||||
msgid "ampm_am"
|
||||
msgstr "AM"
|
||||
|
||||
#. Default: "PM"
|
||||
msgid "ampm_pm"
|
||||
msgstr "PM"
|
||||
|
||||
#. Default: "Jan"
|
||||
msgid "month_Jan_short"
|
||||
msgstr "Jan"
|
||||
|
||||
#. Default: "Feb"
|
||||
msgid "month_Feb_short"
|
||||
msgstr "Feb"
|
||||
|
||||
#. Default: "Mar"
|
||||
msgid "month_Mar_short"
|
||||
msgstr "Mar"
|
||||
|
||||
#. Default: "Apr"
|
||||
msgid "month_Apr_short"
|
||||
msgstr "Apr"
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May_short"
|
||||
msgstr "May"
|
||||
|
||||
#. Default: "Jun"
|
||||
msgid "month_Jun_short"
|
||||
msgstr "Jun"
|
||||
|
||||
#. Default: "Jul"
|
||||
msgid "month_Jul_short"
|
||||
msgstr "Jul"
|
||||
|
||||
#. Default: "Aug"
|
||||
msgid "month_Aug_short"
|
||||
msgstr "Aug"
|
||||
|
||||
#. Default: "Sep"
|
||||
msgid "month_Sep_short"
|
||||
msgstr "Sep"
|
||||
|
||||
#. Default: "Oct"
|
||||
msgid "month_Oct_short"
|
||||
msgstr "Oct"
|
||||
|
||||
#. Default: "Nov"
|
||||
msgid "month_Nov_short"
|
||||
msgstr "Nov"
|
||||
|
||||
#. Default: "Dec"
|
||||
msgid "month_Dec_short"
|
||||
msgstr "Dec"
|
||||
|
||||
#. Default: "January"
|
||||
msgid "month_Jan"
|
||||
msgstr "January"
|
||||
|
||||
#. Default: "February"
|
||||
msgid "month_Feb"
|
||||
msgstr "February"
|
||||
|
||||
#. Default: "March"
|
||||
msgid "month_Mar"
|
||||
msgstr "March"
|
||||
|
||||
#. Default: "April"
|
||||
msgid "month_Apr"
|
||||
msgstr "April"
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May"
|
||||
msgstr "May"
|
||||
|
||||
#. Default: "June"
|
||||
msgid "month_Jun"
|
||||
msgstr "June"
|
||||
|
||||
#. Default: "July"
|
||||
msgid "month_Jul"
|
||||
msgstr "July"
|
||||
|
||||
#. Default: "Augustus"
|
||||
msgid "month_Aug"
|
||||
msgstr "Augustus"
|
||||
|
||||
#. Default: "September"
|
||||
msgid "month_Sep"
|
||||
msgstr "September"
|
||||
|
||||
#. Default: "October"
|
||||
msgid "month_Oct"
|
||||
msgstr "October"
|
||||
|
||||
#. Default: "November"
|
||||
msgid "month_Nov"
|
||||
msgstr "November"
|
||||
|
||||
#. Default: "December"
|
||||
msgid "month_Dec"
|
||||
msgstr "December"
|
||||
|
||||
#. Default: "Today"
|
||||
msgid "today"
|
||||
msgstr "Today"
|
||||
|
||||
#. Default: "Which event type would you like to create?"
|
||||
msgid "which_event"
|
||||
msgstr "Which event type would you like to create?"
|
||||
|
||||
#. Default: "Extend the event on the following number of days (leave blank to create an event on the current day only):"
|
||||
msgid "event_span"
|
||||
msgstr "Extend the event on the following number of days (leave blank to create an event on the current day only):"
|
||||
|
||||
#. Default: "Also delete successive events of the same type."
|
||||
msgid "del_next_events"
|
||||
msgstr "Also delete successive events of the same type."
|
||||
|
||||
#. Default: "Several events"
|
||||
msgid "several_events"
|
||||
msgstr "Several events"
|
||||
|
||||
#. Default: "Timeslot"
|
||||
msgid "timeslot"
|
||||
msgstr "Timeslot"
|
||||
|
||||
#. Default: "All day"
|
||||
msgid "timeslot_main"
|
||||
msgstr "All day"
|
||||
|
||||
#. Default: "Cannot create such an event in the ${slot} slot."
|
||||
msgid "timeslot_misfit"
|
||||
msgstr "Cannot create such an event in the ${slot} slot."
|
||||
|
||||
#. Default: "Validate events"
|
||||
msgid "validate_events"
|
||||
msgstr "Validate events"
|
||||
|
||||
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
|
||||
msgid "validate_events_confirm"
|
||||
msgstr "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
|
||||
|
||||
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
|
||||
msgid "validate_events_done"
|
||||
msgstr "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
|
||||
|
||||
#. Default: "Inserted by ${userName}"
|
||||
msgid "history_insert"
|
||||
msgstr "Inserted by ${userName}"
|
||||
|
||||
#. Default: "Deleted by ${userName}"
|
||||
msgid "history_delete"
|
||||
msgstr "Deleted by ${userName}"
|
||||
|
||||
#. Default: "Show changes"
|
||||
msgid "changes_show"
|
||||
msgstr "Show changes"
|
||||
|
||||
#. Default: "Hide changes"
|
||||
msgid "changes_hide"
|
||||
msgstr "Hide changes"
|
||||
|
||||
#. Default: "an anonymous user"
|
||||
msgid "anonymous"
|
||||
msgstr "an anonymous user"
|
||||
|
||||
#. Default: "${date} - This page is locked by ${user}."
|
||||
msgid "page_locked"
|
||||
msgstr "${date} - This page is locked by ${user}."
|
||||
|
||||
#. Default: "In some situations, by leaving this page this way, you may lose encoded data or prevent other users from editing it afterwards. Please use buttons instead."
|
||||
msgid "warn_leave_form"
|
||||
msgstr "In some situations, by leaving this page this way, you may lose encoded data or prevent other users from editing it afterwards. Please use buttons instead."
|
||||
|
||||
#. Default: "You are not allowed to consult this."
|
||||
msgid "unauthorized"
|
||||
msgstr "You are not allowed to consult this."
|
||||
|
||||
#. Default: "Microsoft Internet Explorer ${version} is not supported. Please upgrade your browser to version ${min} or above."
|
||||
msgid "wrong_browser"
|
||||
msgstr "Microsoft Internet Explorer ${version} is not supported. Please upgrade your browser to version ${min} or above."
|
||||
|
||||
#. Default: "Send by email"
|
||||
msgid "email_send"
|
||||
msgstr "Send by email"
|
||||
|
||||
#. Default: "${site} - ${title} - ${template}"
|
||||
msgid "podmail_subject"
|
||||
msgstr "${site} - ${title} - ${template}"
|
||||
|
||||
#. Default: "Hello, this email that was sent to you via ${site}. Please consult the attached file(s)."
|
||||
msgid "podmail_body"
|
||||
msgstr "Hello, this email that was sent to you via ${site}. Please consult the attached file(s)."
|
||||
|
||||
#. Default: "List"
|
||||
msgid "result_mode_list"
|
||||
msgstr "List"
|
||||
|
||||
#. Default: "Grid"
|
||||
msgid "result_mode_grid"
|
||||
msgstr "Grid"
|
804
gen/tr/es.po
|
@ -1,804 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Appy\n"
|
||||
"POT-Creation-Date: 2013-07-23 10:41-28\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"Language-code: es\n"
|
||||
"Language-name: es\n"
|
||||
"Preferred-encodings: utf-8 latin1\n"
|
||||
"Domain: Appy\n"
|
||||
|
||||
#. Default: "Appy"
|
||||
msgid "app_name"
|
||||
msgstr "Appy"
|
||||
|
||||
#. Default: "state"
|
||||
msgid "workflow_state"
|
||||
msgstr "estado"
|
||||
|
||||
#. Default: "Optional comment"
|
||||
msgid "workflow_comment"
|
||||
msgstr "Comentario"
|
||||
|
||||
#. Default: "Manager"
|
||||
msgid "role_Manager"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Anonymous"
|
||||
msgid "role_Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Authenticated"
|
||||
msgid "role_Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Owner"
|
||||
msgid "role_Owner"
|
||||
msgstr "Proprietario"
|
||||
|
||||
#. Default: "Title"
|
||||
msgid "appy_title"
|
||||
msgstr "Título"
|
||||
|
||||
#. Default: "Ok"
|
||||
msgid "appy_ok"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Keywords"
|
||||
msgid "appy_SearchableText"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Data change"
|
||||
msgid "data_change"
|
||||
msgstr "Cambio de datos"
|
||||
|
||||
#. Default: "Modified field"
|
||||
msgid "modified_field"
|
||||
msgstr "Campo modificado"
|
||||
|
||||
#. Default: "Previous value or modification"
|
||||
msgid "previous_value"
|
||||
msgstr "Valor precedente"
|
||||
|
||||
#. Default: "phase"
|
||||
msgid "phase"
|
||||
msgstr "Fase"
|
||||
|
||||
#. Default: " - "
|
||||
msgid "choose_a_value"
|
||||
msgstr "[ Elija ]"
|
||||
|
||||
#. Default: "[ Documents ]"
|
||||
msgid "choose_a_doc"
|
||||
msgstr "[ Documentos ]"
|
||||
|
||||
#. Default: "You must choose more elements here."
|
||||
msgid "min_ref_violated"
|
||||
msgstr "Debe elegir más elementos aquí."
|
||||
|
||||
#. Default: "Too much elements are selected here."
|
||||
msgid "max_ref_violated"
|
||||
msgstr "Demasiados elementos son seleccionados aquí."
|
||||
|
||||
#. Default: "-"
|
||||
msgid "no_ref"
|
||||
msgstr "Ningún elemento."
|
||||
|
||||
#. Default: "Add"
|
||||
msgid "add_ref"
|
||||
msgstr "Añadir"
|
||||
|
||||
#. Default: "Available elements"
|
||||
msgid "selectable_objects"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted elements"
|
||||
msgid "selected_objects"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move up"
|
||||
msgid "move_up"
|
||||
msgstr "Mueva hacia arriba"
|
||||
|
||||
#. Default: "Move down"
|
||||
msgid "move_down"
|
||||
msgstr "Mueva hacia abajo"
|
||||
|
||||
#. Default: "Move to top"
|
||||
msgid "move_top"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move to bottom"
|
||||
msgid "move_bottom"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Update the hereabove number and click here to move this element to a new position."
|
||||
msgid "move_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "create"
|
||||
msgid "query_create"
|
||||
msgstr "Crear"
|
||||
|
||||
#. Default: "Nothing to see for the moment."
|
||||
msgid "query_no_result"
|
||||
msgstr "No hay resultados."
|
||||
|
||||
#. Default: "consult all"
|
||||
msgid "query_consult_all"
|
||||
msgstr "Consultar todo"
|
||||
|
||||
#. Default: "Advanced search"
|
||||
msgid "search_title"
|
||||
msgstr "Búsqueda avanzada"
|
||||
|
||||
#. Default: "Search"
|
||||
msgid "search_button"
|
||||
msgstr "Buscar"
|
||||
|
||||
#. Default: "Search results"
|
||||
msgid "search_results"
|
||||
msgstr "Resultados"
|
||||
|
||||
#. Default: " "
|
||||
msgid "search_results_descr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All results"
|
||||
msgid "search_results_all"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "New search"
|
||||
msgid "search_new"
|
||||
msgstr "Nueva búsqueda"
|
||||
|
||||
#. Default: "From"
|
||||
msgid "search_from"
|
||||
msgstr "De"
|
||||
|
||||
#. Default: "to"
|
||||
msgid "search_to"
|
||||
msgstr "a"
|
||||
|
||||
#. Default: "or"
|
||||
msgid "search_or"
|
||||
msgstr "o"
|
||||
|
||||
#. Default: "and"
|
||||
msgid "search_and"
|
||||
msgstr "y"
|
||||
|
||||
#. Default: "No move occurred: please specify a valid number."
|
||||
msgid "ref_invalid_index"
|
||||
msgstr "Ningún desplazamiento tuvo lugar: por favor especifique un número válido."
|
||||
|
||||
#. Default: "An integer value is expected; do not enter any space."
|
||||
msgid "bad_long"
|
||||
msgstr "Un valor entero es requerido."
|
||||
|
||||
#. Default: "A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
|
||||
msgid "bad_float"
|
||||
msgstr "Un valor real es requerido."
|
||||
|
||||
#. Default: "Please specify a valid date."
|
||||
msgid "bad_date"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The value is not among possible values for this field."
|
||||
msgid "bad_select_value"
|
||||
msgstr "Este valor no es permitido para este campo."
|
||||
|
||||
#. Default: "(Un)select all"
|
||||
msgid "select_delesect"
|
||||
msgstr "(des)seleccionar todo"
|
||||
|
||||
#. Default: "Automatic (de)selection"
|
||||
msgid "select_auto"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must select at least one element."
|
||||
msgid "no_elem_selected"
|
||||
msgstr "Debe elegir al menos un elemento."
|
||||
|
||||
#. Default: "Edit"
|
||||
msgid "object_edit"
|
||||
msgstr "Editar"
|
||||
|
||||
#. Default: "Delete"
|
||||
msgid "object_delete"
|
||||
msgstr "Eliminar"
|
||||
|
||||
#. Default: "Delete selection"
|
||||
msgid "object_delete_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Remove"
|
||||
msgid "object_unlink"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Remove selection"
|
||||
msgid "object_unlink_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert"
|
||||
msgid "object_link"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert selection"
|
||||
msgid "object_link_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Un)check everything"
|
||||
msgid "check_uncheck"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unlock"
|
||||
msgid "page_unlock"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Keep the file unchanged"
|
||||
msgid "keep_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Delete the file"
|
||||
msgid "delete_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Replace it with a new file"
|
||||
msgid "replace_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure?"
|
||||
msgid "action_confirm"
|
||||
msgstr "¿Está seguro?"
|
||||
|
||||
#. Default: "The action has been performed."
|
||||
msgid "action_done"
|
||||
msgstr "La acción ha sido efectuada."
|
||||
|
||||
#. Default: "A problem occurred while executing the action."
|
||||
msgid "action_ko"
|
||||
msgstr "Ha surgido un problema."
|
||||
|
||||
#. Default: "Action could not be performed on ${nb} element(s)."
|
||||
msgid "action_partial"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action had no effect."
|
||||
msgid "action_null"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure you want to apply this change?"
|
||||
msgid "save_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure? You are going to permanently change the order of these elements, for all users."
|
||||
msgid "sort_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to top"
|
||||
msgid "goto_first"
|
||||
msgstr "Ir al inicio"
|
||||
|
||||
#. Default: "Go to previous"
|
||||
msgid "goto_previous"
|
||||
msgstr "Ir al elemento precedente"
|
||||
|
||||
#. Default: "Go to next"
|
||||
msgid "goto_next"
|
||||
msgstr "Ir al elemento siguiente"
|
||||
|
||||
#. Default: "Go to end"
|
||||
msgid "goto_last"
|
||||
msgstr "Ir al final"
|
||||
|
||||
#. Default: "Go back"
|
||||
msgid "goto_source"
|
||||
msgstr "Volver"
|
||||
|
||||
#. Default: "Go to number"
|
||||
msgid "goto_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Whatever"
|
||||
msgid "whatever"
|
||||
msgstr "No importa"
|
||||
|
||||
#. Default: "Yes"
|
||||
msgid "yes"
|
||||
msgstr "Sí"
|
||||
|
||||
#. Default: "No"
|
||||
msgid "no"
|
||||
msgstr "No"
|
||||
|
||||
#. Default: "Please fill this field."
|
||||
msgid "field_required"
|
||||
msgstr "Por favor rellene este campo."
|
||||
|
||||
#. Default: "Please fill or correct this."
|
||||
msgid "field_invalid"
|
||||
msgstr "Por favor rellene o corrija este campo."
|
||||
|
||||
#. Default: "Please select a file."
|
||||
msgid "file_required"
|
||||
msgstr "Por favor elija un fichero."
|
||||
|
||||
#. Default: "The uploaded file must be an image."
|
||||
msgid "image_required"
|
||||
msgstr "El fichero a cargar debe ser una imagen."
|
||||
|
||||
#. Default: "ODT"
|
||||
msgid "odt"
|
||||
msgstr "ODT (OpenOffice/LibreOffice)"
|
||||
|
||||
#. Default: "PDF"
|
||||
msgid "pdf"
|
||||
msgstr "PDF"
|
||||
|
||||
#. Default: "DOC"
|
||||
msgid "doc"
|
||||
msgstr "DOC (Microsoft Word)"
|
||||
|
||||
#. Default: "ODS"
|
||||
msgid "ods"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "XLS"
|
||||
msgid "xls"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "frozen"
|
||||
msgid "frozen"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Re-)freeze"
|
||||
msgid "freezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unfreeze"
|
||||
msgid "unfreezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Upload a new file"
|
||||
msgid "uploadField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please upload a file of the same type."
|
||||
msgid "upload_invalid"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Welcome to this Appy-powered site."
|
||||
msgid "front_page_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please type \"${text}\" (without the double quotes) in the field besides, but without the character at position ${number}."
|
||||
msgid "captcha_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The code was not correct. Please try again."
|
||||
msgid "bad_captcha"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Login"
|
||||
msgid "app_login"
|
||||
msgstr "Login"
|
||||
|
||||
#. Default: "Log in"
|
||||
msgid "app_connect"
|
||||
msgstr "Entrar"
|
||||
|
||||
#. Default: "Logout"
|
||||
msgid "app_logout"
|
||||
msgstr "Salir"
|
||||
|
||||
#. Default: "Password"
|
||||
msgid "app_password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#. Default: "Home"
|
||||
msgid "app_home"
|
||||
msgstr "volver a mi página principal"
|
||||
|
||||
#. Default: "This login is reserved."
|
||||
msgid "login_reserved"
|
||||
msgstr "Este login está reservado."
|
||||
|
||||
#. Default: "This login is already in use."
|
||||
msgid "login_in_use"
|
||||
msgstr "El nombre de usuario que ha elegido ya está en uso o no es válido. Por favor, elija otro."
|
||||
|
||||
#. Default: "Login failed."
|
||||
msgid "login_ko"
|
||||
msgstr "Error en el inicio de sesión."
|
||||
|
||||
#. Default: "Welcome! You are now logged in."
|
||||
msgid "login_ok"
|
||||
msgstr "¡Bienvenido! Ha iniciado la sesión."
|
||||
|
||||
#. Default: "Passwords must contain at least ${nb} characters."
|
||||
msgid "password_too_short"
|
||||
msgstr "Las contraseñas deben de contener al menos ${nb} caracteres."
|
||||
|
||||
#. Default: "Passwords do not match."
|
||||
msgid "passwords_mismatch"
|
||||
msgstr "Las contraseñas no coinciden."
|
||||
|
||||
#. Default: "Save"
|
||||
msgid "object_save"
|
||||
msgstr "Guardar"
|
||||
|
||||
#. Default: "Changes saved."
|
||||
msgid "object_saved"
|
||||
msgstr "Cambios guardados."
|
||||
|
||||
#. Default: "Please correct the indicated errors."
|
||||
msgid "validation_error"
|
||||
msgstr "Por favor, corrija los errores indicados."
|
||||
|
||||
#. Default: "Cancel"
|
||||
msgid "object_cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#. Default: "Changes canceled."
|
||||
msgid "object_canceled"
|
||||
msgstr "Cambios cancelados."
|
||||
|
||||
#. Default: "You must enable cookies before you can log in."
|
||||
msgid "enable_cookies"
|
||||
msgstr "Debe habilitar las cookies antes de iniciar la sesión."
|
||||
|
||||
#. Default: "Previous page"
|
||||
msgid "page_previous"
|
||||
msgstr "Anterior"
|
||||
|
||||
#. Default: "Next page"
|
||||
msgid "page_next"
|
||||
msgstr "Siguiente"
|
||||
|
||||
#. Default: "Forgot password?"
|
||||
msgid "forgot_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Ask new password"
|
||||
msgid "ask_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Something went wrong. First possibility: you have already clicked on the link (maybe have you double-clicked?) and your password has already been re-initialized. Please check that you haven't received your new password in another email. Second possibility: the link that you received in your mailer was splitted on several lines. In this case, please re-type the link in one single line and retry. Third possibility: you have waited too long and your request has expired, or a technical error occurred. In this case, please try again to ask a new password from the start."
|
||||
msgid "wrong_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "A mail has been sent to you. Please follow the instructions from this email."
|
||||
msgid "reinit_mail_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Password re-initialisation"
|
||||
msgid "reinit_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>A password re-initialisation has been requested, tied to this email address, for the website ${siteUrl}. If you are not at the origin of this request, please ignore this email. Else, click on the link below to receive a new password.<br/><br/>${url}"
|
||||
msgid "reinit_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password"
|
||||
msgid "new_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>The new password you have requested for website ${siteUrl} is ${password}<br/><br/>Best regards."
|
||||
msgid "new_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password has been sent to you by email."
|
||||
msgid "new_password_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last access"
|
||||
msgid "last_user_access"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "History"
|
||||
msgid "object_history"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "By"
|
||||
msgid "object_created_by"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "On"
|
||||
msgid "object_created_on"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last updated on"
|
||||
msgid "object_modified_on"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action"
|
||||
msgid "object_action"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Author"
|
||||
msgid "object_author"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Date"
|
||||
msgid "action_date"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Comment"
|
||||
msgid "action_comment"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Create from another object"
|
||||
msgid "create_from_predecessor"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mon"
|
||||
msgid "day_Mon_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tue"
|
||||
msgid "day_Tue_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wed"
|
||||
msgid "day_Wed_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thu"
|
||||
msgid "day_Thu_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Fri"
|
||||
msgid "day_Fri_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sat"
|
||||
msgid "day_Sat_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sun"
|
||||
msgid "day_Sun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Off"
|
||||
msgid "day_Off_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Monday"
|
||||
msgid "day_Mon"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tuesday"
|
||||
msgid "day_Tue"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wednesday"
|
||||
msgid "day_Wed"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thursday"
|
||||
msgid "day_Thu"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Friday"
|
||||
msgid "day_Fri"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Saturday"
|
||||
msgid "day_Sat"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sunday"
|
||||
msgid "day_Sun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Day off"
|
||||
msgid "day_Off"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "AM"
|
||||
msgid "ampm_am"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "PM"
|
||||
msgid "ampm_pm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jan"
|
||||
msgid "month_Jan_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Feb"
|
||||
msgid "month_Feb_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mar"
|
||||
msgid "month_Mar_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Apr"
|
||||
msgid "month_Apr_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jun"
|
||||
msgid "month_Jun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jul"
|
||||
msgid "month_Jul_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Aug"
|
||||
msgid "month_Aug_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sep"
|
||||
msgid "month_Sep_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Oct"
|
||||
msgid "month_Oct_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Nov"
|
||||
msgid "month_Nov_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Dec"
|
||||
msgid "month_Dec_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "January"
|
||||
msgid "month_Jan"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "February"
|
||||
msgid "month_Feb"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "March"
|
||||
msgid "month_Mar"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "April"
|
||||
msgid "month_Apr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "June"
|
||||
msgid "month_Jun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "July"
|
||||
msgid "month_Jul"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Augustus"
|
||||
msgid "month_Aug"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "September"
|
||||
msgid "month_Sep"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "October"
|
||||
msgid "month_Oct"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "November"
|
||||
msgid "month_Nov"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "December"
|
||||
msgid "month_Dec"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Today"
|
||||
msgid "today"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Which event type would you like to create?"
|
||||
msgid "which_event"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Extend the event on the following number of days (leave blank to create an event on the current day only):"
|
||||
msgid "event_span"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Also delete successive events of the same type."
|
||||
msgid "del_next_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Several events"
|
||||
msgid "several_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Timeslot"
|
||||
msgid "timeslot"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All day"
|
||||
msgid "timeslot_main"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Cannot create such an event in the ${slot} slot."
|
||||
msgid "timeslot_misfit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Validate events"
|
||||
msgid "validate_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
|
||||
msgid "validate_events_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
|
||||
msgid "validate_events_done"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted by ${userName}"
|
||||
msgid "history_insert"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Deleted by ${userName}"
|
||||
msgid "history_delete"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Show changes"
|
||||
msgid "changes_show"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hide changes"
|
||||
msgid "changes_hide"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "an anonymous user"
|
||||
msgid "anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${date} - This page is locked by ${user}."
|
||||
msgid "page_locked"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "In some situations, by leaving this page this way, you may lose encoded data or prevent other users from editing it afterwards. Please use buttons instead."
|
||||
msgid "warn_leave_form"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You are not allowed to consult this."
|
||||
msgid "unauthorized"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Microsoft Internet Explorer ${version} is not supported. Please upgrade your browser to version ${min} or above."
|
||||
msgid "wrong_browser"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Send by email"
|
||||
msgid "email_send"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${site} - ${title} - ${template}"
|
||||
msgid "podmail_subject"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello, this email that was sent to you via ${site}. Please consult the attached file(s)."
|
||||
msgid "podmail_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "List"
|
||||
msgid "result_mode_list"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Grid"
|
||||
msgid "result_mode_grid"
|
||||
msgstr ""
|
805
gen/tr/fr.po
|
@ -1,805 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Appy\n"
|
||||
"POT-Creation-Date: 2013-07-23 10:41-28\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"Language-code: fr\n"
|
||||
"Language-name: fr\n"
|
||||
"Preferred-encodings: utf-8 latin1\n"
|
||||
"Domain: Appy\n"
|
||||
"X-is-fallback-for: fr-be fr-ca fr-lu fr-mc fr-ch fr-fr\n"
|
||||
|
||||
#. Default: "Appy"
|
||||
msgid "app_name"
|
||||
msgstr "Appy"
|
||||
|
||||
#. Default: "state"
|
||||
msgid "workflow_state"
|
||||
msgstr "état"
|
||||
|
||||
#. Default: "Optional comment"
|
||||
msgid "workflow_comment"
|
||||
msgstr "Commentaire"
|
||||
|
||||
#. Default: "Manager"
|
||||
msgid "role_Manager"
|
||||
msgstr "Administrateur"
|
||||
|
||||
#. Default: "Anonymous"
|
||||
msgid "role_Anonymous"
|
||||
msgstr "Anonyme"
|
||||
|
||||
#. Default: "Authenticated"
|
||||
msgid "role_Authenticated"
|
||||
msgstr "Authentifié"
|
||||
|
||||
#. Default: "Owner"
|
||||
msgid "role_Owner"
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#. Default: "Title"
|
||||
msgid "appy_title"
|
||||
msgstr "Titre"
|
||||
|
||||
#. Default: "Ok"
|
||||
msgid "appy_ok"
|
||||
msgstr "Ok"
|
||||
|
||||
#. Default: "Keywords"
|
||||
msgid "appy_SearchableText"
|
||||
msgstr "Mots-clé"
|
||||
|
||||
#. Default: "Data change"
|
||||
msgid "data_change"
|
||||
msgstr "Changement de données"
|
||||
|
||||
#. Default: "Modified field"
|
||||
msgid "modified_field"
|
||||
msgstr "Champ modifié"
|
||||
|
||||
#. Default: "Previous value or modification"
|
||||
msgid "previous_value"
|
||||
msgstr "Valeur précédente ou modification"
|
||||
|
||||
#. Default: "phase"
|
||||
msgid "phase"
|
||||
msgstr "Phase"
|
||||
|
||||
#. Default: " - "
|
||||
msgid "choose_a_value"
|
||||
msgstr "[ choisissez ]"
|
||||
|
||||
#. Default: "[ Documents ]"
|
||||
msgid "choose_a_doc"
|
||||
msgstr "[ Documents ]"
|
||||
|
||||
#. Default: "You must choose more elements here."
|
||||
msgid "min_ref_violated"
|
||||
msgstr "Vous devez choisir plus d'éléments ici."
|
||||
|
||||
#. Default: "Too much elements are selected here."
|
||||
msgid "max_ref_violated"
|
||||
msgstr "Trop d'éléments sont sélectionnés ici."
|
||||
|
||||
#. Default: "-"
|
||||
msgid "no_ref"
|
||||
msgstr "-"
|
||||
|
||||
#. Default: "Add"
|
||||
msgid "add_ref"
|
||||
msgstr "Ajouter"
|
||||
|
||||
#. Default: "Available elements"
|
||||
msgid "selectable_objects"
|
||||
msgstr "Éléments sélectionnables"
|
||||
|
||||
#. Default: "Inserted elements"
|
||||
msgid "selected_objects"
|
||||
msgstr "Éléments insérés"
|
||||
|
||||
#. Default: "Move up"
|
||||
msgid "move_up"
|
||||
msgstr "Déplacer vers le haut"
|
||||
|
||||
#. Default: "Move down"
|
||||
msgid "move_down"
|
||||
msgstr "Déplacer vers le bas"
|
||||
|
||||
#. Default: "Move to top"
|
||||
msgid "move_top"
|
||||
msgstr "Déplacer en première position"
|
||||
|
||||
#. Default: "Move to bottom"
|
||||
msgid "move_bottom"
|
||||
msgstr "Déplacer en dernière position"
|
||||
|
||||
#. Default: "Update the hereabove number and click here to move this element to a new position."
|
||||
msgid "move_number"
|
||||
msgstr "Modifiez le numéro ci-dessus et cliquez ici pour déplacer l'élément à une nouvelle position."
|
||||
|
||||
#. Default: "create"
|
||||
msgid "query_create"
|
||||
msgstr "Créer"
|
||||
|
||||
#. Default: "Nothing to see for the moment."
|
||||
msgid "query_no_result"
|
||||
msgstr "Pas de résultat."
|
||||
|
||||
#. Default: "consult all"
|
||||
msgid "query_consult_all"
|
||||
msgstr "Tout consulter"
|
||||
|
||||
#. Default: "Advanced search"
|
||||
msgid "search_title"
|
||||
msgstr "Recherche avancée"
|
||||
|
||||
#. Default: "Search"
|
||||
msgid "search_button"
|
||||
msgstr "Rechercher"
|
||||
|
||||
#. Default: "Search results"
|
||||
msgid "search_results"
|
||||
msgstr "Résultats"
|
||||
|
||||
#. Default: " "
|
||||
msgid "search_results_descr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All results"
|
||||
msgid "search_results_all"
|
||||
msgstr "Tous les résultats"
|
||||
|
||||
#. Default: "New search"
|
||||
msgid "search_new"
|
||||
msgstr "Nouvelle recherche"
|
||||
|
||||
#. Default: "From"
|
||||
msgid "search_from"
|
||||
msgstr "De"
|
||||
|
||||
#. Default: "to"
|
||||
msgid "search_to"
|
||||
msgstr "à"
|
||||
|
||||
#. Default: "or"
|
||||
msgid "search_or"
|
||||
msgstr "ou"
|
||||
|
||||
#. Default: "and"
|
||||
msgid "search_and"
|
||||
msgstr "et"
|
||||
|
||||
#. Default: "No move occurred: please specify a valid number."
|
||||
msgid "ref_invalid_index"
|
||||
msgstr "Aucun déplacement n'a eu lieu: veuillez spécifier un nombre valide."
|
||||
|
||||
#. Default: "An integer value is expected; do not enter any space."
|
||||
msgid "bad_long"
|
||||
msgstr "Une valeur entière est attendue."
|
||||
|
||||
#. Default: "A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
|
||||
msgid "bad_float"
|
||||
msgstr "Une valeur réelle est attendue."
|
||||
|
||||
#. Default: "Please specify a valid date."
|
||||
msgid "bad_date"
|
||||
msgstr "Veuillez spécifier une date valide."
|
||||
|
||||
#. Default: "The value is not among possible values for this field."
|
||||
msgid "bad_select_value"
|
||||
msgstr "Cette valeur n'est pas permise pour ce champ."
|
||||
|
||||
#. Default: "(Un)select all"
|
||||
msgid "select_delesect"
|
||||
msgstr "Tout (dé)sélectionner"
|
||||
|
||||
#. Default: "Automatic (de)selection"
|
||||
msgid "select_auto"
|
||||
msgstr "(dé)sélection automatique"
|
||||
|
||||
#. Default: "You must select at least one element."
|
||||
msgid "no_elem_selected"
|
||||
msgstr "Vous devez choisir au moins un élément."
|
||||
|
||||
#. Default: "Edit"
|
||||
msgid "object_edit"
|
||||
msgstr "Modifier"
|
||||
|
||||
#. Default: "Delete"
|
||||
msgid "object_delete"
|
||||
msgstr "Supprimer"
|
||||
|
||||
#. Default: "Delete selection"
|
||||
msgid "object_delete_many"
|
||||
msgstr "Supprimer la sélection"
|
||||
|
||||
#. Default: "Remove"
|
||||
msgid "object_unlink"
|
||||
msgstr "Retirer"
|
||||
|
||||
#. Default: "Remove selection"
|
||||
msgid "object_unlink_many"
|
||||
msgstr "Retirer la sélection"
|
||||
|
||||
#. Default: "Insert"
|
||||
msgid "object_link"
|
||||
msgstr "Insérer"
|
||||
|
||||
#. Default: "Insert selection"
|
||||
msgid "object_link_many"
|
||||
msgstr "Insérer la sélection"
|
||||
|
||||
#. Default: "(Un)check everything"
|
||||
msgid "check_uncheck"
|
||||
msgstr "Tout (dé)sélectionner"
|
||||
|
||||
#. Default: "Unlock"
|
||||
msgid "page_unlock"
|
||||
msgstr "Déverrouiller"
|
||||
|
||||
#. Default: "Keep the file unchanged"
|
||||
msgid "keep_file"
|
||||
msgstr "Conserver le fichier"
|
||||
|
||||
#. Default: "Delete the file"
|
||||
msgid "delete_file"
|
||||
msgstr "Supprimer le fichier"
|
||||
|
||||
#. Default: "Replace it with a new file"
|
||||
msgid "replace_file"
|
||||
msgstr "Le remplacer par un autre"
|
||||
|
||||
#. Default: "Are you sure?"
|
||||
msgid "action_confirm"
|
||||
msgstr "Êtes-vous sûr?"
|
||||
|
||||
#. Default: "The action has been performed."
|
||||
msgid "action_done"
|
||||
msgstr "L'action a été réalisée."
|
||||
|
||||
#. Default: "A problem occurred while executing the action."
|
||||
msgid "action_ko"
|
||||
msgstr "Un problème est survenu."
|
||||
|
||||
#. Default: "Action could not be performed on ${nb} element(s)."
|
||||
msgid "action_partial"
|
||||
msgstr "L'action n'a pu être réalisée pour ${nb} élément(s)."
|
||||
|
||||
#. Default: "Action had no effect."
|
||||
msgid "action_null"
|
||||
msgstr "L'action n'a eu aucun effet."
|
||||
|
||||
#. Default: "Are you sure you want to apply this change?"
|
||||
msgid "save_confirm"
|
||||
msgstr "Êtes-vous sûr de vouloir appliquer ce changement?"
|
||||
|
||||
#. Default: "Are you sure? You are going to permanently change the order of these elements, for all users."
|
||||
msgid "sort_confirm"
|
||||
msgstr "Êtes-vous sûr? Vous allez changer l'ordre de ces éléments de manière permanente, pour tous les utilisateurs."
|
||||
|
||||
#. Default: "Go to top"
|
||||
msgid "goto_first"
|
||||
msgstr "Aller au début"
|
||||
|
||||
#. Default: "Go to previous"
|
||||
msgid "goto_previous"
|
||||
msgstr "Aller à l'élément précédent"
|
||||
|
||||
#. Default: "Go to next"
|
||||
msgid "goto_next"
|
||||
msgstr "Aller à l'élément suivant"
|
||||
|
||||
#. Default: "Go to end"
|
||||
msgid "goto_last"
|
||||
msgstr "Aller à la fin"
|
||||
|
||||
#. Default: "Go back"
|
||||
msgid "goto_source"
|
||||
msgstr "Retour"
|
||||
|
||||
#. Default: "Go to number"
|
||||
msgid "goto_number"
|
||||
msgstr "Aller au numéro"
|
||||
|
||||
#. Default: "Whatever"
|
||||
msgid "whatever"
|
||||
msgstr "Peu importe"
|
||||
|
||||
#. Default: "Yes"
|
||||
msgid "yes"
|
||||
msgstr "Oui"
|
||||
|
||||
#. Default: "No"
|
||||
msgid "no"
|
||||
msgstr "Non"
|
||||
|
||||
#. Default: "Please fill this field."
|
||||
msgid "field_required"
|
||||
msgstr "Veuillez remplir ce champ."
|
||||
|
||||
#. Default: "Please fill or correct this."
|
||||
msgid "field_invalid"
|
||||
msgstr "Veuillez remplir ou corriger ce champ."
|
||||
|
||||
#. Default: "Please select a file."
|
||||
msgid "file_required"
|
||||
msgstr "Veuillez choisir un fichier."
|
||||
|
||||
#. Default: "The uploaded file must be an image."
|
||||
msgid "image_required"
|
||||
msgstr "Le fichier à uploader doit être une image."
|
||||
|
||||
#. Default: "ODT"
|
||||
msgid "odt"
|
||||
msgstr "ODT (LibreOffice Writer)"
|
||||
|
||||
#. Default: "PDF"
|
||||
msgid "pdf"
|
||||
msgstr "PDF"
|
||||
|
||||
#. Default: "DOC"
|
||||
msgid "doc"
|
||||
msgstr "DOC (Microsoft Word)"
|
||||
|
||||
#. Default: "ODS"
|
||||
msgid "ods"
|
||||
msgstr "ODS (LibreOffice Calc)"
|
||||
|
||||
#. Default: "XLS"
|
||||
msgid "xls"
|
||||
msgstr "XLS (Microsoft Excel)"
|
||||
|
||||
#. Default: "frozen"
|
||||
msgid "frozen"
|
||||
msgstr "gelé"
|
||||
|
||||
#. Default: "(Re-)freeze"
|
||||
msgid "freezeField"
|
||||
msgstr "(Re-)geler"
|
||||
|
||||
#. Default: "Unfreeze"
|
||||
msgid "unfreezeField"
|
||||
msgstr "Dégeler"
|
||||
|
||||
#. Default: "Upload a new file"
|
||||
msgid "uploadField"
|
||||
msgstr "Écraser par..."
|
||||
|
||||
#. Default: "Please upload a file of the same type."
|
||||
msgid "upload_invalid"
|
||||
msgstr "Veuillez uploader un fichier du même type."
|
||||
|
||||
#. Default: "Welcome to this Appy-powered site."
|
||||
msgid "front_page_text"
|
||||
msgstr "Bienvenue sur ce site fabriqué avec <a href=\"http://appyframework.org\" target=\"_blank\">Appy</a>"
|
||||
|
||||
#. Default: "Please type \"${text}\" (without the double quotes) in the field besides, but without the character at position ${number}."
|
||||
msgid "captcha_text"
|
||||
msgstr "Tapez \"${text}\" (sans les guillemets) ci-contre, en omettant le caractère numéro ${number}."
|
||||
|
||||
#. Default: "The code was not correct. Please try again."
|
||||
msgid "bad_captcha"
|
||||
msgstr "Le code n'est pas correct. Veuillez réessayer avec ce nouveau code."
|
||||
|
||||
#. Default: "Login"
|
||||
msgid "app_login"
|
||||
msgstr "Login"
|
||||
|
||||
#. Default: "Log in"
|
||||
msgid "app_connect"
|
||||
msgstr "Me connecter"
|
||||
|
||||
#. Default: "Logout"
|
||||
msgid "app_logout"
|
||||
msgstr "Me déconnecter"
|
||||
|
||||
#. Default: "Password"
|
||||
msgid "app_password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
#. Default: "Home"
|
||||
msgid "app_home"
|
||||
msgstr "Revenir à ma page principale"
|
||||
|
||||
#. Default: "This login is reserved."
|
||||
msgid "login_reserved"
|
||||
msgstr "Ce login est réservé."
|
||||
|
||||
#. Default: "This login is already in use."
|
||||
msgid "login_in_use"
|
||||
msgstr "L'identifiant d'utilisateur que vous avez sélectionné est déjà utilisé, ou invalide. Veuillez en choisir un autre."
|
||||
|
||||
#. Default: "Login failed."
|
||||
msgid "login_ko"
|
||||
msgstr "L'authentification a échoué."
|
||||
|
||||
#. Default: "Welcome! You are now logged in."
|
||||
msgid "login_ok"
|
||||
msgstr "Bienvenue! Vous êtes maintenant connecté."
|
||||
|
||||
#. Default: "Passwords must contain at least ${nb} characters."
|
||||
msgid "password_too_short"
|
||||
msgstr "Les mots de passe doivent contenir au moins ${nb} caractères."
|
||||
|
||||
#. Default: "Passwords do not match."
|
||||
msgid "passwords_mismatch"
|
||||
msgstr "Les mots de passe ne correspondent pas."
|
||||
|
||||
#. Default: "Save"
|
||||
msgid "object_save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
#. Default: "Changes saved."
|
||||
msgid "object_saved"
|
||||
msgstr "Modifications enregistrées."
|
||||
|
||||
#. Default: "Please correct the indicated errors."
|
||||
msgid "validation_error"
|
||||
msgstr "Veuillez corriger les erreurs indiquées."
|
||||
|
||||
#. Default: "Cancel"
|
||||
msgid "object_cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#. Default: "Changes canceled."
|
||||
msgid "object_canceled"
|
||||
msgstr "Modifications annulées."
|
||||
|
||||
#. Default: "You must enable cookies before you can log in."
|
||||
msgid "enable_cookies"
|
||||
msgstr "Vous devez activer les cookies avant de vous connecter."
|
||||
|
||||
#. Default: "Previous page"
|
||||
msgid "page_previous"
|
||||
msgstr "Page précédente"
|
||||
|
||||
#. Default: "Next page"
|
||||
msgid "page_next"
|
||||
msgstr "Page suivante"
|
||||
|
||||
#. Default: "Forgot password?"
|
||||
msgid "forgot_password"
|
||||
msgstr "Mot de passe oublié?"
|
||||
|
||||
#. Default: "Ask new password"
|
||||
msgid "ask_password_reinit"
|
||||
msgstr "Demander un nouveau mot de passe"
|
||||
|
||||
#. Default: "Something went wrong. First possibility: you have already clicked on the link (maybe have you double-clicked?) and your password has already been re-initialized. Please check that you haven't received your new password in another email. Second possibility: the link that you received in your mailer was splitted on several lines. In this case, please re-type the link in one single line and retry. Third possibility: you have waited too long and your request has expired, or a technical error occurred. In this case, please try again to ask a new password from the start."
|
||||
msgid "wrong_password_reinit"
|
||||
msgstr "Une erreur s'est produite. Première possibilité: vous avez déjà cliqué sur le lien (peut-être avez-vous double-cliqué?) et votre mot de passe a déjà été réinitialisé. Veuillez vérifier que vous n'avez pas déjà reçu un second email avec votre nouveau mot de passe. Deuxième possibilité: le lien que vous avez reçu par email a été découpé sur plusieurs lignes. Dans ce cas, veuillez reformer le lien sur une seule ligne. Troisième possibilité: vous avez attendu trop longtemps ou une erreur technique est survenue. Dans ce cas, veuillez recommencer la procédure depuis le début."
|
||||
|
||||
#. Default: "A mail has been sent to you. Please follow the instructions from this email."
|
||||
msgid "reinit_mail_sent"
|
||||
msgstr "Un email vous a été envoyé. Veuillez suivre les instructions de cet email."
|
||||
|
||||
#. Default: "Password re-initialisation"
|
||||
msgid "reinit_password"
|
||||
msgstr "Ré-initialisation de votre mot de passe"
|
||||
|
||||
#. Default: "Hello,<br/><br/>A password re-initialisation has been requested, tied to this email address, for the website ${siteUrl}. If you are not at the origin of this request, please ignore this email. Else, click on the link below to receive a new password.<br/><br/>${url}"
|
||||
msgid "reinit_password_body"
|
||||
msgstr "Bonjour,<br/><br/>Une ré-initialisation de mot de passe a été demandée, liée à votre adresse email, pour le site ${siteUrl}. Si vous n'êtes pas à l'origine de cette demande, veuillez ignorer ce message. Sinon, cliquez sur le lien ci-dessous pour ré-initialiser votre mot de passe.<br/><br/>${url}"
|
||||
|
||||
#. Default: "Your new password"
|
||||
msgid "new_password"
|
||||
msgstr "Votre nouveau mot de passe"
|
||||
|
||||
#. Default: "Hello,<br/><br/>The new password you have requested for website ${siteUrl} is ${password}<br/><br/>Best regards."
|
||||
msgid "new_password_body"
|
||||
msgstr "Bonjour,<br/><br/>Votre nouveau mot de passe pour le site ${siteUrl} est ${password}<br/><br/>Cordialement."
|
||||
|
||||
#. Default: "Your new password has been sent to you by email."
|
||||
msgid "new_password_sent"
|
||||
msgstr "Votre nouveau mot de passe vous a été envoyé par email."
|
||||
|
||||
#. Default: "Last access"
|
||||
msgid "last_user_access"
|
||||
msgstr "Dernier accès"
|
||||
|
||||
#. Default: "History"
|
||||
msgid "object_history"
|
||||
msgstr "Historique"
|
||||
|
||||
#. Default: "By"
|
||||
msgid "object_created_by"
|
||||
msgstr "Créé par"
|
||||
|
||||
#. Default: "On"
|
||||
msgid "object_created_on"
|
||||
msgstr "le"
|
||||
|
||||
#. Default: "Last updated on"
|
||||
msgid "object_modified_on"
|
||||
msgstr "Dernière modification le"
|
||||
|
||||
#. Default: "Action"
|
||||
msgid "object_action"
|
||||
msgstr "Action"
|
||||
|
||||
#. Default: "Author"
|
||||
msgid "object_author"
|
||||
msgstr "Auteur"
|
||||
|
||||
#. Default: "Date"
|
||||
msgid "action_date"
|
||||
msgstr "Date"
|
||||
|
||||
#. Default: "Comment"
|
||||
msgid "action_comment"
|
||||
msgstr "Commentaire"
|
||||
|
||||
#. Default: "Create from another object"
|
||||
msgid "create_from_predecessor"
|
||||
msgstr "Créer depuis un autre élément"
|
||||
|
||||
#. Default: "Mon"
|
||||
msgid "day_Mon_short"
|
||||
msgstr "Lun"
|
||||
|
||||
#. Default: "Tue"
|
||||
msgid "day_Tue_short"
|
||||
msgstr "Mar"
|
||||
|
||||
#. Default: "Wed"
|
||||
msgid "day_Wed_short"
|
||||
msgstr "Mer"
|
||||
|
||||
#. Default: "Thu"
|
||||
msgid "day_Thu_short"
|
||||
msgstr "Jeu"
|
||||
|
||||
#. Default: "Fri"
|
||||
msgid "day_Fri_short"
|
||||
msgstr "Ven"
|
||||
|
||||
#. Default: "Sat"
|
||||
msgid "day_Sat_short"
|
||||
msgstr "Sam"
|
||||
|
||||
#. Default: "Sun"
|
||||
msgid "day_Sun_short"
|
||||
msgstr "Dim"
|
||||
|
||||
#. Default: "Off"
|
||||
msgid "day_Off_short"
|
||||
msgstr "Férié"
|
||||
|
||||
#. Default: "Monday"
|
||||
msgid "day_Mon"
|
||||
msgstr "Lundi"
|
||||
|
||||
#. Default: "Tuesday"
|
||||
msgid "day_Tue"
|
||||
msgstr "Mardi"
|
||||
|
||||
#. Default: "Wednesday"
|
||||
msgid "day_Wed"
|
||||
msgstr "Mercredi"
|
||||
|
||||
#. Default: "Thursday"
|
||||
msgid "day_Thu"
|
||||
msgstr "Jeudi"
|
||||
|
||||
#. Default: "Friday"
|
||||
msgid "day_Fri"
|
||||
msgstr "Vendredi"
|
||||
|
||||
#. Default: "Saturday"
|
||||
msgid "day_Sat"
|
||||
msgstr "Samedi"
|
||||
|
||||
#. Default: "Sunday"
|
||||
msgid "day_Sun"
|
||||
msgstr "Dimanche"
|
||||
|
||||
#. Default: "Day off"
|
||||
msgid "day_Off"
|
||||
msgstr "Férié"
|
||||
|
||||
#. Default: "AM"
|
||||
msgid "ampm_am"
|
||||
msgstr "AM"
|
||||
|
||||
#. Default: "PM"
|
||||
msgid "ampm_pm"
|
||||
msgstr "PM"
|
||||
|
||||
#. Default: "Jan"
|
||||
msgid "month_Jan_short"
|
||||
msgstr "Jan"
|
||||
|
||||
#. Default: "Feb"
|
||||
msgid "month_Feb_short"
|
||||
msgstr "Fév"
|
||||
|
||||
#. Default: "Mar"
|
||||
msgid "month_Mar_short"
|
||||
msgstr "Mar"
|
||||
|
||||
#. Default: "Apr"
|
||||
msgid "month_Apr_short"
|
||||
msgstr "Avr"
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May_short"
|
||||
msgstr "Mai"
|
||||
|
||||
#. Default: "Jun"
|
||||
msgid "month_Jun_short"
|
||||
msgstr "Jun"
|
||||
|
||||
#. Default: "Jul"
|
||||
msgid "month_Jul_short"
|
||||
msgstr "Jui"
|
||||
|
||||
#. Default: "Aug"
|
||||
msgid "month_Aug_short"
|
||||
msgstr "Aoû"
|
||||
|
||||
#. Default: "Sep"
|
||||
msgid "month_Sep_short"
|
||||
msgstr "Sep"
|
||||
|
||||
#. Default: "Oct"
|
||||
msgid "month_Oct_short"
|
||||
msgstr "Oct"
|
||||
|
||||
#. Default: "Nov"
|
||||
msgid "month_Nov_short"
|
||||
msgstr "Nov"
|
||||
|
||||
#. Default: "Dec"
|
||||
msgid "month_Dec_short"
|
||||
msgstr "Déc"
|
||||
|
||||
#. Default: "January"
|
||||
msgid "month_Jan"
|
||||
msgstr "Janvier"
|
||||
|
||||
#. Default: "February"
|
||||
msgid "month_Feb"
|
||||
msgstr "Février"
|
||||
|
||||
#. Default: "March"
|
||||
msgid "month_Mar"
|
||||
msgstr "Mars"
|
||||
|
||||
#. Default: "April"
|
||||
msgid "month_Apr"
|
||||
msgstr "Avril"
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May"
|
||||
msgstr "Mai"
|
||||
|
||||
#. Default: "June"
|
||||
msgid "month_Jun"
|
||||
msgstr "Juin"
|
||||
|
||||
#. Default: "July"
|
||||
msgid "month_Jul"
|
||||
msgstr "Juillet"
|
||||
|
||||
#. Default: "Augustus"
|
||||
msgid "month_Aug"
|
||||
msgstr "Août"
|
||||
|
||||
#. Default: "September"
|
||||
msgid "month_Sep"
|
||||
msgstr "Septembre"
|
||||
|
||||
#. Default: "October"
|
||||
msgid "month_Oct"
|
||||
msgstr "Octobre"
|
||||
|
||||
#. Default: "November"
|
||||
msgid "month_Nov"
|
||||
msgstr "Novembre"
|
||||
|
||||
#. Default: "December"
|
||||
msgid "month_Dec"
|
||||
msgstr "Décembre"
|
||||
|
||||
#. Default: "Today"
|
||||
msgid "today"
|
||||
msgstr "Aujourd'hui"
|
||||
|
||||
#. Default: "Which event type would you like to create?"
|
||||
msgid "which_event"
|
||||
msgstr "Quel type d'événement voulez-vous créer?"
|
||||
|
||||
#. Default: "Extend the event on the following number of days (leave blank to create an event on the current day only):"
|
||||
msgid "event_span"
|
||||
msgstr "Étendre l'événement sur le nombre de jours suivants (laissez vide pour créer un événement sur cet unique jour):"
|
||||
|
||||
#. Default: "Also delete successive events of the same type."
|
||||
msgid "del_next_events"
|
||||
msgstr "Supprimer aussi les événements successifs de même type"
|
||||
|
||||
#. Default: "Several events"
|
||||
msgid "several_events"
|
||||
msgstr "Plusieurs événements"
|
||||
|
||||
#. Default: "Timeslot"
|
||||
msgid "timeslot"
|
||||
msgstr "Plage horaire"
|
||||
|
||||
#. Default: "All day"
|
||||
msgid "timeslot_main"
|
||||
msgstr "Toute la journée"
|
||||
|
||||
#. Default: "Cannot create such an event in the ${slot} slot."
|
||||
msgid "timeslot_misfit"
|
||||
msgstr "Impossible de créer ce type d'événement dans la plage horaire ${slot}."
|
||||
|
||||
#. Default: "Validate events"
|
||||
msgid "validate_events"
|
||||
msgstr "Valider les événements"
|
||||
|
||||
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
|
||||
msgid "validate_events_confirm"
|
||||
msgstr "Tous les événements sélectionnés seront confirmés, tandis que ceux qui sont désélectionnés seront rejetés. Le ou les utilisateurs concernés seront prévenus. Êtes-vous sûr?"
|
||||
|
||||
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
|
||||
msgid "validate_events_done"
|
||||
msgstr "${validated} événement(s) a (ont) été validé(s) et ${discarded} ne l'a (ont) pas été."
|
||||
|
||||
#. Default: "Inserted by ${userName}"
|
||||
msgid "history_insert"
|
||||
msgstr "Inséré par ${userName}"
|
||||
|
||||
#. Default: "Deleted by ${userName}"
|
||||
msgid "history_delete"
|
||||
msgstr "Supprimé par ${userName}"
|
||||
|
||||
#. Default: "Show changes"
|
||||
msgid "changes_show"
|
||||
msgstr "Montrer les changements"
|
||||
|
||||
#. Default: "Hide changes"
|
||||
msgid "changes_hide"
|
||||
msgstr "Masquer les changements"
|
||||
|
||||
#. Default: "an anonymous user"
|
||||
msgid "anonymous"
|
||||
msgstr "un utilisateur anonyme"
|
||||
|
||||
#. Default: "${date} - This page is locked by ${user}."
|
||||
msgid "page_locked"
|
||||
msgstr "${date} - Cette page est verrouillée par ${user}."
|
||||
|
||||
#. Default: "In some situations, by leaving this page this way, you may lose encoded data or prevent other users from editing it afterwards. Please use buttons instead."
|
||||
msgid "warn_leave_form"
|
||||
msgstr "Dans certaines situations, en quittant cette page de cette manière, vous pouvez perdre les données encodées ou empêcher d'autres utilisateurs de les éditer par la suite. Veuillez utilisez les boutons ad hoc."
|
||||
|
||||
#. Default: "You are not allowed to consult this."
|
||||
msgid "unauthorized"
|
||||
msgstr "Vous n'êtes pas autorisé à consulter ceci."
|
||||
|
||||
#. Default: "Microsoft Internet Explorer ${version} is not supported. Please upgrade your browser to version ${min} or above."
|
||||
msgid "wrong_browser"
|
||||
msgstr "Microsoft Internet Explorer ${version} n'est pas supporté. Veuillez mettre à jour votre navigateur à la version ${min} ou supérieure."
|
||||
|
||||
#. Default: "Send by email"
|
||||
msgid "email_send"
|
||||
msgstr "Envoyer par email"
|
||||
|
||||
#. Default: "${site} - ${title} - ${template}"
|
||||
msgid "podmail_subject"
|
||||
msgstr "${site} - ${title} - ${template}"
|
||||
|
||||
#. Default: "Hello, this email that was sent to you via ${site}. Please consult the attached file(s)."
|
||||
msgid "podmail_body"
|
||||
msgstr "Bonjour, cet email vous est envoyé depuis ${site}. Veuillez consulter le(s) fichier(s) joint(s)."
|
||||
|
||||
#. Default: "List"
|
||||
msgid "result_mode_list"
|
||||
msgstr "Liste"
|
||||
|
||||
#. Default: "Grid"
|
||||
msgid "result_mode_grid"
|
||||
msgstr "Grille"
|
804
gen/tr/it.po
|
@ -1,804 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Appy\n"
|
||||
"POT-Creation-Date: 2013-07-23 10:41-28\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"Language-code: it\n"
|
||||
"Language-name: it\n"
|
||||
"Preferred-encodings: utf-8 latin1\n"
|
||||
"Domain: Appy\n"
|
||||
|
||||
#. Default: "Appy"
|
||||
msgid "app_name"
|
||||
msgstr "Appy"
|
||||
|
||||
#. Default: "state"
|
||||
msgid "workflow_state"
|
||||
msgstr "stato"
|
||||
|
||||
#. Default: "Optional comment"
|
||||
msgid "workflow_comment"
|
||||
msgstr "Commento"
|
||||
|
||||
#. Default: "Manager"
|
||||
msgid "role_Manager"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Anonymous"
|
||||
msgid "role_Anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Authenticated"
|
||||
msgid "role_Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Owner"
|
||||
msgid "role_Owner"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Title"
|
||||
msgid "appy_title"
|
||||
msgstr "Qualifica"
|
||||
|
||||
#. Default: "Ok"
|
||||
msgid "appy_ok"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Keywords"
|
||||
msgid "appy_SearchableText"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Data change"
|
||||
msgid "data_change"
|
||||
msgstr "Revisione dati"
|
||||
|
||||
#. Default: "Modified field"
|
||||
msgid "modified_field"
|
||||
msgstr "Campo modificato"
|
||||
|
||||
#. Default: "Previous value or modification"
|
||||
msgid "previous_value"
|
||||
msgstr "Valore precedente"
|
||||
|
||||
#. Default: "phase"
|
||||
msgid "phase"
|
||||
msgstr "Fase"
|
||||
|
||||
#. Default: " - "
|
||||
msgid "choose_a_value"
|
||||
msgstr "[ scelga]"
|
||||
|
||||
#. Default: "[ Documents ]"
|
||||
msgid "choose_a_doc"
|
||||
msgstr "[ Documenti ]"
|
||||
|
||||
#. Default: "You must choose more elements here."
|
||||
msgid "min_ref_violated"
|
||||
msgstr "Qui deve scegliere un maggior numero di elementi"
|
||||
|
||||
#. Default: "Too much elements are selected here."
|
||||
msgid "max_ref_violated"
|
||||
msgstr "Un numero eccessivo di elementi sono scelti"
|
||||
|
||||
#. Default: "-"
|
||||
msgid "no_ref"
|
||||
msgstr "-"
|
||||
|
||||
#. Default: "Add"
|
||||
msgid "add_ref"
|
||||
msgstr "Aggiungi"
|
||||
|
||||
#. Default: "Available elements"
|
||||
msgid "selectable_objects"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted elements"
|
||||
msgid "selected_objects"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move up"
|
||||
msgid "move_up"
|
||||
msgstr "Su"
|
||||
|
||||
#. Default: "Move down"
|
||||
msgid "move_down"
|
||||
msgstr "Giù"
|
||||
|
||||
#. Default: "Move to top"
|
||||
msgid "move_top"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Move to bottom"
|
||||
msgid "move_bottom"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Update the hereabove number and click here to move this element to a new position."
|
||||
msgid "move_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "create"
|
||||
msgid "query_create"
|
||||
msgstr "Creazione in corso"
|
||||
|
||||
#. Default: "Nothing to see for the moment."
|
||||
msgid "query_no_result"
|
||||
msgstr "Nessun risultato"
|
||||
|
||||
#. Default: "consult all"
|
||||
msgid "query_consult_all"
|
||||
msgstr "Consultare tutto"
|
||||
|
||||
#. Default: "Advanced search"
|
||||
msgid "search_title"
|
||||
msgstr "Ricerca avanzata"
|
||||
|
||||
#. Default: "Search"
|
||||
msgid "search_button"
|
||||
msgstr "Ricerca"
|
||||
|
||||
#. Default: "Search results"
|
||||
msgid "search_results"
|
||||
msgstr "Risultati"
|
||||
|
||||
#. Default: " "
|
||||
msgid "search_results_descr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All results"
|
||||
msgid "search_results_all"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "New search"
|
||||
msgid "search_new"
|
||||
msgstr "Nuova ricerca"
|
||||
|
||||
#. Default: "From"
|
||||
msgid "search_from"
|
||||
msgstr "Da"
|
||||
|
||||
#. Default: "to"
|
||||
msgid "search_to"
|
||||
msgstr "A"
|
||||
|
||||
#. Default: "or"
|
||||
msgid "search_or"
|
||||
msgstr "o"
|
||||
|
||||
#. Default: "and"
|
||||
msgid "search_and"
|
||||
msgstr "e"
|
||||
|
||||
#. Default: "No move occurred: please specify a valid number."
|
||||
msgid "ref_invalid_index"
|
||||
msgstr "Nulla di fatto. Precisare un numero valido."
|
||||
|
||||
#. Default: "An integer value is expected; do not enter any space."
|
||||
msgid "bad_long"
|
||||
msgstr "Ci si aspetta un valore intero"
|
||||
|
||||
#. Default: "A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
|
||||
msgid "bad_float"
|
||||
msgstr "Ci si aspetta un numero con decimali"
|
||||
|
||||
#. Default: "Please specify a valid date."
|
||||
msgid "bad_date"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The value is not among possible values for this field."
|
||||
msgid "bad_select_value"
|
||||
msgstr "Il valore digitato non è possibile per questo campo."
|
||||
|
||||
#. Default: "(Un)select all"
|
||||
msgid "select_delesect"
|
||||
msgstr "Eliminare tutte le selezioni"
|
||||
|
||||
#. Default: "Automatic (de)selection"
|
||||
msgid "select_auto"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must select at least one element."
|
||||
msgid "no_elem_selected"
|
||||
msgstr "Deve selezionare almeno un elemento."
|
||||
|
||||
#. Default: "Edit"
|
||||
msgid "object_edit"
|
||||
msgstr "Modifica"
|
||||
|
||||
#. Default: "Delete"
|
||||
msgid "object_delete"
|
||||
msgstr "Elimina"
|
||||
|
||||
#. Default: "Delete selection"
|
||||
msgid "object_delete_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Remove"
|
||||
msgid "object_unlink"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Remove selection"
|
||||
msgid "object_unlink_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert"
|
||||
msgid "object_link"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Insert selection"
|
||||
msgid "object_link_many"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Un)check everything"
|
||||
msgid "check_uncheck"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unlock"
|
||||
msgid "page_unlock"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Keep the file unchanged"
|
||||
msgid "keep_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Delete the file"
|
||||
msgid "delete_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Replace it with a new file"
|
||||
msgid "replace_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure?"
|
||||
msgid "action_confirm"
|
||||
msgstr "È sicuro?"
|
||||
|
||||
#. Default: "The action has been performed."
|
||||
msgid "action_done"
|
||||
msgstr "L'operazione è stata eseguita con successo"
|
||||
|
||||
#. Default: "A problem occurred while executing the action."
|
||||
msgid "action_ko"
|
||||
msgstr "Si è manifestato un problema"
|
||||
|
||||
#. Default: "Action could not be performed on ${nb} element(s)."
|
||||
msgid "action_partial"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action had no effect."
|
||||
msgid "action_null"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure you want to apply this change?"
|
||||
msgid "save_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure? You are going to permanently change the order of these elements, for all users."
|
||||
msgid "sort_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to top"
|
||||
msgid "goto_first"
|
||||
msgstr "Andare all'inizio"
|
||||
|
||||
#. Default: "Go to previous"
|
||||
msgid "goto_previous"
|
||||
msgstr "Andare all'elemento precedente"
|
||||
|
||||
#. Default: "Go to next"
|
||||
msgid "goto_next"
|
||||
msgstr "Andare all'elemento seguente"
|
||||
|
||||
#. Default: "Go to end"
|
||||
msgid "goto_last"
|
||||
msgstr "Andare alla fine"
|
||||
|
||||
#. Default: "Go back"
|
||||
msgid "goto_source"
|
||||
msgstr "Andare indietro"
|
||||
|
||||
#. Default: "Go to number"
|
||||
msgid "goto_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Whatever"
|
||||
msgid "whatever"
|
||||
msgstr "Qualunque"
|
||||
|
||||
#. Default: "Yes"
|
||||
msgid "yes"
|
||||
msgstr "Sì"
|
||||
|
||||
#. Default: "No"
|
||||
msgid "no"
|
||||
msgstr "No"
|
||||
|
||||
#. Default: "Please fill this field."
|
||||
msgid "field_required"
|
||||
msgstr "Riempire questo campo"
|
||||
|
||||
#. Default: "Please fill or correct this."
|
||||
msgid "field_invalid"
|
||||
msgstr "Voglia riempire o correggere questo campo."
|
||||
|
||||
#. Default: "Please select a file."
|
||||
msgid "file_required"
|
||||
msgstr "Selezionare un file."
|
||||
|
||||
#. Default: "The uploaded file must be an image."
|
||||
msgid "image_required"
|
||||
msgstr "Il file caricato deve essere un'immagine."
|
||||
|
||||
#. Default: "ODT"
|
||||
msgid "odt"
|
||||
msgstr "ODT (OpenOffice/OfficeLibero)"
|
||||
|
||||
#. Default: "PDF"
|
||||
msgid "pdf"
|
||||
msgstr "PDF"
|
||||
|
||||
#. Default: "DOC"
|
||||
msgid "doc"
|
||||
msgstr "DOC (Microsoft Word)"
|
||||
|
||||
#. Default: "ODS"
|
||||
msgid "ods"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "XLS"
|
||||
msgid "xls"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "frozen"
|
||||
msgid "frozen"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "(Re-)freeze"
|
||||
msgid "freezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Unfreeze"
|
||||
msgid "unfreezeField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Upload a new file"
|
||||
msgid "uploadField"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please upload a file of the same type."
|
||||
msgid "upload_invalid"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Welcome to this Appy-powered site."
|
||||
msgid "front_page_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Please type \"${text}\" (without the double quotes) in the field besides, but without the character at position ${number}."
|
||||
msgid "captcha_text"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "The code was not correct. Please try again."
|
||||
msgid "bad_captcha"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Login"
|
||||
msgid "app_login"
|
||||
msgstr "Login"
|
||||
|
||||
#. Default: "Log in"
|
||||
msgid "app_connect"
|
||||
msgstr "Fatti riconoscere"
|
||||
|
||||
#. Default: "Logout"
|
||||
msgid "app_logout"
|
||||
msgstr "Esci"
|
||||
|
||||
#. Default: "Password"
|
||||
msgid "app_password"
|
||||
msgstr "Password"
|
||||
|
||||
#. Default: "Home"
|
||||
msgid "app_home"
|
||||
msgstr "Ritorno alla pagina di partenza"
|
||||
|
||||
#. Default: "This login is reserved."
|
||||
msgid "login_reserved"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "This login is already in use."
|
||||
msgid "login_in_use"
|
||||
msgstr "Il nome di login che hai scelto è già stato usato oppure non è valido. Scegline uno diverso, per favore."
|
||||
|
||||
#. Default: "Login failed."
|
||||
msgid "login_ko"
|
||||
msgstr "Riconoscimento fallito."
|
||||
|
||||
#. Default: "Welcome! You are now logged in."
|
||||
msgid "login_ok"
|
||||
msgstr "Piacere di averti tra noi! Adesso sei autorizzato ad accedere al sito."
|
||||
|
||||
#. Default: "Passwords must contain at least ${nb} characters."
|
||||
msgid "password_too_short"
|
||||
msgstr "Le password devono contenere almeno ${nb} lettere."
|
||||
|
||||
#. Default: "Passwords do not match."
|
||||
msgid "passwords_mismatch"
|
||||
msgstr "Le password non combaciano."
|
||||
|
||||
#. Default: "Save"
|
||||
msgid "object_save"
|
||||
msgstr "Conferma le modifiche"
|
||||
|
||||
#. Default: "Changes saved."
|
||||
msgid "object_saved"
|
||||
msgstr "Modifiche memorizzate."
|
||||
|
||||
#. Default: "Please correct the indicated errors."
|
||||
msgid "validation_error"
|
||||
msgstr "Correggi gli errori evidenziati."
|
||||
|
||||
#. Default: "Cancel"
|
||||
msgid "object_cancel"
|
||||
msgstr "Annulla"
|
||||
|
||||
#. Default: "Changes canceled."
|
||||
msgid "object_canceled"
|
||||
msgstr "Modifiche annullate."
|
||||
|
||||
#. Default: "You must enable cookies before you can log in."
|
||||
msgid "enable_cookies"
|
||||
msgstr "Devi abilitare i cookie per poter essere riconosciuto."
|
||||
|
||||
#. Default: "Previous page"
|
||||
msgid "page_previous"
|
||||
msgstr "Precedente"
|
||||
|
||||
#. Default: "Next page"
|
||||
msgid "page_next"
|
||||
msgstr "Successivo"
|
||||
|
||||
#. Default: "Forgot password?"
|
||||
msgid "forgot_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Ask new password"
|
||||
msgid "ask_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Something went wrong. First possibility: you have already clicked on the link (maybe have you double-clicked?) and your password has already been re-initialized. Please check that you haven't received your new password in another email. Second possibility: the link that you received in your mailer was splitted on several lines. In this case, please re-type the link in one single line and retry. Third possibility: you have waited too long and your request has expired, or a technical error occurred. In this case, please try again to ask a new password from the start."
|
||||
msgid "wrong_password_reinit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "A mail has been sent to you. Please follow the instructions from this email."
|
||||
msgid "reinit_mail_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Password re-initialisation"
|
||||
msgid "reinit_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>A password re-initialisation has been requested, tied to this email address, for the website ${siteUrl}. If you are not at the origin of this request, please ignore this email. Else, click on the link below to receive a new password.<br/><br/>${url}"
|
||||
msgid "reinit_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password"
|
||||
msgid "new_password"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello,<br/><br/>The new password you have requested for website ${siteUrl} is ${password}<br/><br/>Best regards."
|
||||
msgid "new_password_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Your new password has been sent to you by email."
|
||||
msgid "new_password_sent"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last access"
|
||||
msgid "last_user_access"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "History"
|
||||
msgid "object_history"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "By"
|
||||
msgid "object_created_by"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "On"
|
||||
msgid "object_created_on"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Last updated on"
|
||||
msgid "object_modified_on"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Action"
|
||||
msgid "object_action"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Author"
|
||||
msgid "object_author"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Date"
|
||||
msgid "action_date"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Comment"
|
||||
msgid "action_comment"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Create from another object"
|
||||
msgid "create_from_predecessor"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mon"
|
||||
msgid "day_Mon_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tue"
|
||||
msgid "day_Tue_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wed"
|
||||
msgid "day_Wed_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thu"
|
||||
msgid "day_Thu_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Fri"
|
||||
msgid "day_Fri_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sat"
|
||||
msgid "day_Sat_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sun"
|
||||
msgid "day_Sun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Off"
|
||||
msgid "day_Off_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Monday"
|
||||
msgid "day_Mon"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Tuesday"
|
||||
msgid "day_Tue"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Wednesday"
|
||||
msgid "day_Wed"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Thursday"
|
||||
msgid "day_Thu"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Friday"
|
||||
msgid "day_Fri"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Saturday"
|
||||
msgid "day_Sat"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sunday"
|
||||
msgid "day_Sun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Day off"
|
||||
msgid "day_Off"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "AM"
|
||||
msgid "ampm_am"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "PM"
|
||||
msgid "ampm_pm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jan"
|
||||
msgid "month_Jan_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Feb"
|
||||
msgid "month_Feb_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mar"
|
||||
msgid "month_Mar_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Apr"
|
||||
msgid "month_Apr_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jun"
|
||||
msgid "month_Jun_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Jul"
|
||||
msgid "month_Jul_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Aug"
|
||||
msgid "month_Aug_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Sep"
|
||||
msgid "month_Sep_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Oct"
|
||||
msgid "month_Oct_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Nov"
|
||||
msgid "month_Nov_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Dec"
|
||||
msgid "month_Dec_short"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "January"
|
||||
msgid "month_Jan"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "February"
|
||||
msgid "month_Feb"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "March"
|
||||
msgid "month_Mar"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "April"
|
||||
msgid "month_Apr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "June"
|
||||
msgid "month_Jun"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "July"
|
||||
msgid "month_Jul"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Augustus"
|
||||
msgid "month_Aug"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "September"
|
||||
msgid "month_Sep"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "October"
|
||||
msgid "month_Oct"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "November"
|
||||
msgid "month_Nov"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "December"
|
||||
msgid "month_Dec"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Today"
|
||||
msgid "today"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Which event type would you like to create?"
|
||||
msgid "which_event"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Extend the event on the following number of days (leave blank to create an event on the current day only):"
|
||||
msgid "event_span"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Also delete successive events of the same type."
|
||||
msgid "del_next_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Several events"
|
||||
msgid "several_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Timeslot"
|
||||
msgid "timeslot"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All day"
|
||||
msgid "timeslot_main"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Cannot create such an event in the ${slot} slot."
|
||||
msgid "timeslot_misfit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Validate events"
|
||||
msgid "validate_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
|
||||
msgid "validate_events_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
|
||||
msgid "validate_events_done"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted by ${userName}"
|
||||
msgid "history_insert"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Deleted by ${userName}"
|
||||
msgid "history_delete"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Show changes"
|
||||
msgid "changes_show"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hide changes"
|
||||
msgid "changes_hide"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "an anonymous user"
|
||||
msgid "anonymous"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${date} - This page is locked by ${user}."
|
||||
msgid "page_locked"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "In some situations, by leaving this page this way, you may lose encoded data or prevent other users from editing it afterwards. Please use buttons instead."
|
||||
msgid "warn_leave_form"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You are not allowed to consult this."
|
||||
msgid "unauthorized"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Microsoft Internet Explorer ${version} is not supported. Please upgrade your browser to version ${min} or above."
|
||||
msgid "wrong_browser"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Send by email"
|
||||
msgid "email_send"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${site} - ${title} - ${template}"
|
||||
msgid "podmail_subject"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello, this email that was sent to you via ${site}. Please consult the attached file(s)."
|
||||
msgid "podmail_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "List"
|
||||
msgid "result_mode_list"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Grid"
|
||||
msgid "result_mode_grid"
|
||||
msgstr ""
|
804
gen/tr/nl.po
|
@ -1,804 +0,0 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Appy\n"
|
||||
"POT-Creation-Date: 2013-07-23 10:41-28\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
"Language-code: nl\n"
|
||||
"Language-name: nl\n"
|
||||
"Preferred-encodings: utf-8 latin1\n"
|
||||
"Domain: Appy\n"
|
||||
|
||||
#. Default: "Appy"
|
||||
msgid "app_name"
|
||||
msgstr "Appy"
|
||||
|
||||
#. Default: "state"
|
||||
msgid "workflow_state"
|
||||
msgstr "staat"
|
||||
|
||||
#. Default: "Optional comment"
|
||||
msgid "workflow_comment"
|
||||
msgstr "Commentaar"
|
||||
|
||||
#. Default: "Manager"
|
||||
msgid "role_Manager"
|
||||
msgstr "Beheerder"
|
||||
|
||||
#. Default: "Anonymous"
|
||||
msgid "role_Anonymous"
|
||||
msgstr "Anoniem"
|
||||
|
||||
#. Default: "Authenticated"
|
||||
msgid "role_Authenticated"
|
||||
msgstr "Geauthenticeerd"
|
||||
|
||||
#. Default: "Owner"
|
||||
msgid "role_Owner"
|
||||
msgstr "Eigenaar"
|
||||
|
||||
#. Default: "Title"
|
||||
msgid "appy_title"
|
||||
msgstr "Titel"
|
||||
|
||||
#. Default: "Ok"
|
||||
msgid "appy_ok"
|
||||
msgstr "OK"
|
||||
|
||||
#. Default: "Keywords"
|
||||
msgid "appy_SearchableText"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Data change"
|
||||
msgid "data_change"
|
||||
msgstr "Wijziging van de gegevens"
|
||||
|
||||
#. Default: "Modified field"
|
||||
msgid "modified_field"
|
||||
msgstr "Veld gewijzigd"
|
||||
|
||||
#. Default: "Previous value or modification"
|
||||
msgid "previous_value"
|
||||
msgstr "Vorige waarde"
|
||||
|
||||
#. Default: "phase"
|
||||
msgid "phase"
|
||||
msgstr "Fase"
|
||||
|
||||
#. Default: " - "
|
||||
msgid "choose_a_value"
|
||||
msgstr "[maak uw keuze]"
|
||||
|
||||
#. Default: "[ Documents ]"
|
||||
msgid "choose_a_doc"
|
||||
msgstr "[Documenten]"
|
||||
|
||||
#. Default: "You must choose more elements here."
|
||||
msgid "min_ref_violated"
|
||||
msgstr "U moet hier meerdere elementen selecteren."
|
||||
|
||||
#. Default: "Too much elements are selected here."
|
||||
msgid "max_ref_violated"
|
||||
msgstr "U hebt teveel elementen geselecteerd."
|
||||
|
||||
#. Default: "-"
|
||||
msgid "no_ref"
|
||||
msgstr "-"
|
||||
|
||||
#. Default: "Add"
|
||||
msgid "add_ref"
|
||||
msgstr "Toevoegen"
|
||||
|
||||
#. Default: "Available elements"
|
||||
msgid "selectable_objects"
|
||||
msgstr "Keuze elementen"
|
||||
|
||||
#. Default: "Inserted elements"
|
||||
msgid "selected_objects"
|
||||
msgstr "Geselecteerde elementen"
|
||||
|
||||
#. Default: "Move up"
|
||||
msgid "move_up"
|
||||
msgstr "Verplaats naar boven"
|
||||
|
||||
#. Default: "Move down"
|
||||
msgid "move_down"
|
||||
msgstr "Verplaats naar beneden"
|
||||
|
||||
#. Default: "Move to top"
|
||||
msgid "move_top"
|
||||
msgstr "Verplaatsen naar eerste positie"
|
||||
|
||||
#. Default: "Move to bottom"
|
||||
msgid "move_bottom"
|
||||
msgstr "Verplaatsen naar laatste positie"
|
||||
|
||||
#. Default: "Update the hereabove number and click here to move this element to a new position."
|
||||
msgid "move_number"
|
||||
msgstr "Wijzig onderstaand nummer en klik hier om het element naar een nieuwe positie te verplaatsen."
|
||||
|
||||
#. Default: "create"
|
||||
msgid "query_create"
|
||||
msgstr "Aanmaken"
|
||||
|
||||
#. Default: "Nothing to see for the moment."
|
||||
msgid "query_no_result"
|
||||
msgstr "Geen resultaat"
|
||||
|
||||
#. Default: "consult all"
|
||||
msgid "query_consult_all"
|
||||
msgstr "Alles raadplegen"
|
||||
|
||||
#. Default: "Advanced search"
|
||||
msgid "search_title"
|
||||
msgstr "Gevorderde opzoeking"
|
||||
|
||||
#. Default: "Search"
|
||||
msgid "search_button"
|
||||
msgstr "Opzoeken"
|
||||
|
||||
#. Default: "Search results"
|
||||
msgid "search_results"
|
||||
msgstr "Resultaten"
|
||||
|
||||
#. Default: " "
|
||||
msgid "search_results_descr"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All results"
|
||||
msgid "search_results_all"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "New search"
|
||||
msgid "search_new"
|
||||
msgstr "Nieuwe opzoeking"
|
||||
|
||||
#. Default: "From"
|
||||
msgid "search_from"
|
||||
msgstr "Van"
|
||||
|
||||
#. Default: "to"
|
||||
msgid "search_to"
|
||||
msgstr "tot"
|
||||
|
||||
#. Default: "or"
|
||||
msgid "search_or"
|
||||
msgstr "of"
|
||||
|
||||
#. Default: "and"
|
||||
msgid "search_and"
|
||||
msgstr "en"
|
||||
|
||||
#. Default: "No move occurred: please specify a valid number."
|
||||
msgid "ref_invalid_index"
|
||||
msgstr "Geen enkele verplaatsing heft plaats gehad: specifieer een geldig aantal."
|
||||
|
||||
#. Default: "An integer value is expected; do not enter any space."
|
||||
msgid "bad_long"
|
||||
msgstr "Vul een hele waarde in."
|
||||
|
||||
#. Default: "A floating-point number is expected; use the dot as decimal separator, not a comma; do not enter any space."
|
||||
msgid "bad_float"
|
||||
msgstr "Vul een reële waarde in."
|
||||
|
||||
#. Default: "Please specify a valid date."
|
||||
msgid "bad_date"
|
||||
msgstr "Specifieer een correcte datum."
|
||||
|
||||
#. Default: "The value is not among possible values for this field."
|
||||
msgid "bad_select_value"
|
||||
msgstr "Deze waarde wordt niet geaccepteerd voor dit veld."
|
||||
|
||||
#. Default: "(Un)select all"
|
||||
msgid "select_delesect"
|
||||
msgstr "Alles (de)selecteren"
|
||||
|
||||
#. Default: "Automatic (de)selection"
|
||||
msgid "select_auto"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "You must select at least one element."
|
||||
msgid "no_elem_selected"
|
||||
msgstr "U moet minstens één element selecteren."
|
||||
|
||||
#. Default: "Edit"
|
||||
msgid "object_edit"
|
||||
msgstr "Bewerken"
|
||||
|
||||
#. Default: "Delete"
|
||||
msgid "object_delete"
|
||||
msgstr "Verwijderen"
|
||||
|
||||
#. Default: "Delete selection"
|
||||
msgid "object_delete_many"
|
||||
msgstr "Verwijder de selectie"
|
||||
|
||||
#. Default: "Remove"
|
||||
msgid "object_unlink"
|
||||
msgstr "Intrekken"
|
||||
|
||||
#. Default: "Remove selection"
|
||||
msgid "object_unlink_many"
|
||||
msgstr "Selectie losmaken"
|
||||
|
||||
#. Default: "Insert"
|
||||
msgid "object_link"
|
||||
msgstr "Invoegen"
|
||||
|
||||
#. Default: "Insert selection"
|
||||
msgid "object_link_many"
|
||||
msgstr "Selectie invoegen"
|
||||
|
||||
#. Default: "(Un)check everything"
|
||||
msgid "check_uncheck"
|
||||
msgstr "Alles (de)selecteren"
|
||||
|
||||
#. Default: "Unlock"
|
||||
msgid "page_unlock"
|
||||
msgstr "Ontgrendelen"
|
||||
|
||||
#. Default: "Keep the file unchanged"
|
||||
msgid "keep_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Delete the file"
|
||||
msgid "delete_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Replace it with a new file"
|
||||
msgid "replace_file"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Are you sure?"
|
||||
msgid "action_confirm"
|
||||
msgstr "Bent u zeker?"
|
||||
|
||||
#. Default: "The action has been performed."
|
||||
msgid "action_done"
|
||||
msgstr "De opdracht werd uitgevoerd."
|
||||
|
||||
#. Default: "A problem occurred while executing the action."
|
||||
msgid "action_ko"
|
||||
msgstr "Er heeft zich een probleem voorgedaan."
|
||||
|
||||
#. Default: "Action could not be performed on ${nb} element(s)."
|
||||
msgid "action_partial"
|
||||
msgstr "De actie kon niet uitgevoerd worden op ${nb} element(en)."
|
||||
|
||||
#. Default: "Action had no effect."
|
||||
msgid "action_null"
|
||||
msgstr "De actie heeft geen effect gehad."
|
||||
|
||||
#. Default: "Are you sure you want to apply this change?"
|
||||
msgid "save_confirm"
|
||||
msgstr "Bent u zeker om deze wijziging door te voeren?"
|
||||
|
||||
#. Default: "Are you sure? You are going to permanently change the order of these elements, for all users."
|
||||
msgid "sort_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Go to top"
|
||||
msgid "goto_first"
|
||||
msgstr "Ga naar het begin"
|
||||
|
||||
#. Default: "Go to previous"
|
||||
msgid "goto_previous"
|
||||
msgstr "Ga naar het vorige element"
|
||||
|
||||
#. Default: "Go to next"
|
||||
msgid "goto_next"
|
||||
msgstr "Ga naar het volgende element"
|
||||
|
||||
#. Default: "Go to end"
|
||||
msgid "goto_last"
|
||||
msgstr "Ga naar het einde"
|
||||
|
||||
#. Default: "Go back"
|
||||
msgid "goto_source"
|
||||
msgstr "Terug"
|
||||
|
||||
#. Default: "Go to number"
|
||||
msgid "goto_number"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Whatever"
|
||||
msgid "whatever"
|
||||
msgstr "Maakt niets uit"
|
||||
|
||||
#. Default: "Yes"
|
||||
msgid "yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#. Default: "No"
|
||||
msgid "no"
|
||||
msgstr "Neen"
|
||||
|
||||
#. Default: "Please fill this field."
|
||||
msgid "field_required"
|
||||
msgstr "Gelieve dit veld in te vullen."
|
||||
|
||||
#. Default: "Please fill or correct this."
|
||||
msgid "field_invalid"
|
||||
msgstr "Gelieve dit veld in te vullen of te verbeteren"
|
||||
|
||||
#. Default: "Please select a file."
|
||||
msgid "file_required"
|
||||
msgstr "Kies een bestand."
|
||||
|
||||
#. Default: "The uploaded file must be an image."
|
||||
msgid "image_required"
|
||||
msgstr "Het up te loaden bestand moet een foto zijn."
|
||||
|
||||
#. Default: "ODT"
|
||||
msgid "odt"
|
||||
msgstr "ODT LibreOffice Writer"
|
||||
|
||||
#. Default: "PDF"
|
||||
msgid "pdf"
|
||||
msgstr "PDF"
|
||||
|
||||
#. Default: "DOC"
|
||||
msgid "doc"
|
||||
msgstr "DOC (Microsoft Word)"
|
||||
|
||||
#. Default: "ODS"
|
||||
msgid "ods"
|
||||
msgstr "ODS (LibreOffice Calc)"
|
||||
|
||||
#. Default: "XLS"
|
||||
msgid "xls"
|
||||
msgstr "XLS (Microsoft Excel)"
|
||||
|
||||
#. Default: "frozen"
|
||||
msgid "frozen"
|
||||
msgstr "Bevroren"
|
||||
|
||||
#. Default: "(Re-)freeze"
|
||||
msgid "freezeField"
|
||||
msgstr "(Her-)bevriezen"
|
||||
|
||||
#. Default: "Unfreeze"
|
||||
msgid "unfreezeField"
|
||||
msgstr "Ontdooien"
|
||||
|
||||
#. Default: "Upload a new file"
|
||||
msgid "uploadField"
|
||||
msgstr "Overschreven door ..."
|
||||
|
||||
#. Default: "Please upload a file of the same type."
|
||||
msgid "upload_invalid"
|
||||
msgstr "Gelieve een bestand van hetzelfde type up te loaden."
|
||||
|
||||
#. Default: "Welcome to this Appy-powered site."
|
||||
msgid "front_page_text"
|
||||
msgstr "Welkom op deze website die gebouwd is met <a href=\"http://appyframework.org\" target=\"_blank\">Appy</a>."
|
||||
|
||||
#. Default: "Please type \"${text}\" (without the double quotes) in the field besides, but without the character at position ${number}."
|
||||
msgid "captcha_text"
|
||||
msgstr "Typ \"${text}\" (zonder de haakjes) hierna, zonder het karakter die op positie nummer ${number} staat."
|
||||
|
||||
#. Default: "The code was not correct. Please try again."
|
||||
msgid "bad_captcha"
|
||||
msgstr "De code was niet correct. Herbegin met een nieuwe code."
|
||||
|
||||
#. Default: "Login"
|
||||
msgid "app_login"
|
||||
msgstr "Login"
|
||||
|
||||
#. Default: "Log in"
|
||||
msgid "app_connect"
|
||||
msgstr "Inloggen"
|
||||
|
||||
#. Default: "Logout"
|
||||
msgid "app_logout"
|
||||
msgstr "Zich afmelden"
|
||||
|
||||
#. Default: "Password"
|
||||
msgid "app_password"
|
||||
msgstr "Wachtwoord"
|
||||
|
||||
#. Default: "Home"
|
||||
msgid "app_home"
|
||||
msgstr "Keer terug naar de hoofdpagina"
|
||||
|
||||
#. Default: "This login is reserved."
|
||||
msgid "login_reserved"
|
||||
msgstr "Dit login is voorbehouden"
|
||||
|
||||
#. Default: "This login is already in use."
|
||||
msgid "login_in_use"
|
||||
msgstr "De gekozen gebruikersnaam bestaat al of is ongeldig. Kies een andere."
|
||||
|
||||
#. Default: "Login failed."
|
||||
msgid "login_ko"
|
||||
msgstr "Inloggen mislukt."
|
||||
|
||||
#. Default: "Welcome! You are now logged in."
|
||||
msgid "login_ok"
|
||||
msgstr "Welkom! U bent nu ingelogd."
|
||||
|
||||
#. Default: "Passwords must contain at least ${nb} characters."
|
||||
msgid "password_too_short"
|
||||
msgstr "Een wachtwoord dient minimaal ${nb} karakters lang te zijn."
|
||||
|
||||
#. Default: "Passwords do not match."
|
||||
msgid "passwords_mismatch"
|
||||
msgstr "Wachtwoorden komen niet overeen."
|
||||
|
||||
#. Default: "Save"
|
||||
msgid "object_save"
|
||||
msgstr "Bewaren"
|
||||
|
||||
#. Default: "Changes saved."
|
||||
msgid "object_saved"
|
||||
msgstr "Wijzigingen zijn opgeslagen."
|
||||
|
||||
#. Default: "Please correct the indicated errors."
|
||||
msgid "validation_error"
|
||||
msgstr "Verbeter de aangegeven fout(en)."
|
||||
|
||||
#. Default: "Cancel"
|
||||
msgid "object_cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#. Default: "Changes canceled."
|
||||
msgid "object_canceled"
|
||||
msgstr "Wijzigingen geannuleerd."
|
||||
|
||||
#. Default: "You must enable cookies before you can log in."
|
||||
msgid "enable_cookies"
|
||||
msgstr "U dient het gebruik van cookies toe te staan voordat uw in kunt loggen."
|
||||
|
||||
#. Default: "Previous page"
|
||||
msgid "page_previous"
|
||||
msgstr "Vorige pagina"
|
||||
|
||||
#. Default: "Next page"
|
||||
msgid "page_next"
|
||||
msgstr "Volgende pagina"
|
||||
|
||||
#. Default: "Forgot password?"
|
||||
msgid "forgot_password"
|
||||
msgstr "Paswoord vergeten?"
|
||||
|
||||
#. Default: "Ask new password"
|
||||
msgid "ask_password_reinit"
|
||||
msgstr "Vraag een nieuw paswoord"
|
||||
|
||||
#. Default: "Something went wrong. First possibility: you have already clicked on the link (maybe have you double-clicked?) and your password has already been re-initialized. Please check that you haven't received your new password in another email. Second possibility: the link that you received in your mailer was splitted on several lines. In this case, please re-type the link in one single line and retry. Third possibility: you have waited too long and your request has expired, or a technical error occurred. In this case, please try again to ask a new password from the start."
|
||||
msgid "wrong_password_reinit"
|
||||
msgstr "Er heeft zich een probleem voorgedaan: u hebt reeds op de link geklikt (misschien hebt u een dubbel klik uitgevoerd?) en uw paswoord werd reeds geïnitialiseerd. Controleer even of u geen tweede mail ontvangen hebt met uw nieuw paswoord. Tweede mogelijkheid: de link die u via e-mail ontvangen hebt, was gesplitst over meerdere regels. In dit geval, probeer de link op één lijn te vormen. Derde mogelijkheid: u hebt iets te lang gewacht of een technische fout is opgetreden. In dat geval dient u de procedure van voor af aan te herbeginnen."
|
||||
|
||||
#. Default: "A mail has been sent to you. Please follow the instructions from this email."
|
||||
msgid "reinit_mail_sent"
|
||||
msgstr "Wij hebben u een e-mail gestuurd. Gelieve de instructies in de mail op te volgen."
|
||||
|
||||
#. Default: "Password re-initialisation"
|
||||
msgid "reinit_password"
|
||||
msgstr "Re-initialisatie van uw paswoord."
|
||||
|
||||
#. Default: "Hello,<br/><br/>A password re-initialisation has been requested, tied to this email address, for the website ${siteUrl}. If you are not at the origin of this request, please ignore this email. Else, click on the link below to receive a new password.<br/><br/>${url}"
|
||||
msgid "reinit_password_body"
|
||||
msgstr "Een re-initialisatie van het paswoord, gekoppeld aan uw e-mail adres, voor de site ${siteUrl} werd aangevraagd. Indien u niet aan de oorsprong ligt van deze aanvraag, gelieve dit bericht te negeren. Zoniet, klik op onderstaande link om een nieuw paswoord aan te vragen.<br/><br/>${url}"
|
||||
|
||||
#. Default: "Your new password"
|
||||
msgid "new_password"
|
||||
msgstr "Uw nieuw paswoord"
|
||||
|
||||
#. Default: "Hello,<br/><br/>The new password you have requested for website ${siteUrl} is ${password}<br/><br/>Best regards."
|
||||
msgid "new_password_body"
|
||||
msgstr "Hallo,<br/><br/>Uw nieuw paswoord voor de site${siteUrl} is ${password}<br/><br/>Beste groeten."
|
||||
|
||||
#. Default: "Your new password has been sent to you by email."
|
||||
msgid "new_password_sent"
|
||||
msgstr "Uw nieuw paswoord is verstuurd via e-mail."
|
||||
|
||||
#. Default: "Last access"
|
||||
msgid "last_user_access"
|
||||
msgstr "Laatste toegang"
|
||||
|
||||
#. Default: "History"
|
||||
msgid "object_history"
|
||||
msgstr "Historiek"
|
||||
|
||||
#. Default: "By"
|
||||
msgid "object_created_by"
|
||||
msgstr "Aangemaakt door"
|
||||
|
||||
#. Default: "On"
|
||||
msgid "object_created_on"
|
||||
msgstr "op"
|
||||
|
||||
#. Default: "Last updated on"
|
||||
msgid "object_modified_on"
|
||||
msgstr "Laatste wijziging op"
|
||||
|
||||
#. Default: "Action"
|
||||
msgid "object_action"
|
||||
msgstr "Actie"
|
||||
|
||||
#. Default: "Author"
|
||||
msgid "object_author"
|
||||
msgstr "Auteur"
|
||||
|
||||
#. Default: "Date"
|
||||
msgid "action_date"
|
||||
msgstr "Datum"
|
||||
|
||||
#. Default: "Comment"
|
||||
msgid "action_comment"
|
||||
msgstr "Commentaar"
|
||||
|
||||
#. Default: "Create from another object"
|
||||
msgid "create_from_predecessor"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Mon"
|
||||
msgid "day_Mon_short"
|
||||
msgstr "Maa"
|
||||
|
||||
#. Default: "Tue"
|
||||
msgid "day_Tue_short"
|
||||
msgstr "Din"
|
||||
|
||||
#. Default: "Wed"
|
||||
msgid "day_Wed_short"
|
||||
msgstr "Woe"
|
||||
|
||||
#. Default: "Thu"
|
||||
msgid "day_Thu_short"
|
||||
msgstr "Don"
|
||||
|
||||
#. Default: "Fri"
|
||||
msgid "day_Fri_short"
|
||||
msgstr "Vrij"
|
||||
|
||||
#. Default: "Sat"
|
||||
msgid "day_Sat_short"
|
||||
msgstr "Zat"
|
||||
|
||||
#. Default: "Sun"
|
||||
msgid "day_Sun_short"
|
||||
msgstr "Zon"
|
||||
|
||||
#. Default: "Off"
|
||||
msgid "day_Off_short"
|
||||
msgstr "Feestdag"
|
||||
|
||||
#. Default: "Monday"
|
||||
msgid "day_Mon"
|
||||
msgstr "Maandag"
|
||||
|
||||
#. Default: "Tuesday"
|
||||
msgid "day_Tue"
|
||||
msgstr "Dinsdag"
|
||||
|
||||
#. Default: "Wednesday"
|
||||
msgid "day_Wed"
|
||||
msgstr "Woensdag"
|
||||
|
||||
#. Default: "Thursday"
|
||||
msgid "day_Thu"
|
||||
msgstr "Donderdag"
|
||||
|
||||
#. Default: "Friday"
|
||||
msgid "day_Fri"
|
||||
msgstr "Vrijdag"
|
||||
|
||||
#. Default: "Saturday"
|
||||
msgid "day_Sat"
|
||||
msgstr "Zaterdag"
|
||||
|
||||
#. Default: "Sunday"
|
||||
msgid "day_Sun"
|
||||
msgstr "Zondag"
|
||||
|
||||
#. Default: "Day off"
|
||||
msgid "day_Off"
|
||||
msgstr "Feestdag"
|
||||
|
||||
#. Default: "AM"
|
||||
msgid "ampm_am"
|
||||
msgstr "AM"
|
||||
|
||||
#. Default: "PM"
|
||||
msgid "ampm_pm"
|
||||
msgstr "PM"
|
||||
|
||||
#. Default: "Jan"
|
||||
msgid "month_Jan_short"
|
||||
msgstr "Jan"
|
||||
|
||||
#. Default: "Feb"
|
||||
msgid "month_Feb_short"
|
||||
msgstr "Feb"
|
||||
|
||||
#. Default: "Mar"
|
||||
msgid "month_Mar_short"
|
||||
msgstr "Mar"
|
||||
|
||||
#. Default: "Apr"
|
||||
msgid "month_Apr_short"
|
||||
msgstr "Apr"
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May_short"
|
||||
msgstr "Mei"
|
||||
|
||||
#. Default: "Jun"
|
||||
msgid "month_Jun_short"
|
||||
msgstr "Jun"
|
||||
|
||||
#. Default: "Jul"
|
||||
msgid "month_Jul_short"
|
||||
msgstr "Jul"
|
||||
|
||||
#. Default: "Aug"
|
||||
msgid "month_Aug_short"
|
||||
msgstr "Aug"
|
||||
|
||||
#. Default: "Sep"
|
||||
msgid "month_Sep_short"
|
||||
msgstr "Sep"
|
||||
|
||||
#. Default: "Oct"
|
||||
msgid "month_Oct_short"
|
||||
msgstr "Okt"
|
||||
|
||||
#. Default: "Nov"
|
||||
msgid "month_Nov_short"
|
||||
msgstr "Nov"
|
||||
|
||||
#. Default: "Dec"
|
||||
msgid "month_Dec_short"
|
||||
msgstr "Dec"
|
||||
|
||||
#. Default: "January"
|
||||
msgid "month_Jan"
|
||||
msgstr "Januari"
|
||||
|
||||
#. Default: "February"
|
||||
msgid "month_Feb"
|
||||
msgstr "Februari"
|
||||
|
||||
#. Default: "March"
|
||||
msgid "month_Mar"
|
||||
msgstr "Maart"
|
||||
|
||||
#. Default: "April"
|
||||
msgid "month_Apr"
|
||||
msgstr "April"
|
||||
|
||||
#. Default: "May"
|
||||
msgid "month_May"
|
||||
msgstr "Mei"
|
||||
|
||||
#. Default: "June"
|
||||
msgid "month_Jun"
|
||||
msgstr "Juni"
|
||||
|
||||
#. Default: "July"
|
||||
msgid "month_Jul"
|
||||
msgstr "Julie"
|
||||
|
||||
#. Default: "Augustus"
|
||||
msgid "month_Aug"
|
||||
msgstr "Augustus"
|
||||
|
||||
#. Default: "September"
|
||||
msgid "month_Sep"
|
||||
msgstr "September"
|
||||
|
||||
#. Default: "October"
|
||||
msgid "month_Oct"
|
||||
msgstr "Oktober"
|
||||
|
||||
#. Default: "November"
|
||||
msgid "month_Nov"
|
||||
msgstr "November"
|
||||
|
||||
#. Default: "December"
|
||||
msgid "month_Dec"
|
||||
msgstr "December"
|
||||
|
||||
#. Default: "Today"
|
||||
msgid "today"
|
||||
msgstr "Vandaag"
|
||||
|
||||
#. Default: "Which event type would you like to create?"
|
||||
msgid "which_event"
|
||||
msgstr "Welk type event wil u aanmaken?"
|
||||
|
||||
#. Default: "Extend the event on the following number of days (leave blank to create an event on the current day only):"
|
||||
msgid "event_span"
|
||||
msgstr "Het event uitbreiden naar de volgende dagen (leeg laten om een event aan te maken op deze enige dag):"
|
||||
|
||||
#. Default: "Also delete successive events of the same type."
|
||||
msgid "del_next_events"
|
||||
msgstr "Verwijder ook alle opeenvolgende events van hetzelfde type"
|
||||
|
||||
#. Default: "Several events"
|
||||
msgid "several_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Timeslot"
|
||||
msgid "timeslot"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All day"
|
||||
msgid "timeslot_main"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Cannot create such an event in the ${slot} slot."
|
||||
msgid "timeslot_misfit"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Validate events"
|
||||
msgid "validate_events"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
|
||||
msgid "validate_events_confirm"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
|
||||
msgid "validate_events_done"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Inserted by ${userName}"
|
||||
msgid "history_insert"
|
||||
msgstr "Ingevuld door ${userName}"
|
||||
|
||||
#. Default: "Deleted by ${userName}"
|
||||
msgid "history_delete"
|
||||
msgstr "Verwijderd door ${userName}"
|
||||
|
||||
#. Default: "Show changes"
|
||||
msgid "changes_show"
|
||||
msgstr "Toon de wijzigingen"
|
||||
|
||||
#. Default: "Hide changes"
|
||||
msgid "changes_hide"
|
||||
msgstr "Verberg de wijzigingen"
|
||||
|
||||
#. Default: "an anonymous user"
|
||||
msgid "anonymous"
|
||||
msgstr "een anonieme gebruiker"
|
||||
|
||||
#. Default: "${date} - This page is locked by ${user}."
|
||||
msgid "page_locked"
|
||||
msgstr "${date} - Deze pagina is vergrendeld door ${user}."
|
||||
|
||||
#. Default: "In some situations, by leaving this page this way, you may lose encoded data or prevent other users from editing it afterwards. Please use buttons instead."
|
||||
msgid "warn_leave_form"
|
||||
msgstr "In bepaalde situaties, wanneer u deze pagina op deze wijze verlaat, is het mogelijk dat ingevoerde gegevens verloren gaan of dat u andere gebruikers verhindert om ze aan te passen. Gelieve de juiste knoppen te gebruiken."
|
||||
|
||||
#. Default: "You are not allowed to consult this."
|
||||
msgid "unauthorized"
|
||||
msgstr "U hebt geen toelating om dit te consulteren."
|
||||
|
||||
#. Default: "Microsoft Internet Explorer ${version} is not supported. Please upgrade your browser to version ${min} or above."
|
||||
msgid "wrong_browser"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Send by email"
|
||||
msgid "email_send"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "${site} - ${title} - ${template}"
|
||||
msgid "podmail_subject"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Hello, this email that was sent to you via ${site}. Please consult the attached file(s)."
|
||||
msgid "podmail_body"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "List"
|
||||
msgid "result_mode_list"
|
||||
msgstr ""
|
||||
|
||||
#. Default: "Grid"
|
||||
msgid "result_mode_grid"
|
||||
msgstr ""
|
Before Width: | Height: | Size: 566 B |
BIN
gen/ui/add.png
Before Width: | Height: | Size: 240 B |
Before Width: | Height: | Size: 209 B |
203
gen/ui/appy.css
|
@ -1,203 +0,0 @@
|
|||
html { height: 100% }
|
||||
body { font: 75% "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif;
|
||||
height: 100%; background-color: #EAEAEA; margin-top: 18px }
|
||||
pre { font: 100% "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif;
|
||||
margin: 0 }
|
||||
h1 { font-size: 14pt; margin:6px 0 6px 0 }
|
||||
h2 { font-size: 13pt; margin:6px 0 6px 0; font-style: italic;
|
||||
font-weight: normal }
|
||||
h3 { font-size: 12pt; margin:4px 0 4px 0; font-weight: bold }
|
||||
h4 { font-size: 11pt; margin:4px 0 4px 0 }
|
||||
h5 { font-size: 10pt; margin:0; font-style: italic; font-weight: normal;
|
||||
background-color: #d7dee4 }
|
||||
h6 { font-size: 9pt; margin:0; font-weight: bold }
|
||||
a { text-decoration: none; color: #114353 }
|
||||
a:visited { color: #114353 }
|
||||
.unclickable { pointer-events: none; color: grey !important }
|
||||
table { font-size: 100%; border-spacing: 0px; border-collapse:collapse }
|
||||
form { margin: 0; padding: 0 }
|
||||
p { margin: 0 0 5px 0 }
|
||||
acronym { cursor: help }
|
||||
input { font: 92% "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif }
|
||||
input[type=image] { border: 0; background: none; cursor: pointer }
|
||||
input[type=checkbox] { border: 0; background: none; cursor: pointer }
|
||||
input[type=radio] { border: 0; background: none; cursor: pointer }
|
||||
input[type=file] { border: 0px solid #d0d0d0; cursor: pointer }
|
||||
input[type=button] { border: 1px solid #d0d0d0; margin: 0 3px;
|
||||
background-color: #f8f8f8; cursor: pointer }
|
||||
input[type=submit] { border: 1px solid #d0d0d0; background-color: #f8f8f8;
|
||||
cursor: pointer }
|
||||
input[type=password] { border: 1px solid #d0d0d0; padding: 0 2px;
|
||||
margin-bottom:1px;
|
||||
font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif }
|
||||
input[type=text] { border: 1px solid #d0d0d0; padding: 0 2px; margin-bottom:1px;
|
||||
font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif }
|
||||
select { border: 1px solid #d0d0d0; background-color: white }
|
||||
|
||||
textarea { width: 99%; font: 100% "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif;
|
||||
border: 1px solid #d0d0d0; background-color: white }
|
||||
label { color: #555555; font-size: 11px; margin: 3px 0;
|
||||
text-transform: uppercase }
|
||||
legend { padding-bottom: 2px; padding-right: 3px; color: black }
|
||||
ul { line-height: 1.2em; margin: 0 0 0.2em 0.6em; padding: 0;
|
||||
list-style: none outside none }
|
||||
ul li { margin: 0; background-image: url("ui/li.gif"); padding-left: 10px;
|
||||
background-repeat: no-repeat; background-position: 0 4px }
|
||||
img { border: 0; vertical-align: middle }
|
||||
|
||||
.main { width: 900px; height: 95%; box-shadow: 3px 3px 3px #A9A9A9 }
|
||||
.mainWide { width: 100%; height: 100% }
|
||||
.wrongBrowser { height: 15px; color: red; text-align: center }
|
||||
.top { height: 89px; margin-left: 3em; vertical-align: top;
|
||||
background-color: white }
|
||||
.lang { margin-right: 6px }
|
||||
.userStrip { background-color: #A3A3C8; border-top: 3px solid #cbcbcb;
|
||||
border-bottom: 2px solid #e4e4e4; width: 100%; height: 100% }
|
||||
.userStripText { padding: 0 0.3em 0 0.3em; color: whitesmoke }
|
||||
.userStrip a { color: #e7e7e7 }
|
||||
.userStrip a:visited { color: #e7e7e7 }
|
||||
.changePassword { color: #494949 !important; font-style:italic; font-size: 90% }
|
||||
.breadcrumb { font-size: 11pt; padding-bottom: 6px }
|
||||
.login { margin: 3px; color: black }
|
||||
input.button { color: #666666; height: 20px; cursor:pointer; font-size: 90%;
|
||||
padding-left: 26px; padding-right: 12px; background-color: white;
|
||||
background-repeat: no-repeat; background-position: 8px 25%;
|
||||
box-shadow: 2px 2px 2px #888888}
|
||||
input.buttonSmall { font-size: 85%; height: 18px }
|
||||
input.buttonFixed { width:110px; padding: 0 0 0 10px }
|
||||
.fake { background-color: #e6e6e6 !important ; cursor:help !important }
|
||||
.xhtml { background-color: white; padding: 4px; font-size: 95% }
|
||||
.xhtml img { margin-right: 5px }
|
||||
.xhtml p { margin: 3px 0 7px 0 }
|
||||
.clickable { cursor: pointer }
|
||||
.help { cursor: help }
|
||||
.refLink { font-style: italic; padding-left: 5px; font-size: 90%;
|
||||
color: #555555 }
|
||||
.buttons { margin-left: 4px }
|
||||
.objectButtons { margin-top: 5px }
|
||||
.message { position: absolute; top: -40px; left: 50%; font-size: 90%;
|
||||
width: 600px; border: 1px #F0C36D solid; padding: 6px;
|
||||
background-color: #F9EDBE; text-align: center; margin-left: -300px;
|
||||
border-radius: 2px 2px 2px 2px; box-shadow: 0 2px 4px #A9A9A9;
|
||||
z-index: 10 }
|
||||
.messagePopup { width: 80%; top: 0; left: 0; margin-left: 10px }
|
||||
.focus { font-size: 90%; margin: 7px 0 7px 0; padding: 7px;
|
||||
background-color: #d7dee4; border-radius: 2px 2px 2px 2px;
|
||||
box-shadow: 0 2px 4px #A9A9A9 }
|
||||
.focus td { padding: 4px 0px 4px 4px }
|
||||
.discreet { font-size: 90%; color: #555555 }
|
||||
.title {}
|
||||
.lostPassword { font-size: 90%; color: white; padding-left: 1em }
|
||||
.current { font-weight: bold }
|
||||
.portlet { width: 150px; border-right: 3px solid #e4e4e4;
|
||||
background-color: whitesmoke }
|
||||
.portletContent { padding: 4px 9px }
|
||||
.portletTitle { font-size: 110%; margin-bottom: 4px }
|
||||
.portletSep { border-top: 3px solid #e4e4e4 }
|
||||
.portletGroup { font-variant: small-caps; font-weight: bold; font-size: 110%;
|
||||
margin: 0.1em 0 0.3em ; border-bottom: 1px dashed #c0c0c0 }
|
||||
.portletSearch { font-size: 90%; font-style: italic }
|
||||
.portletCurrent { font-weight: bold }
|
||||
.inputSearch { height: 15px; width: 132px; margin: 3px 3px 2px 3px !important }
|
||||
td.search { padding-top: 8px }
|
||||
.searchFields { width: 100%; margin-bottom: 8px }
|
||||
.content { padding: 9px; background-color: #fbfbfb }
|
||||
.popup { display: none; position: absolute; z-index : 100; background: white;
|
||||
padding: 8px; border: 1px solid grey; box-shadow: 2px 2px 2px #888888}
|
||||
.dropdown { display:none; position: absolute; top: 12px; left: 1px;
|
||||
border: 1px solid #cccccc; background-color: white;
|
||||
padding: 3px 4px 3px; font-size: 8pt; font-weight: normal;
|
||||
text-align: left; z-index: 2; line-height: normal }
|
||||
.liveSearch { top: 19px; padding: 0; width: 250px }
|
||||
.lsSelected { background-color: #d9d7d9 }
|
||||
.lsNoResult { text-align: center; padding: 3px 3px; font-style: italic }
|
||||
.dropdownMenu { cursor: pointer; font-size: 93%; position: relative }
|
||||
.dropdown a:hover { text-decoration: underline }
|
||||
.addForm { display: inline }
|
||||
.addFormMenu { display: inline; padding: 0 5px 0 3px }
|
||||
.inline { display: inline }
|
||||
.list { margin-bottom: 3px }
|
||||
.list td, .list th {
|
||||
border: 3px solid #ededed; color: #555555; padding: 3px 5px 3px 5px }
|
||||
.list th { background-color: #e5e5e5; font-style: italic; font-weight: normal }
|
||||
.compact { font-size: 90%; width: 100% }
|
||||
.compact th, .compact td { padding: 0px 3px 0px 3px }
|
||||
.grid th { font-style: italic; font-weight: normal;
|
||||
border-bottom: 5px solid #fdfdfd; padding: 3px 5px 0 5px }
|
||||
.grid td { padding: 0 3px }
|
||||
.timeline { font-size: 85%; color: #555555 }
|
||||
.timeline tr { height: 18px }
|
||||
.timeline td { text-align: center; padding: 0 1px }
|
||||
.timeline th { padding: 1px }
|
||||
.tlLeft { text-align: left !important; padding-left: 1px !important }
|
||||
.tlRight { text-align: right !important; padding-right: 1px !important }
|
||||
.msgTable { margin: 6px 0; width: 100%; font-size: 93% }
|
||||
.msgTable tr { vertical-align: top }
|
||||
.msgTable td, .msgTable th { border: 1px solid grey; color: #555555;
|
||||
padding: 0px 5px; text-align: left }
|
||||
.msgTable th { background-color: #f0e3b8; font-style: italic;
|
||||
font-weight: normal }
|
||||
.cellGap { padding-right: 0.4em }
|
||||
.cellDashed { border: 1px dashed grey !important }
|
||||
.no, .no td, .no th {
|
||||
border: 0 !important; padding: 0 !important; margin: 0 !important;
|
||||
background-color: transparent !important }
|
||||
.hidden { visibility: hidden }
|
||||
.simpleLabel { text-transform: none; padding-right: 3px }
|
||||
.translationLabel { background-color: #EAEAEA; border-bottom: 1px dashed grey;
|
||||
margin-top: 0.5em; margin-bottom: 0.5em }
|
||||
.section1 { font-size: 120%; margin: 0.45em 0em 0.1em 0;
|
||||
padding: 0.3em 0em 0.2em 0.1em; background-color: #eef3f5;
|
||||
border-top: 1px solid #8CACBB;border-bottom: 1px solid #8cacbb }
|
||||
.section2 { font-size: 14px; padding: 6px; text-transform: uppercase;
|
||||
border-bottom: 1px dashed #cccccc; border-top: 1px dashed #cccccc;
|
||||
background-color: #f9f9f9 }
|
||||
.section3 { font-size: 11px; font-weight: bold; text-transform: uppercase;
|
||||
padding: 2px 0px; background-color: #a1aeb5; text-align: center;
|
||||
color: white }
|
||||
.even { background-color: #fbfbfb }
|
||||
.odd { background-color: #f6f6f6 }
|
||||
.odd2 { background-color: #f2f2f2 }
|
||||
.refMenuItem { border-top: grey 1px dashed; margin-top: 3px; padding-top: 3px }
|
||||
.summary { margin-bottom: 5px; background-color: whitesmoke;
|
||||
border: 3px solid white }
|
||||
.by { padding: 5px; color: grey; font-size: 97% }
|
||||
.underline { border-bottom: 1px dotted grey }
|
||||
.state { font-weight: bold; border-bottom: 1px dashed grey }
|
||||
.historyLabel { font-variant: small-caps; font-weight: bold }
|
||||
.history td { border-top: 1px solid #e6e6e6; padding: 0 5px 0 5px }
|
||||
.history th { font-style: italic; text-align: left; padding: 0 5px 0 5px }
|
||||
.topSpace { margin-top: 15px }
|
||||
.bottomSpace { margin-bottom: 15px }
|
||||
.phase { background-color: whitesmoke; border: 1px solid #d0d0d0;
|
||||
box-shadow: 2px 2px 2px #888888; margin-bottom: 7px }
|
||||
.phase td { padding: 3px 7px 3px 7px; border-right: 1px solid #d0d0d0;
|
||||
font-size: 96% }
|
||||
.currentPage { background-color: #e6e6e6 }
|
||||
.pageLink { margin-right: 8px }
|
||||
.footer { font-size: 95%; height: 100% }
|
||||
.footer td { background-color: #CBCBC9; border-top: 3px solid #e4e4e4;
|
||||
padding: 0.4em 1em 0.5em }
|
||||
.code { font-family: "Lucida Console","Courier New" }
|
||||
.codePara { background-color: #EEFFCC; border-color: grey;
|
||||
border-style: solid none; border-width: 1px medium;
|
||||
color: #333333; line-height: 120%;
|
||||
padding: 10px; margin: 10px 0 10px 0 }
|
||||
.homeTable { background-color: #E3E3E3; border-top: 1px solid grey }
|
||||
.homeTable td { padding: 10px 5px 10px 10px }
|
||||
.homeTable th { padding-top: 5px; font-size: 105% }
|
||||
.first { margin-top: 0px }
|
||||
.error { margin: 5px }
|
||||
.smaller { font-size: 95% }
|
||||
.pod { padding-right: 15px }
|
||||
.podTable { line-height: 20px }
|
||||
.cbCell { width: 10px; text-align: center}
|
||||
.tabs { position:relative; bottom:-2px }
|
||||
.tab { padding: 0 10px 0 10px; text-align: center; font-size: 90%;
|
||||
font-weight: bold}
|
||||
.language {color: #555555; font-size: 7pt; border: grey 1px solid; padding: 2px;
|
||||
margin: 0 2px 0 4px; font-family: monospace }
|
||||
.highlight { background-color: yellow }
|
||||
.globalActions { margin-bottom: 4px }
|
||||
.objectActions { margin: 2px 0 }
|
||||
.smallbox { margin: 0; vertical-align: middle }
|
1386
gen/ui/appy.js
Before Width: | Height: | Size: 904 B |
|
@ -1,8 +0,0 @@
|
|||
ul { margin: 0 0.6em 0.2em 0 }
|
||||
ul li { background-image: url("ui/lirtl.gif");
|
||||
background-position: right center;
|
||||
padding-right: 15px; padding-left: 0px}
|
||||
.portlet { border-right: none; border-left: 3px solid #e4e4e4 }
|
||||
.lang { margin-right: 0px; margin-left: 6px; }
|
||||
.cellGap { padding-left: 0.4em; padding-right: 0;}
|
||||
.pageLink { margin-right: 0px; margin-left: 8px }
|
Before Width: | Height: | Size: 234 B |
Before Width: | Height: | Size: 218 B |
Before Width: | Height: | Size: 229 B |
Before Width: | Height: | Size: 234 B |
Before Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 236 B |
Before Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 241 B |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 953 B |
|
@ -1,206 +0,0 @@
|
|||
function toggleVisibility(node, nodeType){
|
||||
// Toggle visibility of all elements having p_nodeType within p_node
|
||||
var elements = node.getElementsByTagName(nodeType);
|
||||
for (var i=0; i<elements.length; i++){
|
||||
var sNode = elements[i];
|
||||
if (sNode.style.visibility == 'hidden') sNode.style.visibility = 'visible';
|
||||
else sNode.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
// Sends an Ajax request for getting the calendar, at p_month
|
||||
function askMonth(hookId, month) {askAjax(hookId, null, {'month': month})}
|
||||
|
||||
function enableOptions(select, enabled, selectFirst, message){
|
||||
/* This function disables, in p_select, all options that are not in p_enabled.
|
||||
p_enabled is a string containing a comma-separated list of option names.
|
||||
If p_selectFirst is True, the first option from p_enabled will be selected
|
||||
by default. p_message will be shown (as "title") for disabled options. */
|
||||
// Get p_enabled as a dict
|
||||
var l = enabled.split(',');
|
||||
var d = {};
|
||||
for (var i=0; i < l.length; i++) d[l[i]] = true;
|
||||
// Remember if we have already selected the first enabled option
|
||||
var isSelected = false;
|
||||
var options = select.options;
|
||||
// Disable options not being p_enabled
|
||||
for (var i=0; i<options.length; i++) {
|
||||
options[i].selected = false;
|
||||
if (!options[i].value) continue;
|
||||
if (options[i].value in d) {
|
||||
options[i].disabled = false;
|
||||
options[i].title = '';
|
||||
// Select it?
|
||||
if (selectFirst && !isSelected) {
|
||||
options[i].selected = true;
|
||||
isSelected = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
options[i].disabled = true;
|
||||
options[i].title = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openEventPopup(hookId, action, day, timeslot, spansDays,
|
||||
applicableEventTypes, message, freeSlots) {
|
||||
/* Opens the popup for creating (or deleting, depending on p_action) a
|
||||
calendar event at some p_day. When action is "del", we need to know the
|
||||
p_timeslot where the event is assigned and if the event spans more days
|
||||
(from p_spansDays), in order to propose a checkbox allowing to delete
|
||||
events for those successive days. When action is "new", a possibly
|
||||
restricted list of applicable event types for this day is given in
|
||||
p_applicableEventTypes; p_message contains an optional message explaining
|
||||
why not applicable types are not applicable. When "new", p_freeSlots may
|
||||
list the available timeslots at p_day. */
|
||||
var popupId = hookId + '_' + action;
|
||||
var f = document.getElementById(popupId + 'Form');
|
||||
f.day.value = day;
|
||||
if (action == 'del') {
|
||||
if (f.timeslot) f.timeslot.value = timeslot;
|
||||
// Show or hide the checkbox for deleting the event for successive days
|
||||
var elem = document.getElementById(hookId + '_DelNextEvent');
|
||||
var cb = elem.getElementsByTagName('input');
|
||||
cb[0].checked = false;
|
||||
cb[1].value = 'False';
|
||||
if (spansDays == 'True') elem.style.display = 'block';
|
||||
else elem.style.display = 'none';
|
||||
}
|
||||
else if (action == 'new') {
|
||||
// Reinitialise field backgrounds
|
||||
f.eventType.style.background = '';
|
||||
if (f.eventSpan) f.eventSpan.style.background = '';
|
||||
// Disable unapplicable events and non-free timeslots
|
||||
enableOptions(f.eventType, applicableEventTypes, false, message);
|
||||
if (f.timeslot) enableOptions(f.timeslot, freeSlots, true, 'Not free');
|
||||
}
|
||||
openPopup(popupId);
|
||||
}
|
||||
|
||||
function triggerCalendarEvent(hookId, action, maxEventLength) {
|
||||
/* Sends an Ajax request for triggering a calendar event (create or delete an
|
||||
event) and refreshing the view month. */
|
||||
var popupId = hookId + '_' + action;
|
||||
var formId = popupId + 'Form';
|
||||
var f = document.getElementById(formId);
|
||||
if (action == 'new') {
|
||||
// Check that an event span has been specified
|
||||
if (f.eventType.selectedIndex == 0) {
|
||||
f.eventType.style.background = wrongTextInput;
|
||||
return;
|
||||
}
|
||||
if (f.eventSpan) {
|
||||
// Check that eventSpan is empty or contains a valid number
|
||||
var spanNumber = f.eventSpan.value.replace(' ', '');
|
||||
if (spanNumber) {
|
||||
spanNumber = parseInt(spanNumber);
|
||||
if (isNaN(spanNumber) || (spanNumber > maxEventLength)) {
|
||||
f.eventSpan.style.background = wrongTextInput;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
closePopup(popupId);
|
||||
askAjax(hookId, formId);
|
||||
}
|
||||
|
||||
// Function that collects the status of all validation checkboxes
|
||||
function getValidationStatus(hookId) {
|
||||
var res = {'validated': [], 'discarded': []};
|
||||
var node = document.getElementById(hookId + '_cal');
|
||||
var cbs = node.getElementsByTagName('input');
|
||||
var key = null;
|
||||
for (var i=0; i<cbs.length; i++) {
|
||||
if (cbs[i].type != 'checkbox') continue;
|
||||
key = (cbs[i].checked)? 'validated': 'discarded';
|
||||
res[key].push(cbs[i].id);
|
||||
}
|
||||
// Convert lists to comma-separated strings
|
||||
for (key in res) res[key] = res[key].join();
|
||||
return res;
|
||||
}
|
||||
|
||||
// Function for validating and discarding calendar events
|
||||
function validateEvents(hookId, month) {
|
||||
// Collect checkboxes from hookId and identify checked and unchecked ones
|
||||
var params = {'action': 'validateEvents', 'mode': 'POST', 'month': month};
|
||||
var status = getValidationStatus(hookId);
|
||||
for (var key in status) params[key] = status[key];
|
||||
askAjax(hookId, null, params);
|
||||
}
|
||||
|
||||
// Function for (un)-checking checkboxes automatically
|
||||
function onCheckCbCell(cb, hook, totalRows, totalCols) {
|
||||
// Is automatic selection on/off?
|
||||
var auto = document.getElementById(hook + '_auto');
|
||||
if (auto.checked) {
|
||||
// Get the current render mode
|
||||
var render = document.getElementById(hook)['ajax'].params['render'];
|
||||
// Change the state of every successive checkbox
|
||||
var timeline = render == 'timeline'; // Else, render is "month"
|
||||
// From the checkbox id, extract the date and the remaining part
|
||||
var elems = cb.id.split('_');
|
||||
if (timeline) {
|
||||
var date = elems[2], part = elems[0] + '_' + elems[1] + '_'; }
|
||||
else { var date = elems[0], part = '_' + elems[1] + '_' + elems[2]; }
|
||||
// Create a Date instance
|
||||
var year = parseInt(date.slice(0,4)), month = parseInt(date.slice(4,6))-1,
|
||||
day = parseInt(date.slice(6,8));
|
||||
var next = new Date(year, month, day);
|
||||
// Change the status of successive checkboxes if found
|
||||
var checked = cb.checked;
|
||||
var nextId = nextCb = null;
|
||||
while (true) {
|
||||
// Compute the date at the next day
|
||||
next.setDate(next.getDate() + 1);
|
||||
month = (next.getMonth() + 1).toString();
|
||||
if (month.length == 1) month = '0' + month;
|
||||
day = next.getDate().toString();
|
||||
if (day.length == 1) day = '0' + day;
|
||||
date = next.getFullYear().toString() + month + day;
|
||||
// Find the next checkbox
|
||||
if (timeline) nextId = part + date;
|
||||
else nextId = date + part;
|
||||
nextCb = document.getElementById(nextId);
|
||||
if (!nextCb) break;
|
||||
nextCb.checked = checked;
|
||||
}
|
||||
}
|
||||
// Refresh the total rows if requested
|
||||
if (totalRows || totalCols) {
|
||||
var params = getValidationStatus(hook);
|
||||
params['mode'] = 'POST';
|
||||
if (totalRows) {
|
||||
params['totalType'] = 'rows';
|
||||
askAjax(hook + '_trs', null, params, 'loadingPod');
|
||||
}
|
||||
if (totalCols) {
|
||||
params['totalType'] = 'cols';
|
||||
askAjax(hook + '_tcs', null, params, 'loadingPod');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Switches a layer on/off within a calendar
|
||||
function switchCalendarLayer(hookId, checkbox) {
|
||||
/* Update the ajax data about active layers from p_checkbox, that represents
|
||||
the status of some layer */
|
||||
var layer = checkbox.id.split('_').pop();
|
||||
var d = getAjaxHook(hookId)['ajax'];
|
||||
var activeLayers = d.params['activeLayers'];
|
||||
if (checkbox.checked) {
|
||||
// Add the layer to active layers
|
||||
activeLayers = (!activeLayers)? layer: activeLayers + ',' + layer;
|
||||
}
|
||||
else {
|
||||
// Remove the layer from active layers
|
||||
var res = [];
|
||||
var splitted = activeLayers.split(',');
|
||||
for (var i=0; i<splitted.length; i++) {
|
||||
if (splitted[i] != layer) res.push(splitted[i]);
|
||||
}
|
||||
activeLayers = res.join();
|
||||
}
|
||||
askAjax(hookId, null, {'activeLayers': activeLayers});
|
||||
}
|
Before Width: | Height: | Size: 272 B |
Before Width: | Height: | Size: 255 B |
Before Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 235 B |
|
@ -1,5 +0,0 @@
|
|||
Appy is integrated with CKeditor 4.4.7 via CDN (see http://cdn.ckeditor.com).
|
||||
This folder stores the custom files for configuring ckeditor for Appy:
|
||||
config.js, contents.css and styles.js.
|
||||
Those files were copied from the same download version of ckeditor and
|
||||
customized (on 2015-02-22).
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
|
||||
For licensing, see LICENSE.html or http://ckeditor.com/license
|
||||
*/
|
||||
CKEDITOR.editorConfig = function(config) {
|
||||
config.toolbar = 'Appy';
|
||||
config.toolbar_Appy = [
|
||||
{ name: 'basicstyles',
|
||||
items: ["Format", "Bold", "Italic", "Underline", "Subscript",
|
||||
"Superscript", "RemoveFormat"]},
|
||||
{ name: 'paragraph',
|
||||
items: ["NumberedList", "BulletedList", "Outdent", "Indent"]},
|
||||
{ name: 'clipboard',
|
||||
items: ["Cut", "Copy", "Paste", "PasteText", "Undo", "Redo"]},
|
||||
{ name: 'editing', items: ["Scayt"]},
|
||||
{ name: 'insert',
|
||||
items: ["Image", "Table", "SpecialChar", "Link", "Unlink", "Source", "Maximize"]}
|
||||
];
|
||||
config.entities = false;
|
||||
config.entities_greek = false;
|
||||
config.entities_latin = false;
|
||||
config.fillEmptyBlocks = false;
|
||||
config.removePlugins = 'elementspath';
|
||||
config.scayt_sLang = 'fr_BE';
|
||||
config.scayt_uiTabs = '0,1,0';
|
||||
config.removeDialogTabs = 'image:advanced;link:advanced';
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
/* Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
|
||||
For licensing, see LICENSE.html or http://ckeditor.com/license */
|
||||
|
||||
/* From ckeditor default contents.css */
|
||||
body { font-family: sans-serif, Arial, Verdana, "Trebuchet MS"; font-size: 12px;
|
||||
color: #333; background-color: #fff; margin: 20px }
|
||||
.cke_editable { font-size: 12px; line-height: 1.4 }
|
||||
blockquote { font-style: italic;
|
||||
font-family: Georgia, Times, "Times New Roman", serif; padding: 2px 0;
|
||||
border-style: solid; border-color: #ccc; border-width: 0 }
|
||||
.cke_contents_ltr blockquote {
|
||||
padding-left: 5px; padding-right: 5px; border-left-width: 4px }
|
||||
.cke_contents_rtl blockquote {
|
||||
padding-left: 5px; padding-right: 5px; border-right-width: 4px }
|
||||
a { color: #0782C1 }
|
||||
ol,ul,dl { *margin-right: 0px; padding: 0 30px }
|
||||
h1,h2,h3,h4,h5,h6 { font-weight: normal; line-height: 1.2 }
|
||||
hr { border: 0px; border-top: 1px solid #ccc }
|
||||
img.right { border: 1px solid #ccc; float: right; margin-left: 15px;
|
||||
padding: 5px }
|
||||
img.left { border: 1px solid #ccc; float: left; margin-right: 15px;
|
||||
padding: 5px }
|
||||
pre { white-space: pre-wrap; word-wrap: break-word; -moz-tab-size: 4;
|
||||
-o-tab-size: 4; -webkit-tab-size: 4; tab-size: 4 }
|
||||
.marker { background-color: Yellow }
|
||||
span[lang] { font-style: italic }
|
||||
figure { text-align: center; border: solid 1px #ccc; border-radius: 2px;
|
||||
background: rgba(0,0,0,0.05); padding: 10px; margin: 10px 20px;
|
||||
display: inline-block }
|
||||
figure > figcaption { text-align: center; display: block }
|
||||
a > img { padding: 1px; margin: 1px; border: none; outline: 1px solid #0782C1 }
|
||||
|
||||
/* Added by Appy */
|
||||
p { margin: 0; padding: 0 0 3px 0 }
|
||||
table { border-collapse: collapse; border-spacing: 0 }
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
CKEDITOR.stylesSet.add('default',[]);
|
BIN
gen/ui/close.png
Before Width: | Height: | Size: 219 B |
Before Width: | Height: | Size: 70 B |
Before Width: | Height: | Size: 244 B |
Before Width: | Height: | Size: 231 B |
Before Width: | Height: | Size: 259 B |