diff --git a/appy/shared/packaging.py b/appy/shared/packaging.py deleted file mode 100644 index c79cd3c..0000000 --- a/appy/shared/packaging.py +++ /dev/null @@ -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 -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 - - level info - - path $LOG/event.log - level info - - - - level WARN - - path $LOG/Z2.log - format %%(message)s - - - - address $HTTPPORT - - - - path $DATA/Data.fs - - mount-point / - - - - name temporary storage for sessioning - - mount-point /temp_folder - container-class Products.TemporaryFolder.TemporaryContainer - -''' -# 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) - # 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() - # 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/ (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/ (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/.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/ (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) -# ------------------------------------------------------------------------------ diff --git a/appy/shared/sap.py b/appy/shared/sap.py deleted file mode 100644 index 5c5859c..0000000 --- a/appy/shared/sap.py +++ /dev/null @@ -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.(params) instead of - calling self.call(, 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)) -# ------------------------------------------------------------------------------ diff --git a/bin/__init__.py b/bin/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bin/asksap.py b/bin/asksap.py deleted file mode 100644 index 193309b..0000000 --- a/bin/asksap.py +++ /dev/null @@ -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() -# ------------------------------------------------------------------------------ diff --git a/bin/backup.py b/bin/backup.py deleted file mode 100644 index 33ae555..0000000 --- a/bin/backup.py +++ /dev/null @@ -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 ' \ - '::' \ - '[:]') - - 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: ::" \ - ":[:]. is the " \ - "user name of the Zope administrator; is the " \ - "path, within Zope, to the Plone Site object (if not at the " \ - "root of the Zope hierarchy, use '/' as folder separator); " \ - " is the name of the Appy application; " \ - " is the name of the method to call on the tool " \ - "in this Appy application; (optional) 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() -# ------------------------------------------------------------------------------ diff --git a/bin/checkldap.py b/bin/checkldap.py deleted file mode 100644 index a1c55d3..0000000 --- a/bin/checkldap.py +++ /dev/null @@ -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() -# ------------------------------------------------------------------------------ diff --git a/bin/checklo.py b/bin/checklo.py deleted file mode 100644 index cddf2bd..0000000 --- a/bin/checklo.py +++ /dev/null @@ -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() -# ------------------------------------------------------------------------------ diff --git a/bin/clean.py b/bin/clean.py deleted file mode 100644 index 5341c4c..0000000 --- a/bin/clean.py +++ /dev/null @@ -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() -# ------------------------------------------------------------------------------ diff --git a/bin/createdebrepo.py b/bin/createdebrepo.py deleted file mode 100755 index 8ac1d95..0000000 --- a/bin/createdebrepo.py +++ /dev/null @@ -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.') diff --git a/bin/eggify.py b/bin/eggify.py deleted file mode 100644 index 1fcc16e..0000000 --- a/bin/eggify.py +++ /dev/null @@ -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() -# ------------------------------------------------------------------------------ diff --git a/bin/generate.py b/bin/generate.py deleted file mode 100644 index a08737f..0000000 --- a/bin/generate.py +++ /dev/null @@ -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 /lib/python. - - This command generates a Zope product in /zope, which must be - or symlinked in /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() -# ------------------------------------------------------------------------------ diff --git a/bin/job.py b/bin/job.py deleted file mode 100644 index 4dca931..0000000 --- a/bin/job.py +++ /dev/null @@ -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: - - :::[:]. - - is the userName of the Zope administrator for this instance. - 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 - - is the name of the Appy application. If it begins with - "path=", it does not represent an Appy application, but - the path, within , to any Zope object - (use '/' as folder separator); leave blank if using - appy.gen > 0.8; - - 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=". - - (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() -# ------------------------------------------------------------------------------ diff --git a/bin/new.py b/bin/new.py deleted file mode 100644 index f74767c..0000000 --- a/bin/new.py +++ /dev/null @@ -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('', 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('',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 += ' \n' % missingInclude - content = content.replace('', '%s\n' % 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() -# ------------------------------------------------------------------------------ diff --git a/bin/odfgrep.py b/bin/odfgrep.py deleted file mode 100644 index af00819..0000000 --- a/bin/odfgrep.py +++ /dev/null @@ -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() -# ------------------------------------------------------------------------------ diff --git a/bin/odfwalk.py b/bin/odfwalk.py deleted file mode 100644 index 3bbc6c4..0000000 --- a/bin/odfwalk.py +++ /dev/null @@ -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.' -# ------------------------------------------------------------------------------ diff --git a/bin/publish.py b/bin/publish.py deleted file mode 100644 index ba9ecad..0000000 --- a/bin/publish.py +++ /dev/null @@ -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('\n\n%s\n\n' \ - '\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('
    \n') - inList = True - self.htmlFile.write( - '
  • %s
  • \n' % self.getCleanLine(line)) - elif firstChar == ' ': - pass - else: - # It is a title - if inList: - self.htmlFile.write('
\n') - inList = False - self.htmlFile.write( - '

%s

\n' % self.getCleanLine(line, True)) - self.htmlFile.write('\n\n\n') - self.txtFile.close() - self.htmlFile.close() - -# ------------------------------------------------------------------------------ -class Publisher: - '''Publishes Appy on the web.''' - pageBody = re.compile('(.*)', 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(''), \ - pageContent.find('') - pageTitle = '%s' % 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('(.*?)', re.S) - tocLink = re.compile('(.*?)') - subSection = re.compile('

(.*?)

') - subSectionContent = re.compile('.*?(.*)') - def createDocToc(self): - res = '' - 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 += '' - res += '
' - else: - tag = 'p' - indent = 2 - styleBegin = '' - styleEnd = '' - tabs = ' ' * indent * 2 - res += '<%s>%s%s%s%s\n' % \ - (tag, tabs, styleBegin, url, self._getPageTitle(url), - styleEnd, tag) - # Parse each HTML file and retrieve sections title that have an - # anchor defined - docFile = file('%s/doc/%s' % (appyPath, url)) - docContent = docFile.read() - docFile.close() - sections = self.subSection.findall(docContent) - for section in sections: - r = self.subSectionContent.search(section) - if r: - sectionNb += 1 - tabs = ' ' * 8 - res += '
%s%d. %s
\n' % \ - (tabs, sectionNb, url, r.group(1), r.group(2)) - res += '
' - 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() -# ------------------------------------------------------------------------------ diff --git a/bin/restore.py b/bin/restore.py deleted file mode 100644 index b8fb343..0000000 --- a/bin/restore.py +++ /dev/null @@ -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() -# ------------------------------------------------------------------------------ diff --git a/bin/startoo b/bin/startoo deleted file mode 100755 index 0e7f9aa..0000000 --- a/bin/startoo +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -soffice "--accept=socket,host=localhost,port=2002;urp;" -echo "Press ..." -read R diff --git a/bin/zip.py b/bin/zip.py deleted file mode 100644 index 5feb399..0000000 --- a/bin/zip.py +++ /dev/null @@ -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() -# ------------------------------------------------------------------------------ diff --git a/bin/zopectl.py b/bin/zopectl.py deleted file mode 100644 index 758d2f8..0000000 --- a/bin/zopectl.py +++ /dev/null @@ -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) -# ------------------------------------------------------------------------------ diff --git a/fields/__init__.py b/fields/__init__.py deleted file mode 100644 index b254f2a..0000000 --- a/fields/__init__.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''' - :tool.pxLayoutedObject''') - - 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(''' - - - - :field.pxObjectTitle - :tied.title - :field.pxObjectActions - -
- - :_('unauthorized')
-
- - - :field.pxRender - ''') - - # Show the field content for some object on a list of results - pxRenderAsResult = Px(''' - - - - ::sup - ::zobj.getListTitle(mode=titleMode, nav=navInfo, target=target, \ - page=pageName, inPopup=inPopup, selectJs=selectJs, highlight=True) - ::zobj.highlight(sub) - - -
- - - - - - - - :targetObj.appy().pxTransitions - - - - :field.pxCell - -
-
- - - :_('unauthorized') - -
- - - :field.pxRender - ''') - - # Displays a field label - pxLabel = Px('''''') - - # Displays a field description - pxDescription = Px('''::_('descr', field=field)''') - - # Displays a field help - pxHelp = Px('''''') - - # Displays validation-error-related info about a field - pxValidation = Px('''''') - - # Displays the fact that a field is required - pxRequired = Px('''''') - - # Button for showing changes to the field - pxChanges = Px(''' -
- - - - -
''') - - 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: ": 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 '/onProcess.''' - return obj.goto(obj.absolute_url()) -# ------------------------------------------------------------------------------ diff --git a/fields/action.py b/fields/action.py deleted file mode 100644 index 31cb4b2..0000000 --- a/fields/action.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''' -
- - - -
''') - - # 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) -# ------------------------------------------------------------------------------ diff --git a/fields/boolean.py b/fields/boolean.py deleted file mode 100644 index 83b851d..0000000 --- a/fields/boolean.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''':value - ''') - - pxEdit = Px(''' - - - - - - -
- - -
''') - - pxSearch = Px(''' - - - - - - - - - - - -
''') - - 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 -# ------------------------------------------------------------------------------ diff --git a/fields/calendar.py b/fields/calendar.py deleted file mode 100644 index 551037d..0000000 --- a/fields/calendar.py +++ /dev/null @@ -1,1772 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -import types -from appy import Object -from appy.shared import utils as sutils -from appy.gen import Field -from appy.px import Px -from DateTime import DateTime -from BTrees.IOBTree import IOBTree -from persistent.list import PersistentList -from persistent import Persistent - -# ------------------------------------------------------------------------------ -class Timeslot: - '''A timeslot defines a time range within a single day''' - def __init__(self, id, start=None, end=None, name=None, eventTypes=None): - # A short, human-readable string identifier, unique among all timeslots - # for a given Calendar. Id "main" is reserved for the main timeslot that - # represents the whole day. - self.id = id - # The time range can be defined by p_start ~(i_hour, i_minute)~ and - # p_end (idem), or by a simple name, like "AM" or "PM". - self.start = start - self.end = end - self.name = name or id - # The event types (among all event types defined at the Calendar level) - # that can be assigned to this slot. - self.eventTypes = eventTypes # "None" means "all" - # "day part" is the part of the day (from 0 to 1.0) that is taken by - # the timeslot. - self.dayPart = 1.0 - - def allows(self, eventType): - '''It is allowed to have an event of p_eventType in this timeslot?''' - # self.eventTypes being None means that no restriction applies - if not self.eventTypes: return True - return eventType in self.eventTypes - -# ------------------------------------------------------------------------------ -class Validation: - '''The validation process for a calendar consists in "converting" some event - types being "wishes" to other event types being the corresponding - validated events. This class holds information about this validation - process. For more information, see the Calendar constructor, parameter - "validation".''' - def __init__(self, method, schema, removeDiscarded=False): - # p_method holds a method that must return True if the currently logged - # user can validate whish events. - self.method = method - # p_schema must hold a dict whose keys are the event types being wishes - # and whose values are the event types being the corresponding validated - # event types. - self.schema = schema - # When discarding events, mmust we simply let them there or remove it? - self.removeDiscarded = removeDiscarded - -# ------------------------------------------------------------------------------ -class Other: - '''Identifies a Calendar field that must be shown within another Calendar - (see parameter "others" in class Calendar).''' - def __init__(self, obj, name, color='grey'): - # The object on which this calendar is defined - self.obj = obj - # The other calendar instance - self.field = obj.getField(name) - # The color into which events from this calendar must be shown (in the - # month rendering) in the calendar integrating this one. - self.color = color - - def getEventsInfoAt(self, res, calendar, date, eventNames, inTimeline, - colors): - '''Gets the events defined at p_date in this calendar and append them in - p_res.''' - events = self.field.getEventsAt(self.obj.o, date) - if not events: return - for event in events: - eventType = event.eventType - # Gathered info will be an Object instance - info = Object(event=event, color=self.color) - if inTimeline: - # Get the background color for this cell if it has been defined, - # or (a) nothing if showUncolored is False, (b) a tooltipped dot - # else. - if eventType in colors: - info.bgColor = colors[eventType] - info.symbol = None - else: - info.bgColor = None - if calendar.showUncolored: - info.symbol = 'â–ª' % \ - eventNames[eventType] - else: - info.symbol = None - else: - # Get the event name - info.name = eventNames[eventType] - res.append(info) - - def mayValidate(self): - '''Is validation enabled for this other calendar?''' - return self.field.mayValidate(self.obj) - -# ------------------------------------------------------------------------------ -class Total: - '''Represents a computation that will be executed on a series of cells - within a timeline calendar.''' - def __init__(self, initValue): self.value = initValue - def __repr__(self): return '' % str(self.value) - -class Totals: - '''For a timeline calendar, if you want to add rows or columns representing - totals computed from other rows/columns (representing agendas), specify - it via Totals instances (see Agenda fields "totalRows" and "totalCols" - below).''' - def __init__(self, name, label, onCell, initValue=0): - # "name" must hold a short name or acronym and will directly appear - # at the beginning of the row. It must be unique within all Totals - # instances defined for a given Calendar field. - self.name = name - # "label" is a i18n label that will be used to produce a longer name - # that will be shown as an acronym tag around the name. - self.label = label - # A method that will be called every time a cell is walked in the - # agenda. It will get these args: - # * date - the date representing the current day (a DateTime - # instance); - # * other - the Other instance representing the currently walked - # calendar; - # * events - the list of events (as Event instances) defined at - # that day in this calendar. Be careful: this can be - # None; - # * total - the Total instance (see above) corresponding to the - # current column; - # * last - a boolean that is True if we are walking the last - # shown calendar; - # * checked - a value "checked" indicating the status of the - # possible validation checkbox corresponding to this - # cell. If there is a checkbox in this cell, the value - # will be True or False; else, the value will be None. - # * preComputed - the result of Calendar.preCompute (see below) - self.onCell = onCell - # "initValue" is the initial value given to created Total instances - self.initValue = initValue - -# ------------------------------------------------------------------------------ -class Layer: - '''A layer is a set of additional data that can be activated or not on top - of calendar data. Currently available for timelines only.''' - def __init__(self, name, label, onCell, activeByDefault=False, legend=None): - # "name" must hold a short name or acronym, unique among all layers - self.name = name - # "label" is a i18n label that will be used to produce the layer name in - # the user interface. - self.label = label - # "onCell" must be a method that will be called for every calendar cell - # and must return a 3-tuple (style, title, content). "style" will be - # dumped in the "style" attribute of the current calendar cell, "title" - # in its "title" attribute, while "content" will be shown within the - # cell. If nothing must be shown at all, None must be returned. - # This method must accept those args: - # * date - the currently walked day (a DateTime instance); - # * other - the Other instance representing the currently walked - # calendar; - # * events - the list of events (as a list of custom Object - # instances whose attribute "event" points to an Event - # instance) defined at that day in this calendar. - # * preComputed - the result of Calendar.preCompute (see below) - self.onCell = onCell - # Is this layer activated by default ? - self.activeByDefault = activeByDefault - # "legend" is a method that must produce legend items that are specific - # to this layer. The method must accept no arg and must return a list of - # objects (you can use class appy.Object) having these attributes: - # * name - the legend item name as shown in the calendar - # * style - the content of the "style" attribute that will be - # applied to the little square ("td" tag) for this item; - # * content - the content of this "td" (if any). - self.legend = legend - # Layers will be chained: one layer will access the previous one in the - # stack via attribute "previous". "previous" fields will automatically - # be filled by the Calendar. - self.previous = None - - def getCellInfo(self, obj, activeLayers, date, other, events, preComputed): - '''Get the cell info from this layer or one previous layer when - relevant.''' - # Take this layer into account only if active - if self.name in activeLayers: - info = self.onCell(obj, date, other, events, preComputed) - if info: return info - # Get info from the previous layer - if self.previous: - return self.previous.getCellInfo(obj, activeLayers, date, other, - events, preComputed) - - def getLegendItems(self, obj): - '''Returns the legend items by calling method in self.legend''' - if not self.legend: return - return self.legend(obj) - -# ------------------------------------------------------------------------------ -class Event(Persistent): - '''An event as will be stored in the database''' - def __init__(self, eventType, timeslot='main'): - self.eventType = eventType - self.timeslot = timeslot - - def getName(self, allEventNames=None, xhtml=True): - '''Gets the name for this event, that depends on it type and may include - the timeslot if not "main".''' - # If we have the translated names for event types, use it. - if allEventNames: - res = allEventNames[self.eventType] - else: - res = self.eventType - if self.timeslot != 'main': - # Prefix it with the timeslot - prefix = xhtml and ('[%s] ' % self.timeslot) or \ - ('[%s] ' % self.timeslot) - res = '%s%s' % (prefix, res) - return res - - def sameAs(self, other): - '''Is p_self the same as p_other?''' - return (self.eventType == other.eventType) and \ - (self.timeslot == other.timeslot) - - def getDayPart(self, field): - '''What is the day part taken by this event ?''' - return field.getTimeslot(self.timeslot).dayPart - -# ------------------------------------------------------------------------------ -class Calendar(Field): - '''This field allows to produce an agenda (monthly view) and view/edit - events on it.''' - jsFiles = {'view': ('calendar.js',)} - DateTime = DateTime - # Access to Calendar utility classes via the Calendar class - Timeslot = Timeslot - Validation = Validation - Other = Other - Totals = Totals - Layer = Layer - Event = Event - IterSub = sutils.IterSub - # Error messages - TIMELINE_WITH_EVENTS = 'A timeline calendar has the objective to display ' \ - 'a series of other calendars. Its own calendar is disabled: it is ' \ - 'useless to define event types for it.' - MISSING_EVENT_NAME_METHOD = "When param 'eventTypes' is a method, you " \ - "must give another method in param 'eventNameMethod'." - TIMESLOT_USED = 'An event is already defined at this timeslot.' - DAY_FULL = 'No more place for adding this event.' - TOTALS_MISUSED = 'Totals can only be specified for timelines ' \ - '(render == "timeline").' - - timelineBgColors = {'Fri': '#dedede', 'Sat': '#c0c0c0', 'Sun': '#c0c0c0'} - validCbStatuses = {'validated': True, 'discarded': False} - - # For timeline rendering, the row displaying month names - pxTimeLineMonths = Px(''' - - - ::mInfo.month - - ''') - - # For timeline rendering, the row displaying day letters - pxTimelineDayLetters = Px(''' - - - :namesOfDays[date.aDay()].name[0] - - ''') - - # For timeline rendering, the row displaying day numbers - pxTimelineDayNumbers = Px(''' - - - :str(date.day()).zfill(2) - - ''') - - # Legend for a timeline calendar - pxTimelineLegend = Px(''' - - - - - - - - -
- - -
:item.content or ' '
:item.name
''') - - # Displays the total rows at the bottom of a timeline calendar - pxTotalRows = Px(''' - - - - - :row.name - ::totals[row.name][loop.date.nb].value - - :row.name - - ''') - - # Displays the total columns besides the calendar, as a separate table - pxTotalCols = Px(''' - - - - - - - - - - - - - - - ::field.getOthersSep(\ - len(field.totalCols)) - - - - - - - - - - - -
- :col.name -
::totals[col.name][i].value
 
- :col.name -
''') - - # Ajax-call pxTotalRows or pxTotalCols - pxTotalsFromAjax = Px(''' - :getattr(field, 'pxTotal%s' % totalType)''') - - # Timeline view for a calendar - pxViewTimeline = Px(''' - - - - - - - - - :field.pxTimeLineMonths - :field.pxTimelineDayLetters:field.pxTimelineDayNumbers - - - - - - - - ::field.getTimelineCell(req, obj) - - - - - ::field.getOthersSep(len(grid)+2) - - - - :field.pxTotalRows - - :field.pxTimelineDayNumbers:field.pxTimelineDayLetters - :field.pxTimeLineMonths - -
::tlName::tlName
- - :field.pxTotalCols - :field.pxTimelineLegend''') - - # Popup for adding an event in the month view - pxAddPopup = Px(''' - ''') - - # Popup for removing events in the month view - pxDelPopup = Px(''' - ''') - - # Month view for a calendar - pxViewMonth = Px(''' - - - - - - - - - - - - - - -
:namesOfDays[dayId].short
- :day - :_('month_%s_short' % date.aMonth()) - - - - - - - - -
- - - ::event.getName(allEventNames) - - -
-
- - -
:event.name
-
- - ::info -
- - - - :field.pxAddPopup:field.pxDelPopup''') - - pxView = pxCell = Px(''' -
- - - - -
- - - - - - - :_('month_%s' % monthDayOne.aMonth()) - :month.split('/')[0] - - - - - - - - - - - -
- - ::field.topPx - - :getattr(field, 'pxView%s' % render.capitalize()) - - ::field.bottomPx -
''') - - pxEdit = pxSearch = '' - - def __init__(self, eventTypes=None, eventNameMethod=None, validator=None, - default=None, show=('view', 'xml'), page='main', group=None, - layouts=None, move=0, specificReadPermission=False, - specificWritePermission=False, width=None, height=300, - colspan=1, master=None, masterValue=None, focus=False, - mapping=None, label=None, maxEventLength=50, render='month', - others=None, timelineName=None, additionalInfo=None, - startDate=None, endDate=None, defaultDate=None, timeslots=None, - colors=None, showUncolored=False, columnColors=None, - preCompute=None, applicableEvents=None, totalRows=None, - totalCols=None, validation=None, layers=None, topPx=None, - bottomPx=None, view=None, xml=None, delete=True): - # The "validator" attribute, allowing field-specific validation, behaves - # differently for the Calendar field. If specified, it must hold a - # method that will be executed every time a user wants to create an - # event (or series of events) in the calendar. This method must accept - # those args: - # - date the date of the event (as a DateTime instance); - # - eventType the event type (one among p_eventTypes); - # - timeslot the timeslot for the event (see param "timeslots" - # below); - # - span the number of additional days on wich the event will - # span (will be 0 if the user wants to create an event - # for a single day). - # If validation succeeds (ie, the event creation can take place), the - # method must return True (boolean). Else, it will be canceled and an - # error message will be shown. If the method returns False (boolean), it - # will be a standard error message. If the method returns a string, it - # will be used as specific error message. - Field.__init__(self, validator, (0,1), default, show, page, group, - layouts, move, False, True, False, specificReadPermission, - specificWritePermission, width, height, None, colspan, - master, masterValue, focus, False, mapping, label, None, - None, None, None, True, view, xml) - # eventTypes can be a "static" list or tuple of strings that identify - # the types of events that are supported by this calendar. It can also - # be a method that computes such a "dynamic" list or tuple. When - # specifying a static list, an i18n label will be generated for every - # event type of the list. When specifying a dynamic list, you must also - # give, in p_eventNameMethod, a method that will accept a single arg - # (=one of the event types from your dynamic list) and return the "name" - # of this event as it must be shown to the user. - self.eventTypes = eventTypes - if (render == 'timeline') and eventTypes: - raise Exception(Calendar.TIMELINE_WITH_EVENTS) - self.eventNameMethod = eventNameMethod - if callable(eventTypes) and not eventNameMethod: - raise Exception(Calendar.MISSING_EVENT_NAME_METHOD) - # It is not possible to create events that span more days than - # maxEventLength. - self.maxEventLength = maxEventLength - # Various render modes exist. Default is the classical "month" view. - # It can also be "timeline": in this case, on the x axis, we have one - # column per day, and on the y axis, we have one row per calendar (this - # one and others as specified in "others", see below). - self.render = render - # When displaying a given month for this agenda, one may want to - # pre-compute, once for the whole month, some information that will then - # be given as arg for other methods specified in subsequent parameters. - # This mechanism exists for performance reasons, to avoid recomputing - # this global information several times. If you specify a method in - # p_preCompute, it will be called every time a given month is shown, and - # will receive 2 args: the first day of the currently shown month (as a - # DateTime instance) and the grid of all shown dates (as a result of - # calling m_getGrid below). This grid may hold a little more than dates - # of the current month. Subsequently, the return of your method will be - # given as arg to other methods that you may specify as args of other - # parameters of this Calendar class (see comments below). - self.preCompute = preCompute - # If a method is specified in parameter "others" below, it must accept a - # single arg (the result of self.preCompute) and must return a list of - # calendars whose events must be shown within this agenda. More - # precisely, the method can return: - # - a single Other instance (see at the top of this file); - # - a list of Other instances; - # - a list of lists of Other instances, when it has sense to group other - # calendars (the timeline rendering exploits this). - self.others = others - # When displaying a timeline calendar, a name is shown for every other - # calendar. If "timelineName" is None (the default), this name will be - # the title of the object where the other calendar is defined. Else, it - # will be the result of the method specified in "timelineName". This - # method must return a string and accepts an Other instance as single - # arg. - self.timelineName = timelineName - # One may want to add, day by day, custom information in the calendar. - # When a method is given in p_additionalInfo, for every cell of the - # month view, this method will be called with 2 args: the cell's date - # and the result of self.preCompute. The method's result (a string that - # can hold text or a chunk of XHTML) will be inserted in the cell. - self.additionalInfo = additionalInfo - # One may limit event encoding and viewing to some period of time, - # via p_startDate and p_endDate. Those parameters, if given, must hold - # methods accepting no arg and returning a Zope DateTime instance. The - # startDate and endDate will be converted to UTC at 00.00. - self.startDate = startDate - self.endDate = endDate - # If a default date is specified, it must be a method accepting no arg - # and returning a DateTime instance. As soon as the calendar is shown, - # the month where this date is included will be shown. If not default - # date is specified, it will be 'now' at the moment the calendar is - # shown. - self.defaultDate = defaultDate - # "timeslots" are a way to define, within a single day, time ranges. It - # must be a list of Timeslot instances (see above). If you define - # timeslots, the first one must be the one representing the whole day - # and must have id "main". - if not timeslots: self.timeslots = [Timeslot('main')] - else: - self.timeslots = timeslots - self.checkTimeslots() - # "colors" must be or return a dict ~{s_eventType: s_color}~ giving a - # color to every event type defined in this calendar or in any calendar - # from "others". In a timeline, cells are too small to display - # translated names for event types, so colors are used instead. - self.colors = colors or {} - # For event types that are not present in self.colors hereabove, must we - # still show them? If yes, they will be represented by a dot with a - # tooltip containing the event name. - self.showUncolored = showUncolored - # In the timeline, the background color for columns can be defined in a - # method you specify here. This method must accept the current date (as - # a DateTime instance) as unique arg. If None, a default color scheme - # is used (see Calendar.timelineBgColors). Every time your method - # returns None, the default color scheme will apply. - self.columnColors = columnColors - # For a specific day, all event types may not be applicable. If this is - # the case, one may specify here a method that defines, for a given day, - # a sub-set of all event types. This method must accept 3 args: the day - # in question (as a DateTime instance), the list of all event types, - # which is a copy of the (possibly computed) self.eventTypes) and - # the result of calling self.preCompute. The method must modify - # the 2nd arg and remove from it potentially not applicable events. - # This method can also return a message, that will be shown to the user - # for explaining him why he can, for this day, only create events of a - # sub-set of the possible event types (or even no event at all). - self.applicableEvents = applicableEvents - # In a timeline calendar, if you want to specify additional rows - # representing totals, give in "totalRows" a list of TotalRow instances - # (see above). - if totalRows and (self.render != 'timeline'): - raise Exception(Calendar.TOTALS_MISUSED) - self.totalRows = totalRows or [] - # Similarly, you can specify additional columns in "totalCols" - if totalCols and (self.render != 'timeline'): - raise Exception(Calendar.TOTALS_MISUSED) - self.totalCols = totalCols or [] - # A validation process can be associated to a Calendar event. It - # consists in identifying validators and letting them "convert" event - # types being wished to final, validated event types. If you want to - # enable this, define a Validation instance (see the hereabove class) - # in parameter "validation". - self.validation = validation - # "layers" define a stack of layers (as a list or tuple). Every layer - # must be a Layer instance and represents a set of data that can be - # shown or not on top of calendar data (currently, only for timelines). - self.layers = self.formatLayers(layers) - # May the user delete events in this calendar? If "delete" is a method, - # it must accept an event type as single arg. - self.delete = delete - # You may specify PXs that will show specific information, respectively, - # before and after the calendar. - self.topPx = topPx - self.bottomPx = bottomPx - - def checkTimeslots(self): - '''Checks whether self.timeslots defines corect timeslots''' - # The first timeslot must be the global one, named 'main' - if self.timeslots[0].id != 'main': - raise Exception('The first timeslot must have id "main" and is ' \ - 'the one representing the whole day.') - # Set the day parts for every timeslot - count = len(self.timeslots) - 1 # Count the timeslots, main excepted - for timeslot in self.timeslots: - if timeslot.id == 'main': continue - timeslot.dayPart = 1.0 / count - - def formatLayers(self, layers): - '''Chain layers via attribute "previous"''' - if not layers: return () - i = len(layers) - 1 - while i >= 1: - layers[i].previous = layers[i-1] - i -= 1 - return layers - - def log(self, obj, msg, date=None): - '''Logs m_msg, field-specifically prefixed.''' - prefix = '%s:%s' % (obj.id, self.name) - if date: prefix += '@%s' % date.strftime('%Y/%m/%d') - obj.log('%s: %s' % (prefix, msg)) - - def getPreComputedInfo(self, obj, monthDayOne, grid): - '''Returns the result of calling self.preComputed, or None if no such - method exists.''' - if self.preCompute: - return self.preCompute(obj.appy(), monthDayOne, grid) - - def getSiblingMonth(self, month, prevNext): - '''Gets the next or previous month (depending of p_prevNext) relative - to p_month.''' - dayOne = DateTime('%s/01 UTC' % month) - if prevNext == 'previous': - refDate = dayOne - 1 - elif prevNext == 'next': - refDate = dayOne + 33 - return refDate.strftime('%Y/%m') - - weekDays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') - def getNamesOfDays(self, _): - '''Returns the translated names of all week days, short and long - versions.''' - res = {} - for day in self.weekDays: - name = _('day_%s' % day) - short = _('day_%s_short' % day) - res[day] = Object(name=name, short=short) - return res - - def getGrid(self, month, render): - '''Creates a list of DateTime objects representing the calendar grid to - render for a given p_month. If p_render is "month", it is a list of - lists (one sub-list for every week; indeed, every week is rendered as - a row). If p_render is "timeline", the result is a linear list of - DateTime instances.''' - # Month is a string "YYYY/mm" - currentDay = DateTime('%s/01 UTC' % month) - currentMonth = currentDay.month() - isLinear = render == 'timeline' - if isLinear: res = [] - else: res = [[]] - dayOneNb = currentDay.dow() or 7 # This way, Sunday is 7 and not 0 - if dayOneNb != 1: - # If I write "previousDate = DateTime(currentDay)", the date is - # converted from UTC to GMT - previousDate = DateTime('%s/01 UTC' % month) - # If the 1st day of the month is not a Monday, integrate the last - # days of the previous month. - for i in range(1, dayOneNb): - previousDate -= 1 - if isLinear: - target = res - else: - target = res[0] - target.insert(0, previousDate) - finished = False - while not finished: - # Insert currentDay in the result - if isLinear: - res.append(currentDay) - else: - if len(res[-1]) == 7: - # Create a new row - res.append([currentDay]) - else: - res[-1].append(currentDay) - currentDay += 1 - if currentDay.month() != currentMonth: - finished = True - # Complete, if needed, the last row with the first days of the next - # month. Indeed, we must have a complete week, ending with a Sunday. - if isLinear: target = res - else: target = res[-1] - while target[-1].dow() != 0: - target.append(currentDay) - currentDay += 1 - return res - - def getOthers(self, obj, preComputed): - '''Returns the list of other calendars whose events must also be shown - on this calendar.''' - res = None - if self.others: - res = self.others(obj.appy(), preComputed) - if res: - # Ensure we have a list of lists - if isinstance(res, Other): res = [res] - if isinstance(res[0], Other): res = [res] - if res != None: return res - return [[]] - - def getOthersSep(self, colspan): - '''Produces the separator between groups of other calendars''' - return '' % colspan - - def getTimelineName(self, other): - '''Returns the name of some p_other calendar as must be shown in a - timeline.''' - if not self.timelineName: - return '%s' % (other.obj.url, other.obj.title) - return self.timelineName(self, other) - - def getTimelineCell(self, req, obj): - '''Gets the content of a cell in a timeline calendar''' - # Unwrap some variables from the PX context - c = req.pxContext - date = c['date']; other = c['other']; render = 'timeline' - allEventNames = c['allEventNames']; activeLayers = c['activeLayers'] - # Get the events defined at that day, in the current calendar - events = self.getOtherEventsAt(date, other, allEventNames, render, - c['colors']) - # In priority we will display info from a layer - if activeLayers: - # Walk layers in reverse order - layer = self.layers[-1] - info = layer.getCellInfo(obj, activeLayers, date, other, events, - c['preComputed']) - if info: - style, title, content = info - style = style and (' style="%s"' % style) or '' - title = title and (' title="%s"' % title) or '' - content = content or '' - return '%s' % (style, title, content) - # Define the cell's style - style = self.getCellStyle(obj, date, render, events) or '' - if style: style = ' style="%s"' % style - # If a timeline cell hides more than one event, put event names in the - # "title" attribute. - title = '' - if len(events) > 1: - title = ', '.join(['%s (%s)' % (allEventNames[e.event.eventType], \ - e.event.timeslot) for e in events]) - title = ' title="%s"' % title - # Define its content - content = '' - if events and c['mayValidate']: - # If at least one event from p_events is in the validation schema, - # propose a unique checkbox, that will allow to validate or not all - # validable events at p_date. - for info in events: - if info.event.eventType in other.field.validation.schema: - cbId = '%s_%s_%s' % (other.obj.id, other.field.name, - date.strftime('%Y%m%d')) - totalRows = self.totalRows and 'true' or 'false' - totalCols = self.totalCols and 'true' or 'false' - content = '' % \ - (cbId, c['ajaxHookId'], totalRows, totalCols) - break - elif len(events) == 1: - # A single event: if not colored, show a symbol. When there are - # multiple events, a background image is already shown (see the - # "style" attribute), so do not show any additional info. - content = events[0].symbol or '' - return '%s' % (style, title, content) - - def getLegendItems(self, obj, allEventTypes, allEventNames, colors, url, _, - activeLayers): - '''Gets information needed to produce the legend for a timeline''' - # Produce one legend item by event type shown and colored - res = [] - for eventType in allEventTypes: - if eventType not in colors: continue - res.append(Object(name=allEventNames[eventType], content='', - style='background-color: %s' % colors[eventType])) - # Add the background indicating that several events are hidden behind - # the timeline cell - res.append(Object(name=_('several_events'), content='', - style=url('angled', bg=True))) - # Add layer-specific items - for layer in self.layers: - if layer.name not in activeLayers: continue - items = layer.getLegendItems(obj) - if items: res += items - return res - - def getTimelineMonths(self, grid, obj): - '''Given the p_grid of dates, this method returns the list of - corresponding months.''' - res = [] - for date in grid: - if not res: - # Get the month correspoding to the first day in the grid - m = Object(month=date.aMonth(), colspan=1, year=date.year()) - res.append(m) - else: - # Augment current month' colspan or create a new one - current = res[-1] - if date.aMonth() == current.month: - current.colspan += 1 - else: - m = Object(month=date.aMonth(), colspan=1, year=date.year()) - res.append(m) - # Replace month short names by translated names whose format may vary - # according to colspan (a higher colspan allow us to produce a longer - # month name). - for m in res: - text = '%s %d' % (obj.translate('month_%s' % m.month), m.year) - if m.colspan < 6: - # Short version: a single letter with an acronym - m.month = '%s' % (text, text[0]) - else: - m.month = text - return res - - def getAdditionalInfoAt(self, obj, date, preComputed): - '''If the user has specified a method in self.additionalInfo, we call - it for displaying this additional info in the calendar, at some - p_date.''' - if not self.additionalInfo: return - return self.additionalInfo(obj.appy(), date, preComputed) - - def getEventTypes(self, obj): - '''Returns the (dynamic or static) event types as defined in - self.eventTypes.''' - if callable(self.eventTypes): return self.eventTypes(obj.appy()) - return self.eventTypes - - def getColors(self, obj): - '''Gets the colors for event types managed by this calendar and others - (from self.colors).''' - if callable(self.colors): return self.colors(obj) - return self.colors - - def dayIsFull(self, date, events): - '''In the calendar full at p_date? Defined events at this p_date are in - p_events. We check here if the main timeslot is used or if all - others are used.''' - if not events: return - for e in events: - if e.timeslot == 'main': return True - return len(events) == len(self.timeslots)-1 - - def dateInRange(self, date, startDate, endDate): - '''Is p_date within the range (possibly) defined for this calendar by - p_startDate and p_endDate ?''' - tooEarly = startDate and (date < startDate) - tooLate = endDate and not tooEarly and (date > endDate) - return not tooEarly and not tooLate - - def getApplicableEventTypesAt(self, obj, date, eventTypes, preComputed, - forBrowser=False): - '''Returns the event types that are applicable at a given p_date. More - precisely, it returns an object with 2 attributes: - * "events" is the list of applicable event types; - * "message", not empty if some event types are not applicable, - contains a message explaining those event types are - not applicable. - ''' - if not eventTypes: return # There may be no event type at all - if not self.applicableEvents: - # Keep p_eventTypes as is - message = None - else: - eventTypes = eventTypes[:] - message = self.applicableEvents(obj.appy(), date, eventTypes, - preComputed) - res = Object(eventTypes=eventTypes, message=message) - if forBrowser: - res.eventTypes = ','.join(res.eventTypes) - if not res.message: res.message = '' - return res - - def getFreeSlotsAt(self, date, events, slotIds, slotIdsStr, - forBrowser=False): - '''Gets the free timeslots in this calendar for some p_date. As a - precondition, we know that the day is not full (so timeslot "main" - cannot be taken). p_events are those already defined at p_date. - p_slotIds is the precomputed list of timeslot ids.''' - if not events: return forBrowser and slotIdsStr or slotIds - # Remove any taken slot - res = slotIds[1:] # "main" cannot be chosen: p_events is not empty - for event in events: res.remove(event.timeslot) - # Return the result - if not forBrowser: return res - return ','.join(res) - - def getTimeslot(self, id): - '''Get the timeslot corresponding to p_id''' - for slot in self.timeslots: - if slot.id == id: return slot - - def getEventsAt(self, obj, date): - '''Returns the list of events that exist at some p_date (=day). p_date - can be: - * a DateTime instance; - * a tuple (i_year, i_month, i_day); - * a string YYYYmmdd. - ''' - obj = obj.o # Ensure p_obj is not a wrapper - if not hasattr(obj.aq_base, self.name): return - years = getattr(obj, self.name) - # Get year, month and name from p_date - if isinstance(date, tuple): - year, month, day = date - elif isinstance(date, str): - year, month, day = int(date[:4]), int(date[4:6]), int(date[6:8]) - else: - year, month, day = date.year(), date.month(), date.day() - # Dig into the oobtree - if year not in years: return - months = years[year] - if month not in months: return - days = months[month] - if day not in days: return - return days[day] - - def getEventTypeAt(self, obj, date): - '''Returns the event type of the first event defined at p_day, or None - if unspecified.''' - events = self.getEventsAt(obj, date) - if not events: return - return events[0].eventType - - def standardizeDateRange(self, range): - '''p_range can have various formats (see m_walkEvents below). This - method standardizes the date range as a 6-tuple - (startYear, startMonth, startDay, endYear, endMonth, endDay).''' - if not range: return - if isinstance(range, int): - # p_range represents a year - return (range, 1, 1, range, 12, 31) - elif isinstance(range[0], int): - # p_range represents a month - year, month = range - return (year, month, 1, year, month, 31) - else: - # p_range is a tuple (start, end) of DateTime instances - start, end = range - return (start.year(), start.month(), start.day(), - end.year(), end.month(), end.day()) - - def walkEvents(self, obj, callback, dateRange=None): - '''Walks on p_obj, the calendar value in chronological order for this - field and calls p_callback for every day containing events. The - callback must accept 3 args: p_obj, the current day (as a DateTime - instance) and the list of events at that day (the database-stored - PersistentList instance). If the callback returns True we stop the - walk. - - If p_dateRange is specified, it limits the walk to this range. It - can be: - * an integer, representing a year; - * a tuple of integers (year, month) representing a given month - (first month is numbered 1); - * a tuple (start, end) of DateTime instances. - ''' - obj = obj.o - if not hasattr(obj, self.name): return - yearsDict = getattr(obj, self.name) - if not yearsDict: return - # Standardize date range - if dateRange: - startYear, startMonth, startDay, endYear, endMonth, endDay = \ - self.standardizeDateRange(dateRange) - # Browse years - years = list(yearsDict.keys()) - years.sort() - for year in years: - # Ignore this year if out of range - if dateRange: - if (year < startYear) or (year > endYear): continue - isStartYear = year == startYear - isEndYear = year == endYear - # Browse this year's months - monthsDict = yearsDict[year] - if not monthsDict: continue - months = list(monthsDict.keys()) - months.sort() - for month in months: - # Ignore this month if out of range - if dateRange: - if (isStartYear and (month < startMonth)) or \ - (isEndYear and (month > endMonth)): continue - isStartMonth = isStartYear and (month == startMonth) - isEndMonth = isEndYear and (month == endMonth) - # Browse this month's days - daysDict = monthsDict[month] - if not daysDict: continue - days = list(daysDict.keys()) - days.sort() - for day in days: - # Ignore this day if out of range - if dateRange: - if (isStartMonth and (day < startDay)) or \ - (isEndMonth and (day > endDay)): continue - date = DateTime('%d/%d/%d UTC' % (year, month, day)) - stop = callback(obj, date, daysDict[day]) - if stop: return - - def getEventsByType(self, obj, eventType, minDate=None, maxDate=None, - sorted=True, groupSpanned=False): - '''Returns all the events of a given p_eventType. If p_eventType is - None, it returns events of all types. p_eventType can also be a - list or tuple. The return value is a list of 2-tuples whose 1st elem - is a DateTime instance and whose 2nd elem is the event. - - If p_sorted is True, the list is sorted in chronological order. Else, - the order is random, but the result is computed faster. - - If p_minDate and/or p_maxDate is/are specified, it restricts the - search interval accordingly. - - If p_groupSpanned is True, events spanned on several days are - grouped into a single event. In this case, tuples in the result - are 3-tuples: (DateTime_startDate, DateTime_endDate, event). - ''' - # Prevent wrong combinations of parameters - if groupSpanned and not sorted: - raise Exception('Events must be sorted if you want to get ' \ - 'spanned events to be grouped.') - obj = obj.o # Ensure p_obj is not a wrapper - res = [] - if not hasattr(obj, self.name): return res - # Compute "min" and "max" tuples - if minDate: - minYear = minDate.year() - minMonth = (minYear, minDate.month()) - minDay = (minYear, minDate.month(), minDate.day()) - if maxDate: - maxYear = maxDate.year() - maxMonth = (maxYear, maxDate.month()) - maxDay = (maxYear, maxDate.month(), maxDate.day()) - # Browse years - years = getattr(obj, self.name) - for year in list(years.keys()): - # Don't take this year into account if outside interval - if minDate and (year < minYear): continue - if maxDate and (year > maxYear): continue - months = years[year] - # Browse this year's months - for month in list(months.keys()): - # Don't take this month into account if outside interval - thisMonth = (year, month) - if minDate and (thisMonth < minMonth): continue - if maxDate and (thisMonth > maxMonth): continue - days = months[month] - # Browse this month's days - for day in list(days.keys()): - # Don't take this day into account if outside interval - thisDay = (year, month, day) - if minDate and (thisDay < minDay): continue - if maxDate and (thisDay > maxDay): continue - events = days[day] - # Browse this day's events - for event in events: - # Filter unwanted events - if eventType: - if isinstance(eventType, str): - keepIt = (event.eventType == eventType) - else: - keepIt = (event.eventType in eventType) - if not keepIt: continue - # We have found a event - date = DateTime('%d/%d/%d UTC' % (year, month, day)) - if groupSpanned: - singleRes = [date, None, event] - else: - singleRes = (date, event) - res.append(singleRes) - # Sort the result if required - if sorted: res.sort(key=lambda x: x[0]) - # Group events spanned on several days if required - if groupSpanned: - # Browse events in reverse order and merge them when appropriate - i = len(res) - 1 - while i > 0: - currentDate = res[i][0] - lastDate = res[i][1] - previousDate = res[i-1][0] - currentType = res[i][2].eventType - previousType = res[i-1][2].eventType - if (previousDate == (currentDate-1)) and \ - (previousType == currentType): - # A merge is needed - del res[i] - res[i-1][1] = lastDate or currentDate - i -= 1 - return res - - def hasEventsAt(self, obj, date, events): - '''Returns True if, at p_date, events are exactly of the same type as - p_events.''' - if not events: return - others = self.getEventsAt(obj, date) - if not others: return - if len(events) != len(others): return - i = 0 - while i < len(events): - if not events[i].sameAs(others[i]): return - i += 1 - return True - - def getOtherEventsAt(self, date, others, eventNames, render, colors): - '''Gets events that are defined in p_others at some p_date. If p_single - is True, p_others does not contain the list of all other calendars, - but information about a single calendar.''' - res = [] - isTimeline = render == 'timeline' - if isinstance(others, Other): - others.getEventsInfoAt(res, self, date, eventNames, isTimeline, - colors) - else: - for other in sutils.IterSub(others): - other.getEventsInfoAt(res, self, date, eventNames, isTimeline, - colors) - return res - - def getEventName(self, obj, eventType): - '''Gets the name of the event corresponding to p_eventType as it must - appear to the user.''' - if self.eventNameMethod: - return self.eventNameMethod(obj.appy(), eventType) - else: - return obj.translate('%s_event_%s' % (self.labelId, eventType)) - - def getAllEvents(self, obj, eventTypes, others): - '''Computes: - * the list of all event types (from this calendar and p_others); - * a dict of event names, keyed by event types, for all events - in this calendar and p_others).''' - res = [[], {}] - if eventTypes: - for et in eventTypes: - res[0].append(et) - res[1][et] = self.getEventName(obj, et) - if not others: return res - for other in sutils.IterSub(others): - eventTypes = other.field.getEventTypes(other.obj) - if eventTypes: - for et in eventTypes: - if et not in res[1]: - res[0].append(et) - res[1][et] = other.field.getEventName(other.obj, et) - return res - - def getStartDate(self, obj): - '''Get the start date for this calendar if defined''' - if self.startDate: - d = self.startDate(obj.appy()) - # Return the start date without hour, in UTC - return DateTime('%d/%d/%d UTC' % (d.year(), d.month(), d.day())) - - def getEndDate(self, obj): - '''Get the end date for this calendar if defined''' - if self.endDate: - d = self.endDate(obj.appy()) - # Return the end date without hour, in UTC - return DateTime('%d/%d/%d UTC' % (d.year(), d.month(), d.day())) - - def getDefaultDate(self, obj): - '''Get the default date that must appear as soon as the calendar is - shown.''' - if self.defaultDate: - return self.defaultDate(obj.appy()) - else: - return DateTime() # Now - - def checkCreateEvent(self, obj, eventType, timeslot, events): - '''Checks if one may create an event of p_eventType in p_timeslot. - Events already defined at p_date are in p_events. If the creation is - not possible, an error message is returned.''' - # The following errors should not occur if we have a normal user behind - # the ui. - for e in events: - if e.timeslot == timeslot: return Calendar.TIMESLOT_USED - elif e.timeslot == 'main': return Calendar.DAY_FULL - if events and (timeslot == 'main'): return Calendar.DAY_FULL - # Get the Timeslot and check if, at this timeslot, it is allowed to - # create an event of p_eventType. - for slot in self.timeslots: - if slot.id == timeslot: - # I have the timeslot - if not slot.allows(eventType): - _ = obj.translate - return _('timeslot_misfit', mapping={'slot': timeslot}) - - def mergeEvent(self, eventType, timeslot, events): - '''If, after adding an event of p_eventType, all timeslots are used with - events of the same type, we can merge them and create a single event - of this type in the main timeslot.''' - # When defining an event in the main timeslot, no merge is needed - if timeslot == 'main': return - # Merge is required when all non-main timeslots are used by events of - # the same type. - if len(events) != (len(self.timeslots)-2): return - for event in events: - if event.eventType != eventType: return - # If we are here, we must merge all events - del events[:] - events.append(Event(eventType)) - return True - - def createEvent(self, obj, date, eventType, timeslot='main', eventSpan=None, - handleEventSpan=True): - '''Create a new event in the calendar, at some p_date (day). If - p_handleEventSpan is True, we will use p_eventSpan and also create - the same event for successive days.''' - obj = obj.o # Ensure p_obj is not a wrapper - rq = obj.REQUEST - # Get values from parameters - if not eventType: eventType = rq['eventType'] - # Split the p_date into separate parts - year, month, day = date.year(), date.month(), date.day() - # Check that the "preferences" dict exists or not - if not hasattr(obj.aq_base, self.name): - # 1st level: create a IOBTree whose keys are years - setattr(obj, self.name, IOBTree()) - yearsDict = getattr(obj, self.name) - # Get the sub-dict storing months for a given year - if year in yearsDict: - monthsDict = yearsDict[year] - else: - yearsDict[year] = monthsDict = IOBTree() - # Get the sub-dict storing days of a given month - if month in monthsDict: - daysDict = monthsDict[month] - else: - monthsDict[month] = daysDict = IOBTree() - # Get the list of events for a given day - if day in daysDict: - events = daysDict[day] - else: - daysDict[day] = events = PersistentList() - # Return an error if the creation cannot occur - error = self.checkCreateEvent(obj, eventType, timeslot, events) - if error: return error - # Merge this event with others when relevant - merged = self.mergeEvent(eventType, timeslot, events) - if not merged: - # Create and store the event - events.append(Event(eventType, timeslot)) - # Sort events in the order of timeslots - if len(events) > 1: - timeslots = [slot.id for slot in self.timeslots] - events.data.sort(key=lambda e: timeslots.index(e.timeslot)) - events._p_changed = 1 - # Span the event on the successive days if required - suffix = '' - if handleEventSpan and eventSpan: - for i in range(eventSpan): - date = date + 1 - self.createEvent(obj, date, eventType, timeslot, - handleEventSpan=False) - suffix = ', span+%d' % eventSpan - if handleEventSpan: - msg = 'added %s, slot %s%s' % (eventType, timeslot, suffix) - self.log(obj, msg, date) - - def mayDelete(self, obj, events): - '''May the user delete p_events?''' - if not self.delete: return - if callable(self.delete): return self.delete(obj, events[0].eventType) - return True - - def deleteEvent(self, obj, date, timeslot, handleEventSpan=True): - '''Deletes an event. If t_timeslot is "main", it deletes all events at - p_date, be there a single event on the main timeslot or several - events on other timeslots. Else, it only deletes the event at - p_timeslot. If p_handleEventSpan is True, we will use - rq["deleteNext"] to delete successive events, too.''' - obj = obj.o # Ensure p_obj is not a wrapper - if not self.getEventsAt(obj, date): return - daysDict = getattr(obj, self.name)[date.year()][date.month()] - events = self.getEventsAt(obj, date) - count = len(events) - eNames = ', '.join([e.getName(xhtml=False) for e in events]) - if timeslot == 'main': - # Delete all events; delete them also in the following days when - # relevant. - del daysDict[date.day()] - rq = obj.REQUEST - suffix = '' - if handleEventSpan and rq.has_key('deleteNext') and \ - (rq['deleteNext'] == 'True'): - nbOfDays = 0 - while True: - date = date + 1 - if self.hasEventsAt(obj, date, events): - self.deleteEvent(obj, date, timeslot, - handleEventSpan=False) - nbOfDays += 1 - else: - break - if nbOfDays: suffix = ', span+%d' % nbOfDays - if handleEventSpan: - msg = '%s deleted (%d)%s.' % (eNames, count, suffix) - self.log(obj, msg, date) - else: - # Delete the event at p_timeslot - i = len(events) - 1 - while i >= 0: - if events[i].timeslot == timeslot: - msg = '%s deleted at slot %s.' % \ - (events[i].getName(xhtml=False), timeslot) - del events[i] - self.log(obj, msg, date) - break - i -= 1 - - def validate(self, obj, date, eventType, timeslot, span=0): - '''The validation process for a calendar is a bit different from the - standard one, that checks a "complete" request value. Here, we only - check the validity of some insertion of events within the - calendar.''' - if not self.validator: return - res = self.validator(obj, date, eventType, timeslot, span) - if isinstance(res, basestring): - # Validation failed, and we have the error message in "res" - return res - if not res: - # Validation failed, without specific message: return a standard one - return obj.translate('field_invalid') - return res - - def process(self, obj): - '''Processes an action coming from the calendar widget, ie, the creation - or deletion of a calendar event.''' - rq = obj.REQUEST - action = rq['actionType'] - # Security check - obj.mayEdit(self.writePermission, raiseError=True) - # Get the date and timeslot for this action - date = DateTime(rq['day']) - eventType = rq.get('eventType') - timeslot = rq.get('timeslot', 'main') - eventSpan = rq.get('eventSpan') or 0 - eventSpan = min(int(eventSpan), self.maxEventLength) - if action == 'createEvent': - # Trigger validation - valid = self.validate(obj.appy(), date, eventType, timeslot, - eventSpan) - if isinstance(valid, basestring): return valid - return self.createEvent(obj, date, eventType, timeslot, eventSpan) - elif action == 'deleteEvent': - return self.deleteEvent(obj, date, timeslot) - - def getColumnStyle(self, obj, date, render, today): - '''What style(s) must apply to the table column representing p_date - in the calendar? For timelines only.''' - if render != 'timeline': return '' - # Cells representing specific days must have a specific background color - res = '' - day = date.aDay() - # Do we have a custom color scheme where to get a color ? - color = None - if self.columnColors: - color = self.columnColors(obj.appy(), date) - if not color and (day in Calendar.timelineBgColors): - color = Calendar.timelineBgColors[day] - if color: res = 'background-color: %s' % color - return res - - def getCellStyle(self, obj, date, render, events): - '''Gets the cell style to apply to the cell corresponding to p_date''' - if render != 'timeline': return # Currently, for timelines only - if not events: return - elif len(events) > 1: - # Return a special background indicating that several events are - # hidden behing this cell. - return 'background-image: url(%s/ui/angled.png)' % \ - obj.o.getTool().getSiteUrl() - else: - event = events[0] - if event.bgColor: return 'background-color: %s' % event.bgColor - - def getCellClass(self, obj, date, render, today): - '''What CSS class(es) must apply to the table cell representing p_date - in the calendar?''' - if render != 'month': return '' # Currently, for month rendering only - res = [] - # We must distinguish between past and future dates - if date < today: - res.append('even') - else: - res.append('odd') - # Week-end days must have a specific style - if date.aDay() in ('Sat', 'Sun'): - res.append('cellDashed') - return ' '.join(res) - - def splitList(self, l, sub): return sutils.splitList(l, sub) - def mayValidate(self, obj): - '''May the currently logged user validate wish events ?''' - if not self.validation: return - return self.validation.method(obj.appy()) - - def getAjaxData(self, hook, zobj, **params): - '''Initializes an AjaxData object on the DOM node corresponding to - this calendar field.''' - params = sutils.getStringDict(params) - return "new AjaxData('%s', '%s:pxView', %s, null, '%s')" % \ - (hook, self.name, params, zobj.absolute_url()) - - def getAjaxDataTotals(self, type, hook): - '''Initializes an AjaxData object on the DOM node corresponding to - the zone containing the total rows/cols (depending on p_type) in a - timeline calendar.''' - suffix = (type == 'rows') and 'trs' or 'tcs' - return "new AjaxData('%s_%s', '%s:pxTotalsFromAjax', {}, '%s')" % \ - (hook, suffix, self.name, hook) - - def validateEvents(self, obj): - '''Validate or discard events from the request''' - rq = obj.REQUEST.form - counts = {'validated': 0, 'discarded': 0} - removeDiscarded = self.validation.removeDiscarded - tool = obj.getTool() - for action in ('validated', 'discarded'): - if not rq[action]: continue - for info in rq[action].split(','): - if rq['render'] == 'month': - # Every checkbox corresponds to an event at at given date, - # with a given event type at a given timeslot, in this - # calendar (self) on p_obj. - date, eventType, timeslot = info.split('_') - # Get the events defined at that date - events = self.getEventsAt(obj, date) - i = len(events) - 1 - while i >= 0: - # Get the event at that timeslot - event = events[i] - if event.timeslot == timeslot: - # We have found the event - if event.eventType != eventType: - raise Exception('Wrong event type') - # Validate or discard it - if action == 'validated': - schema = self.validation.schema - event.eventType = schema[eventType] - else: - if removeDiscarded: del events[i] - counts[action] += 1 - i -= 1 - elif rq['render'] == 'timeline': - # Every checkbox corresponds to a given date in some - # calendar (self, or one among self.others). It means that - # all "impactable" events at that date will be the target - # of the action. - otherId, fieldName, date = info.split('_') - otherObj = tool.getObject(otherId) - otherField = otherObj.getAppyType(fieldName) - # Get, on this calendar, the events defined at that date - events = otherField.getEventsAt(otherObj, date) - # Among them, validate or discard any impactable one - schema = otherField.validation.schema - i = len(events) - 1 - while i >= 0: - event = events[i] - # Take this event into account only if in the schema - if event.eventType in schema: - if action == 'validated': - event.eventType = schema[event.eventType] - else: - # "self" imposes its own "removeDiscarded" - if removeDiscarded: del events[i] - counts[action] += 1 - i -= 1 - if not counts['validated'] and not counts['discarded']: - return obj.translate('action_null') - part = not removeDiscarded and ' (but not removed)' or '' - self.log(obj, '%d event(s) validated and %d discarded%s.' % \ - (counts['validated'], counts['discarded'], part)) - return obj.translate('validate_events_done', mapping=counts) - - def getValidationCheckboxesStatus(self, obj): - '''Gets the status of the validation checkboxes from the request''' - res = {} - req = obj.REQUEST - for status, value in Calendar.validCbStatuses.iteritems(): - ids = req.get(status) - if ids: - for id in ids.split(','): res[id] = value - return res - - def computeTotals(self, totalType, obj, grid, others, preComputed): - '''Compute the totals for every column (p_totalType == 'row') or row - (p_totalType == "col").''' - allTotals = getattr(self, 'total%ss' % totalType.capitalize()) - if not allTotals: return - # Count other calendars and dates in the grid - othersCount = 0 - for group in others: othersCount += len(group) - datesCount = len(grid) - isRow = totalType == 'row' - # Initialise, for every (row or col) totals, Total instances - totalCount = isRow and datesCount or othersCount - lastCount = isRow and othersCount or datesCount - res = {} - for totals in allTotals: - res[totals.name] = [Total(totals.initValue) \ - for i in range(totalCount)] - # Get the status of validation checkboxes - status = self.getValidationCheckboxesStatus(obj.request) - # Walk every date within every calendar - indexes = {'i': -1, 'j': -1} - ii = isRow and 'i' or 'j' - jj = isRow and 'j' or 'i' - for other in sutils.IterSub(others): - indexes['i'] += 1 - indexes['j'] = -1 - for date in grid: - indexes['j'] += 1 - # Get the events in this other calendar at this date - events = other.field.getEventsAt(other.obj, date) - # From info @this date, update the total for every totals - last = indexes[ii] == lastCount - 1 - # Get the status of the validation checkbox that is possibly - # present at this date for this calendar - checked = None - cbId = '%s_%s_%s' % (other.obj.id, other.field.name, - date.strftime('%Y%m%d')) - if cbId in status: checked = status[cbId] - # Update the Total instance for every totals at this date - for totals in allTotals: - total = res[totals.name][indexes[jj]] - totals.onCell(obj, date, other, events, total, last, - checked, preComputed) - return res - - def getActiveLayers(self, req): - '''Gets the layers that are currently active''' - if req.has_key('activeLayers'): - # Get the from the request - layers = req['activeLayers'] or () - if not layers: return layers - return layers.split(',') - else: - # Get the layers that are active by default - res = [layer for layer in self.layers if layer.activeByDefault] - return res -# ------------------------------------------------------------------------------ diff --git a/fields/computed.py b/fields/computed.py deleted file mode 100644 index 376e411..0000000 --- a/fields/computed.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''':value - ::value''') - - pxSearch = Px(''' - ''') - - 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 -# ------------------------------------------------------------------------------ diff --git a/fields/date.py b/fields/date.py deleted file mode 100644 index 4393c2d..0000000 --- a/fields/date.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''':value''') - pxEdit = Px(''' - - - - - - - - - - - - - - - - - - - - : - - - ''') - - pxSearch = Px(''' - - - - - - - - - - - - - -
  - / - / - - - - - - - -
     - / - / - - - - - - - -
''') - - # 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)) -# ------------------------------------------------------------------------------ diff --git a/fields/dict.py b/fields/dict.py deleted file mode 100644 index d76642c..0000000 --- a/fields/dict.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''' - - :row[1] - :field.pxRender - ''') - - # PX for rendering the dict (shared between pxView and pxEdit) - pxTable = Px(''' - - - - - - - - :field.pxRow -
::_(info[1].labelId)
''') - - 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 -# ------------------------------------------------------------------------------ diff --git a/fields/file.py b/fields/file.py deleted file mode 100644 index f255647..0000000 --- a/fields/file.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''' - - - :value.uploadName  - - :shownSize - - - - - - ''') - - pxEdit = Px(''' - - :field.pxView
- - - -
- - - -
-
- - -
-
- - -
''') - - 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) -# ------------------------------------------------------------------------------ diff --git a/fields/float.py b/fields/float.py deleted file mode 100644 index e977a08..0000000 --- a/fields/float.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''' - :value - - ''') - - pxEdit = Px(''' - ''') - - pxSearch = Px(''' - - - - - - - - - -
''') - - 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) -# ------------------------------------------------------------------------------ diff --git a/fields/group.py b/fields/group.py deleted file mode 100644 index bedf94e..0000000 --- a/fields/group.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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 _. - 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('''''') - - # PX that renders the content of a group (which is referred as var "field"). - pxContent = Px(''' - - - - - - - - - - - - - - - - -
- ::_(field.labelId):field.pxHelp -
::_(field.descrId)
::field.hasHeaders and \ - _('%s_col%d' % (field.labelId, (colNb+1))) or ''
- - :field.pxView - :field.pxRender - -
''') - - # PX that renders a group of fields (the group is refered as var "field"). - pxView = Px(''' - - - -
- - ::_(field.labelId)>:field.pxHelp - -
::_(field.descrId)
- :field.pxContent -
- - - :field.pxContent - - - - - - - - - - - -
- - - - - - - - -
- :_(sub.labelId) -
-
- :field.pxView - :field.pxRender -
- -
-
''') - - # PX that renders a group of searches - pxViewSearches = Px(''' - - -
:collapse.px - :_(field.labelId) - :field.translated -
- -
- - - - :field.pxViewSearches - - :search.pxView - - -
''') - - # PX that renders a group of transitions. - pxViewTransitions = Px(''' - - - - - -
:transition.pxView
''') - - # 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) -# ------------------------------------------------------------------------------ diff --git a/fields/info.py b/fields/info.py deleted file mode 100644 index 684f525..0000000 --- a/fields/info.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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 -# ------------------------------------------------------------------------------ diff --git a/fields/integer.py b/fields/integer.py deleted file mode 100644 index bb8110f..0000000 --- a/fields/integer.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -from appy.fields import Field -from appy.px import Px - -# ------------------------------------------------------------------------------ -class Integer(Field): - - pxView = pxCell = Px(''' - :value - - ''') - - pxEdit = Px(''' - ''') - - pxSearch = Px(''' - - - - - - - - - -
''') - - 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) -# ------------------------------------------------------------------------------ diff --git a/fields/list.py b/fields/list.py deleted file mode 100644 index 494cc82..0000000 --- a/fields/list.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''' - - :field.pxRender - - - - - ''') - - # PX for rendering the list (shared between pxView and pxEdit) - pxTable = Px(''' - - - - - - - - - - :field.pxRow - - - - :field.pxRow -
::_(info[1].labelId) - -
''') - - pxView = pxCell = Px(''':field.pxTable''') - pxEdit = Px(''' - - :field.pxTable - ''') - - 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) -# ------------------------------------------------------------------------------ diff --git a/fields/ogone.py b/fields/ogone.py deleted file mode 100644 index 28b1a5b..0000000 --- a/fields/ogone.py +++ /dev/null @@ -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(''' - - -
- - - -
-
''') - - 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) -# ------------------------------------------------------------------------------ diff --git a/fields/page.py b/fields/page.py deleted file mode 100644 index ba53d02..0000000 --- a/fields/page.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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 _; - (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)) -# ------------------------------------------------------------------------------ diff --git a/fields/phase.py b/fields/phase.py deleted file mode 100644 index 3f999be..0000000 --- a/fields/phase.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -from appy import Object -from appy.px import Px - -# ------------------------------------------------------------------------------ -class Phase: - '''A group of pages.''' - - pxView = Px(''' - - - - - -
- - - - ::label - :label - - - - - - - - - - - - - - -
''') - - # "Static" PX that displays all phases of a given object. - pxAllPhases = Px(''' - - :phase.pxView - - - - - - - - - -
- - - - - - - - -
- :_('%s_phase_%s' % (zobj.meta_type, \ - phase.name)) -
-
:phase.pxView
- -
-
''') - - 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 -# ------------------------------------------------------------------------------ diff --git a/fields/pod.py b/fields/pod.py deleted file mode 100644 index 4e8106e..0000000 --- a/fields/pod.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''' - ''') - - pxView = pxCell = Px(''' - - - - :field.pxIcon - - - :field.pxIcon - - - - - - - - - - - - - - - - - - - - - - - - - - - - :field.getTemplateName(obj, info.template) -
-
''') - - 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." 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 . The - plug-in will write: - - .templates = ["Item.odt", "Decision.odt", "Other.odt"] - .setTemplateFolder('../PlugInApp/pod') - - The following code is equivalent, will work, but is precisely the - kind of things we want to avoid. - - .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')) -# ------------------------------------------------------------------------------ diff --git a/fields/ref.py b/fields/ref.py deleted file mode 100644 index 480575b..0000000 --- a/fields/ref.py +++ /dev/null @@ -1,1550 +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 . - -# ------------------------------------------------------------------------------ -import sys, re, os.path -from appy import Object -from appy.fields import Field -from appy.fields.search import Search -from appy.px import Px -from appy.gen.layout import Table -from appy.gen import utils as gutils -from appy.shared import utils as sutils -import collections - -# ------------------------------------------------------------------------------ -class Ref(Field): - # Some default layouts. "w" stands for "wide": those layouts produce tables - # of Ref objects whose width is 100%. - wLayouts = Table('lrv-f', width='100%') - # "d" stands for "description": a description label is added - wdLayouts = {'view': Table('l-d-f', width='100%')} - - # This PX displays the title of a referenced object, with a link on it to - # reach the consult view for this object. If we are on a back reference, the - # link allows to reach the correct page where the forward reference is - # defined. If we are on a forward reference, the "nav" parameter is added to - # the URL for allowing to navigate from one object to the next/previous one. - pxObjectTitle = Px(''' - - ::tied.o.getSupTitle(navInfo) - ::tied.o.getListTitle(nav=navInfo, target=target, page=pageName, \ - inPopup=inPopup) - ::sub - ''') - - # This PX displays buttons for triggering global actions on several linked - # objects (delete many, unlink many,...) - pxGlobalActions = Px(''' -
- - - - - - -
''') - - # This PX displays icons for triggering actions on a given referenced object - # (edit, delete, etc). - pxObjectActions = Px(''' -
- - - - - - - - - - - - - - - - - - - - - - - :tied.pxTransitions - - - - :field.pxCell - -
''') - - # Displays the button allowing to add a new object through a Ref field, if - # it has been declared as addable and if multiplicities allow it. - pxAdd = Px(''' -
- - - - - -
''') - - # Displays the button allowing to select from a popup objects to be linked - # via the Ref field. - pxLink = Px(''' - - - ''') - - # This PX displays, in a cell header from a ref table, icons for sorting the - # ref field according to the field that corresponds to this column. - pxSortIcons = Px(''' - - - - ''') - - # Shows the object number in a numbered list of tied objects - pxNumber = Px(''' - :objectIndex+1 - ''') - - # PX that displays referred objects as a list - pxViewList = Px(''' -
-
- :collapse.px - :_(subLabel) - (:totalNumber) - :field.pxAdd - - :field.pxLink - - -
- - - - :tool.pxNavigate - - -

:_('no_ref')

- - - - - - - - - - - :tied.pxViewAsTied -
- - - :_(refField.labelId) - :field.pxSortIcons - :tool.pxShowDetails -
- - :field.pxGlobalActions - - :tool.pxNavigate - - -
''') - - # PX that displays referred objects as dropdown menus. - pxMenu = Px(''' - :menu.text - - :len(menu.objects)''') - - pxViewMenus = Px(''' - - -
- -
:field.pxAdd
''') - - # Simplified widget showing minimal info about tied objects. - pxViewMinimal = Px(''' - :', '.join(infos) or _('no_ref')''') - - # PX that displays referred objects through this field. - # In mode link="list", if request key "scope" is: - # - not in the request, the whole field is shown (both available and already - # tied objects); - # - "objs", only tied objects are rendered; - # - "poss", only available objects are rendered (the pick list). - # ! scope is forced to "objs" on non-view "inner" (cell, buttons) layouts. - pxView = Px(''' - - - - - :field.pxView - - :field.pxViewList - :getattr(field, 'pxView%s' % \ - render.capitalize()) - ''') - - pxCell = pxView - - # Edit widget, for Refs with link='popup'. - pxEditPopup = Px(''' - - - -
-
- - :field.pxLink
''') - - pxEdit = Px(''' - - - :field.pxEditPopup''') - - pxSearch = Px(''' - - - - - -
-
- - ''') - - def __init__(self, klass=None, attribute=None, validator=None, - multiplicity=(0,1), default=None, add=False, addConfirm=False, - delete=None, noForm=False, link=True, unlink=None, - unlinkElement=None, insert=None, beforeLink=None, - afterLink=None, afterUnlink=None, back=None, show=True, - page='main', group=None, layouts=None, showHeaders=False, - shownInfo=None, select=None, maxPerPage=30, move=0, - indexed=False, mustIndex=True, searchable=False, - specificReadPermission=False, specificWritePermission=False, - width=None, height=5, maxChars=None, colspan=1, master=None, - masterValue=None, focus=False, historized=False, mapping=None, - label=None, queryable=False, queryFields=None, queryNbCols=1, - navigable=False, changeOrder=True, numbered=False, - checkboxes=True, checkboxesDefault=None, sdefault='', - scolspan=1, swidth=None, sheight=None, sselect=None, - persist=True, render='list', menuIdMethod=None, - menuInfoMethod=None, menuUrlMethod=None, view=None, xml=None, - showActions=True, collapsible=False): - self.klass = klass - self.attribute = attribute - # May the user add new objects through this ref ? "add" may also contain - # a method whose result must be a boolean value. - self.add = add - # When the user adds a new object, must a confirmation popup be shown? - self.addConfirm = addConfirm - # May the user delete objects via this Ref? - self.delete = delete - if delete == None: - # By default, one may delete objects via a Ref for which one can - # add objects. - self.delete = bool(self.add) - # If noForm is True, when clicking to create an object through this ref, - # the object will be created automatically, and no creation form will - # be presented to the user. - self.noForm = noForm - # May the user link existing objects through this ref? If "link" is; - # True, the user will, on the edit page, choose objects from a - # dropdown menu; - # "list", the user will, on the view page, choose objects from a list - # of objects which is similar to those rendered in pxViewList; - # "popup", the user will, on the edit page, choose objects from a popup - # menu. In this case, parameter "select" must hold a Search - # instance. - self.link = link - # May the user unlink existing objects? - self.unlink = unlink - if unlink == None: - # By default, one may unlink objects via a Ref for which one can - # link objects. - self.unlink = bool(self.link) - # "unlink" above is a global flag. If it is True, you can go further and - # determine, for every linked object, if it can be unlinked or not by - # defining a method in parameter "unlinkElement" below. This method - # accepts the linked object as unique arg. - self.unlinkElement = unlinkElement - # When an object is inserted through this Ref field, at what position is - # it inserted? If "insert" is: - # None, it will be inserted at the end; - # "start", it will be inserted at the start of the tied objects; - # a method, (called with the object to insert as single arg), its return - # value (a number or a tuple of numbers) will be - # used to insert the object at the corresponding position - # (this method will also be applied to other objects to know - # where to insert the new one); - # a tuple, ('sort', method), the given method (called with the object - # to insert as single arg) will be used to sort tied objects - # and will be given as param "key" of the standard Python - # method "sort" applied on the list of tied objects. - # With value ('sort', method), a full sort is performed and may hardly - # reshake the tied objects; with value "method" alone, the tied - # object is inserted at some given place: tied objects are more - # maintained in the order of their insertion. - self.insert = insert - # Immediately before an object is going to be linked via this Ref field, - # method potentially specified in "beforeLink" will be executed and will - # take the object to link as single parameter. - self.beforeLink = beforeLink - # Immediately after an object has been linked via this Ref field, method - # potentially specified in "afterLink" will be executed and will take - # the linked object as single parameter. - self.afterLink = afterLink - # Immediately after an object as been unlinked from this Ref field, - # method potentially specified in "afterUnlink" will be executed and - # will take the unlinked object as single parameter. - self.afterUnlink = afterUnlink - self.back = None - if back: - # It is a forward reference - self.isBack = False - # Initialise the backward reference - self.back = back - back.back = self - # klass may be None in the case we are defining an auto-Ref to the - # same class as the class where this field is defined. In this case, - # when defining the field within the class, write - # myField = Ref(None, ...) - # and, at the end of the class definition (name it K), write: - # K.myField.klass = K - # setattr(K, K.myField.back.attribute, K.myField.back) - if klass: setattr(klass, back.attribute, back) - else: - self.isBack = True - # When displaying a tabular list of referenced objects, must we show - # the table headers? - self.showHeaders = showHeaders - # "shownInfo" is a tuple or list (or a method producing) containing - # the names of the fields that will be shown when displaying tables of - # tied objects. Field "title" should be present: by default it is a - # clickable link to the "view" page of every tied object. - if shownInfo == None: - self.shownInfo = ['title'] - elif isinstance(shownInfo, tuple): - self.shownInfo = list(shownInfo) - else: - self.shownInfo = shownInfo - # If a method is defined in this field "select", it will be used to - # return the list of possible tied objects. Be careful: this method can - # receive, in its first argument ("self"), the tool instead of an - # instance of the class where this field is defined. This little cheat - # is: - # - not really a problem: in this method you will mainly use methods - # that are available on a tool as well as on any object (like - # "search"); - # - necessary because in some cases we do not have an instance at our - # disposal, ie, when we need to compute a list of objects on a - # search screen. - # "select" can also hold a Search instance. In this case, put any name - # in Search's mandatory parameter "name": it will be ignored and - # replaced with an internal technical name. - # NOTE that when a method is defined in field "masterValue" (see parent - # class "Field"), it will be used instead of select (or sselect below). - self.select = select - if isinstance(select, Search): - select.name = '_field_' - select.checkboxes = True - select.checkboxesDefault = False - # If you want to specify, for the search screen, a list of objects that - # is different from the one produced by self.select, define an - # alternative method in field "sselect" below. - self.sselect = sselect or self.select - # Maximum number of referenced objects shown at once. - self.maxPerPage = maxPerPage - # If param p_queryable is True, the user will be able to perform queries - # from the UI within referenced objects. - self.queryable = queryable - # Here is the list of fields that will appear on the search screen. - # If None is specified, by default we take every indexed field - # defined on referenced objects' class. - self.queryFields = queryFields - # The search screen will have this number of columns - self.queryNbCols = queryNbCols - # Within the portlet, will referred elements appear ? - self.navigable = navigable - # If "changeOrder" is or returns False, it even if the user has the - # right to modify the field, it will not be possible to move objects or - # sort them. - self.changeOrder = changeOrder - # If "numbered" is or returns True, a leading column will show the - # number of every tied object. Moreover, if the user can change order of - # tied objects, an input field will allow him to enter a new number for - # the tied object. If "numbered" is or returns a string, it will be used - # as width for the column containing the number. Else, a default width - # will be used. - self.numbered = numbered - # If "checkboxes" is or returns True, every linked object will be - # "selectable" via a checkbox. Global actions will be activated and will - # act on the subset of selected objects: delete, unlink, etc. - self.checkboxes = checkboxes - # Default value for checkboxes, if enabled. - if checkboxesDefault == None: - self.checkboxesDefault = bool(self.link) - else: - self.checkboxesDefault = checkboxesDefault - # There are different ways to render a bunch of linked objects: - # - "list" (the default) renders them as a list (=a XHTML table); - # - "menus" renders them as a series of popup menus, grouped by type. - # Note that render mode "menus" will only be applied in "cell" and - # "buttons" layouts. Indeed, we need to keep the "list" rendering in - # the "view" layout because the "menus" rendering is minimalist and does - # not allow to perform all operations on linked objects (add, move, - # delete, edit...); - # - "minimal" renders a list of comma-separated, not-even-clickable, - # data about the tied objects (according to shownInfo). - self.render = render - # If render is 'menus', 2 methods must be provided. - # "menuIdMethod" will be called, with every linked object as single arg, - # and must return an ID that identifies the menu into which the object - # will be inserted. - self.menuIdMethod = menuIdMethod - # "menuInfoMethod" will be called with every collected menu ID (from - # calls to the previous method) to get info about this menu. This info - # must be a tuple (text, icon): - # - "text" is the menu name; - # - "icon" (can be None) gives the URL of an icon, if you want to render - # the menu as an icon instead of a text. - self.menuInfoMethod = menuInfoMethod - # "menuUrlMethod" is an optional method that allows to compute an - # alternative URL for the tied object that is shown within the menu - # (when render is "menus"). It can also be used with render being "list" - # as well. The method can return a URL as a string, or, alternately, a - # tuple (url, target), "target" being a string that will be used for - # the "target" attribute of the corresponding XHTML "a" tag. - self.menuUrlMethod = menuUrlMethod - # "showActions" determines if we must show or not actions on every tied - # object. Values 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 - # If "collapsible" is True, a "+/-" icon will allow to expand/collapse - # the tied or available objects. - self.collapsible = collapsible - if showActions == True: self.showActions = 'block' - # Call the base constructor - 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.validable = bool(self.link) - self.checkParameters() - - def checkParameters(self): - '''Ensures this Ref is correctly defined.''' - # For forward Refs, "add" and "link" can't both be used. - if not self.isBack and (self.add and self.link): - raise Exception('Parameters "add" and "link" can\'t both be used.') - # If link is "popup", "select" must hold a Search instance. - if (self.link == 'popup') and not isinstance(self.select, Search): - raise Exception('When "link" is "popup", "select" must be a ' \ - 'appy.fields.search.Search instance.') - - def getDefaultLayouts(self): - return {'view': Table('l-f', width='100%'), 'edit': 'lrv-f'} - - def isShowable(self, obj, layoutType): - res = Field.isShowable(self, obj, layoutType) - if not res: return res - # We add here specific Ref rules for preventing to show the field under - # some inappropriate circumstances. - if layoutType == 'edit': - if self.mayAdd(obj): return - if self.link in (False, 'list'): return - if self.isBack: - if layoutType == 'edit': return - else: return getattr(obj.aq_base, self.name, None) - return res - - def isRenderable(self, layoutType): - '''Only Ref fields with render = "menus" can be rendered on "button" - layouts.''' - if layoutType == 'buttons': return self.render == 'menus' - return True - - def getValue(self, obj, appy=True, noListIfSingleObj=False, - startNumber=None, someObjects=False): - '''Returns the objects linked to p_obj through this Ref field. It - returns Appy wrappers if p_appy is True, the Zope objects else. - - * If p_startNumber is None, it returns all referred objects; - * if p_startNumber is a number, it returns self.maxPerPage objects, - starting at p_startNumber. - - If p_noListIfSingleObj is True, it returns the single reference as - an object and not as a list. - - If p_someObjects is True, it returns an instance of SomeObjects - instead of returning a list of references.''' - uids = getattr(obj.aq_base, self.name, []) - if not uids: - # Maybe is there a default value? - defValue = Field.getValue(self, obj) - if defValue: - if type(defValue) in sutils.sequenceTypes: - uids = [o.o.id for o in defValue] - else: - uids = [defValue.o.id] - # Prepare the result: an instance of SomeObjects, that will be unwrapped - # if not required. - res = gutils.SomeObjects() - res.totalNumber = res.batchSize = len(uids) - batchNeeded = startNumber != None - if batchNeeded: - res.batchSize = self.maxPerPage - if startNumber != None: - res.startNumber = startNumber - # Get the objects given their uids - i = res.startNumber - while i < (res.startNumber + res.batchSize): - if i >= res.totalNumber: break - # Retrieve every reference in the correct format according to p_type - tied = obj.getTool().getObject(uids[i]) - if appy: tied = tied.appy() - res.objects.append(tied) - i += 1 - # Manage parameter p_noListIfSingleObj - if res.objects and noListIfSingleObj: - if self.multiplicity[1] == 1: - res.objects = res.objects[0] - if someObjects: return res - return res.objects - - def getCopyValue(self, obj): - '''Here, as "value ready-to-copy", we return the list of tied object - UIDs, because m_store on the destination object can store tied - objects based on such a list.''' - res = getattr(obj.aq_base, self.name, ()) - # Return a copy: it can be dangerous to give the real database value - if res: return list(res) - - def getXmlValue(self, obj, value): - '''The default XML value for a Ref is the list of tied object URLs.''' - # Bypass the default behaviour if a custom method is given - if self.xml: return self.xml(obj, value) - return ['%s/xml' % tied.o.absolute_url() for tied in value] - - def getPossibleValues(self, obj, startNumber=None, someObjects=False, - removeLinked=False): - '''This method returns the list of all objects that can be selected - to be linked as references to p_obj via p_self. It is applicable only - for Ref fields with link!=False. If master values are present in the - request, we use field.masterValues method instead of self.select. - - If p_startNumber is a number, it returns self.maxPerPage objects, - starting at p_startNumber. If p_someObjects is True, it returns an - instance of SomeObjects instead of returning a list of objects. - - If p_removeLinked is True, we remove, from the result, objects which - are already linked. For example, for Ref fields rendered as a - dropdown menu or a multi-selection box (with link=True), on the edit - page, we need to display all possible values: those that are already - linked appear to be selected in the widget. But for Ref fields - rendered as pick lists (link="list"), once an object is linked, it - must disappear from the "pick list". - ''' - req = obj.REQUEST - obj = obj.appy() - paginated = startNumber != None - isSearch = False - if 'masterValues' in req: - # Convert masterValue(s) from id(s) to real object(s) - masterValues = req['masterValues'].strip() - if not masterValues: masterValues = None - else: - masterValues = masterValues.split('*') - tool = obj.tool - if len(masterValues) == 1: - masterValues = tool.getObject(masterValues[0]) - else: - masterValues = [tool.getObject(v) for v in masterValues] - objects = self.masterValue(obj, masterValues) - else: - # If this field is an ajax-updatable slave, no need to compute - # possible values: it will be overridden by method self.masterValue - # by a subsequent ajax request (=the "if" statement above). - if self.masterValue and isinstance(self.masterValue, collections.Callable): - objects = [] - else: - if not self.select: - # No select method or search has been defined: we must - # retrieve all objects of the referred type that the user - # is allowed to access. - objects = obj.search(self.klass) - else: - if isinstance(self.select, Search): - isSearch = True - maxResults = paginated and self.maxPerPage or 'NO_LIMIT' - start = startNumber or 0 - className = obj.tool.o.getPortalType(self.klass) - objects = obj.o.executeQuery(className, - startNumber=start, search=self.select, - maxResults=maxResults) - objects.objects = [o.appy() for o in objects.objects] - else: - objects = self.select(obj) - # Remove already linked objects if required - if removeLinked: - uids = getattr(obj.o.aq_base, self.name, None) - if uids: - # Browse objects in reverse order and remove linked objects - if isSearch: objs = objects.objects - else: objs = objects - i = len(objs) - 1 - while i >= 0: - if objs[i].id in uids: del objs[i] - i -= 1 - # If possible values are not retrieved from a Search, restrict (if - # required) the result to self.maxPerPage starting at p_startNumber. - # Indeed, in this case, unlike m_getValue, we already have all objects - # in "objects": we can't limit objects "waking up" to at most - # self.maxPerPage. - total = len(objects) - if paginated and not isSearch: - objects = objects[startNumber:startNumber + self.maxPerPage] - # Return the result, wrapped in a SomeObjects instance if required - if not someObjects: - if isSearch: return objects.objects - return objects - if isSearch: return objects - res = gutils.SomeObjects() - res.totalNumber = total - res.batchSize = self.maxPerPage - res.startNumber = startNumber - res.objects = objects - return res - - def getViewValues(self, obj, startNumber, scope): - '''Gets the values as must be shown on pxView. If p_scope is "poss", it - is the list of possible, not-yet-linked, values. Else, it is the list - of linked values. In both cases, we take the subset starting at - p_startNumber.''' - if scope == 'poss': - return self.getPossibleValues(obj, startNumber=startNumber, - someObjects=True, removeLinked=True) - # Return the list of already linked values - return self.getValue(obj, startNumber=startNumber, someObjects=True) - - def getLinkedObjectsByMenu(self, obj, objects): - '''This method groups p_objects into sub-lists of objects, grouped by - menu (happens when self.render == 'menus').''' - if not objects: return () - res = [] - # We store in "menuIds" the already encountered menus: - # ~{s_menuId : i_indexInRes}~ - menuIds = {} - # Browse every object from p_objects and put them in their menu - # (within "res"). - for tied in objects: - menuId = self.menuIdMethod(obj, tied) - if menuId in menuIds: - # We have already encountered this menu - menuIndex = menuIds[menuId] - res[menuIndex].objects.append(tied) - else: - # A new menu - menu = Object(id=menuId, objects=[tied]) - res.append(menu) - menuIds[menuId] = len(res) - 1 - # Complete information about every menu by calling self.menuInfoMethod - for menu in res: - text, icon = self.menuInfoMethod(obj, menu.id) - menu.text = text - menu.icon = icon - return res - - def isNumbered(self, obj): - '''Must we show the order number of every tied object?''' - res = self.getAttribute(obj, 'numbered') - if not res: return res - # Returns the column width. - if not isinstance(res, str): return '15px' - return res - - def getMenuUrl(self, zobj, tied): - '''We must provide the URL of the p_tied object, when shown in a Ref - field in render mode 'menus'. If self.menuUrlMethod is specified, - use it. Else, returns the "normal" URL of the view page for the tied - object, but without any navigation information, because in this - render mode, tied object's order is lost and navigation is - impossible.''' - if self.menuUrlMethod: - res = self.menuUrlMethod(zobj.appy(), tied) - if isinstance(res, str): return res, '_self' - return res - return tied.o.getUrl(nav=''), '_self' - - def getStartNumber(self, render, req, hookId): - '''This method returns the index of the first linked object that must be - shown, or None if all linked objects must be shown at once (it - happens when p_render is "menus").''' - # When using 'menus' render mode, all linked objects must be shown - if render == 'menus': return - # When using 'list' (=default) render mode, the index of the first - # object to show is in the request. - key = '%s_startNumber' % hookId - nb = req.has_key(key) and req[key] or req.get('startNumber', 0) - return int(nb) - - def getFormattedValue(self, obj, value, layoutType='view', - showChanges=False, language=None): - return value - - def getIndexType(self): return 'ListIndex' - - def getValidCatalogValue(self, value): - '''p_value is the new value we want to index in the catalog, for this - field, for some object. p_value as is may not be an acceptable value - for the catalog: if it it an empty list, instead of using it, the - catalog will keep the previously catalogued value! For this case, - this method produces an "empty" value that will really overwrite the - previous one. Moreover, the catalog does not like persistent - lists.''' - # The index does not like persistent lists. Moreover, I don't want to - # give to anyone access to the persistent list in the DB. - if value: return list(value) - # Ugly catalog: if I return an empty list, the previous value is kept - return [''] - - def getIndexValue(self, obj, forSearch=False): - '''Value for indexing is the list of UIDs of linked objects. If - p_forSearch is True, it will return a list of the linked objects' - titles instead.''' - # Must we produce an index value? - if not self.getAttribute(obj, 'mustIndex'): return - if not forSearch: - res = getattr(obj.aq_base, self.name, None) - return self.getValidCatalogValue(res) - else: - # For the global search: return linked objects' titles - return ' '.join([o.getShownValue('title') \ - for o in self.getValue(obj, appy=False)]) - - def hasSortIndex(self): - '''An indexed Ref field is of type "ListIndex", which is not sortable. - So an additional FieldIndex is required.''' - return True - - def validateValue(self, obj, value): - if not self.link: return - # We only check "link" Refs because in edit views, "add" Refs are - # not visible. So if we check "add" Refs, on an "edit" view we will - # believe that that there is no referred object even if there is. - # Also ensure that multiplicities are enforced. - if not value: - nbOfRefs = 0 - elif isinstance(value, str): - nbOfRefs = 1 - else: - nbOfRefs = len(value) - minRef = self.multiplicity[0] - maxRef = self.multiplicity[1] - if maxRef == None: - maxRef = sys.maxsize - if nbOfRefs < minRef: - return obj.translate('min_ref_violated') - elif nbOfRefs > maxRef: - return obj.translate('max_ref_violated') - - def linkObject(self, obj, value, back=False, noSecurity=True, - executeMethods=True): - '''This method links p_value (which can be a list of objects) to p_obj - through this Ref field. When linking 2 objects via a Ref, - p_linkObject must be called twice: once on the forward Ref and once - on the backward Ref. p_back indicates if we are calling it on the - forward or backward Ref. If p_noSecurity is True, we bypass security - checks (has the logged user the right to modify this Ref field?). - If p_executeMethods is False, we do not execute methods that - customize the object insertion (parameters insert, beforeLink, - afterLink...). This can be useful while migrating data or duplicating - an object.''' - zobj = obj.o - # Security check - if not noSecurity: zobj.mayEdit(self.writePermission, raiseError=True) - # p_value can be a list of objects - if type(value) in sutils.sequenceTypes: - for v in value: - self.linkObject(obj, v, back, noSecurity, executeMethods) - return - # Gets the list of referred objects (=list of uids), or create it. - refs = getattr(zobj.aq_base, self.name, None) - if refs == None: - refs = zobj.getProductConfig().PersistentList() - setattr(zobj, self.name, refs) - # Insert p_value into it - uid = value.o.id - if uid in refs: return - # Execute self.beforeLink if present - if executeMethods and self.beforeLink: self.beforeLink(obj, value) - # Where must we insert the object? - if not self.insert or not executeMethods: - refs.append(uid) - elif self.insert == 'start': - refs.insert(0, uid) - elif callable(self.insert): - # It is a method. Use it on every tied object until we find where to - # insert the new object. - tool = zobj.getTool() - insertOrder = self.insert(obj, value) - i = 0 - inserted = False - while i < len(refs): - tied = tool.getObject(refs[i], appy=True) - if self.insert(obj, tied) > insertOrder: - refs.insert(i, uid) - inserted = True - break - i += 1 - if not inserted: refs.append(uid) - else: - # It is a tuple ('sort', method). Perform a full sort. - refs.append(uid) - tool = zobj.getTool() - # Warning: "refs" is a persistent list whose method "sort" has no - # param "key". - refs.data.sort(key=lambda uid:self.insert[1](obj, \ - tool.getObject(uid, appy=True))) - refs._p_changed = 1 - # Execute self.afterLink if present - if executeMethods and self.afterLink: self.afterLink(obj, value) - # Update the back reference - if not back: - self.back.linkObject(value, obj, True, noSecurity, executeMethods) - - def unlinkObject(self, obj, value, back=False, noSecurity=True, - executeMethods=True): - '''This method unlinks p_value (which can be a list of objects) from - p_obj through this Ref field. For an explanation about parameters - p_back, p_noSecurity and p_executeMethods, check m_linkObject's doc - above.''' - zobj = obj.o - # Security check - if not noSecurity: - zobj.mayEdit(self.writePermission, raiseError=True) - if executeMethods: - self.mayUnlinkElement(obj, value, raiseError=True) - # p_value can be a list of objects - if type(value) in sutils.sequenceTypes: - for v in value: - self.unlinkObject(obj, v, back, noSecurity, executeMethods) - return - refs = getattr(zobj.aq_base, self.name, None) - if not refs: return - # Unlink p_value - uid = value.o.id - if uid in refs: - refs.remove(uid) - # Execute self.afterUnlink if present - if executeMethods and self.afterUnlink: self.afterUnlink(obj, value) - # Update the back reference - if not back: - self.back.unlinkObject(value,obj,True,noSecurity,executeMethods) - - def store(self, obj, value): - '''Stores on p_obj, the p_value, which can be: - * None; - * an object UID (=string); - * a list of object UIDs (=list of strings). Generally, UIDs or lists - of UIDs come from Ref fields with link:True edited through the web; - * a Zope object; - * a Appy object; - * a list of Appy or Zope objects.''' - if not self.persist: return - # Standardize p_value into a list of Appy objects - objects = value - if not objects: objects = [] - if type(objects) not in sutils.sequenceTypes: objects = [objects] - tool = obj.getTool() - for i in range(len(objects)): - if isinstance(objects[i], str): - # We have an UID here - objects[i] = tool.getObject(objects[i], appy=True) - else: - # Be sure to have an Appy object - objects[i] = objects[i].appy() - uids = [o.o.id for o in objects] - appyObj = obj.appy() - # Unlink objects that are not referred anymore - refs = getattr(obj.aq_base, self.name, None) - if refs: - i = len(refs)-1 - while i >= 0: - if refs[i] not in uids: - # Object having this UID must unlink p_obj - tied = tool.getObject(refs[i], appy=True) - self.back.unlinkObject(tied, appyObj) - i -= 1 - # Link new objects - if objects: self.linkObject(appyObj, objects) - - def mayAdd(self, obj, mode='create', checkMayEdit=True): - '''May the user create (if p_mode == "create") or link - (if mode == "link") (a) new referred object(s) from p_obj via this - Ref? If p_checkMayEdit is False, it means that the condition of being - allowed to edit this Ref field has already been checked somewhere - else (it is always required, we just want to avoid checking it - twice).''' - # We can't (yet) do that on back references. - if self.isBack: return gutils.No('is_back') - # Check if this Ref is addable/linkable. - if mode == 'create': - add = self.getAttribute(obj, 'add') - if not add: return gutils.No('no_add') - elif mode == 'link': - if (self.link != 'popup') or not self.isMultiValued(): return - # Have we reached the maximum number of referred elements? - if self.multiplicity[1] != None: - refCount = len(getattr(obj, self.name, ())) - if refCount >= self.multiplicity[1]: return gutils.No('max_reached') - # May the user edit this Ref field? - if checkMayEdit: - if not obj.mayEdit(self.writePermission): - return gutils.No('no_write_perm') - # May the user create instances of the referred class? - if mode == 'create': - if not obj.getTool().userMayCreate(self.klass): - return gutils.No('no_create_perm') - return True - - def checkAdd(self, obj): - '''Compute m_mayAdd above, and raise an Unauthorized exception if - m_mayAdd returns False.''' - may = self.mayAdd(obj) - if not may: - obj.raiseUnauthorized("User can't write Ref field '%s' (%s)." % \ - (self.name, may.msg)) - - def getOnAdd(self, q, formName, addConfirmMsg, target, hookId, startNumber): - '''Computes the JS code to execute when button "add" is clicked.''' - if self.noForm: - # Ajax-refresh the Ref with a special param to link a newly created - # object. - res = "askAjax('%s', null, {'startNumber':'%d', " \ - "'action':'doCreateWithoutForm'})" % (hookId, startNumber) - if self.addConfirm: - res = "askConfirm('script', %s, %s)" % \ - (q(res, False), q(addConfirmMsg)) - else: - # In the basic case, no JS code is executed: target.openPopup is - # empty and the button-related form is submitted in the main page. - res = target.openPopup - if self.addConfirm and not target.openPopup: - res = "askConfirm('form','%s',%s)" % (formName,q(addConfirmMsg)) - elif self.addConfirm and target.openPopup: - res = "askConfirm('form+script',%s,%s)" % \ - (q(formName + '+' + target.openPopup, False), \ - q(addConfirmMsg)) - return res - - def getAddLabel(self, obj, addLabel, tiedClassLabel, inMenu): - '''Gets the label of the button allowing to add a new tied object. If - p_inMenu, the label must contain the name of the class whose instance - will be created by clincking on the button.''' - if not inMenu: return obj.translate('add_ref') - return tiedClassLabel - - def getListLabel(self, inPickList): - '''If self.link == "list", a label must be shown in front of the list. - Moreover, the label is different if the list is a pick list or the - list of tied objects.''' - if self.link != 'list': return - return inPickList and 'selectable_objects' or 'selected_objects' - - def mayUnlinkElement(self, obj, tied, raiseError=False): - '''May we unlink from this Ref field this specific p_tied object?''' - if not self.unlinkElement: return True - res = self.unlinkElement(obj, tied) - if res: return True - else: - if not raiseError: return - # Raise an exception. - obj.o.raiseUnauthorized('field.unlinkElement prevents you to ' \ - 'unlink this object.') - - def getCbJsInit(self, obj): - '''When checkboxes are enabled, this method defines a JS associative - array (named "_appy_objs_cbs") that will store checkboxes' statuses. - This array is needed because all linked objects are not visible at - the same time (pagination). - - Moreover, if self.link is "list", an additional array (named - "_appy_poss_cbs") is defined for possible values. - - Semantics of this (those) array(s) can be as follows: if a key is - present in it for a given linked object, it means that the - checkbox is unchecked. In this case, all linked objects are selected - by default. But the semantics can be inverted: presence of a key may - mean that the checkbox is checked. The current array semantics is - stored in a variable named "_appy_objs_sem" (or "_appy_poss_sem") - and may hold "unchecked" (initial semantics) or "checked" (inverted - semantics). Inverting semantics allows to keep the array small even - when checking/unchecking all checkboxes. - - The mentioned JS arrays and variables are stored as attributes of the - DOM node representing this field.''' - # The initial semantics depends on the checkboxes default value. - default = self.getAttribute(obj, 'checkboxesDefault') and \ - 'unchecked' or 'checked' - code = "\nnode['_appy_%%s_cbs']={};\nnode['_appy_%%s_sem']='%s';" % \ - default - poss = (self.link == 'list') and (code % ('poss', 'poss')) or '' - return "var node=findNode(this, '%s_%s');%s%s" % \ - (obj.id, self.name, code % ('objs', 'objs'), poss) - - def getAjaxData(self, hook, zobj, **params): - '''Initializes an AjaxData object on the DOM node corresponding to this - Ref field.''' - # Complete params with default parameters - params['ajaxHookId'] = hook; - params['scope'] = hook.rsplit('_', 1)[-1] - params = sutils.getStringDict(params) - return "new AjaxData('%s', '%s:pxView', %s, null, '%s')" % \ - (hook, self.name, params, zobj.absolute_url()) - - def getAjaxDataRow(self, obj, parentHook, **params): - '''Initializes an AjaxData object on the DOM node corresponding to - p_hook = a row within the list of referred objects.''' - hook = obj.id - return "new AjaxData('%s', 'pxViewAsTiedFromAjax', %s, '%s', '%s')" % \ - (hook, sutils.getStringDict(params), parentHook, obj.url) - - def doChangeOrder(self, obj): - '''Moves a referred object up/down/top/bottom.''' - rq = obj.REQUEST - # How to move the item? - move = rq['move'] - # Get the UID of the tied object to move - uid = rq['refObjectUid'] - uids = getattr(obj.aq_base, self.name) - oldIndex = uids.index(uid) - if move == 'up': - newIndex = oldIndex - 1 - elif move == 'down': - newIndex = oldIndex + 1 - elif move == 'top': - newIndex = 0 - elif move == 'bottom': - newIndex = len(uids) - 1 - elif move.startswith('index'): - # New index starts at 1 (oldIndex starts at 0) - try: - newIndex = int(move.split('_')[1]) - 1 - except ValueError: - newIndex = -1 - # If newIndex is negative, it means that the move can't occur - if newIndex > -1: - uids.remove(uid) - uids.insert(newIndex, uid) - - def doCreateWithoutForm(self, obj): - '''This method is called when a user wants to create a object from a - reference field, automatically (without displaying a form).''' - obj.appy().create(self.name) - - xhtmlToText = re.compile('<.*?>', re.S) - def getReferenceLabel(self, obj, refObject, unlimited=False): - '''p_self must have link=True. I need to display, on an edit view, the - p_refObject in the listbox that will allow the user to choose which - object(s) to link through the Ref. The information to display may - only be the object title or more if "shownInfo" is used.''' - res = '' - for name in self.getAttribute(obj, 'shownInfo'): - refType = refObject.o.getAppyType(name) - value = getattr(refObject, name) - value = refType.getShownValue(refObject.o, value) - if refType.type == 'String': - if refType.format == 2: - value = self.xhtmlToText.sub(' ', value) - elif type(value) in sutils.sequenceTypes: - value = ', '.join(value) - prefix = res and ' | ' or '' - res += prefix + value - if unlimited: return res - maxWidth = self.width or 30 - if len(res) > maxWidth: - res = refObject.tool.o.truncateValue(res, maxWidth) - return res - - def getIndexOf(self, obj, tiedUid, raiseError=True): - '''Gets the position of tied object identified by p_tiedUid within this - field on p_obj.''' - uids = getattr(obj.aq_base, self.name, None) - if not uids: - if raiseError: raise IndexError() - else: return - if tiedUid in uids: - return uids.index(tiedUid) - else: - if raiseError: raise IndexError() - else: return - - def sort(self, obj): - '''Called when the user wants to sort the content of this field.''' - rq = obj.REQUEST - sortKey = rq.get('sortKey') - reverse = rq.get('reverse') == 'True' - obj.appy().sort(self.name, sortKey=sortKey, reverse=reverse) - - def getRenderMode(self, layoutType): - '''Gets the render mode, determined by self.render and some - exceptions.''' - if (layoutType == 'view') and (self.render == 'menus'): return 'list' - return self.render - - def getPopupObjects(self, obj, rq, requestValue): - '''Gets the list of objects that were selected in the popup (for Ref - fields with link="popup").''' - if requestValue: - # We are validating the form. Return the request value instead of - # the popup value. - tool = obj.tool - if isinstance(requestValue, basestring): - return [tool.getObject(requestValue)] - else: - return [tool.getObject(rv) for rv in requestValue] - res = [] - # No object can be selected if the popup has not been opened yet - if 'semantics' not in rq: - # In this case, display already linked objects if any - if not obj.isEmpty(self.name): return self.getValue(obj.o) - return res - uids = rq['selected'].split(',') - tool = obj.tool - if rq['semantics'] == 'checked': - # Simply get the selected objects from their uid - return [tool.getObject(uid) for uid in uids] - else: - # Replay the search in self.select to get the list of uids that were - # shown in the popup. - className = tool.o.getPortalType(self.klass) - brains = obj.o.executeQuery(className, search=self.select, - maxResults='NO_LIMIT', brainsOnly=True, - sortBy=rq.get('sortKey'), sortOrder=rq.get('sortOrder'), - filterKey=rq.get('filterKey'),filterValue=rq.get('filterValue')) - queryUids = [os.path.basename(b.getPath()) for b in brains] - for uid in queryUids: - if uid not in uids: - res.append(tool.getObject(uid)) - return res - - def onSelectFromPopup(self, obj): - '''This method is called on Ref fields with link="popup", when a user - has selected objects from the popup, to be added to existing tied - objects, from the view widget.''' - obj = obj.appy() - for tied in self.getPopupObjects(obj, obj.request, None): - self.linkObject(obj, tied, noSecurity=False) - - def onUiRequest(self, obj, rq): - '''This method is called when an action tied to this Ref field is - triggered from the user interface (link, unlink, link_many, - unlink_many, delete_many).''' - action = rq['linkAction'] - tool = obj.getTool() - msg = None - appyObj = obj.appy() - if not action.endswith('_many'): - # "link" or "unlink" - tied = tool.getObject(rq['targetUid'], appy=True) - exec 'self.%sObject(appyObj, tied, noSecurity=False)' % action - else: - # "link_many", "unlink_many", "delete_many". As a preamble, perform - # a security check once, instead of doing it on every object-level - # operation. - obj.mayEdit(self.writePermission, raiseError=True) - # Get the (un-)checked objects from the request. - uids = rq['targetUid'].split(',') - unchecked = rq['semantics'] == 'unchecked' - if action == 'link_many': - # Get possible values (objects) - values = self.getPossibleValues(obj, removeLinked=True) - isObj = True - else: - # Get current values (uids) - values = getattr(obj.aq_base, self.name, ()) - isObj = False - # Collect the objects onto which the action must be performed. - targets = [] - for value in values: - uid = not isObj and value or value.uid - if unchecked: - # Keep only objects not among uids. - if uid in uids: continue - else: - # Keep only objects being in uids. - if uid not in uids: continue - # Collect this object - target = not isObj and tool.getObject(value, appy=True) or \ - value - targets.append(target) - if not targets: - msg = obj.translate('action_null') - else: - # Perform the action on every target. Count the number of failed - # operations. - failed = 0 - singleAction = action.split('_')[0] - mustDelete = singleAction == 'delete' - for target in targets: - if mustDelete: - # Delete - if target.o.mayDelete(): target.o.delete() - else: failed += 1 - else: - # Link or unlink. For unlinking, we need to perform an - # additional check. - if (singleAction == 'unlink') and \ - not self.mayUnlinkElement(appyObj, target): - failed += 1 - else: - exec 'self.%sObject(appyObj, target)' % singleAction - if failed: - msg = obj.translate('action_partial', mapping={'nb':failed}) - urlBack = obj.getUrl(rq['HTTP_REFERER']) - if not msg: msg = obj.translate('action_done') - appyObj.say(msg) - tool.goto(urlBack) - - def getNavInfo(self, obj, nb, total, inPickList=False): - '''Gets the navigation info allowing to navigate from tied object number - p_nb to its siblings.''' - if self.isBack or inPickList: return '' - # If p_nb is None, we want to produce a generic nav info into which we - # will insert a specific number afterwards. - if nb == None: return 'ref.%s.%s.%%d.%d' % (obj.id, self.name, total) - return 'ref.%s.%s.%d.%d' % (obj.id, self.name, nb, total) - - def onGotoTied(self, obj): - '''Called when the user wants to go to a tied object whose number is in - the request.''' - number = int(obj.REQUEST['number']) - 1 - uids = getattr(obj.aq_base, self.name) - tiedUid = uids[number] - tied = obj.getTool().getObject(tiedUid) - tiedUrl = tied.getUrl(nav=self.getNavInfo(obj, number+1, len(uids))) - return obj.goto(tiedUrl) - - def getCollapseInfo(self, obj, inPickList): - '''Returns a Collapsible instance, that determines if the "tied objects" - or "available objects" zone (depending on p_inPickList) is collapsed - or expanded.''' - # Create the ID of the collapsible zone. - suffix = inPickList and 'poss' or 'objs' - id = '%s_%s_%s' % (obj.klass.__name__, self.name, suffix) - return gutils.Collapsible(id, obj.request, default='expanded', - display='table') - -def autoref(klass, field): - '''klass.field is a Ref to p_klass. This kind of auto-reference can't be - declared in the "normal" way, like this: - - class A: - attr1 = Ref(A) - - because at the time Python encounters the static declaration - "attr1 = Ref(A)", class A is not completely defined yet. - - This method allows to overcome this problem. You can write such - auto-reference like this: - - class A: - attr1 = Ref(None) - autoref(A, A.attr1) - - This function can also be used to avoid circular imports between 2 - classes from 2 different packages. Imagine class P1 in package p1 has a - Ref to class P2 in package p2; and class P2 has another Ref to p1.P1 - (which is not the back Ref of the previous one: it is another, - independent Ref). - - In p1, you have - - from p2 import P2 - class P1: - ref1 = Ref(P2) - - Then, if you write the following in p2, python will complain because of a - circular import: - - from p1 import P1 - class P2: - ref2 = Ref(P1) - - The solution is to write this. In p1: - - from p2 import P2 - class P1: - ref1 = Ref(P2) - autoref(P1, P2.ref2) - - And, in p2: - class P2: - ref2 = Ref(None) - ''' - field.klass = klass - setattr(klass, field.back.attribute, field.back) -# ------------------------------------------------------------------------------ diff --git a/fields/search.py b/fields/search.py deleted file mode 100644 index 4260fb3..0000000 --- a/fields/search.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -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(''' - ''') - - # Search results, as a list (used by pxResult below) - pxResultList = Px(''' - - - - - - - - - - - - :zobj.appy().pxViewAsResult -
- - - ::ztool.truncateText(_(field.labelId)) - :tool.pxSortAndFilter - :tool.pxShowDetails -
:_('query_no_result')
- -
- -
- - - ''') - - # Search results, as a grid (used by pxResult below) - pxResultGrid = Px(''' - - - - -
- :field.pxRenderAsResult -
''') - - # Render search results - pxResult = Px(''' -
- - - - - - - -
:field.pxRender
- - -

- ::uiSearch.translated (:totalNumber) -  — -  :_('search_new') - -

- - - - - - - -
- :uiSearch.translatedDescr
-
:tool.pxNavigate
- - - :uiSearch.getPx(resultMode, klass) - - - :tool.pxNavigate -
- - - :_('query_no_result') -
- :_('search_new')
-
-
''') - - 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) -# ------------------------------------------------------------------------------ diff --git a/fields/string.py b/fields/string.py deleted file mode 100644 index 685274c..0000000 --- a/fields/string.py +++ /dev/null @@ -1,1070 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# 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 . - -# ------------------------------------------------------------------------------ -import re, random -from appy.gen.layout import Table -from appy.gen.indexer import XhtmlTextExtractor -from appy.fields import Field -from appy.px import Px -from appy.shared.data import countries -from appy.shared.xml_parser import XhtmlCleaner -from appy.shared.diff import HtmlDiff -from appy.shared import utils as sutils -import collections - -# ------------------------------------------------------------------------------ -digit = re.compile('[0-9]') -alpha = re.compile('[a-zA-Z0-9]') -letter = re.compile('[a-zA-Z]') -digits = '0123456789' -letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' -# No "0" or "1" that could be interpreted as letters "O" or "l". -passwordDigits = '23456789' -# No letters i, l, o (nor lowercase nor uppercase) that could be misread. -passwordLetters = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ' -emptyTuple = () - -# ------------------------------------------------------------------------------ -class Selection: - '''Instances of this class may be given as validator of a String, in order - to tell Appy that the validator is a selection that will be computed - dynamically.''' - def __init__(self, methodName): - # The p_methodName parameter must be the name of a method that will be - # called every time Appy will need to get the list of possible values - # for the related field. It must correspond to an instance method of - # the class defining the related field. This method accepts no argument - # and must return a list (or tuple) of pairs (lists or tuples): - # (id, text), where "id" is one of the possible values for the - # field, and "text" is the value as will be shown on the screen. - # You can use self.translate within this method to produce an - # internationalized version of "text" if needed. - self.methodName = methodName - - def getText(self, obj, value, field, language=None): - '''Gets the text that corresponds to p_value.''' - if language: - withTranslations = language - else: - withTranslations = True - vals = field.getPossibleValues(obj, ignoreMasterValues=True,\ - withTranslations=withTranslations) - for v, text in vals: - if v == value: return text - return value - -# ------------------------------------------------------------------------------ -class String(Field): - # Javascript files sometimes required by this type. Method String.getJs - # below determines when the files must be included. - cdnUrl = '//cdn.ckeditor.com/%s/%s/ckeditor.js' - - # Some predefined regular expressions that may be used as validators - c = re.compile - EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \ - '[a-zA-Z][a-zA-Z\.]*[a-zA-Z]') - ALPHANUMERIC = c('[\w-]+') - URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \ - '(([0-9]{1,5})?\/.*)?') - - # Possible values for "format" - LINE = 0 - TEXT = 1 - XHTML = 2 - PASSWORD = 3 - CAPTCHA = 4 - - # Default ways to render multingual fields - defaultLanguagesLayouts = { - LINE: {'edit': 'vertical', 'view': 'vertical'}, - TEXT: {'edit': 'horizontal', 'view': 'vertical'}, - XHTML: {'edit': 'horizontal', 'view': 'horizontal'}, - } - - # pxView part for formats String.LINE (but that are not selections) and - # String.PASSWORD (a fake view for String.PASSWORD and no view at all for - # String.CAPTCHA). - pxViewLine = Px(''' - - - - - ******** - - :value - - ::value - ''') - - # pxView part for format String.TEXT - pxViewText = Px(''' - -::value''') - - # pxView part for format String.XHTML - pxViewRich = Px(''' -
::value or '-'
- -
::value or '-'
- -
''') - - # PX displaying the language code and name besides the part of the - # multilingual field storing content in this language. - pxLanguage = Px(''' - - :lg.upper() - ''') - - pxMultilingual = Px(''' - - - :field.pxLanguage - -
:field.subPx[layoutType][fmt]
- - - - - :field.pxLanguage - -
:field.subPx[layoutType][fmt]
''') - - pxView = Px(''' - - - - - ::value -
  • ::sv
-
- - :field.subPx['view'][fmt] - - :field.pxMultilingual - -
''') - - # pxEdit part for formats String.LINE (but that are not selections), - # String.PASSWORD and String.CAPTCHA. - pxEditLine = Px(''' - - - :_('captcha_text', \ - mapping=field.getCaptchaChallenge(req.SESSION)) - ''') - - # pxEdit part for formats String.TEXT and String.XHTML - pxEditTextArea = Px(''' - - ''') - - pxEdit = Px(''' - - - - :field.subPx['edit'][fmt] - - :field.pxMultilingual - ''') - - pxCell = Px(''' - - :', '.join(value) - :field.pxView - ''') - - pxSearch = Px(''' - - - - - - - - - -
-
- - -

''') - - # Sub-PX to use according to String format. - subPx = { - 'edit': {LINE:pxEditLine, TEXT:pxEditTextArea, XHTML:pxEditTextArea, - PASSWORD:pxEditLine, CAPTCHA:pxEditLine}, - 'view': {LINE:pxViewLine, TEXT:pxViewText, XHTML:pxViewRich, - PASSWORD:pxViewLine, CAPTCHA:pxViewLine} - } - subPx['cell'] = subPx['view'] - - # Some predefined functions that may also be used as validators - @staticmethod - def _MODULO_97(obj, value, complement=False): - '''p_value must be a string representing a number, like a bank account. - this function checks that the 2 last digits are the result of - computing the modulo 97 of the previous digits. Any non-digit - character is ignored. If p_complement is True, it does compute the - complement of modulo 97 instead of modulo 97. p_obj is not used; - it will be given by the Appy validation machinery, so it must be - specified as parameter. The function returns True if the check is - successful.''' - if not value: return True - # First, remove any non-digit char - v = '' - for c in value: - if digit.match(c): v += c - # There must be at least 3 digits for performing the check - if len(v) < 3: return False - # Separate the real number from the check digits - number = int(v[:-2]) - checkNumber = int(v[-2:]) - # Perform the check - if complement: - return (97 - (number % 97)) == checkNumber - else: - # The check number can't be 0. In this case, we force it to be 97. - # This is the way Belgian bank account numbers work. I hope this - # behaviour is general enough to be implemented here. - mod97 = (number % 97) - if mod97 == 0: return checkNumber == 97 - else: return checkNumber == mod97 - @staticmethod - def MODULO_97(obj, value): return String._MODULO_97(obj, value) - @staticmethod - def MODULO_97_COMPLEMENT(obj, value): - return String._MODULO_97(obj, value, True) - BELGIAN_ENTERPRISE_NUMBER = MODULO_97_COMPLEMENT - - @staticmethod - def BELGIAN_NISS(obj, value): - '''Returns True if the NISS in p_value is valid.''' - if not value: return True - # Remove any non-digit from nrn - niss = sutils.keepDigits(value) - # NISS must be made of 11 numbers - if len(niss) != 11: return False - # When NRN begins with 0 or 1, it must be prefixed with number "2" for - # checking the modulo 97 complement. - nissForModulo = niss - if niss.startswith('0') or niss.startswith('1'): - nissForModulo = '2'+niss - # Check modulo 97 complement - return String.MODULO_97_COMPLEMENT(None, nissForModulo) - - @staticmethod - def IBAN(obj, value): - '''Checks that p_value corresponds to a valid IBAN number. IBAN stands - for International Bank Account Number (ISO 13616). If the number is - valid, the method returns True.''' - if not value: return True - # First, remove any non-digit or non-letter char - v = '' - for c in value: - if alpha.match(c): v += c - # Maximum size is 34 chars - if (len(v) < 8) or (len(v) > 34): return False - # 2 first chars must be a valid country code - if not countries.exists(v[:2].upper()): return False - # 2 next chars are a control code whose value must be between 0 and 96. - try: - code = int(v[2:4]) - if (code < 0) or (code > 96): return False - except ValueError: - return False - # Perform the checksum - vv = v[4:] + v[:4] # Put the 4 first chars at the end. - nv = '' - for c in vv: - # Convert each letter into a number (A=10, B=11, etc) - # Ascii code for a is 65, so A=10 if we perform "minus 55" - if letter.match(c): nv += str(ord(c.upper()) - 55) - else: nv += c - return int(nv) % 97 == 1 - - @staticmethod - def BIC(obj, value): - '''Checks that p_value corresponds to a valid BIC number. BIC stands - for Bank Identifier Code (ISO 9362). If the number is valid, the - method returns True.''' - if not value: return True - # BIC number must be 8 or 11 chars - if len(value) not in (8, 11): return False - # 4 first chars, representing bank name, must be letters - for c in value[:4]: - if not letter.match(c): return False - # 2 next chars must be a valid country code - if not countries.exists(value[4:6].upper()): return False - # Last chars represent some location within a country (a city, a - # province...). They can only be letters or figures. - for c in value[6:]: - if not alpha.match(c): return False - return True - - def __init__(self, validator=None, multiplicity=(0,1), default=None, - format=LINE, 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='', scolspan=1, swidth=None, sheight=None, - persist=True, transform='none', placeholder=None, - styles=('p','h1','h2','h3','h4'), allowImageUpload=True, - spellcheck=False, languages=('en',), languagesLayouts=None, - inlineEdit=False, view=None, xml=None): - # According to format, the widget will be different: input field, - # textarea, inline editor... Note that there can be only one String - # field of format CAPTCHA by page, because the captcha challenge is - # stored in the session at some global key. - self.format = format - self.isUrl = validator == String.URL - # When format is XHTML, the list of styles that the user will be able to - # select in the styles dropdown is defined hereafter. - self.styles = styles - # When format is XHTML, do we allow the user to upload images in it ? - self.allowImageUpload = allowImageUpload - # When format is XHTML, do we run the CK spellchecker ? - self.spellcheck = spellcheck - # If "languages" holds more than one language, the field will be - # multi-lingual and several widgets will allow to edit/visualize the - # field content in all the supported languages. The field is also used - # by the CK spell checker. - self.languages = languages - # When content exists in several languages, how to render them? Either - # horizontally (one below the other), or vertically (one besides the - # other). Specify here a dict whose keys are layouts ("edit", "view") - # and whose values are either "horizontal" or "vertical". - self.languagesLayouts = languagesLayouts - # When format in XHTML, can the field be inline-edited (ckeditor)? A - # method can be specified. - self.inlineEdit = inlineEdit - # The following field has a direct impact on the text entered by the - # user. It applies a transformation on it, exactly as does the CSS - # "text-transform" property. Allowed values are those allowed for the - # CSS property: "none" (default), "uppercase", "capitalize" or - # "lowercase". - self.transform = transform - # "placeholder", similar to the HTML attribute of the same name, allows - # to specify a short hint that describes the expected value of the input - # field. It is shown inside the input field and disappears as soon as - # the user encodes something in it. Works only for strings whose format - # is LINE. Does not work with IE < 10. You can specify a method here, - # that can, for example, return an internationalized value. - self.placeholder = placeholder - 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.isSelect = self.isSelection() - # If self.isSelect, self.sdefault must be a list of value(s). - if self.isSelect and not sdefault: - self.sdefault = [] - # Default width, height and maxChars vary according to String format - if width == None: - if format == String.TEXT: self.width = 60 - # This width corresponds to the standard width of an Appy page - elif format == String.XHTML: self.width = None - else: self.width = 30 - if height == None: - if format == String.TEXT: self.height = 5 - elif format == String.XHTML: self.height = None - elif self.isSelect: self.height = 4 - else: self.height = 1 - if maxChars == None: - if self.isSelect: pass - elif format == String.LINE: self.maxChars = 256 - elif format == String.TEXT: self.maxChars = 9999 - elif format == String.XHTML: self.maxChars = 99999 - elif format == String.PASSWORD: self.maxChars = 20 - self.filterable = self.indexed and not self.isSelect and \ - (self.format in (String.LINE, String.TEXT)) - self.swidth = self.swidth or self.width - self.sheight = self.sheight or self.height - self.checkParameters() - - def checkParameters(self): - '''Ensures this String is correctly defined.''' - error = None - if self.isMultilingual(None): - if self.isSelect: - error = "A selection field can't be multilingual." - elif self.format in (String.PASSWORD, String.CAPTCHA): - error = "A password or captcha field can't be multilingual." - if error: raise Exception(error) - - def isSelection(self): - '''Does the validator of this type definition define a list of values - into which the user must select one or more values?''' - res = True - if type(self.validator) in (list, tuple): - for elem in self.validator: - if not isinstance(elem, str): - res = False - break - else: - if not isinstance(self.validator, Selection): - res = False - return res - - def getSelectSize(self, isMultiple): - '''When this field renders as a selection list, get the value of its - "size" attribute.''' - if not isMultiple: return 1 - if isinstance(self.height, int): return self.height - # "height" can be defined as a string. In this case it is used to define - # height via a attribute "style", not "size". - return '' - - def getSelectStyle(self, isMultiple): - '''When thiss field renders as a selection list, get the value of its - "style" attribute.''' - if not isMultiple or not isinstance(self.height, str): return '' - return 'height: %s' % self.height - - def isMultilingual(self, obj, dontKnow=False): - '''Is this field multilingual ? If we don't know, say p_dontKnow.''' - # In the following case, impossible to know: we say no. - if not obj: - if callable(self.languages): return dontKnow - else: return len(self.languages) > 1 - return len(self.getAttribute(obj, 'languages')) > 1 - - def getDefaultLayouts(self): - '''Returns the default layouts for this type. Default layouts can vary - acccording to format, multiplicity or history.''' - if self.format == String.TEXT: - return {'view': 'l-f', 'edit': 'lrv-d-f'} - elif self.format == String.XHTML: - if self.historized: - # self.historized can be a method or a boolean. If it is a - # method, it means that under some condition, historization will - # be enabled. So we come here also in this case. - view = 'lc-f' - else: - view = 'l-f' - return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'} - elif self.isMultiValued(): - return {'view': 'l-f', 'edit': 'lrv-f'} - - def getLanguagesLayout(self, layoutType): - '''Gets the way to render a multilingual field on p_layoutType.''' - if self.languagesLayouts and (layoutType in self.languagesLayouts): - return self.languagesLayouts[layoutType] - # Else, return a default value that depends of the format. - return String.defaultLanguagesLayouts[self.format][layoutType] - - def getValue(self, obj): - # Cheat if this field represents p_obj's state. - if self.name == 'state': return obj.State() - value = Field.getValue(self, obj) - if not value: - if self.isMultiValued(): return emptyTuple - else: return value - if isinstance(value, str) and self.isMultiValued(): - value = [value] - elif isinstance(value, tuple): - value = list(value) - return value - - def getCopyValue(self, obj): - '''If the value is mutable (ie, a dict for a multilingual field), return - a copy of it instead of the value stored in the database.''' - res = self.getValue(obj) - if isinstance(res, dict): res = res.copy() - return res - - def valueIsInRequest(self, obj, request, name, layoutType): - # If we are on the search layout, p_obj, if not None, is certainly not - # the p_obj we want here (can be a home object). - if layoutType == 'search': - return Field.valueIsInRequest(self, obj, request, name, layoutType) - languages = self.getAttribute(obj, 'languages') - if len(languages) == 1: - return Field.valueIsInRequest(self, obj, request, name, layoutType) - # Is is sufficient to check that at least one of the language-specific - # values is in the request. - return request.has_key('%s_%s' % (name, languages[0])) - - def getRequestValue(self, obj, requestName=None): - '''The request value may be multilingual.''' - request = obj.REQUEST - name = requestName or self.name - languages = self.getAttribute(obj, 'languages') - # A unilingual field. - if len(languages) == 1: return request.get(name, None) - # A multilingual field. - res = {} - for language in languages: - res[language] = request.get('%s_%s' % (name, language), None) - return res - - def isEmptyValue(self, obj, value): - '''Returns True if the p_value must be considered as an empty value''' - if not isinstance(obj, dict): - return Field.isEmptyValue(self, obj, value) - # p_value is a dict of multilingual values. For such values, as soon - # as a value is not empty for a given language, the whole value is - # considered as not being empty. - for v in value.itervalues(): - if not Field.isEmptyValue(self, obj, v): return - - def isCompleteValue(self, obj, value): - '''Returns True if the p_value must be considered as complete. For a - unilingual field, being complete simply means not being empty. For a - multilingual field, being complete means that a value is present for - every language.''' - if not self.isMultilingual(obj): - return Field.isCompleteValue(self, obj, value) - # As soon as a given language value is empty, the global value is not - # complete. - if not value: return True - for v in value.itervalues(): - if Field.isEmptyValue(self, obj, v): return - return True - - def getDiffValue(self, obj, value, language): - '''Returns a version of p_value that includes the cumulative diffs - between successive versions. If the field is non-multilingual, it - must be called with p_language being None. Else, p_language - identifies the language-specific part we will work on.''' - res = None - lastEvent = None - for event in obj.workflow_history.values()[0]: - if event['action'] != '_datachange_': continue - if name not in event['changes']: continue - if res == None: - # We have found the first version of the field - res = event['changes'][name][0] or '' - else: - # We need to produce the difference between current result and - # this version. - iMsg, dMsg = obj.getHistoryTexts(lastEvent) - thisVersion = event['changes'][name][0] or '' - comparator = HtmlDiff(res, thisVersion, iMsg, dMsg) - res = comparator.get() - lastEvent = event - if not lastEvent: - # There is no diff to show for this p_language. - return value - # Now we need to compare the result with the current version. - iMsg, dMsg = obj.getHistoryTexts(lastEvent) - comparator = HtmlDiff(res, value or '', iMsg, dMsg) - return comparator.get() - - def getUnilingualFormattedValue(self, obj, value, layoutType='view', - showChanges=False, userLanguage=None, language=None): - '''If no p_language is specified, this method is called by - m_getFormattedValue for getting a non-multilingual value (ie, in - most cases). Else, this method returns a formatted value for the - p_language-specific part of a multilingual value.''' - if Field.isEmptyValue(self, obj, value) and not showChanges: return '' - res = value - if self.isSelect: - if isinstance(self.validator, Selection): - # Value(s) come from a dynamic vocabulary - val = self.validator - if self.isMultiValued(): - return [val.getText(obj, v, self, language=userLanguage) \ - for v in value] - else: - return val.getText(obj, value, self, language=userLanguage) - else: - # Value(s) come from a fixed vocabulary whose texts are in - # i18n files. - _ = obj.translate - if self.isMultiValued(): - res = [_('%s_list_%s' % (self.labelId, v), \ - language=userLanguage) for v in value] - else: - res = _('%s_list_%s' % (self.labelId, value), \ - language=userLanguage) - elif (self.format == String.XHTML) and showChanges: - # Compute the successive changes that occurred on p_value - res = self.getDiffValue(obj, res, language) - elif self.format == String.TEXT: - if layoutType in ('view', 'cell'): - res = obj.formatText(res, format='html') - # If value starts with a carriage return, add a space; else, it will - # be ignored. - if isinstance(res, str) and \ - (res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res - return res - - def getFormattedValue(self, obj, value, layoutType='view', - showChanges=False, language=None): - '''Be careful: p_language represents the UI language, while "languages" - below represents the content language(s) of this field. p_language - can be used, ie, to translate a Selection value.''' - languages = self.getAttribute(obj, 'languages') - if len(languages) == 1: - return self.getUnilingualFormattedValue(obj, value, layoutType, - showChanges, userLanguage=language) - # Return the dict of values whose individual, language-specific values - # have been formatted via m_getUnilingualFormattedValue. - if not value and not showChanges: return value - res = {} - for lg in languages: - if not value: val = '' - else: val = value[lg] - res[lg] = self.getUnilingualFormattedValue(obj, val, layoutType, - showChanges, language=lg) - return res - - def getShownValue(self, obj, value, layoutType='view', - showChanges=False, language=None): - '''Be careful: p_language represents the UI language, while "languages" - below represents the content language(s) of this field. For a - multilingual field, this method only shows one specific language - part.''' - languages = self.getAttribute(obj, 'languages') - if len(languages) == 1: - return self.getUnilingualFormattedValue(obj, value, layoutType, - showChanges, userLanguage=language) - if not value: return value - # Try to propose the part that is in the user language, or the part of - # the first content language else. - lg = obj.getUserLanguage() - if lg not in value: lg = languages[0] - return self.getUnilingualFormattedValue(obj, value[lg], layoutType, - showChanges, language=lg) - - def extractText(self, value): - '''Extracts pure text from XHTML p_value.''' - return XhtmlTextExtractor(raiseOnError=False).parse('

%s

' % value) - - def getValidCatalogValue(self, value, forSearch): - '''p_value is the new value we want to index in the catalog, for this - field, for some object. p_value as is may not be an acceptable value - for the catalog: if it represents some empty value, like an empty - string, None or an empty tuple, instead of using it, the catalog will - keep the previously catalogued value! For those cases, this method - produces "empty" values that will really overwrite previous ones.''' - # Ugly catalog: if I give an empty tuple as index value, it keeps the - # previous value. If I give him a tuple containing an empty string, it - # is ok. - if isinstance(value, tuple) and not value: - value = forSearch and ' ' or ('',) - # Ugly catalog: if value is an empty string or None, it keeps the - # previous index value. - elif value in (None, ''): return ' ' - return value - - def getIndexValue(self, obj, forSearch=False): - '''Pure text must be extracted from rich content; multilingual content - must be concatenated.''' - # Must we produce an index value? - if not self.getAttribute(obj, 'mustIndex'): - return self.getValidCatalogValue(None, forSearch) - isXhtml = self.format == String.XHTML - if self.isMultilingual(obj): - res = self.getValue(obj) - if res: - vals = [] - for v in res.itervalues(): - if isinstance(v, unicode): v = v.encode('utf-8') - if isXhtml: vals.append(self.extractText(v)) - else: vals.append(v) - res = ' '.join(vals) - else: - res = Field.getIndexValue(self, obj, forSearch) - if res and isXhtml: res = self.extractText(res) - return self.getValidCatalogValue(res, forSearch) - - def getPossibleValues(self, obj, withTranslations=False, - withBlankValue=False, className=None, - ignoreMasterValues=False): - '''Returns the list of possible values for this field (only for fields - with self.isSelect=True). If p_withTranslations is True, instead of - returning a list of string values, the result is a list of tuples - (s_value, s_translation). Moreover, p_withTranslations can hold a - given language: in this case, this language is used instead of the - user language. If p_withBlankValue is True, a blank value is - prepended to the list, excepted if the type is multivalued. If - p_className is given, p_obj is the tool and, if we need an instance - of p_className, we will need to use obj.executeQuery to find one.''' - if not self.isSelect: raise Exception('This field is not a selection.') - # Get the user language for translations, from "withTranslations". - lg = isinstance(withTranslations, str) and withTranslations or None - req = obj.REQUEST - if ('masterValues' in req) and not ignoreMasterValues: - # Get possible values from self.masterValue - masterValues = req['masterValues'] - if '*' in masterValues: masterValues = masterValues.split('*') - values = self.masterValue(obj.appy(), masterValues) - if not withTranslations: res = values - else: - res = [] - for v in values: - res.append( (v, self.getFormattedValue(obj,v,language=lg)) ) - else: - # If this field is an ajax-updatable slave, no need to compute - # possible values: it will be overridden by method self.masterValue - # by a subsequent ajax request (=the "if" statement above). - if self.masterValue and isinstance(self.masterValue, collections.Callable) and \ - not ignoreMasterValues: return [] - if isinstance(self.validator, Selection): - # We need to call self.methodName for getting the (dynamic) - # values. If methodName begins with _appy_, it is a special Appy - # method: we will call it on the Mixin (=p_obj) directly. Else, - # it is a user method: we will call it on the wrapper - # (p_obj.appy()). Some args can be hidden into p_methodName, - # separated with stars, like in this example: method1*arg1*arg2. - # Only string params are supported. - methodName = self.validator.methodName - # Unwrap parameters if any. - if methodName.find('*') != -1: - elems = methodName.split('*') - methodName = elems[0] - args = elems[1:] - else: - args = () - # On what object must we call the method that will produce the - # values? - if methodName.startswith('tool:'): - obj = obj.getTool() - methodName = methodName[5:] - else: - # We must call on p_obj. But if we have something in - # p_className, p_obj is the tool and not an instance of - # p_className as required. So find such an instance. - if className: - brains = obj.executeQuery(className, maxResults=1, - brainsOnly=True) - if brains: - obj = brains[0].getObject() - # Do we need to call the method on the object or on the wrapper? - if methodName.startswith('_appy_'): - exec('res = obj.%s(*args)' % methodName) - else: - exec('res = obj.appy().%s(*args)' % methodName) - if not withTranslations: res = [v[0] for v in res] - elif isinstance(res, list): res = res[:] - else: - # The list of (static) values is directly given in - # self.validator. - res = [] - for value in self.validator: - label = '%s_list_%s' % (self.labelId, value) - if withTranslations: - res.append( (value, obj.translate(label, language=lg)) ) - else: - res.append(value) - if withBlankValue and not self.isMultiValued(): - # Create the blank value to insert at the beginning of the list - if withTranslations: - blankValue = ('', obj.translate('choose_a_value', language=lg)) - else: - blankValue = '' - # Insert the blank value in the result - if isinstance(res, tuple): - res = (blankValue,) + res - else: - res.insert(0, blankValue) - return res - - def validateValue(self, obj, value): - if self.format == String.CAPTCHA: - challenge = obj.REQUEST.SESSION.get('captcha', None) - # Compute the challenge minus the char to remove - i = challenge['number']-1 - text = challenge['text'][:i] + challenge['text'][i+1:] - if value != text: - return obj.translate('bad_captcha') - elif self.isSelect: - # Check that the value is among possible values - possibleValues = self.getPossibleValues(obj,ignoreMasterValues=True) - if isinstance(value, str): - error = value not in possibleValues - else: - error = False - for v in value: - if v not in possibleValues: - error = True - break - if error: return obj.translate('bad_select_value') - - def applyTransform(self, value): - '''Applies a transform as required by self.transform on single - value p_value.''' - if self.transform in ('uppercase', 'lowercase'): - # For those transforms, I will remove any accent, because, most of - # the time, if the user wants to apply such effect, it is for ease - # of data manipulation, so I guess without accent. - value = sutils.normalizeString(value, usage='noAccents') - # Apply the transform - if self.transform == 'lowercase': return value.lower() - elif self.transform == 'uppercase': return value.upper() - elif self.transform == 'capitalize': return value.capitalize() - return value - - def getUnilingualStorableValue(self, obj, value): - isString = isinstance(value, str) - isEmpty = Field.isEmptyValue(self, obj, value) - # Apply transform if required - if isString and not isEmpty and (self.transform != 'none'): - value = self.applyTransform(value) - # Clean XHTML strings - if not isEmpty and (self.format == String.XHTML): - # When image upload is allowed, ckeditor inserts some "style" attrs - # (ie for image size when images are resized). So in this case we - # can't remove style-related information. - try: - value = XhtmlCleaner(keepStyles=False).clean(value) - except XhtmlCleaner.Error, e: - # Errors while parsing p_value can't prevent the user from - # storing it. - pass - # Clean TEXT strings - if not isEmpty and (self.format == String.TEXT): - value = value.replace('\r', '') - # Truncate the result if longer than self.maxChars - if isString and self.maxChars and (len(value) > self.maxChars): - value = value[:self.maxChars] - # Get a multivalued value if required. - if value and self.isMultiValued() and \ - (type(value) not in sutils.sequenceTypes): - value = [value] - return value - - def getStorableValue(self, obj, value): - languages = self.getAttribute(obj, 'languages') - if len(languages) == 1: - return self.getUnilingualStorableValue(obj, value) - # A multilingual value is stored as a dict whose keys are ISO 2-letters - # language codes and whose values are strings storing content in the - # language ~{s_language: s_content}~. - if not value: return - for lg in languages: - value[lg] = self.getUnilingualStorableValue(obj, value[lg]) - return value - - def store(self, obj, value): - '''Stores p_value on p_obj for this field.''' - languages = self.getAttribute(obj, 'languages') - if (len(languages) > 1) and value and \ - (not isinstance(value, dict) or (len(value) != len(languages))): - raise Exception('Multilingual field "%s" accepts a dict whose '\ - 'keys are in field.languages and whose ' \ - 'values are strings.' % self.name) - Field.store(self, obj, value) - - def storeFromAjax(self, obj): - '''Stores the new field value from an Ajax request, or do nothing if - the action was canceled.''' - rq = obj.REQUEST - if rq.get('cancel') == 'True': return - requestValue = rq['fieldContent'] - # Remember previous value if the field is historized - isHistorized = self.getAttribute(obj, 'historized') - previousData = None - if isHistorized: previousData = obj.rememberPreviousData([self]) - if self.isMultilingual(obj): - if isHistorized: - # We take a copy of previousData because it is mutable (dict) - prevData = previousData[self.name] - if prevData != None: prevData = prevData.copy() - previousData[self.name] = prevData - # We get a partial value, for one language only - language = rq['languageOnly'] - v = self.getUnilingualStorableValue(obj, requestValue) - getattr(obj.aq_base, self.name)[language] = v - part = ' (%s)' % language - else: - self.store(obj, self.getStorableValue(obj, requestValue)) - part = '' - # Update the object history when relevant - if isHistorized and previousData: obj.historizeData(previousData) - # Update obj's last modification date - from DateTime import DateTime - obj.modified = DateTime() - obj.reindex() - obj.log('ajax-edited %s%s on %s.' % (self.name, part, obj.id)) - - def getIndexType(self): - '''Index type varies depending on String parameters.''' - # If String.isSelect, be it multivalued or not, we define a ListIndex: - # this way we can use AND/OR operator. - if self.isSelect: - return 'ListIndex' - elif self.format == String.TEXT: - return 'TextIndex' - elif self.format == String.XHTML: - return 'XhtmlIndex' - return Field.getIndexType(self) - - def getJs(self, layoutType, res, config): - if (self.format == String.XHTML) and (layoutType in ('edit', 'view')): - # Compute the URL to ckeditor CDN - ckUrl = String.cdnUrl % (config.ckVersion, config.ckDistribution) - if ckUrl not in res: res.append(ckUrl) - - def getCaptchaChallenge(self, session): - '''Returns a Captcha challenge in the form of a dict. At key "text", - value is a string that the user will be required to re-type, but - without 1 character whose position is at key "number". The challenge - is stored in the p_session, for the server-side subsequent check.''' - length = random.randint(5, 9) # The length of the challenge to encode - number = random.randint(1, length) # The position of the char to remove - text = '' # The challenge the user needs to type (minus one char) - for i in range(length): - j = random.randint(0, 1) - chars = (j == 0) and passwordDigits or passwordLetters - # Choose a char - text += chars[random.randint(0,len(chars)-1)] - res = {'text': text, 'number': number} - session['captcha'] = res - return res - - def generatePassword(self): - '''Generates a password (we recycle here the captcha challenge - generator).''' - return self.getCaptchaChallenge({})['text'] - - ckLanguages = {'en': 'en_US', 'pt': 'pt_BR', 'da': 'da_DK', 'nl': 'nl_NL', - 'fi': 'fi_FI', 'fr': 'fr_FR', 'de': 'de_DE', 'el': 'el_GR', - 'it': 'it_IT', 'nb': 'nb_NO', 'pt': 'pt_PT', 'es': 'es_ES', - 'sv': 'sv_SE'} - def getCkLanguage(self, obj, language): - '''Gets the language for CK editor SCAYT. p_language is one of - self.languages if the field is multilingual, None else. If p_language - is not supported by CK, we use english.''' - if not language: - language = self.getAttribute(obj, 'languages')[0] - if language in self.ckLanguages: return self.ckLanguages[language] - return 'en_US' - - def getCkParams(self, obj, language): - '''Gets the base params to set on a rich text field''' - base = obj.getTool().getSiteUrl() - ckAttrs = {'customConfig': '%s/ui/ckeditor/config.js' % base, - 'contentsCss': '%s/ui/ckeditor/contents.css' % base, - 'stylesSet': '%s/ui/ckeditor/styles.js' % base, - 'toolbar': 'Appy', 'format_tags': ';'.join(self.styles), - 'scayt_sLang': self.getCkLanguage(obj, language)} - if self.width: ckAttrs['width'] = self.width - if self.height: ckAttrs['height'] = self.height - if self.spellcheck: ckAttrs['scayt_autoStartup'] = True - if self.allowImageUpload: - ckAttrs['filebrowserUploadUrl'] = '%s/upload' % obj.absolute_url() - ck = [] - for k, v in ckAttrs.items(): - if isinstance(v, int): sv = str(v) - if isinstance(v, bool): sv = str(v).lower() - else: sv = '"%s"' % v - ck.append('%s: %s' % (k, sv)) - return ', '.join(ck) - - def getJsInit(self, obj, language): - '''Gets the Javascript init code for displaying a rich editor for this - field (rich field only). If the field is multilingual, we must init - the rich text editor for a given p_language (among self.languages). - Else, p_languages is None.''' - name = not language and self.name or ('%s_%s' % (self.name, language)) - return 'CKEDITOR.replace("%s", {%s})' % \ - (name, self.getCkParams(obj, language)) - - def getJsInlineInit(self, obj, name, language): - '''Gets the Javascript init code for enabling inline edition of this - field (rich text only). If the field is multilingual, the current - p_language is given and p_name includes it. Else, p_language is - None.''' - uid = obj.id - fieldName = language and name.rsplit('_',1)[0] or name - lg = language or '' - return "CKEDITOR.disableAutoInline = true;\n" \ - "CKEDITOR.inline('%s_%s_ck', {%s, on: {blur: " \ - "function( event ) { var content = event.editor.getData(); " \ - "doInlineSave('%s','%s','%s',content,'%s')}}})" % \ - (uid, name, self.getCkParams(obj, language), uid, fieldName, - obj.absolute_url(), lg) - - def isSelected(self, obj, fieldName, vocabValue, dbValue): - '''When displaying a selection box (only for fields with a validator - being a list), must the _vocabValue appear as selected? p_fieldName - is given and used instead of field.name because it may be a a fake - name containing a row number from a field within a list field.''' - rq = obj.REQUEST - # Get the value we must compare (from request or from database) - if fieldName in rq: - compValue = rq.get(fieldName) - else: - compValue = dbValue - # Compare the value - if type(compValue) in sutils.sequenceTypes: - return vocabValue in compValue - return vocabValue == compValue -# ------------------------------------------------------------------------------ diff --git a/fields/workflow.py b/fields/workflow.py deleted file mode 100644 index 258f629..0000000 --- a/fields/workflow.py +++ /dev/null @@ -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 . -# ------------------------------------------------------------------------------ -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(''' - - - - - - ''') - - 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) -# ------------------------------------------------------------------------------ diff --git a/gen/__init__.py b/gen/__init__.py deleted file mode 100644 index 698dbc7..0000000 --- a/gen/__init__.py +++ /dev/null @@ -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 . - -# ------------------------------------------------------------------------------ -# 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. -# ------------------------------------------------------------------------------ diff --git a/gen/descriptors.py b/gen/descriptors.py deleted file mode 100644 index f289a13..0000000 --- a/gen/descriptors.py +++ /dev/null @@ -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 '' % 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 '' % (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('
') - 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 -# ------------------------------------------------------------------------------ diff --git a/gen/generator.py b/gen/generator.py deleted file mode 100644 index 0b4c09c..0000000 --- a/gen/generator.py +++ /dev/null @@ -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 '' % (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( - '' % 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) -# ------------------------------------------------------------------------------ diff --git a/gen/indexer.py b/gen/indexer.py deleted file mode 100644 index d54317d..0000000 --- a/gen/indexer.py +++ /dev/null @@ -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('

%s

' % 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 -# ------------------------------------------------------------------------------ diff --git a/gen/installer.py b/gen/installer.py deleted file mode 100644 index 13f25c3..0000000 --- a/gen/installer.py +++ /dev/null @@ -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 = '' - -# 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('
For security reasons, your session has ' \ - 'expired.
') - -# ------------------------------------------------------------------------------ -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 -# ------------------------------------------------------------------------------ diff --git a/gen/layout.py b/gen/layout.py deleted file mode 100644 index e2caa51..0000000 --- a/gen/layout.py +++ /dev/null @@ -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: [*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 -# ------------------------------------------------------------------------------ diff --git a/gen/mail.py b/gen/mail.py deleted file mode 100644 index c223d60..0000000 --- a/gen/mail.py +++ /dev/null @@ -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') -# ------------------------------------------------------------------------------ diff --git a/gen/migrator.py b/gen/migrator.py deleted file mode 100644 index 0fc3829..0000000 --- a/gen/migrator.py +++ /dev/null @@ -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) -# ------------------------------------------------------------------------------ diff --git a/gen/mixins/TestMixin.py b/gen/mixins/TestMixin.py deleted file mode 100644 index 7ad0ba0..0000000 --- a/gen/mixins/TestMixin.py +++ /dev/null @@ -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) -# ------------------------------------------------------------------------------ diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py deleted file mode 100644 index 11bc99c..0000000 --- a/gen/mixins/ToolMixin.py +++ /dev/null @@ -1,1307 +0,0 @@ -# ------------------------------------------------------------------------------ -import os, os.path, sys, re, time, random, types, base64 -from appy import Object -import appy.gen -from appy.gen import Search, UiSearch, String, Page -from appy.gen.layout import ColumnLayout -from appy.gen import utils as gutils -from appy.gen.mixins import BaseMixin -from appy.gen.wrappers import AbstractWrapper -from appy.gen.descriptors import ClassDescriptor -from appy.gen.navigate import Siblings -from appy.shared import mimeTypes -from appy.shared import utils as sutils -from appy.shared.data import languages -from appy.shared.ldap_connector import LdapConnector -import collections -try: - from AccessControl.ZopeSecurityPolicy import _noroles -except ImportError: - _noroles = [] - -# Global JS internationalized messages that will be computed in every page ----- -jsMessages = ('no_elem_selected', 'action_confirm', 'save_confirm', - 'warn_leave_form') - -# Error messages --------------------------------------------------------------- -USER_NOT_FOUND = 'User %s not found. Probably a problem implying several ' \ - 'Appy apps put behind the same domain name or dev machine.' - -# ------------------------------------------------------------------------------ -class ToolMixin(BaseMixin): - _appy_meta_type = 'Tool' - xhtmlEncoding = 'text/html;charset=UTF-8' - - def getPortalType(self, metaTypeOrAppyClass): - '''Returns the name of the portal_type that is based on - p_metaTypeOrAppyType.''' - appName = self.getProductConfig().PROJECTNAME - res = metaTypeOrAppyClass - if not isinstance(metaTypeOrAppyClass, str): - res = gutils.getClassName(metaTypeOrAppyClass, appName) - if res.find('_wrappers') != -1: - elems = res.split('_') - res = '%s%s' % (elems[1], elems[4]) - if res in ('User', 'Group', 'Translation'): res = appName + res - return res - - def home(self): - '''Returns the content of px ToolWrapper.pxHome.''' - tool = self.appy() - return tool.pxHome({'obj': None, 'tool': tool}) - - def query(self): - '''Returns the content of px ToolWrapper.pxQuery.''' - tool = self.appy() - return tool.pxQuery({'obj': None, 'tool': tool}) - - def search(self): - '''Returns the content of px ToolWrapper.pxSearch.''' - tool = self.appy() - return tool.pxSearch({'obj': None, 'tool': tool}) - - def getHomePage(self): - '''Return the home page when a user hits the app.''' - # If the app defines a method "getHomePage", call it. - tool = self.appy() - url = None - try: - url = tool.getHomePage() - except AttributeError: - pass - if not url: - # Bring Managers to the config, lead others to pxHome - user = self.getUser() - if not user: - raise Exception(USER_NOT_FOUND % self.identifyUser()[0]) - if user.has_role('Manager'): - url = self.absolute_url() - else: - url = '%s/home' % self.absolute_url() - return url - - def getHomeObject(self, inPopup=False): - '''The concept of "home object" is the object where the user must "be", - even if he is "nowhere". For example, if the user is on a search - screen, there is no contextual object. In this case, if we have a - home object for him, we will use it as contextual object, and its - portlet menu will nevertheless appear: the user will not have the - feeling of being lost.''' - # If we are in the popup, we do not want any home object in the way. - if inPopup: return - # If the app defines a method "getHomeObject", call it. - try: - return self.appy().getHomeObject() - except AttributeError: - # For managers, the home object is the config. For others, there is - # no default home object. - if self.getUser().has_role('Manager'): return self.appy() - - def getCatalog(self): - '''Returns the catalog object.''' - return self.getParentNode().catalog - - def getCatalogValue(self, obj, indexName): - '''Get, for p_obj, the value stored in the catalog for the index - named p_indexName.''' - catalogBrain = self.getObject(obj.id, brain=True) - catalog = self.getApp().catalog - index = catalog.Indexes[indexName] - indexType = index.getTagName() - if indexType == 'ZCTextIndex': - # Zope bug: the lexicon can't be retrieved correctly - index._v_lexicon = getattr(catalog, index.lexicon_id) - res = index.getEntryForObject(catalogBrain.getRID()) - if indexType == 'DateIndex': - # The index value is a number. Add a DateTime representation too. - from appy.fields.date import getDateFromIndexValue - res = '%d (%s)' % (res, getDateFromIndexValue(res)) - return res - - def getApp(self): - '''Returns the root Zope object.''' - return self.getPhysicalRoot() - - def getSiteUrl(self): - '''Returns the absolute URL of this site.''' - return self.getApp().absolute_url() - - def getIncludeUrl(self, name, bg=False): - '''Gets the full URL of an external resource, like an image, a - Javascript or a CSS file, named p_name. If p_bg is True, p_name is - an image that is meant to be used in a "style" attribute for defining - the background image of some XHTML tag.''' - # If no extension is found in p_name, we suppose it is a png image. - if '.' not in name: name += '.png' - url = '%s/ui/%s' % (self.getPhysicalRoot().absolute_url(), name) - if not bg: return url - return 'background-image: url(%s)' % url - - def doPod(self): - '''Performs an action linked to a pod field: generate, freeze, - unfreeze... a document from a pod field.''' - rq = self.REQUEST - # Get the object that is the target of this action. - obj = self.getObject(rq.get('objectUid'), appy=True) - return obj.getField(rq.get('fieldName')).onUiRequest(obj, rq) - - def getAppName(self): - '''Returns the name of the application.''' - return self.getProductConfig().PROJECTNAME - - def getPath(self, path): - '''Returns the folder or object whose absolute path p_path.''' - res = self.getPhysicalRoot() - if path == '/': return res - path = path[1:] - if '/' not in path: return res._getOb(path) # For performance - for elem in path.split('/'): res = res._getOb(elem) - return res - - def showLanguageSelector(self): - '''We must show the language selector if the app config requires it and - it there is more than 2 supported languages. Moreover, on some pages, - switching the language is not allowed.''' - cfg = self.getProductConfig(True) - if not cfg.languageSelector: return - if len(cfg.languages) < 2: return - page = self.REQUEST.get('ACTUAL_URL').split('/')[-1] - return page not in ('edit', 'query', 'search', 'do') - - def showForgotPassword(self): - '''We must show link "forgot password?" when the app requires it.''' - return self.getProductConfig(True).activateForgotPassword - - def getLanguages(self): - '''Returns the supported languages. First one is the default.''' - return self.getProductConfig(True).languages - - def getLanguageName(self, code, lowerize=False): - '''Gets the language name (in this language) from a 2-chars language - p_code.''' - res = languages.get(code)[2] - if not lowerize: return res - return res.lower() - - def changeLanguage(self): - '''Sets the language cookie with the new desired language code that is - in request["language"].''' - rq = self.REQUEST - rq.RESPONSE.setCookie('_ZopeLg', rq['language'], path='/') - return self.goto(rq['HTTP_REFERER']) - - def flipLanguageDirection(self, align, dir): - '''According to language direction p_dir ('ltr' or 'rtl'), this method - turns p_align from 'left' to 'right' (or the inverse) when - required.''' - if dir == 'ltr': return align - if align == 'left': return 'right' - if align == 'right': return 'left' - return align - - def getGlobalCssJs(self, dir): - '''Returns the list of CSS and JS files to include in the main template. - The method ensures that appy.css and appy.js come first. If p_dir - (=language *dir*rection) is "rtl" (=right-to-left), the stylesheet - for rtl languages is also included.''' - names = self.getPhysicalRoot().ui.objectIds('File') - # The single Appy Javascript file - names.remove('appy.js'); names.insert(0, 'appy.js') - # CSS changes for left-to-right languages - names.remove('appyrtl.css') - if dir == 'rtl': names.insert(0, 'appyrtl.css') - names.remove('appy.css'); names.insert(0, 'appy.css') - return names - - def consumeMessages(self): - '''Returns the list of messages to show to a web page and clean it in - the session.''' - rq = self.REQUEST - res = rq.SESSION.get('messages', '') - if res: - del rq.SESSION['messages'] - res = ' '.join([m[1] for m in res]) - return res - - def getRootClasses(self): - '''Returns the list of root classes for this application''' - cfg = self.getProductConfig().appConfig - rootClasses = cfg.rootClasses - if rootClasses == None: return [] # No root class at all - if not rootClasses: - # We consider every class as being a root class - rootClasses = self.getProductConfig().appClassNames - return [self.getAppyClass(k) for k in rootClasses] - - def getSearchInfo(self, className, refInfo=None): - '''Returns, as an object: - - the list of searchable fields (some among all indexed fields); - - the number of columns for layouting those fields.''' - fields = [] - if refInfo: - # The search is triggered from a Ref field - refObject, fieldName = self.getRefInfo(refInfo) - refField = refObject.getAppyType(fieldName) - fieldNames = refField.queryFields or () - nbOfColumns = refField.queryNbCols - else: - # The search is triggered from an app-wide search - klass = self.getAppyClass(className) - fieldNames = getattr(klass, 'searchFields', None) - if callable(fieldNames): fieldNames = fieldNames(self.appy()) - if not fieldNames: - # Gather all the indexed fields on this class - fieldNames = [f.name for f in self.getAllAppyTypes(className) \ - if f.indexed] - # Put SearchableText in front of the list and ensure "title" - # is not there - if 'title' in fieldNames: fieldNames.remove('title') - st = 'SearchableText' - if st in fieldNames: fieldNames.remove(st) - fieldNames.insert(0, st) - nbOfColumns = getattr(klass, 'numberOfSearchColumns', 3) - for name in fieldNames: - field = self.getAppyType(name, className=className) - fields.append(field) - return Object(fields=fields, nbOfColumns=nbOfColumns) - - def showPortlet(self, obj, layoutType): - '''When must the portlet be shown? p_obj and p_layoutType can be None - if we are not browing any objet (ie, we are on the home page).''' - # Not on 'edit' pages or if there is no root class - classes = self.getProductConfig(True).rootClasses - if not classes or (layoutType == 'edit'): return - res = True - if obj and hasattr(obj, 'showPortlet'): - res = obj.showPortlet() - else: - tool = self.appy() - if hasattr(tool, 'showPortletAt'): - res = tool.showPortletAt(self.REQUEST['ACTUAL_URL']) - return res - - def getObject(self, uid, appy=False, brain=False): - '''Allows to retrieve an object from its p_uid.''' - res = self.getPhysicalRoot().catalog(UID=uid) - if not res: return - res = res[0] - if brain: return res - res = res._unrestrictedGetObject() - if not appy: return res - return res.appy() - - def getAllowedValue(self): - '''Gets, for the current user, the value of index "Allowed".''' - user = self.getUser() - # Get the user roles. If we do not make a copy of the list here, we will - # really add user logins among user roles! - res = user.getRoles()[:] - # Get the user logins - if user.login != 'anon': - for login in user.getLogins(): - res.append('user:%s' % login) - return res - - def executeQuery(self, className, searchName=None, startNumber=0, - search=None, remember=False, brainsOnly=False, - maxResults=None, noSecurity=False, sortBy=None, - sortOrder='asc', filterKey=None, filterValue=None, - refObject=None, refField=None): - '''Executes a query on instances of a given p_className in the catalog. - If p_searchName is specified, it corresponds to: - 1) a search defined on p_className: additional search criteria - will be added to the query, or; - 2) "customSearch": in this case, additional search criteria will - also be added to the query, but those criteria come from the - session (in key "searchCriteria") and came from pxSearch. - - We will retrieve objects from p_startNumber. If p_search is defined, - it corresponds to a custom Search instance (instead of a predefined - named search like in p_searchName). If both p_searchName and p_search - are given, p_search is ignored. - - This method returns a list of objects in the form of an instance of - SomeObjects (see in appy.gen.utils). If p_brainsOnly is True, it - returns a list of brains instead (can be useful for some usages like - knowing the number of objects without needing to get information - about them). If no p_maxResults is specified, the method returns - maximum self.numberOfResultsPerPage. The method returns all objects - if p_maxResults equals string "NO_LIMIT". - - If p_noSecurity is True, it gets all the objects, even those that the - currently logged user can't see. - - The result is sorted according to the potential sort key defined in - the Search instance (Search.sortBy, together with Search.sortOrder). - But if parameter p_sortBy is given, it defines or overrides the sort. - In this case, p_sortOrder gives the order (*asc*ending or - *desc*ending). - - If p_filterKey is given, it represents an additional search parameter - to take into account: the corresponding search value is in - p_filterValue. - - If p_refObject and p_refField are given, the query is limited to the - objects that are referenced from p_refObject through p_refField.''' - params = {'ClassName': className} - klass = self.getAppyClass(className, wrapper=True) - if not brainsOnly: params['batch'] = True - # Manage additional criteria from a search when relevant - if searchName: search = self.getSearch(className, searchName) - if search: - # Add in params search and sort criteria - search.updateSearchCriteria(params, klass) - # Determine or override sort if specified - if sortBy: - params['sort_on'] = Search.getIndexName(sortBy, klass, usage='sort') - params['sort_order'] = (sortOrder == 'desc') and 'reverse' or None - # If defined, add the filter among search parameters - if filterKey: - filterKey = Search.getIndexName(filterKey, klass) - filterValue = Search.getSearchValue(filterKey, filterValue, klass) - params[filterKey] = filterValue - # TODO This value needs to be merged with an existing one if already - # in params, or, in a first step, we should avoid to display the - # corresponding filter widget on the screen. - if refObject: - refField = refObject.getAppyType(refField) - params['UID'] = getattr(refObject, refField.name).data - # Use index "Allowed" if noSecurity is False - if not noSecurity: params['Allowed'] = self.getAllowedValue() - brains = self.getPath("/catalog")(**params) - # Compute maxResults - if not maxResults: - if refField: maxResults = refField.maxPerPage - else: maxResults = search.maxPerPage or \ - self.appy().numberOfResultsPerPage - elif maxResults == 'NO_LIMIT': - maxResults = None - # Return brains only if required - if brainsOnly: - if not maxResults: return brains - else: return brains[:maxResults] - res = gutils.SomeObjects(brains, maxResults, startNumber, - noSecurity=noSecurity) - res.brainsToObjects() - # In some cases (p_remember=True), we need to keep some information - # about the query results in the current user's session, allowing him - # to navigate within elements without re-triggering the query every - # time a page for an element is consulted. - if remember: - if not searchName: - if not search: - searchName = className - else: - searchName = search.getSessionKey(className, full=False) - uids = {} - i = -1 - for obj in res.objects: - i += 1 - uids[startNumber+i] = obj.id - self.REQUEST.SESSION['search_%s' % searchName] = uids - return res - - def getResultColumnsLayouts(self, className, refInfo): - '''Returns the column layouts for displaying objects of - p_className.''' - if refInfo[0]: - return refInfo[0].getAppyType(refInfo[1]).shownInfo - else: - k = self.getAppyClass(className) - if not hasattr(k, 'listColumns'): return ('title',) - if callable(k.listColumns): return k.listColumns(self.appy()) - return k.listColumns - - def truncateValue(self, value, width=20): - '''Truncates the p_value according to p_width. p_value has to be - unicode-encoded for being truncated (else, one char may be spread on - 2 chars).''' - # Param p_width can be None. - if not width: width = 20 - if isinstance(value, str): value = value.decode('utf-8') - if len(value) > width: return value[:width] + '...' - return value - - def truncateText(self, text, width=20): - '''Truncates p_text to max p_width chars. If the text is longer than - p_width, the truncated part is put in a "acronym" html tag. p_text - has to be unicode-encoded for being truncated (else, one char may be - spread on 2 chars).''' - # Param p_width can be None. - if not width: width = 20 - if isinstance(text, str): text = text.decode('utf-8') - if len(text) <= width: return text - return '%s...' % (text, text[:width]) - - def splitList(self, l, sub): - '''Returns a list made of the same elements as p_l, but grouped into - sub-lists of p_sub elements.''' - return sutils.splitList(l, sub) - - def quote(self, s, escapeWithEntity=True): - '''Returns the quoted version of p_s.''' - if not isinstance(s, str): s = str(s) - repl = escapeWithEntity and ''' or "\\'" - s = s.replace('\r\n', '').replace('\n', '').replace("'", repl) - return "'%s'" % s - - def getLayoutType(self): - '''Guess the current layout type, according to actual URL.''' - url = self.REQUEST['ACTUAL_URL'] - if url.endswith('/view'): return 'view' - if url.endswith('/edit') or url.endswith('/do'): return 'edit' - - def getZopeClass(self, name): - '''Returns the Zope class whose name is p_name.''' - exec('from Products.%s.%s import %s as C'% (self.getAppName(),name,name)) - return C - - def getAppyClass(self, zopeName, wrapper=False): - '''Gets the Appy class corresponding to the Zope class named p_name. - If p_wrapper is True, it returns the Appy wrapper. Else, it returns - the user-defined class.''' - # p_zopeName may be the name of the Zope class *or* the name of the Appy - # class (shorter, not prefixed with the underscored package path). - classes = self.getProductConfig().allShortClassNames - if zopeName in classes: zopeName = classes[zopeName] - zopeClass = self.getZopeClass(zopeName) - if wrapper: return zopeClass.wrapperClass - else: return zopeClass.wrapperClass.__bases__[-1] - - def getAllClassNames(self): - '''Returns the name of all classes within this app, including default - Appy classes (Tool, Translation, Page, etc).''' - return self.getProductConfig().allClassNames + [self.__class__.__name__] - - def getCreateMeans(self, klass): - '''Gets the different ways objects of p_klass can be created (currently: - via a web form or programmatically only). Result is a list.''' - res = [] - if 'create' not in klass.__dict__: - return ['form'] - else: - means = klass.create - if means: - if isinstance(means, str): res = [means] - else: res = means - return res - - def userMaySearch(self, klass): - '''May the user search among instances of root p_klass ?''' - # When editing a form, one should avoid annoying the user with this. - url = self.REQUEST['ACTUAL_URL'] - if url.endswith('/edit') or url.endswith('/do'): return - if hasattr(klass, 'maySearch'): return klass.maySearch(self.appy()) - return True - - def userMayCreate(self, klass): - '''May the logged user create instances of p_klass ? This information - can be defined on p_klass, in static attribute "creators". - 1. If this attr holds a list, we consider it to be a list of roles, - and we check that the user has at least one of those roles. - 2. If this attr holds a boolean, we consider that the user can create - instances of this class if the boolean is True. - 3. If this attr stores a method, we execute the method, and via its - result, we fall again in cases 1 or 2. - - If p_klass does not define this attr "creators", we will use a - default list of roles as defined in the config.''' - # Get the value of attr "creators", or a default value if not present - if hasattr(klass, 'creators'): - creators = klass.creators - else: - creators = self.getProductConfig().appConfig.defaultCreators - # Resolve case (3): if "creators" is a method, execute it. - if isinstance(creators, collections.Callable): creators = creators(self.appy()) - # Resolve case (2) - if isinstance(creators, bool) or not creators: return creators - # Resolve case (1): checks whether the user has at least one of the - # roles listed in "creators". - for role in self.getUser().getRoles(): - if role in creators: - return True - - def subTitleIsUsed(self, className): - '''Does class named p_className define a method "getSubTitle"?''' - klass = self.getAppyClass(className) - return hasattr(klass, 'getSubTitle') - - def _searchValueIsEmpty(self, key): - '''Returns True if request value in key p_key can be considered as - empty.''' - rq = self.REQUEST.form - if key.endswith('*int') or key.endswith('*float'): - # We return True if "from" AND "to" values are empty. - toKey = '%s_to' % key[2:key.find('*')] - return not rq[key].strip() and not rq[toKey].strip() - elif key.endswith('*date'): - # We return True if "from" AND "to" values are empty. A value is - # considered as not empty if at least the year is specified. - toKey = '%s_to_year' % key[2:-5] - return not rq[key] and not rq[toKey] - else: - return not rq[key] - - def _getDateTime(self, year, month, day, setMin): - '''Gets a valid DateTime instance from date information coming from the - request as strings in p_year, p_month and p_day. Returns None if - p_year is empty. If p_setMin is True, when some - information is missing (month or day), we will replace it with the - minimum value (=1). Else, we will replace it with the maximum value - (=12, =31).''' - if not year: return None - if not month: - if setMin: month = 1 - else: month = 12 - if not day: - if setMin: day = 1 - else: day = 31 - DateTime = self.getProductConfig().DateTime - # Set the hour - if setMin: hour = '00:00' - else: hour = '23:59' - # We loop until we find a valid date. For example, we could loop from - # 2009/02/31 to 2009/02/28. - dateIsWrong = True - while dateIsWrong: - try: - res = DateTime('%s/%s/%s %s' % (year, month, day, hour)) - dateIsWrong = False - except: - day = int(day)-1 - return res - - def _getDefaultSearchCriteria(self): - '''We are about to perform an advanced search on instances of a given - class. Check, on this class, if in field Class.searchAdvanced, some - default criteria (field values, sort filters, etc) exist, and, if - yes, return it.''' - res = {} - rq = self.REQUEST - if 'className' not in rq.form: return res - klass = self.getAppyClass(rq.form['className']) - if not hasattr(klass, 'searchAdvanced'): return res - # In klass.searchAdvanced, we have the Search instance representing - # default advanced search criteria. - wrapperClass = self.getAppyClass(rq.form['className'], wrapper=True) - klass.searchAdvanced.updateSearchCriteria(res, wrapperClass, - advanced=True) - return res - - transformMethods = {'uppercase': 'upper', 'lowercase': 'lower', - 'capitalize': 'capitalize'} - def storeSearchCriteria(self): - '''Stores the search criteria coming from the request into the - session.''' - rq = self.REQUEST - # Store the search criteria in the session - criteria = self._getDefaultSearchCriteria() - for name in list(rq.form.keys()): - if name.startswith('w_') and not self._searchValueIsEmpty(name): - hasStar = name.find('*') != -1 - fieldName = not hasStar and name[2:] or name[2:name.find('*')] - field = self.getAppyType(fieldName, rq.form['className']) - if field and not field.persist and not field.indexed: continue - # We have a(n interval of) value(s) that is not empty for a - # given field or index. - value = rq.form[name] - if hasStar: - value = value.strip() - # The type of the value is encoded after char "*". - name, type = name.split('*') - if type == 'bool': - exec('value = %s' % value) - elif type in ('int', 'float'): - # Get the "from" value - if not value: value = None - else: - exec('value = %s(value)' % type) - # Get the "to" value - toValue = rq.form['%s_to' % name[2:]].strip() - if not toValue: toValue = None - else: - exec('toValue = %s(toValue)' % type) - value = (value, toValue) - elif type == 'date': - prefix = name[2:] - # Get the "from" value - year = value - month = rq.form['%s_from_month' % prefix] - day = rq.form['%s_from_day' % prefix] - fromDate = self._getDateTime(year, month, day, True) - # Get the "to" value" - year = rq.form['%s_to_year' % prefix] - month = rq.form['%s_to_month' % prefix] - day = rq.form['%s_to_day' % prefix] - toDate = self._getDateTime(year, month, day, False) - value = (fromDate, toDate) - elif type.startswith('string'): - # In the case of a string, it could be necessary to - # apply some text transform. - if len(type) > 6: - transform = type.split('-')[1] - if (transform != 'none') and value: - exec('value = value.%s()' % \ - self.transformMethods[transform]) - if isinstance(value, list): - # It is a list of values. Check if we have an operator for - # the field, to see if we make an "and" or "or" for all - # those values. "or" will be the default. - operKey = 'o_%s' % name[2:] - oper = ' %s ' % rq.form.get(operKey, 'or').upper() - value = oper.join(value) - criteria[name[2:]] = value - # Complete criteria with Ref info if the search is restricted to - # referenced objects of a Ref field. - refInfo = rq.get('ref', None) - if refInfo: criteria['_ref'] = refInfo - rq.SESSION['searchCriteria'] = criteria - - def onSearchObjects(self): - '''This method is called when the user triggers a search from - pxSearch.''' - rq = self.REQUEST - self.storeSearchCriteria() - # Go to the screen that displays search results - backUrl = '%s/query?className=%s&&search=customSearch' % \ - (self.absolute_url(), rq['className']) - return self.goto(backUrl) - - def getJavascriptMessages(self): - '''Returns the translated version of messages that must be shown in - Javascript popups.''' - res = '' - for msg in jsMessages: - res += 'var %s = "%s";\n' % (msg, self.translate(msg)) - return res - - def getColumnsSpecifiers(self, className, columnLayouts, dir): - '''Extracts and returns, from a list of p_columnLayouts, info required - for displaying columns of field values for instances of p_className, - either in a result screen or for a Ref field.''' - res = [] - for info in columnLayouts: - fieldName, width, align = ColumnLayout(info).get() - align = self.flipLanguageDirection(align, dir) - field = self.getAppyType(fieldName, className) - if not field: - self.log('field "%s", used in a column specifier, was not ' \ - 'found.' % fieldName, type='warning') - else: - res.append(Object(field=field, width=width, align=align)) - return res - - def getRefInfo(self, refInfo=None): - '''When a search is restricted to objects referenced through a Ref - field, this method returns information about this reference: the - source class and the Ref field. If p_refInfo is not given, we search - it among search criteria in the session.''' - if not refInfo and (self.REQUEST.get('search', None) == 'customSearch'): - criteria = self.REQUEST.SESSION.get('searchCriteria', None) - if criteria and '_ref' in criteria: refInfo = criteria['_ref'] - if not refInfo: return None, None - objectUid, fieldName = refInfo.split(':') - obj = self.getObject(objectUid) - return obj, fieldName - - def getGroupedSearches(self, klass): - '''Returns an object with 2 attributes: - * "searches" stores the searches that are defined for p_klass; - * "default" stores the search defined as the default one. - Every item representing a search is a dict containing info about a - search or about a group of searches. - ''' - res = [] - default = None # Also retrieve the default one here - groups = {} # The already encountered groups - page = Page('searches') # A dummy page required by class UiGroup - # Get the searches statically defined on the class - className = self.getPortalType(klass) - searches = ClassDescriptor.getSearches(klass, tool=self.appy()) - # Get the dynamically computed searches - if hasattr(klass, 'getDynamicSearches'): - searches += klass.getDynamicSearches(self.appy()) - for search in searches: - # Create the search descriptor - uiSearch = UiSearch(search, className, self) - if not search.group: - # Insert the search at the highest level, not in any group - res.append(uiSearch) - else: - uiGroup = search.group.insertInto(res, groups, page, className, - content='searches') - uiGroup.addElement(uiSearch) - # Is this search the default search? - if search.default: default = uiSearch - return Object(searches=res, default=default) - - def getSearch(self, className, name, ui=False): - '''Gets the Search instance (or a UiSearch instance if p_ui is True) - corresponding to the search named p_name, on class p_className.''' - initiator = None - if name == 'customSearch': - # It is a custom search whose parameters are in the session - fields = self.REQUEST.SESSION['searchCriteria'] - res = Search('customSearch', **fields) - elif (name == 'allSearch') or not name: - # It is the search for every instance of p_className - res = Search('allSearch') - elif '*' in name: - # The search is defined in a Ref field with link=popup. Get the - # search, the initiator object and the Ref field. - uid, ref, mode = name.split('*') - initiator = self.getObject(uid, appy=True) - initiatorField = initiator.getField(ref) - res = getattr(initiator.klass, ref).select - else: - appyClass = self.getAppyClass(className) - # Search among static searches - res = ClassDescriptor.getSearch(appyClass, name) - if not res and hasattr(appyClass, 'getDynamicSearches'): - # Search among dynamic searches - for search in appyClass.getDynamicSearches(self.appy()): - if search.name == name: - res = search - break - # Return a UiSearch if required - if ui: - res = UiSearch(res, className, self) - if initiator: res.setInitiator(initiator, initiatorField, mode) - return res - - def getLiveSearch(self, klass, keywords): - '''Gets the Search instance for performing a live search on p_klass.''' - if not keywords.endswith('*'): keywords += '*' - res = Search('liveSearch', SearchableText=keywords) - if hasattr(klass, 'searchAdvanced'): - # Take into account default params for the advanced search - advanced = klass.searchAdvanced - res.fields.update(advanced.fields) - res.sortBy = advanced.sortBy - res.sortOrder = advanced.sortOrder - return res - - def advancedSearchEnabledFor(self, klass): - '''Is advanced search visible for p_klass ?''' - # By default, advanced search is enabled - if not hasattr(klass, 'searchAdvanced'): return True - # Evaluate attribute "show" on this Search instance representing the - # advanced search. - return klass.searchAdvanced.isShowable(klass, self.appy()) - - def portletBottom(self, klass): - '''Is there a custom zone to display at the bottom of the portlet zone - for p_klass?''' - if not hasattr(klass, 'getPortletBottom'): return '' - res = klass.getPortletBottom(self.appy()) - if not res: return '' - return res - - def getNavigationInfo(self, nav, inPopup): - '''Produces a Siblings instance from navigation info p_nav.''' - return Siblings.get(nav, self, inPopup) - - def getGroupedSearchFields(self, searchInfo): - '''This method transforms p_searchInfo.fields, which is a "flat" - list of fields, into a list of lists, where every sub-list having - length p_searchInfo.nbOfColumns. For every field, scolspan - (=colspan "for search") is taken into account.''' - res = [] - row = [] - rowLength = 0 - for field in searchInfo.fields: - # Can I insert this field in the current row? - remaining = searchInfo.nbOfColumns - rowLength - if field.scolspan <= remaining: - # Yes. - row.append(field) - rowLength += field.scolspan - else: - # We must put the field on a new line. Complete the current one - # if not complete. - while rowLength < searchInfo.nbOfColumns: - row.append(None) - rowLength += 1 - res.append(row) - row = [field] - rowLength = field.scolspan - # Complete the last unfinished line if required. - if row: - while rowLength < searchInfo.nbOfColumns: - row.append(None) - rowLength += 1 - res.append(row) - return res - - # -------------------------------------------------------------------------- - # Authentication-related methods - # -------------------------------------------------------------------------- - def identifyUser(self, alsoSpecial=False): - '''To identify a user means: get its login and password. There are - several places to look for this information: http authentication, - cookie of credentials coming from the web form. - - If no user could be identified, and p_alsoSpecial is True, we will - nevertheless identify a "special user": "system", representing the - system itself (running at startup or in batch mode) or "anon", - representing an anonymous user.''' - tool = self.appy() - req = tool.request - login = password = None - # a. Identify the user from http basic authentication. - if getattr(req, '_auth', None): - # HTTP basic authentication credentials are present (used when - # connecting to the ZMI). Decode it. - creds = req._auth - if creds.lower().startswith('basic '): - try: - creds = creds.split(' ')[-1] - login, password = base64.decodestring(creds).split(':', 1) - except Exception as e: - pass - # b. Identify the user from the authentication cookie. - if not login: - login, password = gutils.readCookie(req) - # c. Identify the user from the authentication form. - if not login: - login = req.get('__ac_name', None) - password = req.get('__ac_password', '') - # Stop identification here if we don't need to return a special user - if not alsoSpecial: return login, password - # d. All the identification methods failed. So identify the user as - # "anon" or "system". - if not login: - # If we have a fake request, we are at startup or in batch mode and - # the user is "system". Else, it is "anon". At Zope startup, Appy - # uses an Object instance as a fake request. In "zopectl run" mode - # (the Zope batch mode), Appy adds a param "_fake_" on the request - # object created by Zope. - if (req.__class__.__name__ == 'Object') or \ - (hasattr(req, '_fake_') and req._fake_): - login = 'system' - else: - login = 'anon' - return login, password - - def getUser(self, authentify=False, source='zodb'): - '''Gets the current user. If p_authentify is True, in addition to - finding the logged user and returning it (=identification), we check - if found credentials are valid (=authentification). - - If p_authentify is True and p_source is "zodb", authentication is - performed locally. Else (p_source is "ldap"), authentication is - performed on a LDAP (if a LDAP configuration is found). If p_source - is "any", authentication is performed on the local User object, be it - really local or a copy of a LDAP user.''' - tool = self.appy() - req = tool.request - # Try first to return the user that can be cached on the request. In - # this case, we suppose authentication has previously been done, and we - # just return the cached user. - if hasattr(req, 'user'): return req.user - # Identify the user (=find its login and password). If we don't need - # to authentify the user, we ask to identify a user or, if impossible, - # a special user. - login, password = self.identifyUser(alsoSpecial=not authentify) - # Stop here if no user was found and authentication was required - if authentify and not login: return - # Now, get the User instance - if source == 'zodb': - # Get the User object, but only if it is a true local user - user = tool.search1('User', noSecurity=True, login=login) - if user and (user.source != 'zodb'): user = None # Not a local one - elif source == 'ldap': - user = None - cfg = self.getProductConfig(True).ldap - if cfg: user = cfg.getUser(self.appy(), login, password) - elif source == 'any': - # Get the User object, be it really local or representing an - # external user. This way, we avoid contacting the distant source - # every time authentification is required. - user = tool.search1('User', noSecurity=True, login=login) - if not user: return - # Authentify the user if required - if authentify: - if (user.state == 'inactive') or (not user.checkPassword(password)): - # Disable the authentication cookie and remove credentials - # stored on the request. - req.RESPONSE.expireCookie('_appy_', path='/') - k = 'HTTP_AUTHORIZATION' - req._auth = req[k] = req._orig_env[k] = None - return - # Create an authentication cookie for this user - gutils.writeCookie(login, password, req) - # Cache the user and some precomputed values, for performance - req.user = user - req.userLogin = user.login - req.userRoles = user.getRoles() - req.userLogins = user.getLogins() - req.zopeUser = user.getZopeUser() - return user - - def performLogin(self): - '''Logs the user in.''' - rq = self.REQUEST - jsEnabled = rq.get('js_enabled', False) in ('1', 1) - cookiesEnabled = rq.get('cookies_enabled', False) in ('1', 1) - urlBack = rq['HTTP_REFERER'] - if jsEnabled and not cookiesEnabled: - msg = self.translate('enable_cookies') - return self.goto(urlBack, msg) - # Authenticate the user - if self.getUser(authentify=True, source='zodb') or \ - self.getUser(authentify=True, source='ldap'): - msg = self.translate('login_ok') - logMsg = 'logged in.' - else: - msg = self.translate('login_ko') - login = rq.get('__ac_name') or '' - logMsg = 'authentication failed with login %s.' % login - self.log(logMsg) - return self.goto(self.getApp().absolute_url(), msg) - - def performLogout(self): - '''Logs out the current user when he clicks on "disconnect".''' - rq = self.REQUEST - userId = self.getUser().login - # Perform the logout in acl_users - rq.RESPONSE.expireCookie('_appy_', path='/') - # Invalidate the user session. - try: - sdm = self.session_data_manager - except AttributeError as ae: - # When ran in test mode, session_data_manager is not there. - sdm = None - if sdm: - session = sdm.getSessionData(create=0) - if session is not None: - session.invalidate() - self.log('logged out.') - # Remove user from variable "loggedUsers" - if userId in self.loggedUsers: del self.loggedUsers[userId] - return self.goto(self.getApp().absolute_url()) - - # This dict stores, for every logged user, the date/time of its last access - loggedUsers = {} - staticExtensions = ('.jpg', '.jpeg', '.gif', '.png', '.js', '.css', '.htm', - '.html') - def rememberAccess(self, user): - '''Every time there is a hit on the server, this method is called in - order to update global dict loggedUsers (see above).''' - self.loggedUsers[user.login] = time.time() - # "Touch" the SESSION object. Else, expiration won't occur. - session = self.REQUEST.SESSION - - def validate(self, request, auth='', roles=_noroles): - '''This method performs authentication and authorization. It is used as - a replacement for Zope's AccessControl.User.BasicUserFolder.validate, - that allows to manage cookie-based authentication.''' - v = request['PUBLISHED'] # The published object - tool = self.getParentNode().config - # v is the object (value) we're validating access to - # n is the name used to access the object - # a is the object the object was accessed through - # c is the physical container of the object - a, c, n, v = self._getobcontext(v, request) - # Authorize anyone to static content (image, js, css...) - id = a.getId() - if id and (os.path.splitext(id)[-1].lower() in tool.staticExtensions): - return self._nobody.__of__(self) - # Skip authorization when the performing http login: else, it will be - # done twice. - if (id == 'config') and (v.__name__ == 'performLogin'): - return self._nobody.__of__(self) - # Identify and authentify the user - user = tool.getUser(authentify=True, source='any') - if not user: - # Login and/or password incorrect. Try to authorize and return the - # anonymous user. - if self.authorize(self._nobody, a, c, n, v, roles): - return self._nobody.__of__(self) - else: - return - else: - # We found a user and his password was correct. Try to authorize him - # against the published object. By the way, remember its last access - # to this system. - tool.rememberAccess(user) - user = user.getZopeUser() - if self.authorize(user, a, c, n, v, roles): - return user.__of__(self) - # That didn't work. Try to authorize the anonymous user. - elif self.authorize(self._nobody, a, c, n, v, roles): - return self._nobody.__of__(self) - else: - return - - # Patch BasicUserFolder with our version of m_validate above. - from AccessControl.User import BasicUserFolder - BasicUserFolder.validate = validate - - def getUserName(self, login=None, normalized=False): - '''Gets the user name corresponding to p_login (or the currently logged - user if None), or the p_login itself if the user does not exist - anymore. If p_normalized is True, special chars in the first and last - names are normalized.''' - tool = self.appy() - if not login: - user = tool.user - else: - user = tool.search1('User', noSecurity=True, login=login) - if not user: return login - return user.getTitle(normalized=normalized) - - def tempFile(self): - '''A temp file has been created in a temp folder. This method returns - this file to the browser.''' - rq = self.REQUEST - baseFolder = os.path.join(sutils.getOsTempFolder(), self.getAppName()) - baseFolder = os.path.join(baseFolder, rq.SESSION.id) - fileName = os.path.join(baseFolder, rq.get('name', '')) - if os.path.exists(fileName): - f = file(fileName) - content = f.read() - f.close() - # Remove the temp file - os.remove(fileName) - return content - return 'File does not exist' - - def getResultPodFields(self, contentType): - '''Finds, among fields defined on p_contentType, which ones are Pod - fields that need to be shown on a page displaying query results.''' - # Skip this if we are searching multiple content types. - if ',' in contentType: return () - return [f for f in self.getAllAppyTypes(contentType) \ - if (f.type == 'Pod') and (f.show == 'result')] - - def formatDate(self, date, format=None, withHour=True, language=None): - '''Returns p_date formatted as specified by p_format, or tool.dateFormat - if not specified. If p_withHour is True, hour is appended, with a - format specified in tool.hourFormat.''' - tool = self.appy() - fmt = format or tool.dateFormat - # Resolve appy-specific formatting symbols used for getting translated - # names of days or months: - # - %dt: translated name of day - # - %DT: translated name of day, capitalized - # - %mt: translated name of month - # - %MT: translated name of month, capitalized - if ('%dt' in fmt) or ('%DT' in fmt): - day = self.translate('day_%s' % date._aday, language=language) - fmt = fmt.replace('%dt', day.lower()).replace('%DT', day) - if ('%mt' in fmt) or ('%MT' in fmt): - month = self.translate('month_%s' % date._amon, language=language) - fmt = fmt.replace('%mt', month.lower()).replace('%MT', month) - # Resolve all other, standard, symbols - res = date.strftime(fmt) - # Append hour from tool.hourFormat - if withHour: res += ' (%s)' % date.strftime(tool.hourFormat) - return res - - def generateUid(self, className): - '''Generates a UID for an instance of p_className.''' - name = className.split('_')[-1] - randomNumber = str(random.random()).split('.')[1].replace('e-', '') - timestamp = ('%f' % time.time()).replace('.', '') - return '%s%s%s' % (name, timestamp, randomNumber) - - def manageError(self, error): - '''Manages an error''' - tb = sys.exc_info() - if error.type.__name__ == 'Unauthorized': - siteUrl = self.getSiteUrl() - htmlMessage = 'Back You are not allowed to ' \ - 'access this page.' - userId = self.appy().user.login - textMessage = 'unauthorized for %s @%s.' % \ - (userId, self.REQUEST.get('PATH_INFO')) - else: - from zExceptions.ExceptionFormatter import format_exception - htmlMessage = format_exception(tb[0], tb[1], tb[2], as_html=1) - htmlMessage = '\n'.join(htmlMessage) - textMessage = format_exception(tb[0], tb[1], tb[2], as_html=0) - textMessage = ''.join(textMessage).strip() - self.log(textMessage, type='error') - return '
%s
' % htmlMessage - - def getMainPages(self): - '''Returns the main pages.''' - if hasattr(self.o.aq_base, 'pages') and self.o.pages: - return [self.getObject(uid) for uid in self.o.pages ] - return () - - def askPasswordReinit(self): - '''A user (anonymmous) does not remember its password. Here we will - send him a mail containing a link that will trigger password - re-initialisation.''' - login = self.REQUEST.get('login').strip() - appyTool = self.appy() - user = appyTool.search1('User', login=login, noSecurity=True) - msg = self.translate('reinit_mail_sent') - backUrl = self.REQUEST['HTTP_REFERER'] - if not user: - # Return the message nevertheless. This way, malicious users can't - # deduce information about existing users. - return self.goto(backUrl, msg) - # If login is an email, use it. Else, use user.email instead. - email = user.login - if not String.EMAIL.match(email): - email = user.email - if not email: - # Impossible to re-initialise the password. - return self.goto(backUrl, msg) - # Create a temporary file whose name is the user login and whose - # content is a generated token. - f = file(os.path.join(sutils.getOsTempFolder(), login), 'w') - token = String().generatePassword() - f.write(token) - f.close() - # Send an email - initUrl = '%s/doPasswordReinit?login=%s&token=%s' % \ - (self.absolute_url(), login, token) - subject = self.translate('reinit_password') - map = {'url':initUrl, 'siteUrl':self.getSiteUrl()} - body= self.translate('reinit_password_body', mapping=map, format='text') - appyTool.sendMail(email, subject, body) - return self.goto(backUrl, msg) - - def doPasswordReinit(self): - '''Performs the password re-initialisation.''' - rq = self.REQUEST - login = rq['login'] - token = rq['token'] - # Check if such token exists in temp folder - res = None - siteUrl = self.getSiteUrl() - tokenFile = os.path.join(sutils.getOsTempFolder(), login) - if os.path.exists(tokenFile): - f = file(tokenFile) - storedToken = f.read() - f.close() - if storedToken == token: - # Generate a new password for this user - appyTool = self.appy() - user = appyTool.search1('User', login=login, noSecurity=True) - newPassword = user.setPassword() - # Send the new password by email - email = login - if not String.EMAIL.match(email): - email = user.email - subject = self.translate('new_password') - map = {'password': newPassword, 'siteUrl': siteUrl} - body = self.translate('new_password_body', mapping=map, - format='text') - appyTool.sendMail(email, subject, body) - os.remove(tokenFile) - res = self.goto(siteUrl, self.translate('new_password_sent')) - if not res: - res = self.goto(siteUrl, self.translate('wrong_password_reinit')) - return res - - def getGoogleAnalyticsCode(self): - '''If the config defined a Google Analytics ID, this method returns the - Javascript code to be included in every page, allowing Google - Analytics to work.''' - # Disable Google Analytics when we are in debug mode. - if self.isDebug(): return - # Disable Google Analytics if no ID is found in the config. - gaId = self.getProductConfig(True).googleAnalyticsId - if not gaId: return - # Google Analytics must be enabled: return the chunk of Javascript - # code specified by Google. - code = "var _gaq = _gaq || [];\n" \ - "_gaq.push(['_setAccount', '%s']);\n" \ - "_gaq.push(['_trackPageview']);\n" \ - "(function() {\n" \ - " var ga = document.createElement('script'); " \ - "ga.type = 'text/javascript'; ga.async = true;\n" \ - " ga.src = ('https:' == document.location.protocol ? " \ - "'https://ssl' : 'http://www') + " \ - "'.google-analytics.com/ga.js';\n" \ - " var s = document.getElementsByTagName('script')[0]; " \ - "s.parentNode.insertBefore(ga, s);\n" \ - "})();\n" % gaId - return code - - def getButtonCss(self, label, small=True): - '''Gets the CSS class(es) to set on a button, given it l_label and its - size (p_small or not).''' - # CSS for a small button. No minimum width applies: small button are - # meant to be small. - if small: return 'buttonSmall button' - # CSS for a normal button. A minimum width (via buttonFixed) is defined - # when the label is small: it produces ranges of buttons of the same - # width (excepted when labels are too large), which is more beautiful. - if len(label) < 15: return 'buttonFixed button' - return 'button' - - def getLinksTargetInfo(self, klass, back=None): - '''Appy allows to open links to view or edit instances of p_klass - either via the same browser window, or via a popup. This method - returns info about that, as an object having 2 attributes: - - target is "_self" if the link leads to the same browser window, - "appyIFrame" if the link must be opened in a popup; - - openPopup is unused if target is "_self" or contains the - Javascript code to open the popup.''' - res = Object(target='_self', openPopup='') - if hasattr(klass, 'popup'): - res.target = 'appyIFrame' - d = klass.popup - if isinstance(d, str): - # Width only - params = d[:-2] - else: - # Width and height - params = "%s, %s" % (d[0][:-2], d[1][:-2]) - # If "back" is specified, it corresponds to some tag on the main - # page, that must be ajax-refreshed when coming back from the popup. - # Else, when back, the entire page will be reloaded. - if back: params += ", '%s'" % back - res.openPopup = "openPopup('iframePopup',null,%s)" % params - return res - - def backFromPopup(self): - '''Returns the PX allowing to close the iframe popup and refresh the - base page.''' - return self.appy().pxBack({'ztool': self}) - - ieRex = re.compile('MSIE\s+(\d\.\d)') - ieMin = '9' # We do not support IE below this version. - def getBrowserIncompatibility(self): - '''Produces an error message if the browser in use is not compatible - with Appy.''' - res = self.ieRex.search(self.REQUEST.get('HTTP_USER_AGENT')) - if not res: return - version = res.group(1) - if version < self.ieMin: - mapping = {'version': version, 'min': self.ieMin} - return self.translate('wrong_browser', mapping=mapping) - - def executeAjaxAction(self, action, obj, field): - '''When PX "pxAjax" is called to get some chunk of XHTML via an Ajax - request, a server action can be executed before rendering the XHTML - chunk. This method executes this action.''' - if action.startswith(':'): - # The action corresponds to a method on Appy p_obj - msg = getattr(obj, action[1:])() - else: - # The action must be executed on p_field if present, on obj.o else. - if field: msg = getattr(field, action)(obj.o) - else: msg = getattr(obj.o, action)() - return msg - - def updatePxContextFromRequest(self): - '''Takes any user-defined key from the request and put it as a variable - on the current PX context.''' - req = self.REQUEST - ctx = req.pxContext - # Get "form" data (get, post) and cookie values - for source in (req.form, req.cookies): - for k, v in source.iteritems(): - # Convert v to some Python data when relevant - if v in ('True', 'False', 'true', 'false'): - exec 'v = %s' % v.capitalize() - elif v.isdigit(): v = int(v) - ctx[k] = v -# ------------------------------------------------------------------------------ diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py deleted file mode 100644 index 9d87457..0000000 --- a/gen/mixins/__init__.py +++ /dev/null @@ -1,1800 +0,0 @@ -'''This package contains mixin classes that are mixed in with generated classes: - - mixins/BaseMixin is mixed in with standard Zope classes; - - mixins/ToolMixin is mixed in with the generated application Tool class.''' - -# ------------------------------------------------------------------------------ -import os, os.path, re, sys, types, urllib.request, urllib.parse, urllib.error, cgi -from appy import Object -from appy.px import Px -from appy.fields.workflow import UiTransition -import appy.gen as gen -from appy.gen.utils import * -from appy.gen.layout import Table -from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor -from appy.shared import utils as sutils -from appy.shared.data import rtlLanguages -from appy.shared.xml_parser import XmlMarshaller, XmlUnmarshaller -from appy.shared.diff import HtmlDiff -import collections - -# ------------------------------------------------------------------------------ -NUMBERED_ID = re.compile('.+\d{4}$') - -# ------------------------------------------------------------------------------ -class BaseMixin: - '''Every Zope class generated by appy.gen inherits from this class or a - subclass of it.''' - _appy_meta_type = 'Class' - - def get_o(self): - '''In some cases, we want the Zope object, we don't know if the current - object is a Zope or Appy object. By defining this property, - "someObject.o" produces always the Zope object, be someObject an Appy - or Zope object.''' - return self - o = property(get_o) - - def getInitiatorInfo(self, appy=False): - '''Gets information about a potential initiator object from the request. - Returns a 2-tuple (initiator, field): - * initiator is the initiator (Zope or Appy) object; - * field is the Ref instance. - ''' - rq = getattr(self, 'REQUEST', None) - if not rq: return None, None - if not rq.get('nav', '').startswith('ref.'): return None, None - splitted = rq['nav'].split('.') - initiator = self.getTool().getObject(splitted[1]) - field = initiator.getAppyType(splitted[2]) - if appy: initiator = initiator.appy() - return initiator, field - - def createOrUpdate(self, created, values, - initiator=None, initiatorField=None): - '''This method creates (if p_created is True) or updates an object. - p_values are manipulated versions of those from the HTTP request. - In the case of an object creation from the web (p_created is True - and a REQUEST object is present), p_self is a temporary object - created in /temp_folder, and this method moves it at its "final" - place. In the case of an update, this method simply updates fields - of p_self.''' - rq = getattr(self, 'REQUEST', None) - obj = self - if created and rq: - # Create the final object and put it at the right place. - tool = self.getTool() - # The app may define a method klass.generateUid for producing an UID - # for instance of this class. If no such method is found, we use the - # standard Appy method to produce an UID. - id = None - klass = tool.getAppyClass(obj.portal_type) - if hasattr(klass, 'generateUid'): - id = klass.generateUid(obj.REQUEST) - if not id: - id = tool.generateUid(obj.portal_type) - if not initiator: - folder = tool.getPath('/data') - else: - folder = initiator.getCreateFolder() - # Check that the user can add objects through this Ref. - initiatorField.checkAdd(initiator) - obj = createObject(folder, id, obj.portal_type, tool.getAppName()) - # Get the fields on the current page - fields = None - if rq: fields = self.getAppyTypes('edit', rq.get('page')) - # Remember the previous values of fields, for potential historization - previousData = None - if not created and fields: - previousData = obj.rememberPreviousData(fields) - # Perform the change on the object - if fields: - # Store in the database the new value coming from the form - for field in fields: - value = getattr(values, field.name, None) - field.store(obj, value) - if previousData: - # Keep in history potential changes on historized fields - obj.historizeData(previousData) - - # Call the custom "onEditEarly" if available. This method is called - # *before* potentially linking the object to its initiator. - appyObject = obj.appy() - if created and hasattr(appyObject, 'onEditEarly'): - appyObject.onEditEarly() - - # Manage potential link with an initiator object - if created and initiator: - initiator.appy().link(initiatorField.name, appyObject) - - # Call the custom "onEdit" if available - msg = None # The message to display to the user. It can be set by onEdit - if hasattr(appyObject, 'onEdit'): msg = appyObject.onEdit(created) - - # Update last modification date - if not created: - from DateTime import DateTime - obj.modified = DateTime() - # Unlock the currently saved page on the object - if rq: self.removeLock(rq['page']) - obj.reindex() - return obj, msg - - def updateField(self, name, value): - '''Updates a single field p_name with new p_value.''' - field = self.getAppyType(name) - # Remember previous value if the field is historized. - previousData = self.rememberPreviousData([field]) - # Store the new value into the database - field.store(self, value) - # Update the object history when relevant - if previousData: self.historizeData(previousData) - # Update last modification date - from DateTime import DateTime - self.modified = DateTime() - - def delete(self): - '''This method is self's suicide.''' - # Call a custom "onDelete" if it exists - appyObj = self.appy() - if hasattr(appyObj, 'onDelete'): appyObj.onDelete() - # Any people referencing me must forget me now - for field in self.getAllAppyTypes(): - if field.type != 'Ref': continue - for obj in field.getValue(self): - field.back.unlinkObject(obj, appyObj, back=True) - # Uncatalog the object - self.reindex(unindex=True) - # Delete the filesystem folder corresponding to this object - folder = os.path.join(*self.getFsFolder()) - if os.path.exists(folder): - sutils.FolderDeleter.delete(folder) - sutils.FolderDeleter.deleteEmpty(os.path.dirname(folder)) - # Delete the object - self.getParentNode().manage_delObjects([self.id]) - - def onDelete(self): - '''Called when an object deletion is triggered from the ui''' - rq = self.REQUEST - tool = self.getTool() - obj = tool.getObject(rq['uid']) - obj.delete() - msg = obj.translate('action_done') - # If we are called from an Ajax request, simply return msg - if hasattr(rq, 'pxContext') and rq.pxContext['ajax']: return msg - if obj.getUrl(rq['HTTP_REFERER'], mode='raw') == obj.getUrl(mode='raw'): - # We were consulting the object that has been deleted. Go back to - # the main page. - urlBack = tool.getSiteUrl() - else: - urlBack = obj.getUrl(rq['HTTP_REFERER']) - obj.say(msg) - obj.goto(urlBack) - - def onDeleteEvent(self): - '''Called when an event (from object history) deletion is triggered - from the ui.''' - rq = self.REQUEST - # Re-create object history, but without the event corresponding to - # rq['eventTime'] - history = [] - from DateTime import DateTime - eventToDelete = DateTime(rq['eventTime']) - for event in self.workflow_history['appy']: - if (event['action'] != '_datachange_') or \ - (event['time'] != eventToDelete): - history.append(event) - self.workflow_history['appy'] = tuple(history) - appy = self.appy() - self.log('data change event deleted for %s.' % appy.uid) - self.goto(self.getUrl(rq['HTTP_REFERER'])) - - def onLink(self): - '''Called when object (un)linking is triggered from the ui.''' - rq = self.REQUEST - tool = self.getTool() - sourceObject = tool.getObject(rq['sourceUid']) - field = sourceObject.getAppyType(rq['fieldName']) - return field.onUiRequest(sourceObject, rq) - - def onCreate(self): - '''This method is called when a user wants to create a root object in - the "data" folder or an object through a reference field. A temporary - object is created in /temp_folder and the edit page to it is - returned.''' - rq = self.REQUEST - className = rq.get('className') - # Create the params to add to the URL we will redirect the user to - # create the object. - urlParams = {'mode':'edit', 'page':'main', 'nav':'', - 'inPopup':rq.get('popup') == '1'} - initiator, initiatorField = self.getInitiatorInfo() - if initiator: - # The object to create will be linked to an initiator object through - # a Ref field. We create here a new navigation string with one more - # item, that will be the currently created item. - splitted = rq.get('nav').split('.') - splitted[-1] = splitted[-2] = str(int(splitted[-1])+1) - urlParams['nav'] = '.'.join(splitted) - # Check that the user can add objects through this Ref field - initiatorField.checkAdd(initiator) - # Create a temp object in /temp_folder - tool = self.getTool() - id = tool.generateUid(className) - appName = tool.getAppName() - obj = createObject(tool.getPath('/temp_folder'), id, className, appName) - return self.goto(obj.getUrl(**urlParams)) - - def getDbFolder(self): - '''Gets the folder, on the filesystem, where the database (Data.fs and - sub-folders) lies.''' - return os.path.dirname(self.getTool().getApp()._p_jar.db().getName()) - - def getFsFolder(self, create=False): - '''Gets the folder where binary files tied to this object will be stored - on the filesystem. If p_create is True and the folder does not exist, - it is created (together with potentially missing parent folders). - This folder is returned as a tuple (s_baseDbFolder, s_subPath).''' - objId = self.id - # Get the root folder where Data.fs lies. - dbFolder = self.getDbFolder() - # Build the list of path elements within this db folder. - path = [] - inConfig = False - for elem in self.getPhysicalPath(): - if not elem: continue - if elem == 'data': continue - if elem == 'config': inConfig = True - if not path or ((len(path) == 1) and inConfig): - # This object is at the root of the filesystem. - if NUMBERED_ID.match(elem): - path.append(elem[-4:]) - path.append(elem) - # We are done if elem corresponds to the object id. - if elem == objId: break - path = os.sep.join(path) - if create: - fullPath = os.path.join(dbFolder, path) - if not os.path.exists(fullPath): os.makedirs(fullPath) - return dbFolder, path - - def view(self): - '''Returns the view PX''' - obj = self.appy() - return obj.pxView({'obj': obj, 'tool': obj.tool}) - - def edit(self): - '''Returns the edit PX''' - obj = self.appy() - return obj.pxEdit({'obj': obj, 'tool': obj.tool}) - - def ajax(self): - '''Called via an Ajax request to render some PX whose name is in the - request.''' - obj = self.appy() - return obj.pxAjax({'obj': obj, 'tool': obj.tool}) - - def setLock(self, user, page): - '''A p_user edits a given p_page on this object: we will set a lock, to - prevent other users to edit this page at the same time.''' - if not hasattr(self.aq_base, 'locks'): - # Create the persistent mapping that will store the lock - # ~{s_page: (s_userId, DateTime_lockDate)}~ - from persistent.mapping import PersistentMapping - self.locks = PersistentMapping() - # Raise an error is the page is already locked by someone else. If the - # page is already locked by the same user, we don't mind: he could have - # used back/forward buttons of its browser... - userId = user.login - if (page in self.locks) and (userId != self.locks[page][0]): - self.raiseUnauthorized('This page is locked.') - # Set the lock - from DateTime import DateTime - self.locks[page] = (userId, DateTime()) - - def isLocked(self, user, page): - '''Is this page locked? If the page is locked by the same user, we don't - mind and consider the page as unlocked. If the page is locked, this - method returns the tuple (userId, lockDate).''' - if hasattr(self.aq_base, 'locks') and (page in self.locks): - if (user.login != self.locks[page][0]): return self.locks[page] - - def removeLock(self, page, force=False): - '''Removes the lock on the current page. This happens: - - after the page has been saved: the lock must be released; - - or when an admin wants to force the deletion of a lock that was - left on p_page for too long (p_force=True). - ''' - if page not in self.locks: return - # Raise an error if the user that saves changes is not the one that - # has locked the page (excepted if p_force is True) - if not force: - userId = self.getTool().getUser().login - if self.locks[page][0] != userId: - self.raiseUnauthorized('This page was locked by someone else.') - # Remove the lock - del self.locks[page] - - def removeMyLock(self, user, page): - '''If p_user has set a lock on p_page, this method removes it. This - method is called when the user that locked a page consults - pxView for this page. In this case, we consider that the user has - left the edit page in an unexpected way and we remove the lock.''' - if hasattr(self.aq_base, 'locks') and (page in self.locks) and \ - (user.login == self.locks[page][0]): - del self.locks[page] - - def onUnlock(self): - '''Called when an admin wants to remove a lock that was left for too - long by some user.''' - rq = self.REQUEST - tool = self.getTool() - obj = tool.getObject(rq['objectUid']) - obj.removeLock(rq['pageName'], force=True) - urlBack = self.getUrl(rq['HTTP_REFERER']) - self.say(self.translate('action_done')) - self.goto(urlBack) - - def intraFieldValidation(self, errors, values): - '''This method performs field-specific validation for every field from - the page that is being created or edited. For every field whose - validation generates an error, we add an entry in p_errors. For every - field, we add in p_values an entry with the "ready-to-store" field - value.''' - rq = self.REQUEST - for field in self.getAppyTypes('edit', rq.form.get('page')): - if not field.validable or not field.isClientVisible(self): continue - value = field.getRequestValue(self) - message = field.validate(self, value) - if message: - setattr(errors, field.name, message) - else: - setattr(values, field.name, field.getStorableValue(self, value)) - # Validate sub-fields within Lists/Dicts - if field.type not in ('List', 'Dict'): continue - i = -1 - for row in value: - i += 1 - for name, subField in field.fields: - message = subField.validate(self, getattr(row,name,None)) - if message: - setattr(errors, '%s*%d' % (subField.name, i), message) - - def interFieldValidation(self, errors, values): - '''This method is called when individual validation of all fields - succeed (when editing or creating an object). Then, this method - performs inter-field validation. This way, the user must first - correct individual fields before being confronted to potential - inter-field validation errors.''' - obj = self.appy() - if not hasattr(obj, 'validate'): return - msg = obj.validate(values, errors) - # Those custom validation methods may have added fields in the given - # p_errors object. Within this object, for every error message that is - # not a string, we replace it with the standard validation error for the - # corresponding field. - for key, value in errors.__dict__.items(): - resValue = value - if not isinstance(resValue, str): - resValue = self.translate('field_invalid') - setattr(errors, key, resValue) - return msg - - def onUpdate(self): - '''This method is executed when a user wants to update an object. - The object may be a temporary object created in /temp_folder. - In this case, the update consists in moving it to its "final" place. - If the object is not a temporary one, this method updates its - fields in the database.''' - rq = self.REQUEST - tool = self.getTool() - errorMessage = self.translate('validation_error') - isNew = self.isTemporary() - inPopup = rq.get('popup') == '1' - # If this object is created from an initiator, get info about him - initiator, initiatorField = self.getInitiatorInfo() - initiatorPage = initiatorField and initiatorField.pageName or None - # If the user clicked on 'Cancel', go back to the previous page - buttonClicked = rq.get('button') - if buttonClicked == 'cancel': - if inPopup: back = tool.backFromPopup() - elif initiator: # Go back to the initiator page - urlBack = initiator.getUrl(page=initiatorPage, nav='') - else: - if isNew: urlBack = tool.getHomePage() # Go back to home page - else: - # Return to the same page, excepted if unshowable on view - phaseObj = self.getAppyPhases(True, 'view') - pageInfo = phaseObj.getPageInfo(rq['page'], 'view') - if not pageInfo: urlBack = tool.getHomePage() - else: urlBack = self.getUrl(page=pageInfo.page.name) - self.removeLock(rq['page']) - self.say(self.translate('object_canceled')) - if inPopup: return back - return self.goto(urlBack) - - # Object for storing validation errors - errors = Object() - # Object for storing the (converted) values from the request - values = Object() - - # Trigger field-specific validation - self.intraFieldValidation(errors, values) - if errors.__dict__: - for k,v in errors.__dict__.items(): rq.set('%s_error' % k, v) - self.say(errorMessage) - return self.gotoEdit() - - # Trigger inter-field validation - msg = self.interFieldValidation(errors, values) - if not msg: msg = errorMessage - if errors.__dict__: - for k,v in errors.__dict__.items(): rq.set('%s_error' % k, v) - self.say(msg) - return self.gotoEdit() - - # Before saving data, must we ask a confirmation by the user ? - appyObj = self.appy() - saveConfirmed = rq.get('confirmed') == 'True' - if hasattr(appyObj, 'confirm') and not saveConfirmed: - msg = appyObj.confirm(values) - if msg: - rq.set('confirmMsg', msg.replace("'", "\\'")) - return self.gotoEdit() - - # Create or update the object in the database - obj, msg = self.createOrUpdate(isNew, values, initiator, initiatorField) - - # Redirect the user to the appropriate page - if not msg: msg = self.translate('object_saved') - # If the object has already been deleted (ie, it is a kind of transient - # object like a one-shot form and has already been deleted in method - # onEdit) or if the user can't access the object anymore, redirect him - # to the user's home page. - if not getattr(obj.getParentNode().aq_base, obj.id, None) or \ - not obj.mayView(): - if inPopup: return tool.backFromPopup() - return self.goto(tool.getHomePage(), msg) - if (buttonClicked == 'save') or saveConfirmed: - obj.say(msg) - if inPopup: return tool.backFromPopup() - if isNew and initiator: - return self.goto(initiator.getUrl(page=initiatorPage, nav='')) - # Return to the same page, if showable on view - phaseObj = self.getAppyPhases(True, 'view') - pageInfo = phaseObj.getPageInfo(rq['page'], 'view') - if not pageInfo: return self.goto(tool.getHomePage(), msg) - return self.goto(obj.getUrl(page=pageInfo.page.name)) - # Get the current page name. We keep it in "pageName" because rq['page'] - # can be changed by m_getAppyPhases called below. - pageName = rq['page'] - if buttonClicked in ('previous', 'next'): - # Go to the previous or next page for this object. We recompute the - # list of phases and pages because things may have changed since the - # object has been updated (ie, additional pages may be shown or - # hidden now, so the next and previous pages may have changed). - # Moreover, previous and next pages may not be available in "edit" - # mode, so we return the edit or view pages depending on page.show. - phaseObj = self.getAppyPhases(True, 'edit') - methodName = 'get%sPage' % buttonClicked.capitalize() - pageName, pageInfo = getattr(phaseObj, methodName)(pageName) - if pageName: - # Return to the edit or view page? - if pageInfo.showOnEdit: - # I do not use gotoEdit here because I really need to - # redirect the user to the edit page. Indeed, the object - # edit URL may have moved from temp_folder to another place. - return self.goto(obj.getUrl(mode='edit', page=pageName, - inPopup=inPopup)) - else: - return self.goto(obj.getUrl(page=pageName, inPopup=inPopup)) - else: - obj.say(msg) - return self.goto(obj.getUrl(inPopup=inPopup)) - return obj.gotoEdit() - - def reindex(self, indexes=None, unindex=False): - '''Reindexes this object the catalog. If names of indexes are specified - in p_indexes, recataloging is limited to those indexes. If p_unindex - is True, instead of cataloguing the object, it uncatalogs it.''' - path = '/'.join(self.getPhysicalPath()) - catalog = self.getPhysicalRoot().catalog - if unindex: - catalog.uncatalog_object(path) - else: - if indexes: - catalog.catalog_object(self, path, idxs=indexes) - else: - # Get the list of indexes that apply on this object. Else, Zope - # will reindex all indexes defined in the catalog, and through - # acquisition, wrong methods can be called on wrong objects. - iNames = list(self.wrapperClass.getIndexes().keys()) - catalog.catalog_object(self, path, idxs=iNames) - - def xml(self, action=None): - '''If no p_action is defined, this method returns the XML version of - this object. Else, it calls method named p_action on the - corresponding Appy wrapper and returns, as XML, its result.''' - self.REQUEST.RESPONSE.setHeader('Content-Type','text/xml;charset=utf-8') - # Check if the user is allowed to consult this object - if not self.mayView(): return XmlMarshaller().marshall('Unauthorized') - if not action: - marshaller = XmlMarshaller(rootTag=self.getClass().__name__, - dumpUnicode=True) - res = marshaller.marshall(self, objectType='appy') - else: - appyObj = self.appy() - try: - methodRes = getattr(appyObj, action)() - if isinstance(methodRes, Px): - res = methodRes({'self': self.appy()}) - elif isinstance(methodRes, file): - res = methodRes.read() - methodRes.close() - elif isinstance(methodRes, str) and \ - methodRes.startswith('-.''' - # Add to the p_changes dict the field labels - for name in list(changes.keys()): - # "name" can contain the language for multilingual fields. - if '-' in name: - fieldName, lg = name.split('-') - else: - fieldName = name - lg = None - field = self.getAppyType(fieldName) - if notForPreviouslyEmptyValues: - # Check if the previous field value was empty - if lg: - isEmpty = not changes[name] or not changes[name].get(lg) - else: - isEmpty = field.isEmptyValue(self, changes[name]) - if isEmpty: - del changes[name] - else: - changes[name] = (changes[name], field.labelId) - # Add an event in the history - self.addHistoryEvent('_datachange_', changes=changes) - - def historizeData(self, previousData): - '''Records in the object history potential changes on historized fields. - p_previousData contains the values, before an update, of the - historized fields, while p_self already contains the (potentially) - modified values.''' - # Remove from previousData all values that were not changed - for name in list(previousData.keys()): - field = self.getAppyType(name) - prev = previousData[name] - curr = field.getValue(self) - try: - if (prev == curr) or ((prev == None) and (curr == '')) or \ - ((prev == '') and (curr == None)): - del previousData[name] - continue - except UnicodeDecodeError as ude: - # The string comparisons above may imply silent encoding-related - # conversions that may produce this exception. - continue - # In some cases the old value must be formatted. - if field.type == 'Ref': - previousData[name] = [r.o.getShownValue('title') \ - for r in previousData[name]] - elif field.type == 'String': - languages = field.getAttribute(self, 'languages') - if len(languages) > 1: - # Consider every language-specific value as a first-class - # value. - del previousData[name] - for lg in languages: - lgPrev = prev and prev.get(lg) or None - lgCurr = curr and curr.get(lg) or None - if lgPrev == lgCurr: continue - previousData['%s-%s' % (name, lg)] = lgPrev - if previousData: - self.addDataChange(previousData) - - def goto(self, url, msg=None): - '''Brings the user to some p_url after an action has been executed.''' - if msg: self.say(msg) - return self.REQUEST.RESPONSE.redirect(url) - - def gotoEdit(self): - '''Brings the user to the edit page for this object. This method takes - care of not carrying any password value. Unlike m_goto above, there - is no HTTP redirect here: we execute directly PX "edit" and we - return the result.''' - page = self.REQUEST.get('page', 'main') - for field in self.getAppyTypes('edit', page): - if (field.type == 'String') and (field.format in (3,4)): - self.REQUEST.set(field.name, '') - return self.edit() - - def gotoTied(self): - '''Redirects the user to an object tied to this one.''' - return self.getAppyType(self.REQUEST['field']).onGotoTied(self) - - def getCreateFolder(self): - '''When an object must be created from this one through a Ref field, we - must know where to put the newly create object: within this one if it - is folderish, besides this one in its parent else. - ''' - if self.isPrincipiaFolderish: return self - return self.getParentNode() - - def getFieldValue(self, name, layoutType=None, outerValue=None): - '''Returns the database value of field named p_name for p_self.''' - if layoutType == 'search': return # No object in search screens - field = self.getAppyType(name) - if field.type == 'Pod': return - if '*' not in name: return field.getValue(self) - # The field is an inner field from a List/Dict - listName, name, i = name.split('*') - listType = self.getAppyType(listName) - return listType.getInnerValue(self, outerValue, name, int(i)) - - def getRequestFieldValue(self, name): - '''Gets the value of field p_name as may be present in the request.''' - # Return the request value for standard fields. - if '*' not in name: - return self.getAppyType(name).getRequestValue(self) - # For sub-fields within Lists, the corresponding request values have - # already been computed in the request key corresponding to the whole - # List. - listName, name, rowIndex = name.split('*') - rowIndex = int(rowIndex) - if rowIndex == -1: return '' - allValues = self.REQUEST.get(listName) - if not allValues: return '' - return getattr(allValues[rowIndex], name, '') - - def isDebug(self): - '''Are we in debug mode ?''' - for arg in sys.argv: - if arg == 'debug-mode=on': return True - - def getClass(self, reloaded=False): - '''Returns the Appy class that dictates self's behaviour.''' - if not reloaded: - return self.getTool().getAppyClass(self.__class__.__name__) - else: - klass = self.appy().klass - moduleName = klass.__module__ - exec('import %s' % moduleName) - exec('reload(%s)' % moduleName) - exec('res = %s.%s' % (moduleName, klass.__name__)) - # More manipulations may have occurred in m_update - if hasattr(res, 'update'): - parentName= res.__bases__[-1].__name__ - moduleName= 'Products.%s.wrappers' % self.getTool().getAppName() - exec('import %s' % moduleName) - exec('parent = %s.%s' % (moduleName, parentName)) - res.update(parent) - return res - - def getAppyType(self, name, className=None): - '''Returns the Appy type named p_name. If no p_className is defined, the - field is supposed to belong to self's class.''' - isInnerType = '*' in name # An inner type lies within a List type. - subName = None - if isInnerType: - elems = name.split('*') - if len(elems) == 2: name, subName = elems - else: name, subName, i = elems - if not className: - klass = self.__class__.wrapperClass - else: - klass = self.getTool().getAppyClass(className, wrapper=True) - res = getattr(klass, name, None) - if res and isInnerType: res = res.getField(subName) - return res - - def getAllAppyTypes(self, className=None): - '''Returns the ordered list of all Appy types for self's class if - p_className is not specified, or for p_className else.''' - if not className: - klass = self.__class__.wrapperClass - else: - klass = self.getTool().getAppyClass(className, wrapper=True) - return klass.__fields__ - - def getGroupedFields(self, layoutType, pageName, cssJs=None): - '''Returns the fields sorted by group. If a dict is given in p_cssJs, - we will add it in the css and js files required by the fields.''' - res = [] - groups = {} # The already encountered groups - # If a dict is given in p_cssJs, we must fill it with the CSS and JS - # files required for every returned field. - collectCssJs = isinstance(cssJs, dict) - css = js = None - config = self.getProductConfig(True) - # If param "refresh" is there, we must reload the Python class - refresh = ('refresh' in self.REQUEST) - if refresh: klass = self.getClass(reloaded=True) - for field in self.getAllAppyTypes(): - if refresh: field = field.reload(klass, self) - if field.page.name != pageName: continue - if not field.isShowable(self, layoutType): continue - if collectCssJs: - if css == None: css = [] - field.getCss(layoutType, css, config) - if js == None: js = [] - field.getJs(layoutType, js, config) - if not field.group: - res.append(field) - else: - # Insert the UiGroup instance corresponding to field.group - uiGroup = field.group.insertInto(res, groups, field.page, - self.meta_type) - uiGroup.addElement(field) - if collectCssJs: - cssJs['css'] = css or () - cssJs['js'] = js or () - return res - - def getAppyTypes(self, layoutType, pageName, type=None): - '''Returns the list of fields that belong to a given page (p_pageName) - for a given p_layoutType. If p_pageName is None, fields of all pages - are returned. If p_type is defined, only fields of this p_type are - returned. ''' - res = [] - for field in self.getAllAppyTypes(): - if pageName and (field.page.name != pageName): continue - if type and (field.type != type): continue - if not field.isRenderable(layoutType): continue - if not field.isShowable(self, layoutType): continue - res.append(field) - return res - - def getSlavesRequestInfo(self, pageName): - '''When slave fields must be updated via Ajax requests, we must carry - some information from the global request object to the ajax requests: - - the selected values in slave fields; - - validation errors.''' - requestValues = {} - errors = {} - req = self.REQUEST - for field in self.getAllAppyTypes(): - if field.page.name != pageName: continue - if field.masterValue and isinstance(field.masterValue, collections.Callable): - # We have a slave field that is updated via ajax requests. - name = field.name - # Remember the request value for this field if present. - if name in req and req[name]: - requestValues[name] = req[name] - # Remember the validation error for this field if present. - errorKey = '%s_error' % name - if errorKey in req: - errors[name] = req[errorKey] - return sutils.getStringDict(requestValues), sutils.getStringDict(errors) - - def getCssJs(self, fields, layoutType, res): - '''Gets, in p_res ~{'css':[s_css], 'js':[s_js]}~ the lists of - Javascript and CSS files required by Appy types p_fields when shown - on p_layoutType.''' - # Lists css and js below are not sets, because order of Javascript - # inclusion can be important, and this could be losed by using sets. - css = [] - js = [] - config = self.getProductConfig(True) - for field in fields: - field.getCss(layoutType, css, config) - field.getJs(layoutType, js, config) - res['css'] = css - res['js'] = js - - def getCssFor(self, elem): - '''Gets the name of the CSS class to use for styling some p_elem. If - self's class does not define a dict "styles", the defaut CSS class - to use will be named p_elem.''' - klass = self.getClass() - if hasattr(klass, 'styles') and (elem in klass.styles): - return klass.styles[elem] - return elem - - def getAppyPhases(self, currentOnly=False, layoutType='view'): - '''Gets the list of phases that are defined for this content type. If - p_currentOnly is True, the search is limited to the phase where the - current page (as defined in the request) lies.''' - # Get the list of phases - res = [] # Ordered list of phases - phases = {} # Dict of phases - for field in self.getAllAppyTypes(): - fieldPhase = field.page.phase - if fieldPhase not in phases: - phase = gen.Phase(fieldPhase, self) - res.append(phase) - phases[fieldPhase] = phase - else: - phase = phases[fieldPhase] - phase.addPage(field, self, layoutType) - if (field.type == 'Ref') and field.navigable: - phase.addPageLinks(field, self) - # Remove phases that have no visible page - for i in range(len(res)-1, -1, -1): - if not res[i].pages: - del phases[res[i].name] - del res[i] - # Compute next/previous phases of every phase - for ph in phases.values(): - ph.computeNextPrevious(res) - ph.totalNbOfPhases = len(res) - # Restrict the result to the current phase if required - if currentOnly: - rq = self.REQUEST - page = rq.get('page', None) - if not page: - if layoutType == 'edit': page = self.getDefaultEditPage() - else: page = self.getDefaultViewPage() - for phase in res: - if page in phase.pages: - return phase - # If I am here, it means that the page as defined in the request, - # or the default page, is not existing nor visible in any phase. - # In this case I find the first visible page among all phases. - viewAttr = 'showOn%s' % layoutType.capitalize() - for phase in res: - for page in phase.pages: - if getattr(phase.pagesInfo[page], viewAttr): - rq.set('page', page) - pageFound = True - break - return phase - else: - # Return an empty list if we have a single, link-free page within - # a single phase. - if (len(res) == 1) and (len(res[0].pages) == 1) and \ - not res[0].pagesInfo[res[0].pages[0]].links: - return - return res - - def highlight(self, text): - '''This method highlights parts of p_value if we are in the context of - a query whose keywords must be highlighted.''' - # Must we highlight something? - criteria = self.REQUEST.SESSION.get('searchCriteria') - if not criteria or ('SearchableText' not in criteria): return text - # Highlight every variant of every keyword - for word in criteria['SearchableText'].strip().split(): - sWord = word.strip(' *').lower() - for variant in (sWord, sWord.capitalize(), sWord.upper()): - text = text.replace(variant, '***+%s-***' % variant) - # If I replace immediately with the opening and ending "span" - # tag (see below), I will have problems if the next keyword is - # included in the tag, ie "la" (in 'class', or "spa" (in - # 'span'). - text = text.replace('***+', '') - return text.replace('-***', '') - - def getSupTitle(self, navInfo=''): - '''Gets the html code (icons,...) that can be shown besides the title - of an object.''' - obj = self.appy() - if hasattr(obj, 'getSupTitle'): return obj.getSupTitle(navInfo) - return '' - - def getSubTitle(self): - '''Gets the content that must appear below the title of an object.''' - obj = self.appy() - if hasattr(obj, 'getSubTitle'): return obj.getSubTitle() - return '' - - def getSupBreadCrumb(self): - '''Gets the html code that can be shown besides the title of an object - in the breadcrumb.''' - obj = self.appy() - if hasattr(obj, 'getSupBreadCrumb'): return obj.getSupBreadCrumb() - return '' - - def getSubBreadCrumb(self): - '''Gets the content that must appear below the title of an object in the - breadcrumb.''' - obj = self.appy() - if hasattr(obj, 'getSubBreadCrumb'): return obj.getSubBreadCrumb() - return '' - - def getListTitle(self, mode='link', nav='', target=None, page='main', - inPopup=False, selectJs=None, highlight=False): - '''Gets the title as it must appear in lists of objects (ie in lists of - tied objects in a Ref, in query results...). - - In most cases, a title must appear as a link that leads to the object - view layout. In this case (p_mode == "link"): - * p_nav is the navigation parameter allowing navigation between - this object and others; - * p_target specifies if the link must be opened in the popup or not; - * p_page specifies which page to show on the target object view; - * p_inPopup indicates if we are already in the popup or not. - - Another p_mode is "select". In this case, we are in a popup for - selecting objects: every title must not be a link, but clicking on it - must trigger Javascript code (in p_selectJs) that will select this - object. - - The last p_mode is "text". In this case, we simply show the object - title but with no tied action (link, select). - - If p_highlight is True, keywords will be highlighted if we are in the - context of a query with keywords. - ''' - # Compute common parts - cssClass = self.getCssFor('title') - # Get the title, with highlighted parts when relevant - klass = self.getClass() - if hasattr(klass, 'listTitle'): - title = klass.listTitle(self.appy(), nav) - else: - title = self.getShownValue('title') - if highlight: title = self.highlight(title) - if mode == 'link': - inPopup = inPopup or (target.target != '_self') - url = self.getUrl(page=page, nav=nav, inPopup=inPopup) - onClick = target.openPopup or 'clickOn(this)' - res = '%s' % \ - (url, cssClass, onClick, target.target, title) - elif mode == 'select': - res = '%s' % \ - (cssClass, selectJs, title) - elif mode == 'text': - res = '%s' % (cssClass, title) - return res - - # Workflow methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - def initializeWorkflow(self): - '''Called when an object is created, be it temp or not, for initializing - workflow-related data on the object.''' - wf = self.getWorkflow() - # Get the initial workflow state - initialState = self.State(name=False) - # Create a Transition instance representing the initial transition - initialTransition = gen.Transition((initialState, initialState)) - initialTransition.trigger('_init_', self, wf, '', doSay=False) - - def getWorkflow(self, name=False, className=None): - '''Returns the workflow applicable for p_self (or for any instance of - p_className if given), or its name, if p_name is True.''' - if not className: - wrapperClass = self.wrapperClass - else: - wrapperClass = self.getTool().getAppyClass(className, wrapper=True) - wf = wrapperClass.getWorkflow() - if not name: return wf - return WorkflowDescriptor.getWorkflowName(wf) - - def getWorkflowLabel(self, name=None): - '''Gets the i18n label for p_name (which can denote a state or a - transition), or for the current object state if p_name is None.''' - name = name or self.State() - if name == 'create_from_predecessor': return name - return '%s_%s' % (self.getWorkflow(name=True), name) - - def getTransitions(self, includeFake=True, includeNotShowable=False, - grouped=True): - '''This method returns info about transitions (as UiTransition - instances) that one can trigger from the user interface. - * if p_includeFake is True, it retrieves transitions that the user - can't trigger, but for which he needs to know for what reason he - can't trigger it; - * if p_includeNotShowable is True, it includes transitions for which - show=False. Indeed, because "showability" is only a UI concern, - and not a security concern, in some cases it has sense to set - includeNotShowable=True, because those transitions are triggerable - from a security point of view. - * If p_grouped is True, transitions are grouped according to their - "group" attribute, in a similar way to fields or searches. - ''' - res = [] - groups = {} # The already encountered groups of transitions. - wfPage = gen.Page('workflow') - wf = self.getWorkflow() - currentState = self.State(name=False) - # Loop on every transition - for name in dir(wf): - transition = getattr(wf, name) - if (transition.__class__.__name__ != 'Transition'): continue - # Filter transitions that do not have currentState as start state - if not transition.hasState(currentState, True): continue - # Check if the transition can be triggered - mayTrigger = transition.isTriggerable(self, wf) - # Compute the condition that will lead to including or not this - # transition - if not includeFake: - includeIt = mayTrigger - else: - includeIt = mayTrigger or isinstance(mayTrigger, gen.No) - if not includeNotShowable: - includeIt = includeIt and transition.isShowable(wf, self) - if not includeIt: continue - # Create the UiTransition instance. - info = UiTransition(name, transition, self, mayTrigger) - # Add the transition into the result. - if not transition.group or not grouped: - res.append(info) - else: - # Insert the UiGroup instance corresponding to transition.group. - uiGroup = transition.group.insertInto(res, groups, wfPage, - self.__class__.__name__, content='transitions') - uiGroup.addElement(info) - return res - - def applyUserIdChange(self, oldId, newId): - '''A user whose ID was p_oldId has now p_newId. If the old ID was - mentioned in self's local roles, update it to the new ID. This - method returns 1 if a change occurred, 0 else.''' - if oldId in self.__ac_local_roles__: - localRoles = self.__ac_local_roles__.copy() - localRoles[newId] = localRoles[oldId] - del localRoles[oldId] - self.__ac_local_roles__ = localRoles - self.reindex() - return 1 - return 0 - - def findNewValue(self, field, language, history, stopIndex): - '''This function tries to find a more recent version of value of p_field - on p_self. In the case of a multilingual field, p_language is - specified. The method first tries to find it in - history[:stopIndex+1]. If it does not find it there, it returns the - current value on p_obj.''' - i = stopIndex + 1 - name = language and ('%s-%s' % (field.name, language)) or field.name - while (i-1) >= 0: - i -= 1 - if history[i]['action'] != '_datachange_': continue - if name not in history[i]['changes']: continue - # We have found it! - return history[i]['changes'][name][0] or '' - # A most recent version was not found in the history: return the current - # field value. - val = field.getValue(self) - if not language: return val or '' - return val and val.get(language) or '' - - def getHistoryTexts(self, event): - '''Returns a tuple (insertText, deleteText) containing texts to show on, - respectively, inserted and deleted chunks of text in a XHTML diff.''' - tool = self.getTool() - mapping = {'userName': tool.getUserName(event['actor'])} - res = [] - for type in ('insert', 'delete'): - msg = self.translate('history_%s' % type, mapping=mapping) - date = tool.formatDate(event['time'], withHour=True) - msg = '%s: %s' % (date, msg) - res.append(msg) - return res - - def hasHistory(self, name=None): - '''Has this object an history? If p_name is specified, the question - becomes: has this object an history for field p_name?''' - if not hasattr(self.aq_base, 'workflow_history') or \ - not self.workflow_history: return - # Return False if the user can't consult the object history - klass = self.getClass() - if hasattr(klass, 'showHistory'): - show = klass.showHistory - if callable(show): show = klass.showHistory(self.appy()) - if not show: return - # Get the object history - history = self.workflow_history['appy'] - if not name: - for event in history: - if event['action'] and (event['comments'] != '_invisible_'): - return True - else: - field = self.getAppyType(name) - # Is this field a multilingual field ? - languages = None - if field.type == 'String': - languages = field.getAttribute(self, 'languages') - multilingual = len(languages) > 1 - for event in history: - if event['action'] != '_datachange_': continue - # Is there a value present for this field in this data change? - if not multilingual: - if (name in event['changes']) and \ - (event['changes'][name][0]): - return True - else: - # At least one language-specific value must be present - for lg in languages: - lgName = '%s-%s' % (field.name, lg) - if (lgName in event['changes']) and \ - event['changes'][lgName][0]: - return True - - def getHistory(self, startNumber=0, reverse=True, includeInvisible=False, - batchSize=5): - '''Returns a copy of the history for this object, sorted in p_reverse - order if specified (most recent change first), whose invisible events - have been removed if p_includeInvisible is True.''' - history = list(self.workflow_history['appy'][1:]) - if not includeInvisible: - history = [e for e in history if e['comments'] != '_invisible_'] - if reverse: history.reverse() - # Keep only events which are within the batch - res = [] - stopIndex = startNumber + batchSize - 1 - i = -1 - while (i+1) < len(history): - i += 1 - # Ignore events outside range startNumber:startNumber+batchSize - if i < startNumber: continue - if i > stopIndex: break - if history[i]['action'] == '_datachange_': - # Take a copy of the event: we will modify it and replace - # fields' old values by their formatted counterparts. - event = history[i].copy() - event['changes'] = {} - for name, oldValue in history[i]['changes'].items(): - # "name" can specify a language-specific part in a - # multilingual field. "oldValue" is a tuple - # (value, fieldName). - if '-' in name: - fieldName, lg = name.split('-') - else: - fieldName = name - lg = None - field = self.getAppyType(fieldName) - # Field 'name' may not exist, if the history has been - # transferred from another site. In this case we can't show - # this data change. - if not field: continue - if (field.type == 'String') and \ - (field.format == gen.String.XHTML): - # For rich text fields, instead of simply showing the - # previous value, we propose a diff with the next - # version, excepted if the previous value is empty. - if lg: isEmpty = not oldValue[0] - else: isEmpty = field.isEmptyValue(self, oldValue[0]) - if isEmpty: - val = '-' - else: - newValue= self.findNewValue(field, lg, history, i-1) - # Compute the diff between oldValue and newValue - iMsg, dMsg = self.getHistoryTexts(event) - comparator= HtmlDiff(oldValue[0],newValue,iMsg,dMsg) - val = comparator.get() - else: - fmt = lg and 'getUnilingualFormattedValue' or \ - 'getFormattedValue' - val = getattr(field, fmt)(self, oldValue[0]) or '-' - if isinstance(val, list) or isinstance(val, tuple): - val = '
    %s
' % \ - ''.join(['
  • %s
  • ' % v for v in val]) - event['changes'][name] = (val, oldValue[1]) - else: - event = history[i] - res.append(event) - return Object(events=res, totalNumber=len(history)) - - def getHistoryCollapse(self): - '''Gets a Collapsible instance for showing a collapse or expanded - history in this object.''' - return Collapsible('objectHistory', self.REQUEST) - - def getHistoryAjaxData(self, hook, startNumber, batchSize): - '''Gets data allowing to ajax-ask paginated history data.''' - params = {'startNumber': startNumber, 'maxPerPage': batchSize} - # Convert params into a JS dict - params = sutils.getStringDict(params) - return "getAjaxHook('%s',true)['ajax']=new AjaxData('%s','pxHistory', "\ - "%s, null, '%s')" % (hook, hook, params, self.absolute_url()) - - def mayNavigate(self): - '''May the currently logged user see the navigation panel linked to - this object?''' - appyObj = self.appy() - if hasattr(appyObj, 'mayNavigate'): return appyObj.mayNavigate() - return True - - def getDefaultViewPage(self): - '''Which view page must be shown by default?''' - appyObj = self.appy() - if hasattr(appyObj, 'getDefaultViewPage'): - return appyObj.getDefaultViewPage() - return 'main' - - def getDefaultEditPage(self): - '''Which edit page must be shown by default?''' - appyObj = self.appy() - if hasattr(appyObj, 'getDefaultEditPage'): - return appyObj.getDefaultEditPage() - return 'main' - - def mayAct(self): - '''m_mayAct allows to hide the whole set of actions for an object. - Indeed, beyond workflow security, it can be useful to hide controls - like "edit" icons/buttons. For example, if a user may only edit some - Ref fields with add=True on an object, when clicking on "edit", he - will see an empty edit form.''' - appyObj = self.appy() - if hasattr(appyObj, 'mayAct'): return appyObj.mayAct() - return True - - def mayDelete(self): - '''May the currently logged user delete this object?''' - res = self.allows('delete') - if not res: return - # An additional, user-defined condition, may refine the base permission. - appyObj = self.appy() - if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete() - return True - - def mayEdit(self, permission='write', permOnly=False, raiseError=False): - '''May the currently logged user edit this object? p_permission can be a - field-specific permission. If p_permOnly is True, the specific - user-defined condition is not evaluated. If p_raiseError is True, if - the user may not edit p_self, an error is raised.''' - res = self.allows(permission, raiseError=raiseError) - if not res: return - if permOnly: return res - # An additional, user-defined condition, may refine the base permission. - appyObj = self.appy() - if hasattr(appyObj, 'mayEdit'): - res = appyObj.mayEdit() - if not res and raiseError: self.raiseUnauthorized() - return res - return True - - def mayView(self, permission='read', raiseError=False): - '''May the currently logged user view this object? p_permission can be a - field-specific permission. If p_raiseError is True, if the user may - not view p_self, an error is raised.''' - res = self.allows(permission, raiseError=raiseError) - if not res: return - # An additional, user-defined condition, may refine the base permission. - appyObj = self.appy() - if hasattr(appyObj, 'mayView'): - res = appyObj.mayView() - if not res and raiseError: self.raiseUnauthorized() - return res - return True - - def onExecuteAction(self): - '''Called when a user wants to execute an Appy action on an object.''' - rq = self.REQUEST - return self.getAppyType(rq['fieldName']).onUiRequest(self, rq) - - def onTrigger(self): - '''Called when a user wants to trigger a transition on an object.''' - rq = self.REQUEST - wf = self.getWorkflow() - # Get the transition - name = rq['transition'] - transition = getattr(wf, name, None) - if not transition or (transition.__class__.__name__ != 'Transition'): - raise Exception('Transition "%s" not found.' % name) - return transition.onUiRequest(self, wf, name, rq) - - def getRolesFor(self, permission): - '''Gets, according to the workflow, the roles that are currently granted - p_permission on this object.''' - state = self.State(name=False) - if permission not in state.permissions: - wf = self.getWorkflow().__name__ - raise Exception('Permission "%s" not in permissions dict for ' \ - 'state %s.%s' % \ - (permission, wf, self.State(name=True))) - roles = state.permissions[permission] - if roles: return [role.name for role in roles] - return () - - def appy(self): - '''Returns a wrapper object allowing to manipulate p_self the Appy - way.''' - # Create the dict for storing Appy wrapper on the REQUEST if needed. - rq = getattr(self, 'REQUEST', None) - if not rq: - # We are in test mode or Zope is starting. Use static variable - # config.fakeRequest instead. - rq = self.getProductConfig().fakeRequest - if not hasattr(rq, 'wrappers'): rq.wrappers = {} - # Return the Appy wrapper if already present in the cache - uid = self.id - if uid in rq.wrappers: return rq.wrappers[uid] - # Create the Appy wrapper, cache it in rq.wrappers and return it - wrapper = self.wrapperClass(self) - rq.wrappers[uid] = wrapper - return wrapper - - # -------------------------------------------------------------------------- - # Methods for computing values of standard Appy indexes - # -------------------------------------------------------------------------- - def UID(self): - '''Returns the unique identifier for this object.''' - return self.id - - def Title(self): - '''Returns the title for this object.''' - title = self.getAppyType('title') - if title: return title.getIndexValue(self) - return self.id - - def SortableTitle(self): - '''Returns the title as must be stored in index "SortableTitle".''' - return sutils.normalizeText(self.Title()) - - def SearchableText(self): - '''This method concatenates the content of every field with - searchable=True for indexing purposes.''' - res = [] - for field in self.getAllAppyTypes(): - if not field.searchable: continue - res.append(field.getIndexValue(self, forSearch=True)) - return res - - def Creator(self): - '''Who create this object?''' - return self.creator - - def Created(self): - '''When was this object created ?''' - return self.created - - def Modified(self): - '''When was this object last modified ?''' - if hasattr(self.aq_base, 'modified'): return self.modified - return self.created - - def State(self, name=True, initial=False): - '''Returns information about the current object state. If p_name is - True, the returned info is the state name. Else, it is the State - instance. If p_initial is True, instead of returning info about the - current state, it returns info about the workflow initial state.''' - wf = self.getWorkflow() - if initial or not hasattr(self.aq_base, 'workflow_history'): - # No workflow information is available (yet) on this object, or - # initial state is asked. In both cases, return info about this - # initial state. - res = 'active' - for elem in dir(wf): - attr = getattr(wf, elem) - if (attr.__class__.__name__ == 'State') and attr.initial: - res = elem - break - else: - # Return info about the current object state - res = self.workflow_history['appy'][-1]['review_state'] - # Return state name or state definition? - if name: return res - else: return getattr(wf, res) - - def ClassName(self): - '''Returns the name of the (Zope) class for self.''' - return self.portal_type - - def Allowed(self): - '''Returns the list of roles and users that are allowed to view this - object. This index value will be used within catalog queries for - filtering objects the user is allowed to see.''' - # Get, from the workflow, roles having permission 'read'. - res = self.getRolesFor('read') - # Add users or groups having, locally, this role on this object. - localRoles = getattr(self.aq_base, '__ac_local_roles__', None) - if not localRoles: return res - for id, roles in localRoles.items(): - for role in roles: - if role in res: - usr = 'user:%s' % id - if usr not in res: res.append(usr) - return res - - def showState(self): - '''Must I show self's current state ?''' - stateShow = self.State(name=False).show - if isinstance(stateShow, collections.Callable): - return stateShow(self.getWorkflow(), self.appy()) - return stateShow - - def showTransitions(self, layoutType): - '''Must we show the buttons/icons for triggering transitions on - p_layoutType?''' - # Never show transitions on edit pages. - if layoutType == 'edit': return - # Use the default value if self's class does not specify it. - klass = self.getClass() - if not hasattr(klass, 'showTransitions'): return (layoutType=='view') - showValue = klass.showTransitions - # This value can be a single value or a tuple/list of values. - if isinstance(showValue, str): return layoutType == showValue - return layoutType in showValue - - getUrlDefaults = {'page':True, 'nav':True} - def getUrl(self, base=None, mode='view', inPopup=False, relative=False, - **kwargs): - '''Returns an URL for this object. - * If p_base is None, it will be the base URL for this object - (ie, Zope self.absolute_url() or an URL this is relative to the - root site if p_relative is True). - * p_mode can be "edit", "view" or "raw" (a non-param, base URL) - * If p_inPopup is True, the link will be opened in the Appy iframe. - An additional param "popup=1" will be added to URL params, in order - to tell Appy that the link target will be shown in a popup, in a - minimalistic way (no portlet...). - * p_kwargs can store additional parameters to add to the URL. - In this dict, every value that is a string will be added to the - URL as-is. Every value that is True will be replaced by the value - in the request for the corresponding key (if existing; else, the - param will not be included in the URL at all).''' - # Define the URL suffix - suffix = '' - if mode != 'raw': suffix = '/%s' % mode - # Define the base URL if omitted - if not base: - base = relative and self.absolute_url_path() or self.absolute_url() - base += suffix - existingParams = '' - else: - existingParams = urllib.splitquery(base)[1] - # If a raw URL is asked, remove any param and suffix - if mode == 'raw': - if '?' in base: base = base[:base.index('?')] - base = base.rstrip('/') - for mode in ('view', 'edit'): - if base.endswith(mode): - base = base[:-len(mode)].rstrip('/') - break - return base - # Manage default args - if not kwargs: kwargs = self.getUrlDefaults - if 'page' not in kwargs: kwargs['page'] = True - if 'nav' not in kwargs: kwargs['nav'] = True - # Create URL parameters from kwargs - params = [] - for name, value in kwargs.items(): - if isinstance(value, str): - params.append('%s=%s' % (name, value)) - elif self.REQUEST.get(name, ''): - params.append('%s=%s' % (name, self.REQUEST[name])) - # Manage inPopup - if inPopup and ('popup=' not in existingParams): - params.append('popup=1') - if params: - params = '&'.join(params) - if base.find('?') != -1: params = '&' + params - else: params = '?' + params - else: - params = '' - return '%s%s' % (base, params) - - def getTool(self): - '''Returns the application tool.''' - return self.getPhysicalRoot().config - - def getProductConfig(self, app=False): - '''Returns a reference to the config module. If p_app is True, it - returns the application config.''' - res = self.__class__.config - if app: res = res.appConfig - return res - - def getParent(self): - '''If this object is stored within another one, this method returns it. - Else (if the object is stored directly within the tool or the root - data folder) it returns None.''' - parent = self.getParentNode() - # Not-Managers can't navigate back to the tool - if (parent.id == 'config') and \ - not self.getTool().getUser().has_role('Manager'): - return False - if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent - - def getShownValue(self, name='title', layoutType='view', language=None): - '''Call field.getShownValue on field named p_name''' - field = self.getAppyType(name) - return field.getShownValue(self, field.getValue(self), layoutType, - language=language) - - def getBreadCrumb(self, inPopup=False): - '''Gets breadcrumb info about this object and its parents (if it must - be shown).''' - # Return an empty breadcrumb if it must not be shown - klass = self.getClass() - if hasattr(klass, 'breadcrumb') and not klass.breadcrumb: return () - # Compute the breadcrumb - res = [Object(url=self.getUrl(inPopup=inPopup), - title=self.getShownValue('title'))] - # In a popup, limit the breadcrumb to the current object - if inPopup: return res - parent = self.getParent() - if parent: res = parent.getBreadCrumb() + res - return res - - def index_html(self): - '''Base method called when hitting this object. - - The standard behaviour is to redirect to /view. - - If a parameter named "do" is present in the request, it is supposed - to contain the name of a method to call on this object. In this - case, we call this method and return its result as XML. - - If method is POST, we consider the request to be XML data, that we - marshall to Python, and we call the method in param "do" with, as - arg, this marshalled Python object. While this could sound strange - to expect a query string containing a param "do" in a HTTP POST, - the HTTP spec does not prevent to do it.''' - rq = self.REQUEST - if (rq.REQUEST_METHOD == 'POST') and rq.QUERY_STRING: - # A POST method containing XML data. - rq.args = XmlUnmarshaller().parse(rq.stdin.getvalue()) - # Find the name of the method to call. - methodName = rq.QUERY_STRING.split('=')[1] - return self.xml(action=methodName) - elif 'do' in rq: - # The user wants to call a method on this object and get its result - # as XML. - return self.xml(action=rq['do']) - else: - # The user wants to consult the view page for this object - return rq.RESPONSE.redirect(self.getUrl()) - - def getUserLanguage(self): - '''Gets the language (code) of the current user.''' - if not hasattr(self, 'REQUEST'): - return self.getProductConfig().appConfig.languages[0] - # Return the cached value on the request object if present - rq = self.REQUEST - if hasattr(rq, 'userLanguage'): return rq.userLanguage - # Try the value which comes from the cookie. Indeed, if such a cookie is - # present, it means that the user has explicitly chosen this language - # via the language selector. - if '_ZopeLg' in rq.cookies: - res = rq.cookies['_ZopeLg'] - else: - # Try the LANGUAGE key from the request: it corresponds to the - # language as configured in the user's browser. - res = rq.get('LANGUAGE', None) - if not res: - # Try the HTTP_ACCEPT_LANGUAGE key from the request, which - # stores language preferences as defined in the user's browser. - # Several languages can be listed, from most to less wanted. - res = rq.get('HTTP_ACCEPT_LANGUAGE', None) - if res: - if ',' in res: res = res[:res.find(',')] - if '-' in res: res = res[:res.find('-')] - else: - res = self.getProductConfig().appConfig.languages[0] - # Cache this result - rq.userLanguage = res - return res - - def getLanguageDirection(self, lang): - '''Determines if p_lang is a LTR or RTL language.''' - if lang in rtlLanguages: return 'rtl' - return 'ltr' - - def formatText(self, text, format='html'): - '''Produces a representation of p_text into the desired p_format, which - is "html" by default.''' - if 'html' in format: - if format == 'html_from_text': text = cgi.escape(text) - res = text.replace('\r\n', '
    ').replace('\n', '
    ') - elif format == 'text': - res = text.replace('
    ', '\n') - else: - res = text - return res - - def translate(self, label, mapping={}, domain=None, default=None, - language=None, format='html', field=None): - '''Translates a given p_label into p_domain with p_mapping. - - If p_field is given, p_label does not correspond to a full label - name, but to a label type linked to p_field: "label", "descr" - or "help". Indeed, in this case, a specific i18n mapping may be - available on the field, so we must merge this mapping into - p_mapping.''' - cfg = self.getProductConfig() - if not domain: domain = cfg.PROJECTNAME - # Get the label name, and the field-specific mapping if any. - if field: - if field.type != 'group': - fieldMapping = field.mapping[label] - if fieldMapping: - if isinstance(fieldMapping, collections.Callable): - fieldMapping = field.callMethod(self, fieldMapping) - mapping.update(fieldMapping) - label = getattr(field, '%sId' % label) - # We will get the translation from a Translation object. - # In what language must we get the translation? - if not language: language = self.getUserLanguage() - tool = self.getTool() - try: - translation = getattr(tool, language).appy() - except AttributeError: - # We have no translation for this language. Fallback to 'en'. - translation = getattr(tool, 'en').appy() - res = getattr(translation, label, '') - if not res: - # Fallback to 'en'. - translation = getattr(tool, 'en').appy() - res = getattr(translation, label, '') - # If still no result, put a nice name derived from the label instead of - # a translated message. - if not res: res = produceNiceMessage(label.rsplit('_', 1)[-1]) - else: - # Perform replacements, according to p_format. - res = self.formatText(res, format) - # Perform variable replacements - for name, repl in mapping.items(): - if not isinstance(repl, str): repl = str(repl) - res = res.replace('${%s}' % name, repl) - return res - - def getPageLayout(self, layoutType): - '''Returns the layout corresponding to p_layoutType for p_self.''' - res = self.wrapperClass.getPageLayouts()[layoutType] - if isinstance(res, str): res = Table(res) - return res - - def download(self, name=None): - '''Downloads the content of the file that is in the File field whose - name is in the request. This name can also represent an attribute - storing an image within a rich text field. If p_name is not given, it - is retrieved from the request.''' - rq = self.REQUEST - name = rq.get('name') - if not name: return - # Security check - if '_img_' not in name: - field = self.getAppyType(name) - else: - field = self.getAppyType(name.split('_img_')[0]) - self.mayView(field.readPermission, raiseError=True) - # Write the file in the HTTP response - info = getattr(self.aq_base, name, None) - if info: - # Content disposition may be given in the request - disposition = rq.get('disposition', 'attachment') - if disposition not in ('inline', 'attachment'): - disposition = 'attachment' - info.writeResponse(rq.RESPONSE, self.getDbFolder(), disposition) - - def upload(self): - '''Receives an image uploaded by the user via ckeditor and stores it in - a special field on this object.''' - # Get the name of the rich text field for which an image must be stored. - params = self.REQUEST['QUERY_STRING'].split('&') - fieldName = params[0].split('=')[1] - ckNum = params[1].split('=')[1] - # We will store the image in a field named [fieldName]_img_[nb]. - i = 1 - attrName = '%s_img_%d' % (fieldName, i) - while True: - if not hasattr(self.aq_base, attrName): - break - else: - i += 1 - attrName = '%s_img_%d' % (fieldName, i) - # Store the image. Create a fake File instance for doing the job. - fakeFile = gen.File(isImage=True) - fakeFile.name = attrName - fakeFile.store(self, self.REQUEST['upload']) - # Return the URL of the image. - url = '%s/download?name=%s' % (self.absolute_url(), attrName) - response = self.REQUEST.RESPONSE - response.setHeader('Content-Type', 'text/html') - resp = "" % (ckNum, url) - response.write(resp) - - def allows(self, permission, raiseError=False): - '''Has the logged user p_permission on p_self ?''' - res = self.getTool().getUser().has_permission(permission, self) - if not res and raiseError: self.raiseUnauthorized() - return res - - def isTemporary(self): - '''Is this object temporary ?''' - parent = self.getParentNode() - if not parent: return # Is probably being created through code - return parent.getId() == 'temp_folder' - - def onProcess(self): - '''This method is a general hook for transfering processing of a request - to a given field, whose name must be in the request.''' - return self.getAppyType(self.REQUEST['name']).process(self) - - def onCall(self): - '''Calls a specific method on the corresponding wrapper.''' - self.mayView(raiseError=True) - method = self.REQUEST['method'] - obj = self.appy() - return getattr(obj, method)() - - def onReindex(self): - '''Called for reindexing an index or all indexes on the currently shown - object.''' - if not self.getTool().getUser().has_role('Manager'): - self.raiseUnauthorized() - rq = self.REQUEST - indexName = rq['indexName'] - if indexName == '_all_': - self.reindex() - else: - self.reindex(indexes=(indexName,)) - self.say(self.translate('action_done')) - self.goto(self.getUrl(rq['HTTP_REFERER'])) -# ------------------------------------------------------------------------------ diff --git a/gen/model.py b/gen/model.py deleted file mode 100644 index 814afeb..0000000 --- a/gen/model.py +++ /dev/null @@ -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 -# ------------------------------------------------------------------------------ diff --git a/gen/navigate.py b/gen/navigate.py deleted file mode 100644 index a36e88c..0000000 --- a/gen/navigate.py +++ /dev/null @@ -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(''' - - - - - - - - - :self.number // - :self.total - - - - - -
    :obj.pxGotoNumber
    ''') - - @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 -# ------------------------------------------------------------------------------ diff --git a/gen/po.py b/gen/po.py deleted file mode 100644 index 54c73c5..0000000 --- a/gen/po.py +++ /dev/null @@ -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 '' % \ - (self.id, self.msg, self.default) - - def getMessage(self): - '''Returns self.msg, but with some replacements.''' - return self.msg.replace('
    ', '\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 -# ------------------------------------------------------------------------------ diff --git a/gen/templates/Class.pyt b/gen/templates/Class.pyt deleted file mode 100644 index 38dec98..0000000 --- a/gen/templates/Class.pyt +++ /dev/null @@ -1,33 +0,0 @@ - -from OFS.SimpleItem import SimpleItem -from OFS.Folder import Folder -from appy.gen.utils import createObject -from AccessControl import ClassSecurityInfo -import Products..config as cfg -from appy.gen.mixins import BaseMixin -from appy.gen.mixins.ToolMixin import ToolMixin -from wrappers import _Wrapper as Wrapper - -def manage_add(self, id, title='', REQUEST=None): - '''Creates instances of this class.''' - createObject(self, id, '', '') - if REQUEST is not None: return self.manage_main(self, REQUEST) - -class (): - '''''' - security = ClassSecurityInfo() - meta_type = '' - portal_type = '' - allowed_content_types = () - filter_content_types = 0 - global_allow = 1 - icon = "ui/" - 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(): - if not elem.startswith('__'): security.declarePublic(elem) - diff --git a/gen/templates/__init__.pyt b/gen/templates/__init__.pyt deleted file mode 100644 index fcfbe61..0000000 --- a/gen/templates/__init__.pyt +++ /dev/null @@ -1,38 +0,0 @@ - -# 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 = -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 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): - - # I need to do those imports here; else, types and add permissions will not - # be registered. - classes = [] - ZopeInstaller(context, config, classes).install() -# ------------------------------------------------------------------------------ diff --git a/gen/templates/config.pyt b/gen/templates/config.pyt deleted file mode 100644 index 9d5ccc7..0000000 --- a/gen/templates/config.pyt +++ /dev/null @@ -1,45 +0,0 @@ - -import os, os.path, sys, copy -import appy -import appy.gen -import wrappers - - -# 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('') - -# Some global variables -------------------------------------------------------- -PROJECTNAME = '' -diskFolder = os.path.dirname(.__file__) - -# Applications classes, in various formats -appClasses = [] -appClassNames = [] -allClassNames = [] -allShortClassNames = {} - -# In the following dict, we store, for every Appy class, the ordered list of -# fields. -attributes = {} - -# Application roles -applicationRoles = [] -applicationGlobalRoles = [] -grantableRoles = [] - -try: - appConfig = .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() -# ------------------------------------------------------------------------------ diff --git a/gen/templates/testAll.pyt b/gen/templates/testAll.pyt deleted file mode 100644 index 3f526b9..0000000 --- a/gen/templates/testAll.pyt +++ /dev/null @@ -1,23 +0,0 @@ - - -from unittest import TestSuite -from Testing import ZopeTestCase -from Testing.ZopeTestCase import ZopeDocTestSuite -from appy.gen.mixins.TestMixin import TestMixin, beforeTest, afterTest - - -# Initialize the Zope test system ---------------------------------------------- -ZopeTestCase.installProduct('') - -class Test(TestMixin, ZopeTestCase.ZopeTestCase): - '''Base test class for test cases.''' - -# Data needed for defining the tests ------------------------------------------- -data = {'test_class': Test, 'setUp': beforeTest, 'tearDown': afterTest, - 'globs': {'appName': ''}} -modulesWithTests = [] - -# ------------------------------------------------------------------------------ -def test_suite(): - return TestSuite([ZopeDocTestSuite(m, **data) for m in modulesWithTests]) -# ------------------------------------------------------------------------------ diff --git a/gen/templates/wrappers.pyt b/gen/templates/wrappers.pyt deleted file mode 100644 index 54a4e24..0000000 --- a/gen/templates/wrappers.pyt +++ /dev/null @@ -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"} - - - - - - -autoref(Page, Page.pages) - - - -# ------------------------------------------------------------------------------ diff --git a/gen/tr/Appy.pot b/gen/tr/Appy.pot deleted file mode 100644 index b1988c8..0000000 --- a/gen/tr/Appy.pot +++ /dev/null @@ -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,

    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.

    ${url}" -msgid "reinit_password_body" -msgstr "" - -#. Default: "Your new password" -msgid "new_password" -msgstr "" - -#. Default: "Hello,

    The new password you have requested for website ${siteUrl} is ${password}

    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 "" diff --git a/gen/tr/ar.po b/gen/tr/ar.po deleted file mode 100644 index 4d01a6f..0000000 --- a/gen/tr/ar.po +++ /dev/null @@ -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,

    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.

    ${url}" -msgid "reinit_password_body" -msgstr "" - -#. Default: "Your new password" -msgid "new_password" -msgstr "" - -#. Default: "Hello,

    The new password you have requested for website ${siteUrl} is ${password}

    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 "" diff --git a/gen/tr/de.po b/gen/tr/de.po deleted file mode 100644 index c396326..0000000 --- a/gen/tr/de.po +++ /dev/null @@ -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,

    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.

    ${url}" -msgid "reinit_password_body" -msgstr "" - -#. Default: "Your new password" -msgid "new_password" -msgstr "" - -#. Default: "Hello,

    The new password you have requested for website ${siteUrl} is ${password}

    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 "" diff --git a/gen/tr/en.po b/gen/tr/en.po deleted file mode 100644 index c46b498..0000000 --- a/gen/tr/en.po +++ /dev/null @@ -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,

    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.

    ${url}" -msgid "reinit_password_body" -msgstr "Hello,

    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.

    ${url}" - -#. Default: "Your new password" -msgid "new_password" -msgstr "Your new password" - -#. Default: "Hello,

    The new password you have requested for website ${siteUrl} is ${password}

    Best regards." -msgid "new_password_body" -msgstr "Hello,

    The new password you have requested for website ${siteUrl} is ${password}

    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" diff --git a/gen/tr/es.po b/gen/tr/es.po deleted file mode 100644 index d539996..0000000 --- a/gen/tr/es.po +++ /dev/null @@ -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,

    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.

    ${url}" -msgid "reinit_password_body" -msgstr "" - -#. Default: "Your new password" -msgid "new_password" -msgstr "" - -#. Default: "Hello,

    The new password you have requested for website ${siteUrl} is ${password}

    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 "" diff --git a/gen/tr/fr.po b/gen/tr/fr.po deleted file mode 100644 index 1b257a4..0000000 --- a/gen/tr/fr.po +++ /dev/null @@ -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 Appy" - -#. 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,

    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.

    ${url}" -msgid "reinit_password_body" -msgstr "Bonjour,

    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.

    ${url}" - -#. Default: "Your new password" -msgid "new_password" -msgstr "Votre nouveau mot de passe" - -#. Default: "Hello,

    The new password you have requested for website ${siteUrl} is ${password}

    Best regards." -msgid "new_password_body" -msgstr "Bonjour,

    Votre nouveau mot de passe pour le site ${siteUrl} est ${password}

    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" diff --git a/gen/tr/it.po b/gen/tr/it.po deleted file mode 100644 index 25e76e7..0000000 --- a/gen/tr/it.po +++ /dev/null @@ -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,

    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.

    ${url}" -msgid "reinit_password_body" -msgstr "" - -#. Default: "Your new password" -msgid "new_password" -msgstr "" - -#. Default: "Hello,

    The new password you have requested for website ${siteUrl} is ${password}

    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 "" diff --git a/gen/tr/nl.po b/gen/tr/nl.po deleted file mode 100644 index 291d2f0..0000000 --- a/gen/tr/nl.po +++ /dev/null @@ -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 Appy." - -#. 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,

    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.

    ${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.

    ${url}" - -#. Default: "Your new password" -msgid "new_password" -msgstr "Uw nieuw paswoord" - -#. Default: "Hello,

    The new password you have requested for website ${siteUrl} is ${password}

    Best regards." -msgid "new_password_body" -msgstr "Hallo,

    Uw nieuw paswoord voor de site${siteUrl} is ${password}

    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 "" diff --git a/gen/ui/action.png b/gen/ui/action.png deleted file mode 100644 index e8f3107..0000000 Binary files a/gen/ui/action.png and /dev/null differ diff --git a/gen/ui/add.png b/gen/ui/add.png deleted file mode 100644 index 580ef51..0000000 Binary files a/gen/ui/add.png and /dev/null differ diff --git a/gen/ui/angled.png b/gen/ui/angled.png deleted file mode 100644 index 685a491..0000000 Binary files a/gen/ui/angled.png and /dev/null differ diff --git a/gen/ui/appy.css b/gen/ui/appy.css deleted file mode 100644 index ea93598..0000000 --- a/gen/ui/appy.css +++ /dev/null @@ -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 } diff --git a/gen/ui/appy.js b/gen/ui/appy.js deleted file mode 100644 index c9853a5..0000000 --- a/gen/ui/appy.js +++ /dev/null @@ -1,1386 +0,0 @@ -var wrongTextInput = '#F9EDBE none'; -var loadingLink = ''; -var loadingButton = ''; -var loadingZone = '
    '; -var lsTimeout; // Timout for the live search -var podTimeout; // Timeout for checking status of pod downloads - -// Functions related to user authentication -function cookiesAreEnabled() { - /* Test whether cookies are enabled by attempting to set a cookie and then - change its value. */ - var c = "areYourCookiesEnabled=0"; - document.cookie = c; - var dc = document.cookie; - // Cookie not set? Fail. - if (dc.indexOf(c) == -1) return 0; - // Change test cookie - c = "areYourCookiesEnabled=1"; - document.cookie = c; - dc = document.cookie; - // Cookie not changed? Fail. - if (dc.indexOf(c) == -1) return 0; - // Delete cookie - document.cookie = "areYourCookiesEnabled=; expires=Thu, 01-Jan-70 00:00:01 GMT"; - return 1; -} - -function setLoginVars() { - // Indicate if JS is enabled - document.getElementById('js_enabled').value = 1; - // Indicate if cookies are enabled - document.getElementById('cookies_enabled').value = cookiesAreEnabled(); - /* Copy login and password length to alternative vars since current vars will - be removed from the request by zope's authentication mechanism. */ - var v = document.getElementById('__ac_name').value; - document.getElementById('login_name').value = v; - password = document.getElementById('__ac_password'); - emptyPassword = document.getElementById('pwd_empty'); - if (password.value.length==0) emptyPassword.value = '1'; - else emptyPassword.value = '0'; -} - -function showLoginForm() { - // Hide the login link - var loginLink = document.getElementById('loginLink'); - loginLink.style.display = "none"; - // Displays the login form - var loginFields = document.getElementById('loginFields'); - loginFields.style.display = "inline"; -} - -function goto(url) { window.location = url } -function len(dict) { - var res = 0; - for (var key in dict) res += 1; - return res; -} - -function switchLanguage(select, siteUrl) { - var language = select.options[select.selectedIndex].value; - goto(siteUrl + '/config/changeLanguage?language=' + language); -} - -function switchResultMode(select, hook) { - var mode = select.options[select.selectedIndex].value; - askAjax(hook, null, {'resultMode': mode}); -} - -var isIe = (navigator.appName == "Microsoft Internet Explorer"); - -function getElementsHavingName(tag, name) { - if (!isIe) return document.getElementsByName(name); - var elems = document.getElementsByTagName(tag); - var res = new Array(); - for (var i=0; i:, the PX - will be found on the field named instead of being found - directly on the object at p_url. - - p_hook is the ID of the XHTML element that will be filled with the XHTML - result from the server. If it starts with ':', we will find the element in - the top browser window and not in the current one (that can be an iframe). - - p_beforeSend is a Javascript function to call before sending the request. - This function will get 2 args: the XMLHttpRequest object and the p_params. - This method can return, in a string, additional parameters to send, ie: - "¶m1=blabla¶m2=blabla". - - p_onGet is a Javascript function to call when we will receive the answer. - This function will get 2 args, too: the XMLHttpRequest object and the - HTML node element into which the result has been inserted. - - p_waiting is the name of the animated icon that will be shown while waiting - for the ajax result. If null, it will be loadingBig.gif. Other values can - be "loading", "loadingBtn" or "loadingPod" (the .gif must be omitted). - If "none", there will be no icon at all. - */ - // First, get a non-busy XMLHttpRequest object. - var pos = -1; - for (var i=0; i < xhrObjects.length; i++) { - if (xhrObjects[i].freed == 1) { pos = i; break; } - } - if (pos == -1) { - pos = xhrObjects.length; - xhrObjects[pos] = new XhrObject(); - } - xhrObjects[pos].hook = hook; - xhrObjects[pos].onGet = onGet; - if (xhrObjects[pos].xhr) { - var rq = xhrObjects[pos]; - rq.freed = 0; - // Construct parameters - var paramsFull = 'px=' + px; - if (params) { - for (var paramName in params) - paramsFull = paramsFull + '&' + paramName + '=' + params[paramName]; - } - // Call beforeSend if required - if (beforeSend) { - var res = beforeSend(rq, params); - if (res) paramsFull = paramsFull + res; - } - // Construct the URL to call - var urlFull = url + '/ajax'; - if (mode == 'GET') { - urlFull = urlFull + '?' + paramsFull; - } - showPreloader(rq.hook, waiting); // Display the pre-loader - // Perform the asynchronous HTTP GET or POST - rq.xhr.open(mode, urlFull, true); - if (mode == 'POST') { - // Set the correct HTTP headers - rq.xhr.setRequestHeader( - "Content-Type", "application/x-www-form-urlencoded"); - // rq.xhr.setRequestHeader("Content-length", paramsFull.length); - // rq.xhr.setRequestHeader("Connection", "close"); - rq.xhr.onreadystatechange = function(){ getAjaxChunk(pos); } - rq.xhr.send(paramsFull); - } - else if (mode == 'GET') { - rq.xhr.onreadystatechange = function() { getAjaxChunk(pos); } - if (window.XMLHttpRequest) { rq.xhr.send(null); } - else if (window.ActiveXObject) { rq.xhr.send(); } - } - } -} - -// Object representing all the data required to perform an Ajax request -function AjaxData(hook, px, params, parentHook, url, mode, beforeSend, onGet) { - this.hook = hook; - this.mode = mode; - if (!mode) this.mode = 'GET'; - this.url = url; - this.px = px; - this.params = params; - this.beforeSend = beforeSend; - this.onGet = onGet; - /* If a parentHook is spefified, this AjaxData must be completed with a parent - AjaxData instance. */ - this.parentHook = parentHook; - // Inject this AjaxData instance into p_hook - getAjaxHook(hook, true)['ajax'] = this; -} - -function askAjax(hook, form, params, waiting) { - /* Call askAjaxChunk by getting an AjaxData instance from p_hook, a - potential action from p_form and additional parameters from p_param. */ - var d = getAjaxHook(hook)['ajax']; - // Complete data with a parent data if present - if (d['parentHook']) { - var parentHook = d['parentHook']; - if (hook[0] == ':') parentHook = ':' + parentHook; - var parent = getAjaxHook(parentHook)['ajax']; - for (var key in parent) { - if (key == 'params') continue; // Will get a specific treatment herafter - if (!d[key]) d[key] = parent[key]; // Override if no value on child - } - // Merge parameters - if (parent.params) { - for (var key in parent.params) { - if (key in d.params) continue; // Override if not value on child - d.params[key] = parent.params[key]; - } - } - } - // Resolve dynamic parameter "cbChecked" if present - if ('cbChecked' in d.params) { - var cb = getAjaxHook(d.params['cbChecked'], true); - if (cb) d.params['cbChecked'] = cb.checked; - else delete d.params['cbChecked']; - } - // If a p_form id is given, integrate the form submission in the ajax request - if (form) { - var f = document.getElementById(form); - var mode = 'POST'; - // Deduce the action from the form action - d.params['action'] = _rsplit(f.action, '/', 2)[1]; - // Get the other params - var elems = f.elements; - for (var i=0; i < elems.length; i++) { - var value = elems[i].value; - if (elems[i].name == 'comment') value = encodeURIComponent(value); - d.params[elems[i].name] = value; - } - } - else var mode = d.mode; - // Get p_params if given. Note that they override anything else. - var px = d.px; - if (params) { - if ('mode' in params) { mode = params['mode']; delete params['mode'] }; - if ('px' in params) { px = params['px']; delete params['px'] }; - for (var key in params) d.params[key] = params[key]; - } - askAjaxChunk(hook, mode, d.url, px, d.params, d.beforeSend, evalInnerScripts, - waiting); -} - -function askBunch(hookId, startNumber) { - askAjax(hookId, null, {'startNumber': startNumber})} - -function askBunchSorted(hookId, sortKey, sortOrder) { - var data = {'startNumber': '0', 'sortKey': sortKey, 'sortOrder': sortOrder}; - askAjax(hookId, null, data); -} - -function askBunchFiltered(hookId, filterKey) { - var data = {'startNumber': '0', 'filterKey': filterKey, 'filterValue': ''}; - var node = document.getElementById(hookId + '_' + filterKey); - if (node.value) data['filterValue'] = encodeURIComponent(node.value); - askAjax(hookId, null, data); -} - -function askBunchMove(hookId, startNumber, uid, move){ - var moveTo = move; - if (typeof move == 'object'){ - // Get the new index from an input field - var id = move.id; - id = id.substr(0, id.length-4) + '_v'; - var input = document.getElementById(id); - if (isNaN(input.value)) { - input.style.background = wrongTextInput; - return; - } - moveTo = 'index_' + input.value; - } - var data = {'startNumber': startNumber, 'action': 'doChangeOrder', - 'refObjectUid': uid, 'move': moveTo}; - askAjax(hookId, null, data); -} - -function askBunchSortRef(hookId, startNumber, sortKey, reverse) { - var data = {'startNumber': startNumber, 'action': 'sort', 'sortKey': sortKey, - 'reverse': reverse}; - askAjax(hookId, null, data); -} - -function clickOn(node) { - // If node is a form, disable all form buttons - if (node.tagName == 'FORM') { - var i = node.elements.length -1; - while (i >= 0) { - if (node.elements[i].type == 'button') { clickOn(node.elements[i]); } - i = i - 1; - } - return; - } - // Disable any click on p_node to be protected against double-click - var cn = (node.className)? 'unclickable ' + node.className : 'unclickable'; - node.className = cn; - /* For a button, show the preloader directly. For a link, show it only after - a while, if the target page is still not there. */ - if (node.tagName != 'A') injectChunk(node, loadingButton); - else setTimeout(function(){injectChunk(node, loadingLink)}, 700); -} - -function gotoTied(objectUrl, field, numberWidget, total) { - // Check that the number is correct - try { - var number = parseInt(numberWidget.value); - if (!isNaN(number)) { - if ((number >= 1) && (number <= total)) { - goto(objectUrl + '/gotoTied?field=' + field + '&number=' + number); - } - else numberWidget.style.background = wrongTextInput; } - else numberWidget.style.background = wrongTextInput; } - catch (err) { numberWidget.style.background = wrongTextInput; } -} - -function askField(hookId, objectUrl, layoutType, customParams, showChanges, - masterValues, requestValue, error, className){ - // Sends an Ajax request for getting the content of any field - var fieldName = hookId.split('_')[1]; - var params = {'layoutType': layoutType, 'showChanges': showChanges}; - if (customParams){for (var key in customParams) params[key]=customParams[key]} - if (masterValues) params['masterValues'] = masterValues.join('*'); - if (requestValue) params[fieldName] = requestValue; - if (error) params[fieldName + '_error'] = error; - var px = fieldName + ':pxRender'; - if (className) px = className + ':' + px; - askAjaxChunk(hookId, 'GET', objectUrl, px, params, null, evalInnerScripts); -} - -function doInlineSave(objectUid, name, objectUrl, content, language){ - /* Ajax-saves p_content of field named p_name (or only on part corresponding - to p_language if the field is multilingual) on object whose id is - p_objectUid and whose URL is p_objectUrl. Asks a confirmation before - doing it. */ - var doIt = confirm(save_confirm); - var params = {'action': 'storeFromAjax', 'layoutType': 'view'}; - if (language) params['languageOnly'] = language; - var hook = null; - if (!doIt) { - params['cancel'] = 'True'; - hook = objectUid + '_' + name; - } - else { params['fieldContent'] = encodeURIComponent(content) } - askAjaxChunk(hook, 'POST', objectUrl, name + ':pxRender', params, null, - evalInnerScripts); -} - -// Used by checkbox widgets for having radio-button-like behaviour. -function toggleCheckbox(visibleCheckbox, hiddenBoolean) { - vis = document.getElementById(visibleCheckbox); - hidden = document.getElementById(hiddenBoolean); - if (vis.checked) hidden.value = 'True'; - else hidden.value = 'False'; -} - -// JS implementation of Python ''.rsplit -function _rsplit(s, delimiter, limit) { - var elems = s.split(delimiter); - var exc = elems.length - limit; - if (exc <= 0) return elems; - // Merge back first elements to get p_limit elements - var head = ''; - var res = []; - for (var i=0; i < elems.length; i++) { - if (exc > 0) { head += elems[i] + delimiter; exc -= 1 } - else { if (exc == 0) { res.push(head + elems[i]); exc -= 1 } - else res.push(elems[i]) } - } - return res; -} - -// (Un)checks a checkbox corresponding to a linked object -function toggleCb(checkbox) { - var name = checkbox.getAttribute('name'); - var elems = _rsplit(name, '_', 3); - // Get the DOM node corresponding to the Ref field - var node = document.getElementById(elems[0] + '_' + elems[1]); - // Get the array that stores checkbox statuses. - var statuses = node['_appy_' + elems[2] + '_cbs']; - // Get the array semantics - var semantics = node['_appy_' + elems[2] + '_sem']; - var uid = checkbox.value; - if (semantics == 'unchecked') { - if (!checkbox.checked) statuses[uid] = null; - else {if (uid in statuses) delete statuses[uid]}; - } - else { // semantics is 'checked' - if (checkbox.checked) statuses[uid] = null; - else {if (uid in statuses) delete statuses[uid]}; - } -} - -function findNode(node, id) { - /* When coming back from the iframe popup, we are still in the context of the - iframe, which can cause problems for finding nodes. We have found that this - case can be detected by checking node.window. */ - if (node.window) var container = node.window.document; - else var container = window.parent.document; - return container.getElementById(id); -} - -// Initialise checkboxes of a Ref field or Search -function initCbs(id) { - var elems = _rsplit(id, '_', 3); - // Get the DOM node corresponding to the Ref field - var node = document.getElementById(elems[0] + '_' + elems[1]); - // Get the array that stores checkbox statuses - var statuses = node['_appy_' + elems[2] + '_cbs']; - // Get the array semantics - var semantics = node['_appy_' + elems[2] + '_sem']; - var value = (semantics == 'unchecked')? false: true; - // Update visible checkboxes - var checkboxes = getElementsHavingName('input', id); - for (var i=0; i < checkboxes.length; i++) { - if (checkboxes[i].value in statuses) checkboxes[i].checked = value; - else checkboxes[i].checked = !value; - } -} - -// Toggle all checkboxes of a Ref field or Search -function toggleAllCbs(id) { - var elems = _rsplit(id, '_', 3); - // Get the DOM node corresponding to the Ref field - var node = document.getElementById(elems[0] + '_' + elems[1]); - // Empty the array that stores checkbox statuses - var statuses = node['_appy_' + elems[2] + '_cbs']; - for (var key in statuses) delete statuses[key]; - // Switch the array semantics - var semAttr = '_appy_' + elems[2] + '_sem'; - if (node[semAttr] == 'unchecked') node[semAttr] = 'checked'; - else node[semAttr] = 'unchecked'; - // Update the visible checkboxes - initCbs(id); -} - -// Shows/hides a dropdown menu -function toggleDropdown(dropdownId, forcedValue){ - var dropdown = document.getElementById(dropdownId); - // Force to p_forcedValue if specified - if (forcedValue) {dropdown.style.display = forcedValue} - else { - var displayValue = dropdown.style.display; - if (displayValue == 'block') dropdown.style.display = 'none'; - else dropdown.style.display = 'block'; - } -} - -// Function that sets a value for showing/hiding sub-titles -function setSubTitles(value, tag) { - createCookie('showSubTitles', value); - // Get the sub-titles - var subTitles = getElementsHavingName(tag, 'subTitle'); - if (subTitles.length == 0) return; - // Define the display style depending on p_tag - var displayStyle = 'inline'; - if (tag == 'tr') displayStyle = 'table-row'; - for (var i=0; i < subTitles.length; i++) { - if (value == 'true') subTitles[i].style.display = displayStyle; - else subTitles[i].style.display = 'none'; - } -} - -// Function that toggles the value for showing/hiding sub-titles -function toggleSubTitles(tag) { - // Get the current value - var value = readCookie('showSubTitles'); - if (value == null) value = 'true'; - // Toggle the value - var newValue = 'true'; - if (value == 'true') newValue = 'false'; - if (!tag) tag = 'div'; - setSubTitles(newValue, tag); -} - -// Functions used for master/slave relationships between widgets -function getSlaveInfo(slave, infoType) { - // Returns the appropriate info about slavery, depending on p_infoType - var cssClasses = slave.className.split(' '); - var masterInfo = null; - // Find the CSS class containing master-related info - for (var j=0; j < cssClasses.length; j++) { - if (cssClasses[j].indexOf('slave*') == 0) { - // Extract, from this CSS class, master name or master values - masterInfo = cssClasses[j].split('*'); - if (infoType == 'masterName') return masterInfo[1]; - else return masterInfo.slice(2); - } - } -} - -function getMasterValues(master) { - // Returns the list of values that p_master currently has - var res = null; - if ((master.tagName == 'INPUT') && (master.type != 'checkbox')) { - res = master.value; - if ((res.charAt(0) == '(') || (res.charAt(0) == '[')) { - // There are multiple values, split it - values = res.substring(1, res.length-1).split(','); - res = []; - var v = null; - for (var i=0; i < values.length; i++){ - v = values[i].replace(' ', ''); - res.push(v.substring(1, v.length-1)); - } - } - else res = [res]; // A single value - } - else if (master.type == 'checkbox') { - res = master.checked + ''; - res = res.charAt(0).toUpperCase() + res.substr(1); - res = [res]; - } - else { // SELECT widget - res = []; - for (var i=0; i < master.options.length; i++) { - if (master.options[i].selected) res.push(master.options[i].value); - } - } - return res; -} - -function getSlaves(master) { - // Gets all the slaves of master - allSlaves = getElementsHavingName('table', 'slave'); - res = []; - masterName = master.attributes['name'].value; - // Remove leading 'w_' if the master is in a search screen - if (masterName.indexOf('w_') == 0) masterName = masterName.slice(2); - if (master.type == 'checkbox') { - masterName = masterName.substr(0, masterName.length-8); - } - slavePrefix = 'slave*' + masterName + '*'; - for (var i=0; i < allSlaves.length; i++){ - cssClasses = allSlaves[i].className.split(' '); - for (var j=0; j < cssClasses.length; j++) { - if (cssClasses[j].indexOf(slavePrefix) == 0) { - res.push(allSlaves[i]); - break; - } - } - } - return res; -} - -function updateSlaves(master, slave, objectUrl, layoutType, requestValues, - errors, className){ - /* Given the value(s) in a master field, we must update slave's visibility or - value(s). If p_slave is given, it updates only this slave. Else, it updates - all slaves of p_master. */ - var slaves = null; - if (slave) { slaves = [slave]; } - else { slaves = getSlaves(master); } - masterValues = getMasterValues(master); - for (var i=0; i < slaves.length; i++) { - slaveryValues = getSlaveInfo(slaves[i], 'masterValues'); - if (slaveryValues[0] != '+') { - // Update slaves visibility depending on master values - var showSlave = false; - for (var j=0; j < slaveryValues.length; j++) { - for (var k=0; k< masterValues.length; k++) { - if (slaveryValues[j] == masterValues[k]) showSlave = true; - } - } - if (showSlave) slaves[i].style.display = ''; - else slaves[i].style.display = 'none'; - } - else { - // Update slaves' values depending on master values - var slaveId = slaves[i].id; - var slaveName = slaveId.split('_')[1]; - var reqValue = null; - if (requestValues && (slaveName in requestValues)) - reqValue = requestValues[slaveName]; - var err = null; - if (errors && (slaveName in errors)) - err = errors[slaveName]; - askField(slaveId, objectUrl, layoutType, null, false, masterValues, - reqValue, err, className); - } - } -} - -function initSlaves(objectUrl, layoutType, requestValues, errors) { - /* When the current page is loaded, we must set the correct state for all - slave fields. For those that are updated via Ajax requests, their - p_requestValues and validation p_errors must be carried to those - requests. */ - slaves = getElementsHavingName('table', 'slave'); - i = slaves.length -1; - while (i >= 0) { - masterName = getSlaveInfo(slaves[i], 'masterName'); - master = document.getElementById(masterName); - // If master is not here, we can't hide its slaves when appropriate. - if (master) { - updateSlaves(master,slaves[i],objectUrl,layoutType,requestValues,errors);} - i -= 1; - } -} - -// Function used to submit the appy form on pxEdit -function submitAppyForm(button) { - var f = document.getElementById('appyForm'); - // On which button has the user clicked ? - f.button.value = button.id; - f.submit(); clickOn(button); -} - -function submitForm(formId, msg, showComment, back) { - var f = document.getElementById(formId); - if (!msg) { - /* Submit the form and either refresh the entire page (back is null) - or ajax-refresh a given part only (p_back corresponds to the id of the - DOM node to be refreshed. */ - if (back) { askAjax(back, formId); } - else { f.submit(); clickOn(f) } - } - else { - // Ask a confirmation to the user before proceeding - if (back) { - var js = "askAjax('"+back+"', '"+formId+"');"; - askConfirm('form-script', formId+'+'+js, msg, showComment); } - else askConfirm('form', formId, msg, showComment); - } -} - -// Function used for triggering a workflow transition -function triggerTransition(formId, node, msg, back) { - var f = document.getElementById(formId); - f.transition.value = node.id; - submitForm(formId, msg, true, back); -} - -function onDeleteObject(uid, back) { - var f = document.getElementById('deleteForm'); - f.uid.value = uid; - submitForm('deleteForm', action_confirm, false, back); -} - -function onDeleteEvent(objectUid, eventTime) { - f = document.getElementById('deleteEventForm'); - f.objectUid.value = objectUid; - f.eventTime.value = eventTime; - askConfirm('form', 'deleteEventForm', action_confirm); -} - -function onLink(action, sourceUid, fieldName, targetUid) { - f = document.getElementById('linkForm'); - f.linkAction.value = action; - f.sourceUid.value = sourceUid; - f.fieldName.value = fieldName; - f.targetUid.value = targetUid; - f.submit(); -} - -function stringFromDictKeys(d){ - // Gets a string containing comma-separated keys from dict p_d - var res = []; - for (var key in d) res.push(key); - return res.join(); -} - -function onLinkMany(action, id) { - var elems = _rsplit(id, '_', 3); - // Get the DOM node corresponding to the Ref field - var node = document.getElementById(elems[0] + '_' + elems[1]); - // Get the uids of (un-)checked objects. - var statuses = node['_appy_' + elems[2] + '_cbs']; - var uids = stringFromDictKeys(statuses); - // Get the array semantics - var semantics = node['_appy_' + elems[2] + '_sem']; - // Show an error message if no element is selected - if ((semantics == 'checked') && (len(statuses) == 0)) { - openPopup('alertPopup', no_elem_selected); - return; - } - // Fill the form and ask for a confirmation - f = document.getElementById('linkForm'); - f.linkAction.value = action + '_many'; - f.sourceUid.value = elems[0]; - f.fieldName.value = elems[1]; - f.targetUid.value = uids; - f.semantics.value = semantics; - askConfirm('form', 'linkForm', action_confirm); -} - -function onUnlockPage(objectUid, pageName) { - f = document.getElementById('unlockForm'); - f.objectUid.value = objectUid; - f.pageName.value = pageName; - askConfirm('form', 'unlockForm', action_confirm); -} - -function createCookie(name, value, days) { - if (days) { - var date = new Date(); - date.setTime(date.getTime()+(days*24*60*60*1000)); - var expires = "; expires="+date.toGMTString(); - } else expires = ""; - document.cookie = name+"="+escape(value)+expires+"; path=/;"; -} - -function readCookie(name) { - var nameEQ = name + "="; - var ca = document.cookie.split(';'); - for (var i=0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0)==' ') { c = c.substring(1,c.length); } - if (c.indexOf(nameEQ) == 0) { - return unescape(c.substring(nameEQ.length,c.length)); - } - } - return null; -} - -function toggleCookie(cookieId, display, defaultValue) { - // What is the state of this boolean (expanded/collapsed) cookie? - var state = readCookie(cookieId); - if ((state != 'collapsed') && (state != 'expanded')) { - // No cookie yet, create it - createCookie(cookieId, defaultValue); - state = defaultValue; - } - var hook = document.getElementById(cookieId); // The hook is the part of - // the HTML document that needs to be shown or hidden. - var displayValue = 'none'; - var newState = 'collapsed'; - var imgSrc = 'ui/expand.gif'; - if (state == 'collapsed') { - // Show the HTML zone - displayValue = display; - imgSrc = 'ui/collapse.gif'; - newState = 'expanded'; - } - // Update the corresponding HTML element - hook.style.display = displayValue; - var img = document.getElementById(cookieId + '_img'); - img.src = imgSrc; - // Inverse the cookie value - createCookie(cookieId, newState); -} - -function podDownloadStatus(node, data) { - // Checks the status of cookie "podDownload" - var status = readCookie('podDownload'); - // Stop the timeout if the download is complete - if (status == 'false') return; - clearInterval(podTimeout); - for (var key in data) node.setAttribute(key, data[key]); -} - -// Function that allows to generate a document from a pod template -function generatePod(node, uid, fieldName, template, podFormat, queryData, - customParams, getChecked, mailing) { - var f = document.getElementById('podForm'); - f.objectUid.value = uid; - f.fieldName.value = fieldName; - f.template.value = template; - f.podFormat.value = podFormat; - f.queryData.value = queryData; - if (customParams) { f.customParams.value = customParams; } - else { f.customParams.value = ''; } - if (mailing) f.mailing.value = mailing; - // Transmit value of cookie "showSubTitles" - f.showSubTitles.value = readCookie('showSubTitles') || 'true'; - f.action.value = 'generate'; - f.checkedUids.value = ''; - f.checkedSem.value = ''; - if (getChecked) { - // We must collect selected objects from a Ref field - var cNode = document.getElementById(uid + '_' + getChecked); - if (cNode && cNode.hasOwnProperty('_appy_objs_cbs')) { - f.checkedUids.value = stringFromDictKeys(cNode['_appy_objs_cbs']); - f.checkedSem.value = cNode['_appy_objs_sem']; - } - } - // Submitting the form at the end blocks the animated gifs on FF - f.submit(); - // If p_node is an image, replace it with a preloader to prevent double-clicks - if (node.tagName == 'IMG') { - var data = {'src': node.src, 'class': node.className, - 'onclick': node.attributes.onclick.value}; - node.setAttribute('onclick', ''); - node.className = ''; - var src2 = node.src.replace(podFormat + '.png', 'loadingPod.gif'); - node.setAttribute('src', src2); - // Initialize the pod download cookie. "false" means: not downloaded yet - createCookie('podDownload', 'false'); - // Set a timer that will check the cookie value - podTimeout = window.setInterval(function(){ - podDownloadStatus(node, data)}, 700); - } -} - -// Function that allows to (un-)freeze a document from a pod template -function freezePod(uid, fieldName, template, podFormat, action) { - var f = document.getElementById('podForm'); - f.objectUid.value = uid; - f.fieldName.value = fieldName; - f.template.value = template; - f.podFormat.value = podFormat; - f.action.value = action; - askConfirm('form', 'podForm', action_confirm); -} - -// Function that allows to upload a file for freezing it in a pod field -function uploadPod(uid, fieldName, template, podFormat) { - var f = document.getElementById('uploadForm'); - f.objectUid.value = uid; - f.fieldName.value = fieldName; - f.template.value = template; - f.podFormat.value = podFormat; - f.uploadedFile.value = null; - openPopup('uploadPopup'); -} - -function protectAppyForm() { - window.onbeforeunload = function(e){ - f = document.getElementById("appyForm"); - if (f.button.value == "") { - var e = e || window.event; - if (e) {e.returnValue = warn_leave_form;} - return warn_leave_form; - } - } -} - -// Functions for opening and closing a popup -function openPopup(popupId, msg, width, height, back) { - // Put the message into the popup - if (msg) { - var msgHook = (popupId == 'alertPopup')? 'appyAlertText': 'appyConfirmText'; - var confirmElem = document.getElementById(msgHook); - confirmElem.innerHTML = msg; - } - // Open the popup - var popup = document.getElementById(popupId); - // Put it at the right place on the screen and give it the right dimensions - if (!width) { - width = (popupId == 'iframePopup')? window.innerWidth-200: 350; - } - var scrollTop = document.documentElement.scrollTop || window.pageYOffset || 0; - popup.style.top = (scrollTop + 150) + 'px'; - popup.style.width = width + 'px'; - popup.style.left = ((window.innerWidth - width) / 2).toFixed() + 'px'; - if (height) popup.style.height = height + 'px'; - if (popupId == 'iframePopup') { - // Initialize iframe's width - var iframe = document.getElementById('appyIFrame'); - if (!height) { - height = window.innerHeight - 200; - popup.style.top = ((window.innerHeight - height) / 2).toFixed() + 'px'; - } - iframe.style.width = (width-20) + 'px'; - popup.style.height = height + 'px'; - iframe.style.height = (height-20) + 'px'; - popup['back'] = back; - } - popup.style.display = 'block'; -} - -function closePopup(popupId, clean) { - // Get the popup - var container = null; - if (popupId == 'iframePopup') container = window.parent.document; - else container = window.document; - var popup = container.getElementById(popupId); - // Close the popup - popup.style.display = 'none'; - popup.style.width = null; - // Clean field "clean" if specified - if (clean) { - var elem = popup.getElementsByTagName('form')[0].elements[clean]; - if (elem) elem.value = ''; - } - if (popupId == 'iframePopup') { - // Reinitialise the enclosing iframe - var iframe = container.getElementById('appyIFrame'); - iframe.style.width = null; - while (iframe.firstChild) iframe.removeChild(iframe.firstChild); - // Leave the form silently if we are on an edit page - iframe.contentWindow.onbeforeunload = null; - } - return popup; -} - -function backFromPopup() { - var popup = closePopup('iframePopup'); - if (popup['back']) askAjax(':'+popup['back']); - else window.parent.location = window.parent.location; -} - -function showAppyMessage(message) { - // Fill the message zone with the message to display - var messageZone = getAjaxHook(':appyMessageContent'); - messageZone.innerHTML = message; - // Display the message zone - var messageDiv = getAjaxHook(':appyMessage'); - messageDiv.style.display = 'block'; -} - -// Function triggered when an action needs to be confirmed by the user -function askConfirm(actionType, action, msg, showComment) { - /* Store the actionType (send a form, call an URL or call a script) and the - related action, and shows the confirm popup. If the user confirms, we - will perform the action. If p_showComment is true, an input field allowing - to enter a comment will be shown in the popup. */ - var confirmForm = document.getElementById('confirmActionForm'); - confirmForm.actionType.value = actionType; - confirmForm.action.value = action; - if (!msg) msg = action_confirm; - var commentArea = document.getElementById('commentArea'); - if (showComment) commentArea.style.display = 'block'; - else commentArea.style.display = 'none'; - openPopup("confirmActionPopup", msg); -} - -// Transfer comment from the confirm form to some other form -function transferComment(confirmForm, targetForm) { - if ((confirmForm.popupComment.style.display != 'none') && - (confirmForm.popupComment.value)) { - targetForm.popupComment.value = confirmForm.popupComment.value; - // Clean the confirm form - confirmForm.popupComment.value = ''; - } -} - -// Function triggered when an action confirmed by the user must be performed -function doConfirm() { - // The user confirmed: perform the required action - closePopup('confirmActionPopup'); - var confirmForm = document.getElementById('confirmActionForm'); - var actionType = confirmForm.actionType.value; - var action = confirmForm.action.value; - if (actionType == 'form') { - /* Submit the form whose id is in "action", and transmit him the comment - from the popup when relevant. */ - var f = document.getElementById(action); - transferComment(confirmForm, f); - f.submit(); clickOn(f); - } - else if (actionType == 'url') { goto(action) } // Go to some URL - else if (actionType == 'script') { eval(action) } // Exec some JS code - else if (actionType == 'form+script') { - var elems = action.split('+'); - var f = document.getElementById(elems[0]); - // Submit the form in elems[0] and execute the JS code in elems[1] - transferComment(confirmForm, f); - f.submit(); clickOn(f); - eval(elems[1]); - } - else if (actionType == 'form-script') { - /* Similar to form+script, but the form must not be submitted. It will - probably be used by the JS code, so the comment must be transfered. */ - var elems = action.split('+'); - var f = document.getElementById(elems[0]); - // Submit the form in elems[0] and execute the JS code in elems[1] - transferComment(confirmForm, f); - eval(elems[1]); - } -} - -// Function triggered when the user asks password reinitialisation -function doAskPasswordReinit() { - // Check that the user has typed a login - var f = document.getElementById('askPasswordReinitForm'); - var login = f.login.value.replace(' ', ''); - if (!login) { f.login.style.background = wrongTextInput; } - else { - closePopup('askPasswordReinitPopup'); - f.submit(); - } -} - -// Function that finally posts the edit form after the user has confirmed that -// she really wants to post it. -function postConfirmedEditForm() { - var f = document.getElementById('appyForm'); - f.confirmed.value = "True"; - f.button.value = 'save'; - f.submit(); -} - -// Function that shows or hides a tab. p_action is 'show' or 'hide'. -function manageTab(tabId, action) { - // Manage the tab content (show it or hide it) - var content = document.getElementById('tabcontent_' + tabId); - if (action == 'show') { content.style.display = 'table-row'; } - else { content.style.display = 'none'; } - // Manage the tab itself (show as selected or unselected) - var left = document.getElementById('tab_' + tabId + '_left'); - var tab = document.getElementById('tab_' + tabId); - var right = document.getElementById('tab_' + tabId + '_right'); - if (action == 'show') { - left.src = "ui/tabLeft.png"; - tab.style.backgroundImage = "url(ui/tabBg.png)"; - right.src = "ui/tabRight.png"; - } - if (action == 'hide') { - left.src = "ui/tabLeftu.png"; - tab.style.backgroundImage = "url(ui/tabBgu.png)"; - right.src = "ui/tabRightu.png"; - } -} - -// Function used for displaying/hiding content of a tab -function showTab(tabId) { - // 1st, show the tab to show - manageTab(tabId, 'show'); - // Compute the number of tabs - var idParts = tabId.split('_'); - var prefix = idParts[0] + '_'; - // Store the currently selected tab in a cookie - createCookie('tab_' + idParts[0], tabId); - var nbOfTabs = idParts[2]*1; - // Then, hide the other tabs - for (var i=0; i 0) j = i-1; - else j = len-1; - } - else { - if (i < (len-1)) j = i+1; - else j = 0; - } - results[i].className = ''; - results[j].className = 'lsSelected'; - break; - } - } - if (isNaN(j)) results[0].className = 'lsSelected'; -} - -// Function that allows to go to a selected search result -function gotoLSLink(dropdown) { - var results = dropdown.children[0].getElementsByTagName('div'); - for (var i=0, len=results.length; i