removed unnecessary things

This commit is contained in:
Stefan Klug 2015-10-31 20:06:55 +01:00
parent 120586dd5d
commit 65565c7b16
255 changed files with 0 additions and 35978 deletions

View file

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

View file

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

View file

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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.')

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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.'
# ------------------------------------------------------------------------------

View file

@ -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 = '&nbsp;' * 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 = '&nbsp;' * 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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -1,4 +0,0 @@
#!/bin/sh
soffice "--accept=socket,host=localhost,port=2002;urp;"
echo "Press <enter>..."
read R

View file

@ -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()
# ------------------------------------------------------------------------------

View file

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

View file

@ -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] &gt; 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())
# ------------------------------------------------------------------------------

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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">&nbsp;</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>&nbsp;&nbsp;&nbsp;&nbsp;</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))
# ------------------------------------------------------------------------------

View file

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

View file

@ -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>&nbsp;&nbsp;-
<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)
# ------------------------------------------------------------------------------

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) &gt; 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'))
# ------------------------------------------------------------------------------

File diff suppressed because it is too large Load diff

View file

@ -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&amp;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 &gt; 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 ('&amp;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')">&nbsp;&mdash;
&nbsp;<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)
# ------------------------------------------------------------------------------

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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')
# ------------------------------------------------------------------------------

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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
View file

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

View file

@ -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!>

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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()
# ------------------------------------------------------------------------------

View file

@ -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])
# ------------------------------------------------------------------------------

View file

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

View file

@ -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 ""

View file

@ -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 ""

View file

@ -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 ""

View file

@ -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"

View file

@ -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 ""

View file

@ -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"

View file

@ -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 ""

View file

@ -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 ""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 B

View file

@ -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 }

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 904 B

View file

@ -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 }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 953 B

View file

@ -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});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 B

View file

@ -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).

View file

@ -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';
};

View file

@ -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 }

View file

@ -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',[]);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 B

Some files were not shown because too many files have changed in this diff Show more